99import android .os .Bundle ;
1010import android .os .Handler ;
1111import android .os .Looper ;
12- import android .view .View ;
13- import androidx .annotation .NonNull ;
12+ import android .os .SystemClock ;
1413import io .sentry .FullyDisplayedReporter ;
1514import io .sentry .IHub ;
1615import io .sentry .IScope ;
2928import io .sentry .TransactionOptions ;
3029import io .sentry .android .core .internal .util .ClassUtil ;
3130import io .sentry .android .core .internal .util .FirstDrawDoneListener ;
31+ import io .sentry .android .core .performance .ActivityLifecycleTimeSpan ;
3232import io .sentry .android .core .performance .AppStartMetrics ;
3333import io .sentry .android .core .performance .TimeSpan ;
3434import io .sentry .protocol .MeasurementValue ;
@@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
7777 private @ Nullable ISpan appStartSpan ;
7878 private final @ NotNull WeakHashMap <Activity , ISpan > ttidSpanMap = new WeakHashMap <>();
7979 private final @ NotNull WeakHashMap <Activity , ISpan > ttfdSpanMap = new WeakHashMap <>();
80+ private final @ NotNull WeakHashMap <Activity , ActivityLifecycleTimeSpan > activityLifecycleMap =
81+ new WeakHashMap <>();
8082 private @ NotNull SentryDate lastPausedTime = new SentryNanotimeDate (new Date (0 ), 0 );
81- private final @ NotNull Handler mainHandler = new Handler ( Looper . getMainLooper ()) ;
83+ private long lastPausedUptimeMillis = 0 ;
8284 private @ Nullable Future <?> ttfdAutoCloseFuture = null ;
8385
8486 // WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
@@ -369,9 +371,32 @@ private void finishTransaction(
369371 }
370372 }
371373
374+ @ Override
375+ public void onActivityPreCreated (
376+ final @ NotNull Activity activity , final @ Nullable Bundle savedInstanceState ) {
377+ // The very first activity start timestamp cannot be set to the class instantiation time, as it
378+ // may happen before an activity is started (service, broadcast receiver, etc). So we set it
379+ // here.
380+ if (firstActivityCreated ) {
381+ return ;
382+ }
383+ lastPausedTime =
384+ hub != null
385+ ? hub .getOptions ().getDateProvider ().now ()
386+ : AndroidDateUtils .getCurrentSentryDateTime ();
387+ lastPausedUptimeMillis = SystemClock .uptimeMillis ();
388+
389+ final @ NotNull ActivityLifecycleTimeSpan timeSpan = new ActivityLifecycleTimeSpan ();
390+ timeSpan .getOnCreate ().setStartedAt (lastPausedUptimeMillis );
391+ activityLifecycleMap .put (activity , timeSpan );
392+ }
393+
372394 @ Override
373395 public synchronized void onActivityCreated (
374396 final @ NotNull Activity activity , final @ Nullable Bundle savedInstanceState ) {
397+ if (!isAllActivityCallbacksAvailable ) {
398+ onActivityPreCreated (activity , savedInstanceState );
399+ }
375400 setColdStart (savedInstanceState );
376401 if (hub != null && options != null && options .isEnableScreenTracking ()) {
377402 final @ Nullable String activityClassName = ClassUtil .getClassName (activity );
@@ -387,8 +412,38 @@ public synchronized void onActivityCreated(
387412 }
388413 }
389414
415+ @ Override
416+ public void onActivityPostCreated (
417+ final @ NotNull Activity activity , final @ Nullable Bundle savedInstanceState ) {
418+ if (appStartSpan == null ) {
419+ activityLifecycleMap .remove (activity );
420+ return ;
421+ }
422+
423+ final @ Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap .get (activity );
424+ if (timeSpan != null ) {
425+ timeSpan .getOnCreate ().stop ();
426+ timeSpan .getOnCreate ().setDescription (activity .getClass ().getName () + ".onCreate" );
427+ }
428+ }
429+
430+ @ Override
431+ public void onActivityPreStarted (final @ NotNull Activity activity ) {
432+ if (appStartSpan == null ) {
433+ return ;
434+ }
435+ final @ Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap .get (activity );
436+ if (timeSpan != null ) {
437+ timeSpan .getOnStart ().setStartedAt (SystemClock .uptimeMillis ());
438+ }
439+ }
440+
390441 @ Override
391442 public synchronized void onActivityStarted (final @ NotNull Activity activity ) {
443+ if (!isAllActivityCallbacksAvailable ) {
444+ onActivityPostCreated (activity , null );
445+ onActivityPreStarted (activity );
446+ }
392447 if (performanceEnabled ) {
393448 // The docs on the screen rendering performance tracing
394449 // (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
@@ -400,74 +455,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
400455 }
401456 }
402457
458+ @ Override
459+ public void onActivityPostStarted (final @ NotNull Activity activity ) {
460+ final @ Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap .remove (activity );
461+ if (appStartSpan == null ) {
462+ return ;
463+ }
464+ if (timeSpan != null ) {
465+ timeSpan .getOnStart ().stop ();
466+ timeSpan .getOnStart ().setDescription (activity .getClass ().getName () + ".onStart" );
467+ AppStartMetrics .getInstance ().addActivityLifecycleTimeSpans (timeSpan );
468+ }
469+ }
470+
403471 @ Override
404472 public synchronized void onActivityResumed (final @ NotNull Activity activity ) {
473+ if (!isAllActivityCallbacksAvailable ) {
474+ onActivityPostStarted (activity );
475+ }
405476 if (performanceEnabled ) {
406-
407477 final @ Nullable ISpan ttidSpan = ttidSpanMap .get (activity );
408478 final @ Nullable ISpan ttfdSpan = ttfdSpanMap .get (activity );
409- final View rootView = activity .findViewById (android .R .id .content );
410- if (rootView != null ) {
479+ if (activity .getWindow () != null ) {
411480 FirstDrawDoneListener .registerForNextDraw (
412- rootView , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
481+ activity , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
413482 } else {
414483 // Posting a task to the main thread's handler will make it executed after it finished
415484 // its current job. That is, right after the activity draws the layout.
416- mainHandler .post (() -> onFirstFrameDrawn (ttfdSpan , ttidSpan ));
485+ new Handler ( Looper . getMainLooper ()) .post (() -> onFirstFrameDrawn (ttfdSpan , ttidSpan ));
417486 }
418487 }
419488 }
420489
421490 @ Override
422- public void onActivityPostResumed (@ NonNull Activity activity ) {
491+ public void onActivityPostResumed (@ NotNull Activity activity ) {
423492 // empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
424493 }
425494
426495 @ Override
427- public void onActivityPrePaused (@ NonNull Activity activity ) {
496+ public void onActivityPrePaused (@ NotNull Activity activity ) {
428497 // only executed if API >= 29 otherwise it happens on onActivityPaused
429- if (isAllActivityCallbacksAvailable ) {
430- // as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
431- // well
432- // this ensures any newly launched activity will not use the app start timestamp as txn start
433- firstActivityCreated = true ;
434- if (hub == null ) {
435- lastPausedTime = AndroidDateUtils .getCurrentSentryDateTime ();
436- } else {
437- lastPausedTime = hub .getOptions ().getDateProvider ().now ();
438- }
439- }
498+ // as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
499+ // well
500+ // this ensures any newly launched activity will not use the app start timestamp as txn start
501+ firstActivityCreated = true ;
502+ lastPausedTime =
503+ hub != null
504+ ? hub .getOptions ().getDateProvider ().now ()
505+ : AndroidDateUtils .getCurrentSentryDateTime ();
506+ lastPausedUptimeMillis = SystemClock .uptimeMillis ();
440507 }
441508
442509 @ Override
443510 public synchronized void onActivityPaused (final @ NotNull Activity activity ) {
444511 // only executed if API < 29 otherwise it happens on onActivityPrePaused
445512 if (!isAllActivityCallbacksAvailable ) {
446- // as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
447- // well
448- // this ensures any newly launched activity will not use the app start timestamp as txn start
449- firstActivityCreated = true ;
450- if (hub == null ) {
451- lastPausedTime = AndroidDateUtils .getCurrentSentryDateTime ();
452- } else {
453- lastPausedTime = hub .getOptions ().getDateProvider ().now ();
454- }
513+ onActivityPrePaused (activity );
455514 }
456515 }
457516
458517 @ Override
459- public synchronized void onActivityStopped (final @ NotNull Activity activity ) {
460- // no-op
461- }
518+ public void onActivityStopped (final @ NotNull Activity activity ) {}
462519
463520 @ Override
464- public synchronized void onActivitySaveInstanceState (
465- final @ NotNull Activity activity , final @ NotNull Bundle outState ) {
466- // no-op
467- }
521+ public void onActivitySaveInstanceState (
522+ final @ NotNull Activity activity , final @ NotNull Bundle outState ) {}
468523
469524 @ Override
470525 public synchronized void onActivityDestroyed (final @ NotNull Activity activity ) {
526+ activityLifecycleMap .remove (activity );
471527 if (performanceEnabled ) {
472528
473529 // in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
@@ -494,10 +550,20 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
494550 }
495551
496552 // clear it up, so we don't start again for the same activity if the activity is in the
497- // activity
498- // stack still.
553+ // activity stack still.
499554 // if the activity is opened again and not in memory, transactions will be created normally.
500555 activitiesWithOngoingTransactions .remove (activity );
556+
557+ if (activitiesWithOngoingTransactions .isEmpty ()) {
558+ clear ();
559+ }
560+ }
561+
562+ private void clear () {
563+ firstActivityCreated = false ;
564+ lastPausedTime = new SentryNanotimeDate (new Date (0 ), 0 );
565+ lastPausedUptimeMillis = 0 ;
566+ activityLifecycleMap .clear ();
501567 }
502568
503569 private void finishSpan (final @ Nullable ISpan span ) {
@@ -604,6 +670,17 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
604670 return activitiesWithOngoingTransactions ;
605671 }
606672
673+ @ TestOnly
674+ @ NotNull
675+ WeakHashMap <Activity , ActivityLifecycleTimeSpan > getActivityLifecycleMap () {
676+ return activityLifecycleMap ;
677+ }
678+
679+ @ TestOnly
680+ void setFirstActivityCreated (boolean firstActivityCreated ) {
681+ this .firstActivityCreated = firstActivityCreated ;
682+ }
683+
607684 @ TestOnly
608685 @ NotNull
609686 ActivityFramesTracker getActivityFramesTracker () {
@@ -629,20 +706,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
629706 }
630707
631708 private void setColdStart (final @ Nullable Bundle savedInstanceState ) {
632- // The very first activity start timestamp cannot be set to the class instantiation time, as it
633- // may happen before an activity is started (service, broadcast receiver, etc). So we set it
634- // here.
635- if (hub != null && lastPausedTime .nanoTimestamp () == 0 ) {
636- lastPausedTime = hub .getOptions ().getDateProvider ().now ();
637- } else if (lastPausedTime .nanoTimestamp () == 0 ) {
638- lastPausedTime = AndroidDateUtils .getCurrentSentryDateTime ();
639- }
640709 if (!firstActivityCreated ) {
641- // if Activity has savedInstanceState then its a warm start
642- // https://developer.android.com/topic/performance/vitals/launch-time#warm
643- // SentryPerformanceProvider sets this already
644- // pre-performance-v2: back-fill with best guess
645- if (options != null && !options .isEnablePerformanceV2 ()) {
710+ final @ NotNull TimeSpan appStartSpan = AppStartMetrics .getInstance ().getAppStartTimeSpan ();
711+ // If the app start span already started and stopped, it means the app restarted without
712+ // killing the process, so we are in a warm start
713+ // If the app has an invalid cold start, it means it was started in the background, like
714+ // via BroadcastReceiver, so we consider it a warm start
715+ if ((appStartSpan .hasStarted () && appStartSpan .hasStopped ())
716+ || (!AppStartMetrics .getInstance ().isColdStartValid ())) {
717+ AppStartMetrics .getInstance ().restartAppStart (lastPausedUptimeMillis );
718+ AppStartMetrics .getInstance ().setAppStartType (AppStartMetrics .AppStartType .WARM );
719+ } else {
646720 AppStartMetrics .getInstance ()
647721 .setAppStartType (
648722 savedInstanceState == null
0 commit comments