11package io .sentry .android .core .performance ;
22
3+ import android .app .Activity ;
34import android .app .Application ;
45import android .content .ContentProvider ;
6+ import android .os .Bundle ;
7+ import android .os .Handler ;
8+ import android .os .Looper ;
59import android .os .SystemClock ;
10+ import androidx .annotation .NonNull ;
611import androidx .annotation .Nullable ;
12+ import androidx .annotation .VisibleForTesting ;
713import io .sentry .ITransactionProfiler ;
14+ import io .sentry .SentryDate ;
15+ import io .sentry .SentryNanotimeDate ;
816import io .sentry .TracesSamplingDecision ;
917import io .sentry .android .core .ContextUtils ;
1018import io .sentry .android .core .SentryAndroidOptions ;
1321import java .util .HashMap ;
1422import java .util .List ;
1523import java .util .Map ;
24+ import java .util .concurrent .TimeUnit ;
1625import org .jetbrains .annotations .ApiStatus ;
1726import org .jetbrains .annotations .NotNull ;
1827import org .jetbrains .annotations .TestOnly ;
2332 * transformed into SDK specific txn/span data structures.
2433 */
2534@ ApiStatus .Internal
26- public class AppStartMetrics {
35+ public class AppStartMetrics extends ActivityLifecycleCallbacksAdapter {
2736
2837 public enum AppStartType {
2938 UNKNOWN ,
@@ -45,6 +54,8 @@ public enum AppStartType {
4554 private final @ NotNull List <ActivityLifecycleTimeSpan > activityLifecycles ;
4655 private @ Nullable ITransactionProfiler appStartProfiler = null ;
4756 private @ Nullable TracesSamplingDecision appStartSamplingDecision = null ;
57+ private @ Nullable SentryDate onCreateTime = null ;
58+ private boolean appLaunchTooLong = false ;
4859
4960 public static @ NotNull AppStartMetrics getInstance () {
5061
@@ -65,6 +76,7 @@ public AppStartMetrics() {
6576 applicationOnCreate = new TimeSpan ();
6677 contentProviderOnCreates = new HashMap <>();
6778 activityLifecycles = new ArrayList <>();
79+ appLaunchedInForeground = ContextUtils .isForegroundImportance ();
6880 }
6981
7082 /**
@@ -102,6 +114,11 @@ public boolean isAppLaunchedInForeground() {
102114 return appLaunchedInForeground ;
103115 }
104116
117+ @ VisibleForTesting
118+ public void setAppLaunchedInForeground (final boolean appLaunchedInForeground ) {
119+ this .appLaunchedInForeground = appLaunchedInForeground ;
120+ }
121+
105122 /**
106123 * Provides all collected content provider onCreate time spans
107124 *
@@ -137,12 +154,20 @@ public long getClassLoadedUptimeMs() {
137154 // Only started when sdk version is >= N
138155 final @ NotNull TimeSpan appStartSpan = getAppStartTimeSpan ();
139156 if (appStartSpan .hasStarted ()) {
140- return appStartSpan ;
157+ return validateAppStartSpan ( appStartSpan ) ;
141158 }
142159 }
143160
144161 // fallback: use sdk init time span, as it will always have a start time set
145- return getSdkInitTimeSpan ();
162+ return validateAppStartSpan (getSdkInitTimeSpan ());
163+ }
164+
165+ private @ NotNull TimeSpan validateAppStartSpan (final @ NotNull TimeSpan appStartSpan ) {
166+ // If the app launch took too long or it was launched in the background we return an empty span
167+ if (appLaunchTooLong || !appLaunchedInForeground ) {
168+ return new TimeSpan ();
169+ }
170+ return appStartSpan ;
146171 }
147172
148173 @ TestOnly
@@ -158,6 +183,9 @@ public void clear() {
158183 }
159184 appStartProfiler = null ;
160185 appStartSamplingDecision = null ;
186+ appLaunchTooLong = false ;
187+ appLaunchedInForeground = false ;
188+ onCreateTime = null ;
161189 }
162190
163191 public @ Nullable ITransactionProfiler getAppStartProfiler () {
@@ -195,7 +223,37 @@ public static void onApplicationCreate(final @NotNull Application application) {
195223 final @ NotNull AppStartMetrics instance = getInstance ();
196224 if (instance .applicationOnCreate .hasNotStarted ()) {
197225 instance .applicationOnCreate .setStartedAt (now );
198- instance .appLaunchedInForeground = ContextUtils .isForegroundImportance ();
226+ application .registerActivityLifecycleCallbacks (instance );
227+ instance .appLaunchedInForeground =
228+ instance .appLaunchedInForeground || ContextUtils .isForegroundImportance ();
229+ new Handler (Looper .getMainLooper ())
230+ .post (
231+ () -> {
232+ // if no activity has ever been created, app was launched in background
233+ if (instance .onCreateTime == null ) {
234+ instance .appLaunchedInForeground = false ;
235+ }
236+ });
237+ }
238+ }
239+
240+ @ Override
241+ public void onActivityCreated (@ NonNull Activity activity , @ Nullable Bundle savedInstanceState ) {
242+ // An activity already called onCreate()
243+ if (!appLaunchedInForeground || onCreateTime != null ) {
244+ return ;
245+ }
246+ onCreateTime = new SentryNanotimeDate ();
247+
248+ final long spanStartMillis = appStartSpan .getStartTimestampMs ();
249+ final long spanEndMillis =
250+ appStartSpan .hasStopped ()
251+ ? appStartSpan .getProjectedStopTimestampMs ()
252+ : System .currentTimeMillis ();
253+ final long durationMillis = spanEndMillis - spanStartMillis ;
254+ // If the app was launched more than 1 minute ago, it's likely wrong
255+ if (durationMillis > TimeUnit .MINUTES .toMillis (1 )) {
256+ appLaunchTooLong = true ;
199257 }
200258 }
201259
0 commit comments