1
1
package io .sentry .android .core .performance ;
2
2
3
+ import android .app .Activity ;
3
4
import android .app .Application ;
4
5
import android .content .ContentProvider ;
6
+ import android .os .Bundle ;
7
+ import android .os .Handler ;
8
+ import android .os .Looper ;
5
9
import android .os .SystemClock ;
10
+ import androidx .annotation .NonNull ;
6
11
import androidx .annotation .Nullable ;
12
+ import androidx .annotation .VisibleForTesting ;
7
13
import io .sentry .ITransactionProfiler ;
14
+ import io .sentry .SentryDate ;
15
+ import io .sentry .SentryNanotimeDate ;
8
16
import io .sentry .TracesSamplingDecision ;
9
17
import io .sentry .android .core .ContextUtils ;
10
18
import io .sentry .android .core .SentryAndroidOptions ;
13
21
import java .util .HashMap ;
14
22
import java .util .List ;
15
23
import java .util .Map ;
24
+ import java .util .concurrent .TimeUnit ;
16
25
import org .jetbrains .annotations .ApiStatus ;
17
26
import org .jetbrains .annotations .NotNull ;
18
27
import org .jetbrains .annotations .TestOnly ;
23
32
* transformed into SDK specific txn/span data structures.
24
33
*/
25
34
@ ApiStatus .Internal
26
- public class AppStartMetrics {
35
+ public class AppStartMetrics extends ActivityLifecycleCallbacksAdapter {
27
36
28
37
public enum AppStartType {
29
38
UNKNOWN ,
@@ -45,6 +54,8 @@ public enum AppStartType {
45
54
private final @ NotNull List <ActivityLifecycleTimeSpan > activityLifecycles ;
46
55
private @ Nullable ITransactionProfiler appStartProfiler = null ;
47
56
private @ Nullable TracesSamplingDecision appStartSamplingDecision = null ;
57
+ private @ Nullable SentryDate onCreateTime = null ;
58
+ private boolean appLaunchTooLong = false ;
48
59
49
60
public static @ NotNull AppStartMetrics getInstance () {
50
61
@@ -65,6 +76,7 @@ public AppStartMetrics() {
65
76
applicationOnCreate = new TimeSpan ();
66
77
contentProviderOnCreates = new HashMap <>();
67
78
activityLifecycles = new ArrayList <>();
79
+ appLaunchedInForeground = ContextUtils .isForegroundImportance ();
68
80
}
69
81
70
82
/**
@@ -102,6 +114,11 @@ public boolean isAppLaunchedInForeground() {
102
114
return appLaunchedInForeground ;
103
115
}
104
116
117
+ @ VisibleForTesting
118
+ public void setAppLaunchedInForeground (final boolean appLaunchedInForeground ) {
119
+ this .appLaunchedInForeground = appLaunchedInForeground ;
120
+ }
121
+
105
122
/**
106
123
* Provides all collected content provider onCreate time spans
107
124
*
@@ -137,12 +154,20 @@ public long getClassLoadedUptimeMs() {
137
154
// Only started when sdk version is >= N
138
155
final @ NotNull TimeSpan appStartSpan = getAppStartTimeSpan ();
139
156
if (appStartSpan .hasStarted ()) {
140
- return appStartSpan ;
157
+ return validateAppStartSpan ( appStartSpan ) ;
141
158
}
142
159
}
143
160
144
161
// 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 ;
146
171
}
147
172
148
173
@ TestOnly
@@ -158,6 +183,9 @@ public void clear() {
158
183
}
159
184
appStartProfiler = null ;
160
185
appStartSamplingDecision = null ;
186
+ appLaunchTooLong = false ;
187
+ appLaunchedInForeground = false ;
188
+ onCreateTime = null ;
161
189
}
162
190
163
191
public @ Nullable ITransactionProfiler getAppStartProfiler () {
@@ -195,7 +223,46 @@ public static void onApplicationCreate(final @NotNull Application application) {
195
223
final @ NotNull AppStartMetrics instance = getInstance ();
196
224
if (instance .applicationOnCreate .hasNotStarted ()) {
197
225
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 ;
199
266
}
200
267
}
201
268
0 commit comments