9
9
import android .os .Bundle ;
10
10
import android .os .Handler ;
11
11
import android .os .Looper ;
12
- import android .view .View ;
13
- import androidx .annotation .NonNull ;
12
+ import android .os .SystemClock ;
14
13
import io .sentry .FullyDisplayedReporter ;
15
14
import io .sentry .IHub ;
16
15
import io .sentry .IScope ;
29
28
import io .sentry .TransactionOptions ;
30
29
import io .sentry .android .core .internal .util .ClassUtil ;
31
30
import io .sentry .android .core .internal .util .FirstDrawDoneListener ;
31
+ import io .sentry .android .core .performance .ActivityLifecycleTimeSpan ;
32
32
import io .sentry .android .core .performance .AppStartMetrics ;
33
33
import io .sentry .android .core .performance .TimeSpan ;
34
34
import io .sentry .protocol .MeasurementValue ;
@@ -77,8 +77,10 @@ public final class ActivityLifecycleIntegration
77
77
private @ Nullable ISpan appStartSpan ;
78
78
private final @ NotNull WeakHashMap <Activity , ISpan > ttidSpanMap = new WeakHashMap <>();
79
79
private final @ NotNull WeakHashMap <Activity , ISpan > ttfdSpanMap = new WeakHashMap <>();
80
+ private final @ NotNull WeakHashMap <Activity , ActivityLifecycleTimeSpan > activityLifecycleMap =
81
+ new WeakHashMap <>();
80
82
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 ;
82
84
private @ Nullable Future <?> ttfdAutoCloseFuture = null ;
83
85
84
86
// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
@@ -369,9 +371,32 @@ private void finishTransaction(
369
371
}
370
372
}
371
373
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
+
372
394
@ Override
373
395
public synchronized void onActivityCreated (
374
396
final @ NotNull Activity activity , final @ Nullable Bundle savedInstanceState ) {
397
+ if (!isAllActivityCallbacksAvailable ) {
398
+ onActivityPreCreated (activity , savedInstanceState );
399
+ }
375
400
setColdStart (savedInstanceState );
376
401
if (hub != null && options != null && options .isEnableScreenTracking ()) {
377
402
final @ Nullable String activityClassName = ClassUtil .getClassName (activity );
@@ -387,8 +412,38 @@ public synchronized void onActivityCreated(
387
412
}
388
413
}
389
414
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
+
390
441
@ Override
391
442
public synchronized void onActivityStarted (final @ NotNull Activity activity ) {
443
+ if (!isAllActivityCallbacksAvailable ) {
444
+ onActivityPostCreated (activity , null );
445
+ onActivityPreStarted (activity );
446
+ }
392
447
if (performanceEnabled ) {
393
448
// The docs on the screen rendering performance tracing
394
449
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
@@ -400,74 +455,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
400
455
}
401
456
}
402
457
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
+
403
471
@ Override
404
472
public synchronized void onActivityResumed (final @ NotNull Activity activity ) {
473
+ if (!isAllActivityCallbacksAvailable ) {
474
+ onActivityPostStarted (activity );
475
+ }
405
476
if (performanceEnabled ) {
406
-
407
477
final @ Nullable ISpan ttidSpan = ttidSpanMap .get (activity );
408
478
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 ) {
411
480
FirstDrawDoneListener .registerForNextDraw (
412
- rootView , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
481
+ activity , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
413
482
} else {
414
483
// Posting a task to the main thread's handler will make it executed after it finished
415
484
// 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 ));
417
486
}
418
487
}
419
488
}
420
489
421
490
@ Override
422
- public void onActivityPostResumed (@ NonNull Activity activity ) {
491
+ public void onActivityPostResumed (@ NotNull Activity activity ) {
423
492
// empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
424
493
}
425
494
426
495
@ Override
427
- public void onActivityPrePaused (@ NonNull Activity activity ) {
496
+ public void onActivityPrePaused (@ NotNull Activity activity ) {
428
497
// 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 ();
440
507
}
441
508
442
509
@ Override
443
510
public synchronized void onActivityPaused (final @ NotNull Activity activity ) {
444
511
// only executed if API < 29 otherwise it happens on onActivityPrePaused
445
512
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 );
455
514
}
456
515
}
457
516
458
517
@ Override
459
- public synchronized void onActivityStopped (final @ NotNull Activity activity ) {
460
- // no-op
461
- }
518
+ public void onActivityStopped (final @ NotNull Activity activity ) {}
462
519
463
520
@ 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 ) {}
468
523
469
524
@ Override
470
525
public synchronized void onActivityDestroyed (final @ NotNull Activity activity ) {
526
+ activityLifecycleMap .remove (activity );
471
527
if (performanceEnabled ) {
472
528
473
529
// 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) {
494
550
}
495
551
496
552
// 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.
499
554
// if the activity is opened again and not in memory, transactions will be created normally.
500
555
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 ();
501
567
}
502
568
503
569
private void finishSpan (final @ Nullable ISpan span ) {
@@ -604,6 +670,17 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
604
670
return activitiesWithOngoingTransactions ;
605
671
}
606
672
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
+
607
684
@ TestOnly
608
685
@ NotNull
609
686
ActivityFramesTracker getActivityFramesTracker () {
@@ -629,20 +706,17 @@ WeakHashMap<Activity, ISpan> getTtfdSpanMap() {
629
706
}
630
707
631
708
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
- }
640
709
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 {
646
720
AppStartMetrics .getInstance ()
647
721
.setAppStartType (
648
722
savedInstanceState == null
0 commit comments