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,46 @@ 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+ instance .appLaunchedInForeground =
227+ instance .appLaunchedInForeground || ContextUtils .isForegroundImportance ();
228+ instance .registerApplicationForegroundCheck (application );
229+ }
230+ }
231+
232+ /**
233+ * Register a callback to check if an activity was started after the application was created
234+ *
235+ * @param application The application object to register the callback to
236+ */
237+ public void registerApplicationForegroundCheck (final @ NotNull Application application ) {
238+ application .registerActivityLifecycleCallbacks (instance );
239+ new Handler (Looper .getMainLooper ())
240+ .post (
241+ () -> {
242+ // if no activity has ever been created, app was launched in background
243+ if (onCreateTime == null ) {
244+ appLaunchedInForeground = false ;
245+ }
246+ });
247+ }
248+
249+ @ Override
250+ public void onActivityCreated (@ NonNull Activity activity , @ Nullable Bundle savedInstanceState ) {
251+ // An activity already called onCreate()
252+ if (!appLaunchedInForeground || onCreateTime != null ) {
253+ return ;
254+ }
255+ onCreateTime = new SentryNanotimeDate ();
256+
257+ final long spanStartMillis = appStartSpan .getStartTimestampMs ();
258+ final long spanEndMillis =
259+ appStartSpan .hasStopped ()
260+ ? appStartSpan .getProjectedStopTimestampMs ()
261+ : System .currentTimeMillis ();
262+ final long durationMillis = spanEndMillis - spanStartMillis ;
263+ // If the app was launched more than 1 minute ago, it's likely wrong
264+ if (durationMillis > TimeUnit .MINUTES .toMillis (1 )) {
265+ appLaunchTooLong = true ;
199266 }
200267 }
201268
0 commit comments