11package io .sentry .android .core .performance ;
22
33import android .app .Activity ;
4+ import android .app .ActivityManager ;
45import android .app .Application ;
6+ import android .app .ApplicationStartInfo ;
57import android .content .ContentProvider ;
8+ import android .content .Context ;
9+ import android .os .Build ;
610import android .os .Bundle ;
711import android .os .Handler ;
812import android .os .Looper ;
13+ import android .os .MessageQueue ;
914import android .os .SystemClock ;
1015import androidx .annotation .NonNull ;
1116import androidx .annotation .Nullable ;
2126import io .sentry .android .core .SentryAndroidOptions ;
2227import io .sentry .android .core .internal .util .FirstDrawDoneListener ;
2328import io .sentry .util .AutoClosableReentrantLock ;
29+ import io .sentry .util .LazyEvaluator ;
2430import java .util .ArrayList ;
2531import java .util .Collections ;
2632import java .util .HashMap ;
@@ -56,7 +62,15 @@ public enum AppStartType {
5662 new AutoClosableReentrantLock ();
5763
5864 private @ NotNull AppStartType appStartType = AppStartType .UNKNOWN ;
59- private boolean appLaunchedInForeground ;
65+ private final LazyEvaluator <Boolean > appLaunchedInForeground =
66+ new LazyEvaluator <>(
67+ new LazyEvaluator .Evaluator <Boolean >() {
68+ @ Override
69+ public @ NotNull Boolean evaluate () {
70+ return ContextUtils .isForegroundImportance ();
71+ }
72+ });
73+ private volatile long firstIdle = -1 ;
6074
6175 private final @ NotNull TimeSpan appStartSpan ;
6276 private final @ NotNull TimeSpan sdkInitTimeSpan ;
@@ -89,7 +103,6 @@ public AppStartMetrics() {
89103 applicationOnCreate = new TimeSpan ();
90104 contentProviderOnCreates = new HashMap <>();
91105 activityLifecycles = new ArrayList <>();
92- appLaunchedInForeground = ContextUtils .isForegroundImportance ();
93106 }
94107
95108 /**
@@ -140,12 +153,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
140153 }
141154
142155 public boolean isAppLaunchedInForeground () {
143- return appLaunchedInForeground ;
156+ return appLaunchedInForeground . getValue () ;
144157 }
145158
146159 @ VisibleForTesting
147160 public void setAppLaunchedInForeground (final boolean appLaunchedInForeground ) {
148- this .appLaunchedInForeground = appLaunchedInForeground ;
161+ this .appLaunchedInForeground . setValue ( appLaunchedInForeground ) ;
149162 }
150163
151164 /**
@@ -176,7 +189,7 @@ public void onAppStartSpansSent() {
176189 }
177190
178191 public boolean shouldSendStartMeasurements () {
179- return shouldSendStartMeasurements && appLaunchedInForeground ;
192+ return shouldSendStartMeasurements && appLaunchedInForeground . getValue () ;
180193 }
181194
182195 public long getClassLoadedUptimeMs () {
@@ -191,7 +204,7 @@ public long getClassLoadedUptimeMs() {
191204 final @ NotNull SentryAndroidOptions options ) {
192205 // If the app start type was never determined or app wasn't launched in foreground,
193206 // the app start is considered invalid
194- if (appStartType != AppStartType .UNKNOWN && appLaunchedInForeground ) {
207+ if (appStartType != AppStartType .UNKNOWN && appLaunchedInForeground . getValue () ) {
195208 if (options .isEnablePerformanceV2 ()) {
196209 // Only started when sdk version is >= N
197210 final @ NotNull TimeSpan appStartSpan = getAppStartTimeSpan ();
@@ -212,6 +225,16 @@ public long getClassLoadedUptimeMs() {
212225 return new TimeSpan ();
213226 }
214227
228+ @ TestOnly
229+ void setFirstIdle (final long firstIdle ) {
230+ this .firstIdle = firstIdle ;
231+ }
232+
233+ @ TestOnly
234+ long getFirstIdle () {
235+ return firstIdle ;
236+ }
237+
215238 @ TestOnly
216239 public void clear () {
217240 appStartType = AppStartType .UNKNOWN ;
@@ -229,11 +252,12 @@ public void clear() {
229252 }
230253 appStartContinuousProfiler = null ;
231254 appStartSamplingDecision = null ;
232- appLaunchedInForeground = false ;
255+ appLaunchedInForeground . resetValue () ;
233256 isCallbackRegistered = false ;
234257 shouldSendStartMeasurements = true ;
235258 firstDrawDone .set (false );
236259 activeActivitiesCounter .set (0 );
260+ firstIdle = -1 ;
237261 }
238262
239263 public @ Nullable ITransactionProfiler getAppStartProfiler () {
@@ -301,7 +325,8 @@ public static void onApplicationPostCreate(final @NotNull Application applicatio
301325 }
302326
303327 /**
304- * Register a callback to check if an activity was started after the application was created
328+ * Register a callback to check if an activity was started after the application was created. Must
329+ * be called from the main thread.
305330 *
306331 * @param application The application object to register the callback to
307332 */
@@ -310,61 +335,106 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
310335 return ;
311336 }
312337 isCallbackRegistered = true ;
313- appLaunchedInForeground = appLaunchedInForeground || ContextUtils . isForegroundImportance ();
338+ appLaunchedInForeground . resetValue ();
314339 application .registerActivityLifecycleCallbacks (instance );
315- // We post on the main thread a task to post a check on the main thread. On Pixel devices
316- // (possibly others) the first task posted on the main thread is called before the
317- // Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
318- // callback is called before the application one.
319- new Handler (Looper .getMainLooper ()).post (() -> checkCreateTimeOnMain ());
340+
341+ final @ Nullable ActivityManager activityManager =
342+ (ActivityManager ) application .getSystemService (Context .ACTIVITY_SERVICE );
343+
344+ if (activityManager != null && Build .VERSION .SDK_INT >= Build .VERSION_CODES .VANILLA_ICE_CREAM ) {
345+ final List <ApplicationStartInfo > historicalProcessStartReasons =
346+ activityManager .getHistoricalProcessStartReasons (1 );
347+ if (!historicalProcessStartReasons .isEmpty ()) {
348+ final @ NotNull ApplicationStartInfo info = historicalProcessStartReasons .get (0 );
349+ if (info .getStartupState () == ApplicationStartInfo .STARTUP_STATE_STARTED ) {
350+ if (info .getStartType () == ApplicationStartInfo .START_TYPE_COLD ) {
351+ appStartType = AppStartType .COLD ;
352+ } else {
353+ appStartType = AppStartType .WARM ;
354+ }
355+ }
356+ }
357+ }
358+
359+ if (appStartType == AppStartType .UNKNOWN && Build .VERSION .SDK_INT >= Build .VERSION_CODES .M ) {
360+ Looper .getMainLooper ()
361+ .getQueue ()
362+ .addIdleHandler (
363+ new MessageQueue .IdleHandler () {
364+ @ Override
365+ public boolean queueIdle () {
366+ firstIdle = SystemClock .uptimeMillis ();
367+ checkCreateTimeOnMain ();
368+ return false ;
369+ }
370+ });
371+ } else if (appStartType == AppStartType .UNKNOWN ) {
372+ // We post on the main thread a task to post a check on the main thread. On Pixel devices
373+ // (possibly others) the first task posted on the main thread is called before the
374+ // Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
375+ // callback is called before the application one.
376+ final Handler handler = new Handler (Looper .getMainLooper ());
377+ handler .post (
378+ new Runnable () {
379+ @ Override
380+ public void run () {
381+ // not technically correct, but close enough for pre-M
382+ firstIdle = SystemClock .uptimeMillis ();
383+ handler .post (() -> checkCreateTimeOnMain ());
384+ }
385+ });
386+ }
320387 }
321388
322389 private void checkCreateTimeOnMain () {
323- new Handler (Looper .getMainLooper ())
324- .post (
325- () -> {
326- // if no activity has ever been created, app was launched in background
327- if (activeActivitiesCounter .get () == 0 ) {
328- appLaunchedInForeground = false ;
329-
330- // we stop the app start profilers, as they are useless and likely to timeout
331- if (appStartProfiler != null && appStartProfiler .isRunning ()) {
332- appStartProfiler .close ();
333- appStartProfiler = null ;
334- }
335- if (appStartContinuousProfiler != null && appStartContinuousProfiler .isRunning ()) {
336- appStartContinuousProfiler .close (true );
337- appStartContinuousProfiler = null ;
338- }
339- }
340- });
390+ // if no activity has ever been created, app was launched in background
391+ if (activeActivitiesCounter .get () == 0 ) {
392+ appLaunchedInForeground .setValue (false );
393+
394+ // we stop the app start profilers, as they are useless and likely to timeout
395+ if (appStartProfiler != null && appStartProfiler .isRunning ()) {
396+ appStartProfiler .close ();
397+ appStartProfiler = null ;
398+ }
399+ if (appStartContinuousProfiler != null && appStartContinuousProfiler .isRunning ()) {
400+ appStartContinuousProfiler .close (true );
401+ appStartContinuousProfiler = null ;
402+ }
403+ }
341404 }
342405
343406 @ Override
344407 public void onActivityCreated (@ NonNull Activity activity , @ Nullable Bundle savedInstanceState ) {
408+ final long activityCreatedUptimeMillis = SystemClock .uptimeMillis ();
345409 CurrentActivityHolder .getInstance ().setActivity (activity );
346410
347411 // the first activity determines the app start type
348412 if (activeActivitiesCounter .incrementAndGet () == 1 && !firstDrawDone .get ()) {
349413 final long nowUptimeMs = SystemClock .uptimeMillis ();
350414
351- // If the app (process) was launched more than 1 minute ago, it's likely wrong
415+ // If the app (process) was launched more than 1 minute ago, consider it a warm start
352416 final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan .getStartUptimeMs ();
353- if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit .MINUTES .toMillis (1 )) {
417+ if (!appLaunchedInForeground .getValue ()
418+ || durationSinceAppStartMillis > TimeUnit .MINUTES .toMillis (1 )) {
354419 appStartType = AppStartType .WARM ;
355-
356420 shouldSendStartMeasurements = true ;
357421 appStartSpan .reset ();
358- appStartSpan .start ();
359- appStartSpan .setStartedAt (nowUptimeMs );
360- CLASS_LOADED_UPTIME_MS = nowUptimeMs ;
422+ appStartSpan .setStartedAt (activityCreatedUptimeMillis );
423+ CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis ;
361424 contentProviderOnCreates .clear ();
362425 applicationOnCreate .reset ();
363- } else {
364- appStartType = savedInstanceState == null ? AppStartType .COLD : AppStartType .WARM ;
426+ } else if (appStartType == AppStartType .UNKNOWN ) {
427+ // pre API 35 handling
428+ if (savedInstanceState != null ) {
429+ appStartType = AppStartType .WARM ;
430+ } else if (firstIdle != -1 && activityCreatedUptimeMillis > firstIdle ) {
431+ appStartType = AppStartType .WARM ;
432+ } else {
433+ appStartType = AppStartType .COLD ;
434+ }
365435 }
366436 }
367- appLaunchedInForeground = true ;
437+ appLaunchedInForeground . setValue ( true ) ;
368438 }
369439
370440 @ Override
@@ -403,9 +473,10 @@ public void onActivityDestroyed(@NonNull Activity activity) {
403473
404474 final int remainingActivities = activeActivitiesCounter .decrementAndGet ();
405475 // if the app is moving into background
406- // as the next Activity is considered like a new app start
476+ // as the next onActivityCreated will treat it as a new warm app start
407477 if (remainingActivities == 0 && !activity .isChangingConfigurations ()) {
408- appLaunchedInForeground = false ;
478+ appStartType = AppStartType .WARM ;
479+ appLaunchedInForeground .setValue (true );
409480 shouldSendStartMeasurements = true ;
410481 firstDrawDone .set (false );
411482 }
0 commit comments