diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java index 93e634e154d..6a50b665773 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/DefaultAndroidEventProcessor.java @@ -93,7 +93,7 @@ public DefaultAndroidEventProcessor( this.options = Objects.requireNonNull(options, "The options object is required."); ExecutorService executorService = Executors.newSingleThreadExecutor(); - // dont ref. to method reference, theres a bug on it + // don't ref. to method reference, theres a bug on it //noinspection Convert2MethodRef contextData = executorService.submit(() -> loadContextData()); @@ -128,8 +128,8 @@ public DefaultAndroidEventProcessor( // we only set memory data if it's not a hard crash, when it's a hard crash the event is // enriched on restart, so non static data might be wrong, eg lowMemory or availMem will // be different if the App. crashes because of OOM. - processNonCachedEvent(event); - setThreads(event); + processNonCachedEvent(event, hint); + setThreads(event, hint); } setCommons(event, true, applyScopeData); @@ -201,23 +201,34 @@ private void mergeOS(final @NotNull SentryBaseEvent event) { } // Data to be applied to events that was created in the running process - private void processNonCachedEvent(final @NotNull SentryBaseEvent event) { + private void processNonCachedEvent( + final @NotNull SentryBaseEvent event, final @NotNull Hint hint) { App app = event.getContexts().getApp(); if (app == null) { app = new App(); } - setAppExtras(app); + setAppExtras(app, hint); setPackageInfo(event, app); event.getContexts().setApp(app); } - private void setThreads(final @NotNull SentryEvent event) { + private void setThreads(final @NotNull SentryEvent event, final @NotNull Hint hint) { if (event.getThreads() != null) { - for (SentryThread thread : event.getThreads()) { + final boolean isHybridSDK = HintUtils.isFromHybridSdk(hint); + + for (final SentryThread thread : event.getThreads()) { + final boolean isMainThread = AndroidMainThreadChecker.getInstance().isMainThread(thread); + + // TODO: Fix https://github.com/getsentry/team-mobile/issues/47 if (thread.isCurrent() == null) { - thread.setCurrent(AndroidMainThreadChecker.getInstance().isMainThread(thread)); + thread.setCurrent(isMainThread); + } + + // This should not be set by Hybrid SDKs since they have their own threading model + if (!isHybridSDK && thread.isMain() == null) { + thread.setMain(isMainThread); } } } @@ -241,9 +252,19 @@ private void setDist(final @NotNull SentryBaseEvent event, final @NotNull String } } - private void setAppExtras(final @NotNull App app) { + private void setAppExtras(final @NotNull App app, final @NotNull Hint hint) { app.setAppName(getApplicationName()); app.setAppStartTime(DateUtils.toUtilDate(AppStartState.getInstance().getAppStartTime())); + + // This should not be set by Hybrid SDKs since they have their own app's lifecycle + if (!HintUtils.isFromHybridSdk(hint) && app.getInForeground() == null) { + // This feature depends on the AppLifecycleIntegration being installed, so only if + // enableAutoSessionTracking or enableAppLifecycleBreadcrumbs are enabled. + final @Nullable Boolean isBackground = AppState.getInstance().isInBackground(); + if (isBackground != null) { + app.setInForeground(!isBackground); + } + } } @SuppressWarnings("deprecation") @@ -256,21 +277,21 @@ private void setAppExtras(final @NotNull App app) { return Build.CPU_ABI2; } - @SuppressWarnings({"ObsoleteSdkInt", "deprecation"}) + @SuppressWarnings({"ObsoleteSdkInt", "deprecation", "NewApi"}) private void setArchitectures(final @NotNull Device device) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP) { - String[] supportedAbis = Build.SUPPORTED_ABIS; - device.setArchs(supportedAbis); + final String[] supportedAbis; + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.LOLLIPOP) { + supportedAbis = Build.SUPPORTED_ABIS; } else { - String[] supportedAbis = {getAbi(), getAbi2()}; - device.setArchs(supportedAbis); + supportedAbis = new String[] {getAbi(), getAbi2()}; // we were not checking CPU_ABI2, but I've added to the list now } + device.setArchs(supportedAbis); } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private @NotNull Long getMemorySize(final @NotNull ActivityManager.MemoryInfo memInfo) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN) { return memInfo.totalMem; } // using Runtime as a fallback @@ -393,17 +414,18 @@ private void setDeviceIO(final @NotNull Device device, final boolean applyScopeD } } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private @Nullable String getDeviceName() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR1) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR1) { return Settings.Global.getString(context.getContentResolver(), "device_name"); } else { return null; } } + @SuppressWarnings("NewApi") private TimeZone getTimeZone() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.N) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.N) { LocaleList locales = context.getResources().getConfiguration().getLocales(); if (!locales.isEmpty()) { Locale locale = locales.get(0); @@ -557,9 +579,9 @@ private TimeZone getTimeZone() { } } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private long getBlockSizeLong(final @NotNull StatFs stat) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { return stat.getBlockSizeLong(); } return getBlockSizeDep(stat); @@ -570,9 +592,9 @@ private int getBlockSizeDep(final @NotNull StatFs stat) { return stat.getBlockSize(); } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private long getBlockCountLong(final @NotNull StatFs stat) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { return stat.getBlockCountLong(); } return getBlockCountDep(stat); @@ -583,9 +605,9 @@ private int getBlockCountDep(final @NotNull StatFs stat) { return stat.getBlockCount(); } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private long getAvailableBlocksLong(final @NotNull StatFs stat) { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN_MR2) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.JELLY_BEAN_MR2) { return stat.getAvailableBlocksLong(); } return getAvailableBlocksDep(stat); @@ -627,9 +649,9 @@ private int getAvailableBlocksDep(final @NotNull StatFs stat) { return null; } - @SuppressWarnings("ObsoleteSdkInt") + @SuppressWarnings({"ObsoleteSdkInt", "NewApi"}) private @Nullable File[] getExternalFilesDirs() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.KITKAT) { + if (buildInfoProvider.getSdkInfoVersion() >= Build.VERSION_CODES.KITKAT) { return context.getExternalFilesDirs(null); } else { File single = context.getExternalFilesDir(null); @@ -907,7 +929,7 @@ private void setSideLoadedInfo(final @NotNull SentryBaseEvent event) { final boolean applyScopeData = shouldApplyScopeData(transaction, hint); if (applyScopeData) { - processNonCachedEvent(transaction); + processNonCachedEvent(transaction, hint); } setCommons(transaction, false, applyScopeData); diff --git a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java index 6229a0798cd..97893935803 100644 --- a/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java +++ b/sentry-android-core/src/main/java/io/sentry/android/core/LifecycleWatcher.java @@ -67,6 +67,9 @@ final class LifecycleWatcher implements DefaultLifecycleObserver { public void onStart(final @NotNull LifecycleOwner owner) { startSession(); addAppBreadcrumb("foreground"); + + // Consider using owner.getLifecycle().getCurrentState().isAtLeast(Lifecycle.State.RESUMED); + // in the future. AppState.getInstance().setInBackground(false); } diff --git a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt index 7a8ef414980..ef6d402e240 100644 --- a/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt +++ b/sentry-android-core/src/test/java/io/sentry/android/core/DefaultAndroidEventProcessorTest.kt @@ -56,7 +56,7 @@ class DefaultAndroidEventProcessorTest { private class Fixture { val buildInfo = mock() val options = SentryAndroidOptions().apply { - setDebug(true) + isDebug = true setLogger(mock()) sdkVersion = SdkVersion("test", "1.2.3") } @@ -77,6 +77,7 @@ class DefaultAndroidEventProcessorTest { @BeforeTest fun `set up`() { context = ApplicationProvider.getApplicationContext() + AppState.getInstance().resetInstance() } @Test @@ -161,7 +162,7 @@ class DefaultAndroidEventProcessorTest { } @Test - fun `Current should be true if it comes from main thread`() { + fun `Current and Main should be true if it comes from main thread`() { val sut = fixture.getSut(context) val sentryThread = SentryThread().apply { @@ -174,6 +175,7 @@ class DefaultAndroidEventProcessorTest { assertNotNull(sut.process(event, Hint())) { assertNotNull(it.threads) { threads -> assertTrue(threads.first().isCurrent == true) + assertTrue(threads.first().isMain == true) } } } @@ -193,6 +195,7 @@ class DefaultAndroidEventProcessorTest { assertNotNull(sut.process(event, Hint())) { assertNotNull(it.threads) { threads -> assertFalse(threads.first().isCurrent == true) + assertFalse(threads.first().isMain == true) } } } @@ -497,4 +500,28 @@ class DefaultAndroidEventProcessorTest { assertEquals("en_US", device.locale) } } + + @Test + fun `Event sets InForeground to true if not in the background`() { + val sut = fixture.getSut(context) + + AppState.getInstance().setInBackground(false) + + assertNotNull(sut.process(SentryEvent(), Hint())) { + val app = it.contexts.app!! + assertTrue(app.inForeground!!) + } + } + + @Test + fun `Event sets InForeground to false if in the background`() { + val sut = fixture.getSut(context) + + AppState.getInstance().setInBackground(true) + + assertNotNull(sut.process(SentryEvent(), Hint())) { + val app = it.contexts.app!! + assertFalse(app.inForeground!!) + } + } } diff --git a/sentry/api/sentry.api b/sentry/api/sentry.api index 8692d590107..ea163bd357f 100644 --- a/sentry/api/sentry.api +++ b/sentry/api/sentry.api @@ -2510,6 +2510,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun getAppVersion ()Ljava/lang/String; public fun getBuildType ()Ljava/lang/String; public fun getDeviceAppHash ()Ljava/lang/String; + public fun getInForeground ()Ljava/lang/Boolean; public fun getPermissions ()Ljava/util/Map; public fun getUnknown ()Ljava/util/Map; public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V @@ -2520,6 +2521,7 @@ public final class io/sentry/protocol/App : io/sentry/JsonSerializable, io/sentr public fun setAppVersion (Ljava/lang/String;)V public fun setBuildType (Ljava/lang/String;)V public fun setDeviceAppHash (Ljava/lang/String;)V + public fun setInForeground (Ljava/lang/Boolean;)V public fun setPermissions (Ljava/util/Map;)V public fun setUnknown (Ljava/util/Map;)V } @@ -2539,6 +2541,7 @@ public final class io/sentry/protocol/App$JsonKeys { public static final field APP_VERSION Ljava/lang/String; public static final field BUILD_TYPE Ljava/lang/String; public static final field DEVICE_APP_HASH Ljava/lang/String; + public static final field IN_FOREGROUND Ljava/lang/String; public fun ()V } @@ -3344,11 +3347,13 @@ public final class io/sentry/protocol/SentryThread : io/sentry/JsonSerializable, public fun isCrashed ()Ljava/lang/Boolean; public fun isCurrent ()Ljava/lang/Boolean; public fun isDaemon ()Ljava/lang/Boolean; + public fun isMain ()Ljava/lang/Boolean; public fun serialize (Lio/sentry/JsonObjectWriter;Lio/sentry/ILogger;)V public fun setCrashed (Ljava/lang/Boolean;)V public fun setCurrent (Ljava/lang/Boolean;)V public fun setDaemon (Ljava/lang/Boolean;)V public fun setId (Ljava/lang/Long;)V + public fun setMain (Ljava/lang/Boolean;)V public fun setName (Ljava/lang/String;)V public fun setPriority (Ljava/lang/Integer;)V public fun setStacktrace (Lio/sentry/protocol/SentryStackTrace;)V @@ -3367,6 +3372,7 @@ public final class io/sentry/protocol/SentryThread$JsonKeys { public static final field CURRENT Ljava/lang/String; public static final field DAEMON Ljava/lang/String; public static final field ID Ljava/lang/String; + public static final field MAIN Ljava/lang/String; public static final field NAME Ljava/lang/String; public static final field PRIORITY Ljava/lang/String; public static final field STACKTRACE Ljava/lang/String; diff --git a/sentry/src/main/java/io/sentry/protocol/App.java b/sentry/src/main/java/io/sentry/protocol/App.java index a88067c3ead..aa76f567fc1 100644 --- a/sentry/src/main/java/io/sentry/protocol/App.java +++ b/sentry/src/main/java/io/sentry/protocol/App.java @@ -38,6 +38,11 @@ public final class App implements JsonUnknown, JsonSerializable { private @Nullable String appBuild; /** Application permissions in the form of "permission_name" : "granted|not_granted" */ private @Nullable Map permissions; + /** + * A flag indicating whether the app is in foreground or not. An app is in foreground when it's + * visible to the user. + */ + private @Nullable Boolean inForeground; public App() {} @@ -50,6 +55,7 @@ public App() {} this.buildType = app.buildType; this.deviceAppHash = app.deviceAppHash; this.permissions = CollectionUtils.newConcurrentHashMap(app.permissions); + this.inForeground = app.inForeground; this.unknown = CollectionUtils.newConcurrentHashMap(app.unknown); } @@ -122,6 +128,15 @@ public void setPermissions(@Nullable Map permissions) { this.permissions = permissions; } + @Nullable + public Boolean getInForeground() { + return inForeground; + } + + public void setInForeground(final @Nullable Boolean inForeground) { + this.inForeground = inForeground; + } + // region json @Nullable @@ -144,6 +159,7 @@ public static final class JsonKeys { public static final String APP_VERSION = "app_version"; public static final String APP_BUILD = "app_build"; public static final String APP_PERMISSIONS = "permissions"; + public static final String IN_FOREGROUND = "in_foreground"; } @Override @@ -174,6 +190,9 @@ public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger) if (permissions != null && !permissions.isEmpty()) { writer.name(JsonKeys.APP_PERMISSIONS).value(logger, permissions); } + if (inForeground != null) { + writer.name(JsonKeys.IN_FOREGROUND).value(inForeground); + } if (unknown != null) { for (String key : unknown.keySet()) { Object value = unknown.get(key); @@ -220,6 +239,9 @@ public static final class Deserializer implements JsonDeserializer { CollectionUtils.newConcurrentHashMap( (Map) reader.nextObjectOrNull()); break; + case JsonKeys.IN_FOREGROUND: + app.inForeground = reader.nextBooleanOrNull(); + break; default: if (unknown == null) { unknown = new ConcurrentHashMap<>(); diff --git a/sentry/src/main/java/io/sentry/protocol/SentryThread.java b/sentry/src/main/java/io/sentry/protocol/SentryThread.java index f1206112649..1c955064e33 100644 --- a/sentry/src/main/java/io/sentry/protocol/SentryThread.java +++ b/sentry/src/main/java/io/sentry/protocol/SentryThread.java @@ -35,6 +35,7 @@ public final class SentryThread implements JsonUnknown, JsonSerializable { private @Nullable Boolean crashed; private @Nullable Boolean current; private @Nullable Boolean daemon; + private @Nullable Boolean main; private @Nullable SentryStackTrace stacktrace; @SuppressWarnings("unused") @@ -166,6 +167,29 @@ public void setDaemon(final @Nullable Boolean daemon) { this.daemon = daemon; } + /** + * If applicable, a flag indicating whether the thread was responsible for rendering the user + * interface. On mobile platforms this is oftentimes referred to as the "main thread" or "ui + * thread". + * + * @return if its the main thread or not + */ + @Nullable + public Boolean isMain() { + return main; + } + + /** + * If applicable, a flag indicating whether the thread was responsible for rendering the user + * interface. On mobile platforms this is oftentimes referred to as the "main thread" or "ui + * thread". + * + * @param main if its the main thread or not + */ + public void setMain(final @Nullable Boolean main) { + this.main = main; + } + /** * Gets the state of the thread. * @@ -205,6 +229,7 @@ public static final class JsonKeys { public static final String CRASHED = "crashed"; public static final String CURRENT = "current"; public static final String DAEMON = "daemon"; + public static final String MAIN = "main"; public static final String STACKTRACE = "stacktrace"; } @@ -233,6 +258,9 @@ public void serialize(@NotNull JsonObjectWriter writer, @NotNull ILogger logger) if (daemon != null) { writer.name(JsonKeys.DAEMON).value(daemon); } + if (main != null) { + writer.name(JsonKeys.MAIN).value(main); + } if (stacktrace != null) { writer.name(JsonKeys.STACKTRACE).value(logger, stacktrace); } @@ -278,6 +306,9 @@ public static final class Deserializer implements JsonDeserializer case JsonKeys.DAEMON: sentryThread.daemon = reader.nextBooleanOrNull(); break; + case JsonKeys.MAIN: + sentryThread.main = reader.nextBooleanOrNull(); + break; case JsonKeys.STACKTRACE: sentryThread.stacktrace = reader.nextOrNull(logger, new SentryStackTrace.Deserializer()); diff --git a/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt index 9eef2a6eb78..f07938d4c53 100644 --- a/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/AppSerializationTest.kt @@ -29,6 +29,7 @@ class AppSerializationTest { "WRITE_EXTERNAL_STORAGE" to "not_granted", "CAMERA" to "granted" ) + inForeground = true } } private val fixture = Fixture() diff --git a/sentry/src/test/java/io/sentry/protocol/AppTest.kt b/sentry/src/test/java/io/sentry/protocol/AppTest.kt index a40f5adf36a..0a2c3016158 100644 --- a/sentry/src/test/java/io/sentry/protocol/AppTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/AppTest.kt @@ -18,8 +18,9 @@ class AppTest { app.appVersion = "app version" app.buildType = "build type" app.deviceAppHash = "device app hash" + app.inForeground = true val unknown = mapOf(Pair("unknown", "unknown")) - app.setUnknown(unknown) + app.unknown = unknown val clone = App(app) @@ -41,8 +42,9 @@ class AppTest { app.appVersion = "app version" app.buildType = "build type" app.deviceAppHash = "device app hash" + app.inForeground = true val unknown = mapOf(Pair("unknown", "unknown")) - app.setUnknown(unknown) + app.unknown = unknown val clone = App(app) @@ -55,6 +57,7 @@ class AppTest { assertEquals("app version", clone.appVersion) assertEquals("build type", clone.buildType) assertEquals("device app hash", clone.deviceAppHash) + assertEquals(true, clone.inForeground) assertNotNull(clone.unknown) { assertEquals("unknown", it["unknown"]) } diff --git a/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt b/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt index fd1494e2169..c3c5597ae3e 100644 --- a/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt +++ b/sentry/src/test/java/io/sentry/protocol/SentryThreadSerializationTest.kt @@ -24,6 +24,7 @@ class SentryThreadSerializationTest { isCrashed = false isCurrent = false isDaemon = true + isMain = true stacktrace = SentryStackTrace().apply { frames = listOf( SentryStackFrame().apply { diff --git a/sentry/src/test/resources/json/app.json b/sentry/src/test/resources/json/app.json index 753d6b7580e..5294b48e157 100644 --- a/sentry/src/test/resources/json/app.json +++ b/sentry/src/test/resources/json/app.json @@ -10,5 +10,6 @@ { "WRITE_EXTERNAL_STORAGE": "not_granted", "CAMERA": "granted" - } + }, + "in_foreground": true } diff --git a/sentry/src/test/resources/json/sentry_thread.json b/sentry/src/test/resources/json/sentry_thread.json index 5879c155782..da11f6f7880 100644 --- a/sentry/src/test/resources/json/sentry_thread.json +++ b/sentry/src/test/resources/json/sentry_thread.json @@ -6,6 +6,7 @@ "crashed": false, "current": false, "daemon": true, + "main": true, "stacktrace": { "frames":