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,39 @@ 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
+ final long now = SystemClock .uptimeMillis ();
433
+ if (appStartSpan == null ) {
434
+ return ;
435
+ }
436
+ final @ Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap .get (activity );
437
+ if (timeSpan != null ) {
438
+ timeSpan .getOnStart ().setStartedAt (now );
439
+ }
440
+ }
441
+
390
442
@ Override
391
443
public synchronized void onActivityStarted (final @ NotNull Activity activity ) {
444
+ if (!isAllActivityCallbacksAvailable ) {
445
+ onActivityPostCreated (activity , null );
446
+ onActivityPreStarted (activity );
447
+ }
392
448
if (performanceEnabled ) {
393
449
// The docs on the screen rendering performance tracing
394
450
// (https://firebase.google.com/docs/perf-mon/screen-traces?platform=android#definition),
@@ -400,74 +456,75 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
400
456
}
401
457
}
402
458
459
+ @ Override
460
+ public void onActivityPostStarted (final @ NotNull Activity activity ) {
461
+ final @ Nullable ActivityLifecycleTimeSpan timeSpan = activityLifecycleMap .remove (activity );
462
+ if (appStartSpan == null ) {
463
+ return ;
464
+ }
465
+ if (timeSpan != null ) {
466
+ timeSpan .getOnStart ().stop ();
467
+ timeSpan .getOnStart ().setDescription (activity .getClass ().getName () + ".onStart" );
468
+ AppStartMetrics .getInstance ().addActivityLifecycleTimeSpans (timeSpan );
469
+ }
470
+ }
471
+
403
472
@ Override
404
473
public synchronized void onActivityResumed (final @ NotNull Activity activity ) {
474
+ if (!isAllActivityCallbacksAvailable ) {
475
+ onActivityPostStarted (activity );
476
+ }
405
477
if (performanceEnabled ) {
406
-
407
478
final @ Nullable ISpan ttidSpan = ttidSpanMap .get (activity );
408
479
final @ Nullable ISpan ttfdSpan = ttfdSpanMap .get (activity );
409
- final View rootView = activity .findViewById (android .R .id .content );
410
- if (rootView != null ) {
480
+ if (activity .getWindow () != null ) {
411
481
FirstDrawDoneListener .registerForNextDraw (
412
- rootView , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
482
+ activity , () -> onFirstFrameDrawn (ttfdSpan , ttidSpan ), buildInfoProvider );
413
483
} else {
414
484
// Posting a task to the main thread's handler will make it executed after it finished
415
485
// its current job. That is, right after the activity draws the layout.
416
- mainHandler .post (() -> onFirstFrameDrawn (ttfdSpan , ttidSpan ));
486
+ new Handler ( Looper . getMainLooper ()) .post (() -> onFirstFrameDrawn (ttfdSpan , ttidSpan ));
417
487
}
418
488
}
419
489
}
420
490
421
491
@ Override
422
- public void onActivityPostResumed (@ NonNull Activity activity ) {
492
+ public void onActivityPostResumed (@ NotNull Activity activity ) {
423
493
// empty override, required to avoid a api-level breaking super.onActivityPostResumed() calls
424
494
}
425
495
426
496
@ Override
427
- public void onActivityPrePaused (@ NonNull Activity activity ) {
497
+ public void onActivityPrePaused (@ NotNull Activity activity ) {
428
498
// 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
- }
499
+ // as the SDK may gets (re-)initialized mid activity lifecycle, ensure we set the flag here as
500
+ // well
501
+ // this ensures any newly launched activity will not use the app start timestamp as txn start
502
+ firstActivityCreated = true ;
503
+ lastPausedTime =
504
+ hub != null
505
+ ? hub .getOptions ().getDateProvider ().now ()
506
+ : AndroidDateUtils .getCurrentSentryDateTime ();
507
+ lastPausedUptimeMillis = SystemClock .uptimeMillis ();
440
508
}
441
509
442
510
@ Override
443
511
public synchronized void onActivityPaused (final @ NotNull Activity activity ) {
444
512
// only executed if API < 29 otherwise it happens on onActivityPrePaused
445
513
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
- }
514
+ onActivityPrePaused (activity );
455
515
}
456
516
}
457
517
458
518
@ Override
459
- public synchronized void onActivityStopped (final @ NotNull Activity activity ) {
460
- // no-op
461
- }
519
+ public void onActivityStopped (final @ NotNull Activity activity ) {}
462
520
463
521
@ Override
464
- public synchronized void onActivitySaveInstanceState (
465
- final @ NotNull Activity activity , final @ NotNull Bundle outState ) {
466
- // no-op
467
- }
522
+ public void onActivitySaveInstanceState (
523
+ final @ NotNull Activity activity , final @ NotNull Bundle outState ) {}
468
524
469
525
@ Override
470
526
public synchronized void onActivityDestroyed (final @ NotNull Activity activity ) {
527
+ activityLifecycleMap .remove (activity );
471
528
if (performanceEnabled ) {
472
529
473
530
// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
@@ -494,10 +551,20 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
494
551
}
495
552
496
553
// clear it up, so we don't start again for the same activity if the activity is in the
497
- // activity
498
- // stack still.
554
+ // activity stack still.
499
555
// if the activity is opened again and not in memory, transactions will be created normally.
500
556
activitiesWithOngoingTransactions .remove (activity );
557
+
558
+ if (activitiesWithOngoingTransactions .isEmpty ()) {
559
+ clear ();
560
+ }
561
+ }
562
+
563
+ private void clear () {
564
+ firstActivityCreated = false ;
565
+ lastPausedTime = new SentryNanotimeDate (new Date (0 ), 0 );
566
+ lastPausedUptimeMillis = 0 ;
567
+ activityLifecycleMap .clear ();
501
568
}
502
569
503
570
private void finishSpan (final @ Nullable ISpan span ) {
@@ -604,6 +671,16 @@ WeakHashMap<Activity, ITransaction> getActivitiesWithOngoingTransactions() {
604
671
return activitiesWithOngoingTransactions ;
605
672
}
606
673
674
+ @ TestOnly
675
+ @ NotNull 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