2121import io .sentry .android .core .SentryAndroidOptions ;
2222import io .sentry .android .core .internal .util .FirstDrawDoneListener ;
2323import io .sentry .util .AutoClosableReentrantLock ;
24+ import io .sentry .util .LazyEvaluator ;
2425import java .util .ArrayList ;
2526import java .util .Collections ;
2627import java .util .HashMap ;
@@ -56,7 +57,15 @@ public enum AppStartType {
5657 new AutoClosableReentrantLock ();
5758
5859 private @ NotNull AppStartType appStartType = AppStartType .UNKNOWN ;
59- private boolean appLaunchedInForeground ;
60+ private final LazyEvaluator <Boolean > appLaunchedInForeground =
61+ new LazyEvaluator <>(
62+ new LazyEvaluator .Evaluator <Boolean >() {
63+ @ Override
64+ public @ NotNull Boolean evaluate () {
65+ return ContextUtils .isForegroundImportance ();
66+ }
67+ });
68+ private volatile long firstPostUptimeMillis = -1 ;
6069
6170 private final @ NotNull TimeSpan appStartSpan ;
6271 private final @ NotNull TimeSpan sdkInitTimeSpan ;
@@ -89,7 +98,6 @@ public AppStartMetrics() {
8998 applicationOnCreate = new TimeSpan ();
9099 contentProviderOnCreates = new HashMap <>();
91100 activityLifecycles = new ArrayList <>();
92- appLaunchedInForeground = ContextUtils .isForegroundImportance ();
93101 }
94102
95103 /**
@@ -140,12 +148,12 @@ public void setAppStartType(final @NotNull AppStartType appStartType) {
140148 }
141149
142150 public boolean isAppLaunchedInForeground () {
143- return appLaunchedInForeground ;
151+ return appLaunchedInForeground . getValue () ;
144152 }
145153
146154 @ VisibleForTesting
147155 public void setAppLaunchedInForeground (final boolean appLaunchedInForeground ) {
148- this .appLaunchedInForeground = appLaunchedInForeground ;
156+ this .appLaunchedInForeground . setValue ( appLaunchedInForeground ) ;
149157 }
150158
151159 /**
@@ -176,7 +184,7 @@ public void onAppStartSpansSent() {
176184 }
177185
178186 public boolean shouldSendStartMeasurements () {
179- return shouldSendStartMeasurements && appLaunchedInForeground ;
187+ return shouldSendStartMeasurements && appLaunchedInForeground . getValue () ;
180188 }
181189
182190 public long getClassLoadedUptimeMs () {
@@ -191,7 +199,7 @@ public long getClassLoadedUptimeMs() {
191199 final @ NotNull SentryAndroidOptions options ) {
192200 // If the app start type was never determined or app wasn't launched in foreground,
193201 // the app start is considered invalid
194- if (appStartType != AppStartType .UNKNOWN && appLaunchedInForeground ) {
202+ if (appStartType != AppStartType .UNKNOWN && appLaunchedInForeground . getValue () ) {
195203 if (options .isEnablePerformanceV2 ()) {
196204 // Only started when sdk version is >= N
197205 final @ NotNull TimeSpan appStartSpan = getAppStartTimeSpan ();
@@ -212,6 +220,16 @@ public long getClassLoadedUptimeMs() {
212220 return new TimeSpan ();
213221 }
214222
223+ @ TestOnly
224+ void setFirstPostUptimeMillis (final long firstPostUptimeMillis ) {
225+ this .firstPostUptimeMillis = firstPostUptimeMillis ;
226+ }
227+
228+ @ TestOnly
229+ long getFirstPostUptimeMillis () {
230+ return firstPostUptimeMillis ;
231+ }
232+
215233 @ TestOnly
216234 public void clear () {
217235 appStartType = AppStartType .UNKNOWN ;
@@ -229,11 +247,12 @@ public void clear() {
229247 }
230248 appStartContinuousProfiler = null ;
231249 appStartSamplingDecision = null ;
232- appLaunchedInForeground = false ;
250+ appLaunchedInForeground . setValue ( false ) ;
233251 isCallbackRegistered = false ;
234252 shouldSendStartMeasurements = true ;
235253 firstDrawDone .set (false );
236254 activeActivitiesCounter .set (0 );
255+ firstPostUptimeMillis = -1 ;
237256 }
238257
239258 public @ Nullable ITransactionProfiler getAppStartProfiler () {
@@ -310,13 +329,21 @@ public void registerLifecycleCallbacks(final @NotNull Application application) {
310329 return ;
311330 }
312331 isCallbackRegistered = true ;
313- appLaunchedInForeground = appLaunchedInForeground || ContextUtils . isForegroundImportance ();
332+ appLaunchedInForeground . resetValue ();
314333 application .registerActivityLifecycleCallbacks (instance );
315334 // We post on the main thread a task to post a check on the main thread. On Pixel devices
316335 // (possibly others) the first task posted on the main thread is called before the
317336 // Activity.onCreate callback. This is a workaround for that, so that the Activity.onCreate
318337 // callback is called before the application one.
319- new Handler (Looper .getMainLooper ()).post (() -> checkCreateTimeOnMain ());
338+ new Handler (Looper .getMainLooper ())
339+ .post (
340+ new Runnable () {
341+ @ Override
342+ public void run () {
343+ firstPostUptimeMillis = SystemClock .uptimeMillis ();
344+ checkCreateTimeOnMain ();
345+ }
346+ });
320347 }
321348
322349 private void checkCreateTimeOnMain () {
@@ -325,7 +352,7 @@ private void checkCreateTimeOnMain() {
325352 () -> {
326353 // if no activity has ever been created, app was launched in background
327354 if (activeActivitiesCounter .get () == 0 ) {
328- appLaunchedInForeground = false ;
355+ appLaunchedInForeground . setValue ( false ) ;
329356
330357 // we stop the app start profilers, as they are useless and likely to timeout
331358 if (appStartProfiler != null && appStartProfiler .isRunning ()) {
@@ -342,29 +369,36 @@ private void checkCreateTimeOnMain() {
342369
343370 @ Override
344371 public void onActivityCreated (@ NonNull Activity activity , @ Nullable Bundle savedInstanceState ) {
372+ final long activityCreatedUptimeMillis = SystemClock .uptimeMillis ();
345373 CurrentActivityHolder .getInstance ().setActivity (activity );
346374
347375 // the first activity determines the app start type
348376 if (activeActivitiesCounter .incrementAndGet () == 1 && !firstDrawDone .get ()) {
349377 final long nowUptimeMs = SystemClock .uptimeMillis ();
350378
351- // If the app (process) was launched more than 1 minute ago, it's likely wrong
379+ // If the app (process) was launched more than 1 minute ago, consider it a warm start
352380 final long durationSinceAppStartMillis = nowUptimeMs - appStartSpan .getStartUptimeMs ();
353- if (!appLaunchedInForeground || durationSinceAppStartMillis > TimeUnit .MINUTES .toMillis (1 )) {
381+ if (!appLaunchedInForeground .getValue ()
382+ || durationSinceAppStartMillis > TimeUnit .MINUTES .toMillis (1 )) {
354383 appStartType = AppStartType .WARM ;
355-
356384 shouldSendStartMeasurements = true ;
357385 appStartSpan .reset ();
358- appStartSpan .start ();
359- appStartSpan .setStartedAt (nowUptimeMs );
360- CLASS_LOADED_UPTIME_MS = nowUptimeMs ;
386+ appStartSpan .setStartedAt (activityCreatedUptimeMillis );
387+ CLASS_LOADED_UPTIME_MS = activityCreatedUptimeMillis ;
361388 contentProviderOnCreates .clear ();
362389 applicationOnCreate .reset ();
390+ } else if (savedInstanceState != null ) {
391+ appStartType = AppStartType .WARM ;
392+ } else if (firstPostUptimeMillis != -1
393+ && activityCreatedUptimeMillis > firstPostUptimeMillis ) {
394+ // Application creation always queues Activity creation
395+ // So if Activity is created after our first measured post, it's a warm start
396+ appStartType = AppStartType .WARM ;
363397 } else {
364- appStartType = savedInstanceState == null ? AppStartType .COLD : AppStartType . WARM ;
398+ appStartType = AppStartType .COLD ;
365399 }
366400 }
367- appLaunchedInForeground = true ;
401+ appLaunchedInForeground . setValue ( true ) ;
368402 }
369403
370404 @ Override
@@ -403,9 +437,9 @@ public void onActivityDestroyed(@NonNull Activity activity) {
403437
404438 final int remainingActivities = activeActivitiesCounter .decrementAndGet ();
405439 // if the app is moving into background
406- // as the next Activity is considered like a new app start
440+ // as the next onActivityCreated will treat it as a new warm app start
407441 if (remainingActivities == 0 && !activity .isChangingConfigurations ()) {
408- appLaunchedInForeground = false ;
442+ appLaunchedInForeground . setValue ( true ) ;
409443 shouldSendStartMeasurements = true ;
410444 firstDrawDone .set (false );
411445 }
0 commit comments