3
3
import static android .app .ActivityManager .RunningAppProcessInfo .IMPORTANCE_FOREGROUND ;
4
4
import static io .sentry .TypeCheckHint .ANDROID_ACTIVITY ;
5
5
6
+ import android .annotation .SuppressLint ;
6
7
import android .app .Activity ;
7
8
import android .app .ActivityManager ;
8
9
import android .app .Application ;
9
10
import android .content .Context ;
10
11
import android .os .Build ;
11
12
import android .os .Bundle ;
13
+ import android .os .Handler ;
14
+ import android .os .Looper ;
12
15
import android .os .Process ;
16
+ import android .view .View ;
17
+ import androidx .annotation .NonNull ;
13
18
import io .sentry .Breadcrumb ;
19
+ import io .sentry .DateUtils ;
14
20
import io .sentry .Hint ;
15
21
import io .sentry .IHub ;
16
22
import io .sentry .ISpan ;
23
29
import io .sentry .SpanStatus ;
24
30
import io .sentry .TransactionContext ;
25
31
import io .sentry .TransactionOptions ;
32
+ import io .sentry .android .core .internal .util .FirstDrawDoneListener ;
26
33
import io .sentry .protocol .TransactionNameSource ;
27
34
import io .sentry .util .Objects ;
28
35
import java .io .Closeable ;
@@ -43,8 +50,10 @@ public final class ActivityLifecycleIntegration
43
50
static final String UI_LOAD_OP = "ui.load" ;
44
51
static final String APP_START_WARM = "app.start.warm" ;
45
52
static final String APP_START_COLD = "app.start.cold" ;
53
+ static final String TTID_OP = "ui.load.initial_display" ;
46
54
47
55
private final @ NotNull Application application ;
56
+ private final @ NotNull BuildInfoProvider buildInfoProvider ;
48
57
private @ Nullable IHub hub ;
49
58
private @ Nullable SentryAndroidOptions options ;
50
59
@@ -57,6 +66,9 @@ public final class ActivityLifecycleIntegration
57
66
private boolean foregroundImportance = false ;
58
67
59
68
private @ Nullable ISpan appStartSpan ;
69
+ private final @ NotNull WeakHashMap <Activity , ISpan > ttidSpanMap = new WeakHashMap <>();
70
+ private @ NotNull Date lastPausedTime = DateUtils .getCurrentDateTime ();
71
+ private final @ NotNull Handler mainHandler = new Handler (Looper .getMainLooper ());
60
72
61
73
// WeakHashMap isn't thread safe but ActivityLifecycleCallbacks is only called from the
62
74
// main-thread
@@ -70,7 +82,8 @@ public ActivityLifecycleIntegration(
70
82
final @ NotNull BuildInfoProvider buildInfoProvider ,
71
83
final @ NotNull ActivityFramesTracker activityFramesTracker ) {
72
84
this .application = Objects .requireNonNull (application , "Application is required" );
73
- Objects .requireNonNull (buildInfoProvider , "BuildInfoProvider is required" );
85
+ this .buildInfoProvider =
86
+ Objects .requireNonNull (buildInfoProvider , "BuildInfoProvider is required" );
74
87
this .activityFramesTracker =
75
88
Objects .requireNonNull (activityFramesTracker , "ActivityFramesTracker is required" );
76
89
@@ -146,7 +159,8 @@ private void stopPreviousTransactions() {
146
159
for (final Map .Entry <Activity , ITransaction > entry :
147
160
activitiesWithOngoingTransactions .entrySet ()) {
148
161
final ITransaction transaction = entry .getValue ();
149
- finishTransaction (transaction );
162
+ final ISpan ttidSpan = ttidSpanMap .get (entry .getKey ());
163
+ finishTransaction (transaction , ttidSpan );
150
164
}
151
165
}
152
166
@@ -202,6 +216,18 @@ private void startTracing(final @NotNull Activity activity) {
202
216
getAppStartDesc (coldStart ),
203
217
appStartTime ,
204
218
Instrumenter .SENTRY );
219
+ // The first activity ttidSpan should start at the same time as the app start time
220
+ ttidSpanMap .put (
221
+ activity ,
222
+ transaction .startChild (
223
+ TTID_OP , getTtidDesc (activityName ), appStartTime , Instrumenter .SENTRY ));
224
+ } else {
225
+ // Other activities (or in case appStartTime is not available) the ttid span should
226
+ // start when the previous activity called its onPause method.
227
+ ttidSpanMap .put (
228
+ activity ,
229
+ transaction .startChild (
230
+ TTID_OP , getTtidDesc (activityName ), lastPausedTime , Instrumenter .SENTRY ));
205
231
}
206
232
207
233
// lets bind to the scope so other integrations can pick it up
@@ -250,18 +276,22 @@ private boolean isRunningTransaction(final @NotNull Activity activity) {
250
276
private void stopTracing (final @ NotNull Activity activity , final boolean shouldFinishTracing ) {
251
277
if (performanceEnabled && shouldFinishTracing ) {
252
278
final ITransaction transaction = activitiesWithOngoingTransactions .get (activity );
253
- finishTransaction (transaction );
279
+ finishTransaction (transaction , null );
254
280
}
255
281
}
256
282
257
- private void finishTransaction (final @ Nullable ITransaction transaction ) {
283
+ private void finishTransaction (
284
+ final @ Nullable ITransaction transaction , final @ Nullable ISpan ttidSpan ) {
258
285
if (transaction != null ) {
259
286
// if io.sentry.traces.activity.auto-finish.enable is disabled, transaction may be already
260
287
// finished manually when this method is called.
261
288
if (transaction .isFinished ()) {
262
289
return ;
263
290
}
264
291
292
+ // in case the ttidSpan isn't completed yet, we finish it as cancelled to avoid memory leak
293
+ finishSpan (ttidSpan , SpanStatus .CANCELLED );
294
+
265
295
SpanStatus status = transaction .getStatus ();
266
296
// status might be set by other integrations, let's not overwrite it
267
297
if (status == null ) {
@@ -301,6 +331,7 @@ public synchronized void onActivityStarted(final @NotNull Activity activity) {
301
331
addBreadcrumb (activity , "started" );
302
332
}
303
333
334
+ @ SuppressLint ("NewApi" )
304
335
@ Override
305
336
public synchronized void onActivityResumed (final @ NotNull Activity activity ) {
306
337
if (!firstActivityResumed ) {
@@ -326,6 +357,17 @@ public synchronized void onActivityResumed(final @NotNull Activity activity) {
326
357
firstActivityResumed = true ;
327
358
}
328
359
360
+ final ISpan ttidSpan = ttidSpanMap .get (activity );
361
+ final View rootView = activity .findViewById (android .R .id .content );
362
+ if (buildInfoProvider .getSdkInfoVersion () >= Build .VERSION_CODES .JELLY_BEAN
363
+ && rootView != null ) {
364
+ FirstDrawDoneListener .registerForNextDraw (
365
+ rootView , () -> finishSpan (ttidSpan ), buildInfoProvider );
366
+ } else {
367
+ // Posting a task to the main thread's handler will make it executed after it finished
368
+ // its current job. That is, right after the activity draws the layout.
369
+ mainHandler .post (() -> finishSpan (ttidSpan ));
370
+ }
329
371
addBreadcrumb (activity , "resumed" );
330
372
331
373
// fallback call for API < 29 compatibility, otherwise it happens on onActivityPostResumed
@@ -344,8 +386,20 @@ public synchronized void onActivityPostResumed(final @NotNull Activity activity)
344
386
}
345
387
}
346
388
389
+ @ Override
390
+ public void onActivityPrePaused (@ NonNull Activity activity ) {
391
+ // only executed if API >= 29 otherwise it happens on onActivityPaused
392
+ if (isAllActivityCallbacksAvailable ) {
393
+ lastPausedTime = DateUtils .getCurrentDateTime ();
394
+ }
395
+ }
396
+
347
397
@ Override
348
398
public synchronized void onActivityPaused (final @ NotNull Activity activity ) {
399
+ // only executed if API < 29 otherwise it happens on onActivityPrePaused
400
+ if (!isAllActivityCallbacksAvailable ) {
401
+ lastPausedTime = DateUtils .getCurrentDateTime ();
402
+ }
349
403
addBreadcrumb (activity , "paused" );
350
404
}
351
405
@@ -366,16 +420,19 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
366
420
367
421
// in case the appStartSpan isn't completed yet, we finish it as cancelled to avoid
368
422
// memory leak
369
- if (appStartSpan != null && !appStartSpan .isFinished ()) {
370
- appStartSpan .finish (SpanStatus .CANCELLED );
371
- }
423
+ finishSpan (appStartSpan , SpanStatus .CANCELLED );
424
+
425
+ // we finish the ttidSpan as cancelled in case it isn't completed yet
426
+ final ISpan ttidSpan = ttidSpanMap .get (activity );
427
+ finishSpan (ttidSpan , SpanStatus .CANCELLED );
372
428
373
429
// in case people opt-out enableActivityLifecycleTracingAutoFinish and forgot to finish it,
374
430
// we make sure to finish it when the activity gets destroyed.
375
431
stopTracing (activity , true );
376
432
377
433
// set it to null in case its been just finished as cancelled
378
434
appStartSpan = null ;
435
+ ttidSpanMap .remove (activity );
379
436
380
437
// clear it up, so we don't start again for the same activity if the activity is in the activity
381
438
// stack still.
@@ -385,6 +442,18 @@ public synchronized void onActivityDestroyed(final @NotNull Activity activity) {
385
442
}
386
443
}
387
444
445
+ private void finishSpan (@ Nullable ISpan span ) {
446
+ if (span != null && !span .isFinished ()) {
447
+ span .finish ();
448
+ }
449
+ }
450
+
451
+ private void finishSpan (@ Nullable ISpan span , @ NotNull SpanStatus status ) {
452
+ if (span != null && !span .isFinished ()) {
453
+ span .finish (status );
454
+ }
455
+ }
456
+
388
457
@ TestOnly
389
458
@ NotNull
390
459
WeakHashMap <Activity , ITransaction > getActivitiesWithOngoingTransactions () {
@@ -403,6 +472,12 @@ ISpan getAppStartSpan() {
403
472
return appStartSpan ;
404
473
}
405
474
475
+ @ TestOnly
476
+ @ NotNull
477
+ WeakHashMap <Activity , ISpan > getTtidSpanMap () {
478
+ return ttidSpanMap ;
479
+ }
480
+
406
481
private void setColdStart (final @ Nullable Bundle savedInstanceState ) {
407
482
if (!firstActivityCreated ) {
408
483
// if Activity has savedInstanceState then its a warm start
@@ -411,6 +486,10 @@ private void setColdStart(final @Nullable Bundle savedInstanceState) {
411
486
}
412
487
}
413
488
489
+ private @ NotNull String getTtidDesc (final @ NotNull String activityName ) {
490
+ return activityName + " initial display" ;
491
+ }
492
+
414
493
private @ NotNull String getAppStartDesc (final boolean coldStart ) {
415
494
if (coldStart ) {
416
495
return "Cold Start" ;
0 commit comments