diff --git a/CHANGELOG.md b/CHANGELOG.md index bac92b14f1..b7626ec89d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,12 @@ # Changelog +## TBD + +### Bug fixes + +* Address pre-existing StrictMode violations + [#1328](https://github.com/bugsnag/bugsnag-android/pull/1328) + ## 5.10.1 (2021-07-15) ### Bug fixes diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml index d222bee612..d05058b93c 100644 --- a/bugsnag-android-core/detekt-baseline.xml +++ b/bugsnag-android-core/detekt-baseline.xml @@ -7,11 +7,13 @@ LongParameterList:AppDataCollector.kt$AppDataCollector$( appContext: Context, private val packageManager: PackageManager?, private val config: ImmutableConfig, private val sessionTracker: SessionTracker, private val activityManager: ActivityManager?, private val launchCrashTracker: LaunchCrashTracker, private val logger: Logger ) LongParameterList:AppWithState.kt$AppWithState$( binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, buildUuid: String?, type: String?, versionCode: Number?, /** * The number of milliseconds the application was running before the event occurred */ var duration: Number?, /** * The number of milliseconds the application was running in the foreground before the * event occurred */ var durationInForeground: Number?, /** * Whether the application was in the foreground when the event occurred */ var inForeground: Boolean?, /** * Whether the application was launching when the event occurred */ var isLaunching: Boolean? ) LongParameterList:AppWithState.kt$AppWithState$( config: ImmutableConfig, binaryArch: String?, id: String?, releaseStage: String?, version: String?, codeBundleId: String?, duration: Number?, durationInForeground: Number?, inForeground: Boolean?, isLaunching: Boolean? ) + LongParameterList:DataCollectionModule.kt$DataCollectionModule$( contextModule: ContextModule, configModule: ConfigModule, systemServiceModule: SystemServiceModule, trackerModule: TrackerModule, bgTaskService: BackgroundTaskService, connectivity: Connectivity, deviceId: String? ) LongParameterList:Device.kt$Device$( buildInfo: DeviceBuildInfo, /** * The Application Binary Interface used */ var cpuAbi: Array<String>?, /** * Whether the device has been jailbroken */ var jailbroken: Boolean?, /** * A UUID generated by Bugsnag and used for the individual application on a device */ var id: String?, /** * The IETF language tag of the locale used */ var locale: String?, /** * The total number of bytes of memory on the device */ var totalMemory: Long?, /** * A collection of names and their versions of the primary languages, frameworks or * runtimes that the application is running on */ var runtimeVersions: MutableMap<String, Any>? ) LongParameterList:DeviceBuildInfo.kt$DeviceBuildInfo$( val manufacturer: String?, val model: String?, val osVersion: String?, val apiLevel: Int?, val osBuild: String?, val fingerprint: String?, val tags: String?, val brand: String?, val cpuAbis: Array<String>? ) - LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, bgTaskService: BackgroundTaskService, private val logger: Logger ) + LongParameterList:DeviceDataCollector.kt$DeviceDataCollector$( private val connectivity: Connectivity, private val appContext: Context, resources: Resources, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, private val bgTaskService: BackgroundTaskService, private val logger: Logger ) LongParameterList:DeviceWithState.kt$DeviceWithState$( buildInfo: DeviceBuildInfo, jailbroken: Boolean?, id: String?, locale: String?, totalMemory: Long?, runtimeVersions: MutableMap<String, Any>, /** * The number of free bytes of storage available on the device */ var freeDisk: Long?, /** * The number of free bytes of memory available on the device */ var freeMemory: Long?, /** * The orientation of the device when the event occurred: either portrait or landscape */ var orientation: String?, /** * The timestamp on the device when the event occurred */ var time: Date? ) LongParameterList:EventFilenameInfo.kt$EventFilenameInfo.Companion$( obj: Any, uuid: String = UUID.randomUUID().toString(), apiKey: String?, timestamp: Long = System.currentTimeMillis(), config: ImmutableConfig, isLaunching: Boolean? = null ) + LongParameterList:EventStorageModule.kt$EventStorageModule$( contextModule: ContextModule, configModule: ConfigModule, dataCollectionModule: DataCollectionModule, bgTaskService: BackgroundTaskService, trackerModule: TrackerModule, systemServiceModule: SystemServiceModule, notifier: Notifier ) LongParameterList:StateEvent.kt$StateEvent.Install$( @JvmField val apiKey: String, @JvmField val autoDetectNdkCrashes: Boolean, @JvmField val appVersion: String?, @JvmField val buildUuid: String?, @JvmField val releaseStage: String?, @JvmField val lastRunInfoPath: String, @JvmField val consecutiveLaunchCrashes: Int ) LongParameterList:ThreadState.kt$ThreadState$( stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>, currentThread: java.lang.Thread, exc: Throwable?, isUnhandled: Boolean, projectPackages: Collection<String>, logger: Logger ) MagicNumber:DefaultDelivery.kt$DefaultDelivery$299 @@ -28,6 +30,7 @@ SwallowedException:ConnectivityCompat.kt$ConnectivityLegacy$catch (e: NullPointerException) { // in some rare cases we get a remote NullPointerException via Parcel.readException null } SwallowedException:ContextExtensions.kt$catch (exc: RuntimeException) { null } SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exc: Exception) { false } + SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exc: RejectedExecutionException) { null } SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get battery status") } SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get locationStatus") } SwallowedException:DeviceIdStore.kt$DeviceIdStore$catch (exc: OverlappingFileLockException) { Thread.sleep(FILE_LOCK_WAIT_MS) } diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientUserTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientUserTest.kt index a7ca2fc60d..30ad604c0f 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientUserTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ClientUserTest.kt @@ -78,7 +78,7 @@ class ClientUserTest { client.setUser(USER_ID, USER_EMAIL, USER_NAME) // Check that the user was persisted - val file = File(config.persistenceDirectory, "user-info") + val file = File(client.config.persistenceDirectory.value, "user-info") val expected = "{\"id\":\"123456\",\"email\":\"mr.test@email.com\",\"name\":\"Mr Test\"}" assertEquals(expected, file.readText()) } @@ -89,7 +89,7 @@ class ClientUserTest { client = Client(context, config) // Check that the user was not persisted - val file = File(config.persistenceDirectory, "user-info") + val file = File(client.config.persistenceDirectory.value, "user-info") assertEquals("", file.readText()) client.setUser(USER_ID, USER_EMAIL, USER_NAME) assertEquals("", file.readText()) diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt index e6b503264d..fbff235001 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/LastRunInfoStoreTest.kt @@ -26,7 +26,7 @@ internal class LastRunInfoStoreTest { packageInfo = null, appInfo = null ) - file = File(config.persistenceDirectory, "last-run-info") + file = File(config.persistenceDirectory.value, "last-run-info") file.delete() lastRunInfoStore = LastRunInfoStore(config) } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt new file mode 100644 index 0000000000..3167025131 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/BugsnagStateModule.kt @@ -0,0 +1,37 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.dag.ConfigModule +import com.bugsnag.android.internal.dag.DependencyModule + +/** + * A dependency module which constructs the objects that track state in Bugsnag. For example, this + * class is responsible for creating classes which track the current breadcrumb/metadata state. + */ +internal class BugsnagStateModule( + configModule: ConfigModule, + configuration: Configuration +) : DependencyModule { + + private val cfg = configModule.config + + val clientObservable = ClientObservable() + + val callbackState = configuration.impl.callbackState.copy() + + val contextState = ContextState().apply { + if (configuration.context != null) { + setManualContext(configuration.context) + } + } + + val breadcrumbState = BreadcrumbState(cfg.maxBreadcrumbs, callbackState, cfg.logger) + + val metadataState = copyMetadataState(configuration) + + private fun copyMetadataState(configuration: Configuration): MetadataState { + // performs deep copy of metadata to preserve immutability of Configuration interface + val orig = configuration.impl.metadataState.metadata + val copy = orig.copy() + return configuration.impl.metadataState.copy(metadata = copy) + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 205e42c165..328d85be90 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -1,19 +1,15 @@ package com.bugsnag.android; -import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom; -import static com.bugsnag.android.ContextExtensionsKt.getStorageManagerFrom; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; -import static com.bugsnag.android.internal.ImmutableConfigKt.sanitiseConfiguration; import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.StateObserver; +import com.bugsnag.android.internal.dag.ConfigModule; +import com.bugsnag.android.internal.dag.ContextModule; +import com.bugsnag.android.internal.dag.SystemServiceModule; -import android.app.ActivityManager; import android.app.Application; import android.content.Context; -import android.content.res.Resources; -import android.os.Environment; -import android.os.storage.StorageManager; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -29,7 +25,10 @@ import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.ExecutionException; import java.util.concurrent.RejectedExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. @@ -67,24 +66,16 @@ public class Client implements MetadataAware, CallbackAware, UserAware { @NonNull protected final EventStore eventStore; - private final SessionStore sessionStore; - final SessionTracker sessionTracker; final SystemBroadcastReceiver systemBroadcastReceiver; - private final ActivityBreadcrumbCollector activityBreadcrumbCollector; - private final SessionLifecycleCallback sessionLifecycleCallback; - - final Connectivity connectivity; - - @Nullable - private final StorageManager storageManager; final Logger logger; + final Connectivity connectivity; final DeliveryDelegate deliveryDelegate; final ClientObservable clientObservable; - private PluginClient pluginClient; + PluginClient pluginClient; final Notifier notifier = new Notifier(); @@ -121,8 +112,8 @@ public Client(@NonNull Context androidContext, @NonNull String apiKey) { * @param configuration a configuration for the Client */ public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) { - Context ctx = androidContext.getApplicationContext(); - appContext = ctx != null ? ctx : androidContext; + ContextModule contextModule = new ContextModule(androidContext); + appContext = contextModule.getCtx(); connectivity = new ConnectivityCompat(appContext, new Function2() { @Override @@ -140,78 +131,52 @@ public Unit invoke(Boolean hasConnection, String networkState) { }); // set sensible defaults for delivery/project packages etc if not set - immutableConfig = sanitiseConfiguration(appContext, configuration, connectivity); + ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity); + immutableConfig = configModule.getConfig(); logger = immutableConfig.getLogger(); warnIfNotAppContext(androidContext); - clientObservable = new ClientObservable(); - - // Set up breadcrumbs - callbackState = configuration.impl.callbackState.copy(); - int maxBreadcrumbs = immutableConfig.getMaxBreadcrumbs(); - breadcrumbState = new BreadcrumbState(maxBreadcrumbs, callbackState, logger); - - storageManager = getStorageManagerFrom(appContext); - contextState = new ContextState(); - - if (configuration.getContext() != null) { - contextState.setManualContext(configuration.getContext()); - } - - sessionStore = new SessionStore(immutableConfig, logger, null); - sessionTracker = new SessionTracker(immutableConfig, callbackState, this, - sessionStore, logger, bgTaskService); - metadataState = copyMetadataState(configuration); - ActivityManager am = getActivityManagerFrom(appContext); - - launchCrashTracker = new LaunchCrashTracker(immutableConfig); - appDataCollector = new AppDataCollector(appContext, appContext.getPackageManager(), - immutableConfig, sessionTracker, am, launchCrashTracker, logger); + // setup storage as soon as possible + final StorageModule storageModule = new StorageModule(appContext, + immutableConfig, bgTaskService, logger); + + // setup state trackers for bugsnag + BugsnagStateModule bugsnagStateModule = new BugsnagStateModule( + configModule, configuration); + clientObservable = bugsnagStateModule.getClientObservable(); + callbackState = bugsnagStateModule.getCallbackState(); + breadcrumbState = bugsnagStateModule.getBreadcrumbState(); + contextState = bugsnagStateModule.getContextState(); + metadataState = bugsnagStateModule.getMetadataState(); + + // lookup system services + final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule); + + // block until storage module has resolved everything + storageModule.resolveDependencies(bgTaskService, TaskType.IO); + + // setup further state trackers and data collection + TrackerModule trackerModule = new TrackerModule(configModule, + storageModule, this, bgTaskService, callbackState); + launchCrashTracker = trackerModule.getLaunchCrashTracker(); + sessionTracker = trackerModule.getSessionTracker(); + + DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, + configModule, systemServiceModule, trackerModule, + bgTaskService, connectivity, storageModule.getDeviceId()); + appDataCollector = dataCollectionModule.getAppDataCollector(); + deviceDataCollector = dataCollectionModule.getDeviceDataCollector(); // load the device + user information - SharedPrefMigrator sharedPrefMigrator = new SharedPrefMigrator(appContext); - DeviceIdStore deviceIdStore = new DeviceIdStore(appContext, sharedPrefMigrator, logger); - String deviceId = deviceIdStore.loadDeviceId(); - UserStore userStore = new UserStore(immutableConfig, deviceId, sharedPrefMigrator, logger); - userState = userStore.load(configuration.getUser()); - sharedPrefMigrator.deleteLegacyPrefs(); - - DeviceBuildInfo info = DeviceBuildInfo.Companion.defaultInfo(); - Resources resources = appContext.getResources(); - deviceDataCollector = new DeviceDataCollector(connectivity, appContext, - resources, deviceId, info, Environment.getDataDirectory(), - new RootDetector(logger), bgTaskService, logger); + userState = storageModule.getUserStore().load(configuration.getUser()); + storageModule.getSharedPrefMigrator().deleteLegacyPrefs(); - if (appContext instanceof Application) { - Application application = (Application) appContext; - sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker); - application.registerActivityLifecycleCallbacks(sessionLifecycleCallback); - - if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { - this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector( - new Function2, Unit>() { - @SuppressWarnings("unchecked") - @Override - public Unit invoke(String activity, Map metadata) { - leaveBreadcrumb(activity, (Map) metadata, - BreadcrumbType.STATE); - return null; - } - } - ); - application.registerActivityLifecycleCallbacks(activityBreadcrumbCollector); - } else { - this.activityBreadcrumbCollector = null; - } - } else { - this.activityBreadcrumbCollector = null; - this.sessionLifecycleCallback = null; - } + registerLifecycleCallbacks(); - InternalReportDelegate delegate = new InternalReportDelegate(appContext, logger, - immutableConfig, storageManager, appDataCollector, deviceDataCollector, - sessionTracker, notifier, bgTaskService); - eventStore = new EventStore(immutableConfig, logger, notifier, bgTaskService, delegate); + EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, + dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier); + eventStorageModule.resolveDependencies(bgTaskService, TaskType.IO); + eventStore = eventStorageModule.getEventStore(); deliveryDelegate = new DeliveryDelegate(logger, eventStore, immutableConfig, breadcrumbState, notifier, bgTaskService); @@ -223,8 +188,8 @@ public Unit invoke(String activity, Map metadata) { } // load last run info - lastRunInfoStore = new LastRunInfoStore(immutableConfig); - lastRunInfo = loadLastRunInfo(); + lastRunInfoStore = storageModule.getLastRunInfoStore(); + lastRunInfo = storageModule.getLastRunInfo(); // initialise plugins before attempting to flush any errors loadPlugins(configuration); @@ -258,13 +223,9 @@ public Unit invoke(String activity, Map metadata) { @NonNull AppDataCollector appDataCollector, @NonNull BreadcrumbState breadcrumbState, @NonNull EventStore eventStore, - SessionStore sessionStore, SystemBroadcastReceiver systemBroadcastReceiver, SessionTracker sessionTracker, - ActivityBreadcrumbCollector activityBreadcrumbCollector, - SessionLifecycleCallback sessionLifecycleCallback, Connectivity connectivity, - @Nullable StorageManager storageManager, Logger logger, DeliveryDelegate deliveryDelegate, LastRunInfoStore lastRunInfoStore, @@ -282,13 +243,9 @@ public Unit invoke(String activity, Map metadata) { this.appDataCollector = appDataCollector; this.breadcrumbState = breadcrumbState; this.eventStore = eventStore; - this.sessionStore = sessionStore; this.systemBroadcastReceiver = systemBroadcastReceiver; this.sessionTracker = sessionTracker; - this.activityBreadcrumbCollector = activityBreadcrumbCollector; - this.sessionLifecycleCallback = sessionLifecycleCallback; this.connectivity = connectivity; - this.storageManager = storageManager; this.logger = logger; this.deliveryDelegate = deliveryDelegate; this.lastRunInfoStore = lastRunInfoStore; @@ -297,6 +254,29 @@ public Unit invoke(String activity, Map metadata) { this.exceptionHandler = exceptionHandler; } + void registerLifecycleCallbacks() { + if (appContext instanceof Application) { + Application application = (Application) appContext; + SessionLifecycleCallback sessionCb = new SessionLifecycleCallback(sessionTracker); + application.registerActivityLifecycleCallbacks(sessionCb); + + if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { + ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector( + new Function2, Unit>() { + @SuppressWarnings("unchecked") + @Override + public Unit invoke(String activity, Map metadata) { + leaveBreadcrumb(activity, (Map) metadata, + BreadcrumbType.STATE); + return null; + } + } + ); + application.registerActivityLifecycleCallbacks(activityCb); + } + } + } + /** * Registers listeners for system events in the background. This offloads work from the main * thread that collects useful information from callbacks, but that don't need to be done @@ -316,12 +296,6 @@ public void run() { } } - private LastRunInfo loadLastRunInfo() { - LastRunInfo lastRunInfo = lastRunInfoStore.load(); - LastRunInfo currentRunInfo = new LastRunInfo(0, false, false); - persistRunInfo(currentRunInfo); - return lastRunInfo; - } /** * Load information about the last run, and reset the persisted information to the defaults. @@ -339,24 +313,27 @@ public void run() { } } - private void loadPlugins(@NonNull Configuration configuration) { - NativeInterface.setClient(this); - Set userPlugins = configuration.getPlugins(); - pluginClient = new PluginClient(userPlugins, immutableConfig, logger); - pluginClient.loadPlugins(this); + private void loadPlugins(@NonNull final Configuration configuration) { + try { + bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() { + @Override + public void run() { + NativeInterface.setClient(Client.this); + Set userPlugins = configuration.getPlugins(); + pluginClient = new PluginClient(userPlugins, immutableConfig, logger); + pluginClient.loadPlugins(Client.this); + } + }).get(3, TimeUnit.SECONDS); + } catch (RejectedExecutionException | InterruptedException + | ExecutionException | TimeoutException exc) { + logger.w("Failed to load plugins", exc); + } } private void logNull(String property) { logger.e("Invalid null value supplied to client." + property + ", ignoring"); } - private MetadataState copyMetadataState(@NonNull Configuration configuration) { - // performs deep copy of metadata to preserve immutability of Configuration interface - Metadata orig = configuration.impl.metadataState.getMetadata(); - Metadata copy = orig.copy(); - return configuration.impl.metadataState.copy(copy); - } - private void registerComponentCallbacks() { appContext.registerComponentCallbacks(new ClientComponentCallbacks( deviceDataCollector, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DataCollectionModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DataCollectionModule.kt new file mode 100644 index 0000000000..d779e4d1ec --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DataCollectionModule.kt @@ -0,0 +1,64 @@ +package com.bugsnag.android + +import android.os.Environment +import com.bugsnag.android.internal.dag.ConfigModule +import com.bugsnag.android.internal.dag.ContextModule +import com.bugsnag.android.internal.dag.DependencyModule +import com.bugsnag.android.internal.dag.SystemServiceModule +import com.bugsnag.android.internal.dag.loadDepModuleIoObjects +import java.util.concurrent.Future + +/** + * A dependency module which constructs the objects that collect data in Bugsnag. For example, this + * class is responsible for creating classes which capture device-specific information. + */ +internal class DataCollectionModule( + contextModule: ContextModule, + configModule: ConfigModule, + systemServiceModule: SystemServiceModule, + trackerModule: TrackerModule, + bgTaskService: BackgroundTaskService, + connectivity: Connectivity, + deviceId: String? +) : DependencyModule { + + private val ctx = contextModule.ctx + private val cfg = configModule.config + private val logger = cfg.logger + private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo() + private val dataDir = Environment.getDataDirectory() + + val appDataCollector by lazy { + AppDataCollector( + ctx, + ctx.packageManager, + cfg, + trackerModule.sessionTracker, + systemServiceModule.activityManager, + trackerModule.launchCrashTracker, + logger + ) + } + + private val rootDetector by lazy { + RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo) + } + + val deviceDataCollector by lazy { + DeviceDataCollector( + connectivity, ctx, + ctx.resources, deviceId, deviceBuildInfo, dataDir, + rootDetector, bgTaskService, logger + ) + } + + // trigger initialization on a background thread. Client will then block on the main + // thread with resolveDependencies() if these have not completed by the appropriate time. + private val future: Future<*>? = loadDepModuleIoObjects(bgTaskService) { rootDetector } + + override fun resolveDependencies(bgTaskService: BackgroundTaskService, taskType: TaskType) { + runCatching { + future?.get() + } + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt index a7995164cb..5587ba7491 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DefaultDelivery.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import android.net.TrafficStats import java.io.ByteArrayOutputStream import java.io.IOException import java.io.PrintWriter @@ -42,6 +43,7 @@ internal class DefaultDelivery( headers: Map ): DeliveryStatus { + TrafficStats.setThreadStatsTag(1) if (connectivity != null && !connectivity.hasNetworkConnection()) { return DeliveryStatus.UNDELIVERED } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 5181fa8303..44c4aa2900 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -28,7 +28,7 @@ internal class DeviceDataCollector( private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, rootDetector: RootDetector, - bgTaskService: BackgroundTaskService, + private val bgTaskService: BackgroundTaskService, private val logger: Logger ) { @@ -207,7 +207,12 @@ internal class DeviceDataCollector( fun calculateFreeDisk(): Long { // for this specific case we want the currently usable space, not // StorageManager#allocatableBytes() as the UsableSpace lint inspection suggests - return dataDirectory.usableSpace + return runCatching { + bgTaskService.submitTask( + TaskType.IO, + Callable { dataDirectory.usableSpace } + ).get() + }.getOrDefault(0L) } /** diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceIdStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceIdStore.kt index 5581acabf5..b9db53c296 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceIdStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceIdStore.kt @@ -28,9 +28,7 @@ internal class DeviceIdStore @JvmOverloads constructor( init { try { - if (!file.exists()) { - file.createNewFile() - } + file.createNewFile() } catch (exc: Throwable) { logger.w("Failed to created device ID file", exc) } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStorageModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStorageModule.kt new file mode 100644 index 0000000000..23d46e6bdb --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStorageModule.kt @@ -0,0 +1,50 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.dag.ConfigModule +import com.bugsnag.android.internal.dag.ContextModule +import com.bugsnag.android.internal.dag.DependencyModule +import com.bugsnag.android.internal.dag.SystemServiceModule +import com.bugsnag.android.internal.dag.loadDepModuleIoObjects +import java.util.concurrent.Future + +/** + * A dependency module which constructs the objects that persist events to disk in Bugsnag. + */ +internal class EventStorageModule( + contextModule: ContextModule, + configModule: ConfigModule, + dataCollectionModule: DataCollectionModule, + bgTaskService: BackgroundTaskService, + trackerModule: TrackerModule, + systemServiceModule: SystemServiceModule, + notifier: Notifier +) : DependencyModule { + + private val cfg = configModule.config + + private val delegate by lazy { + InternalReportDelegate( + contextModule.ctx, + cfg.logger, + cfg, + systemServiceModule.storageManager, + dataCollectionModule.appDataCollector, + dataCollectionModule.deviceDataCollector, + trackerModule.sessionTracker, + notifier, + bgTaskService + ) + } + + val eventStore by lazy { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate) } + + // trigger initialization on a background thread. Client will then block on the main + // thread if these have not completed by the appropriate time. + private val future: Future<*>? = loadDepModuleIoObjects(bgTaskService) { eventStore } + + override fun resolveDependencies(bgTaskService: BackgroundTaskService, taskType: TaskType) { + runCatching { + future?.get() + } + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java index d22d8ddb24..e95fc6b1d4 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/EventStore.java @@ -11,7 +11,6 @@ import java.util.Collections; import java.util.Comparator; import java.util.List; -import java.util.Locale; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.concurrent.RejectedExecutionException; @@ -53,7 +52,7 @@ public int compare(File lhs, File rhs) { Notifier notifier, BackgroundTaskService bgTaskSevice, Delegate delegate) { - super(new File(config.getPersistenceDirectory(), "bugsnag-errors"), + super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-errors"), config.getMaxPersistedEvents(), EVENT_COMPARATOR, logger, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java index 4aae1e30e8..cbafdac431 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/FileStore.java @@ -63,13 +63,7 @@ interface Delegate { */ private boolean isStorageDirValid(@NonNull File storageDir) { try { - if (!storageDir.isDirectory() || !storageDir.canWrite()) { - if (!storageDir.mkdirs()) { - this.logger.e("Could not prepare storage directory at " - + storageDir.getAbsolutePath()); - return false; - } - } + storageDir.mkdirs(); } catch (Exception exception) { this.logger.e("Could not prepare file storage directory", exception); return false; diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index 770cb65a92..4e6f125f7f 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LastRunInfoStore.kt @@ -16,7 +16,7 @@ private const val KEY_CRASHED_DURING_LAUNCH = "crashedDuringLaunch" */ internal class LastRunInfoStore(config: ImmutableConfig) { - val file: File = File(config.persistenceDirectory, "last-run-info") + val file: File = File(config.persistenceDirectory.value, "last-run-info") private val logger: Logger = config.logger private val lock = ReentrantReadWriteLock() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java index 56a000d90c..cb845ae389 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/NativeInterface.java @@ -56,7 +56,7 @@ public static String getContext() { @NonNull public static String getNativeReportPath() { ImmutableConfig config = getClient().getConfig(); - File persistenceDirectory = config.getPersistenceDirectory(); + File persistenceDirectory = config.getPersistenceDirectory().getValue(); return new File(persistenceDirectory, "bugsnag-native").getAbsolutePath(); } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java index 66bfa46f55..0d84d8a677 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionStore.java @@ -7,7 +7,6 @@ import java.io.File; import java.util.Comparator; -import java.util.Locale; import java.util.UUID; /** @@ -37,7 +36,7 @@ public int compare(File lhs, File rhs) { SessionStore(@NonNull ImmutableConfig config, @NonNull Logger logger, @Nullable Delegate delegate) { - super(new File(config.getPersistenceDirectory(), "bugsnag-sessions"), + super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-sessions"), config.getMaxPersistedSessions(), SESSION_COMPARATOR, logger, diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/StorageModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/StorageModule.kt new file mode 100644 index 0000000000..ed80f66934 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/StorageModule.kt @@ -0,0 +1,68 @@ +package com.bugsnag.android + +import android.content.Context +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.dag.DependencyModule +import com.bugsnag.android.internal.dag.loadDepModuleIoObjects +import java.util.concurrent.Future + +/** + * A dependency module which constructs the objects that store information to disk in Bugsnag. + */ +internal class StorageModule( + appContext: Context, + immutableConfig: ImmutableConfig, + bgTaskService: BackgroundTaskService, + logger: Logger +) : DependencyModule { + + val sharedPrefMigrator by lazy { SharedPrefMigrator(appContext) } + + private val deviceIdStore by lazy { + DeviceIdStore( + appContext, + sharedPrefMigrator = sharedPrefMigrator, + logger = logger + ) + } + + val deviceId by lazy { deviceIdStore.loadDeviceId() } + + val userStore by lazy { + UserStore( + immutableConfig, + deviceId, + sharedPrefMigrator = sharedPrefMigrator, + logger = logger + ) + } + + val lastRunInfoStore by lazy { LastRunInfoStore(immutableConfig) } + + val sessionStore by lazy { SessionStore(immutableConfig, logger, null) } + + val lastRunInfo by lazy { + val info = lastRunInfoStore.load() + val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false) + lastRunInfoStore.persist(currentRunInfo) + info + } + + // trigger initialization on a background thread. Client will then block on the main + // thread if these have not completed by the appropriate time. + private val future: Future<*>? = loadDepModuleIoObjects(bgTaskService) { + sharedPrefMigrator + deviceIdStore + deviceId + userStore + lastRunInfoStore + lastRunInfo + sessionStore + } + + override fun resolveDependencies(bgTaskService: BackgroundTaskService, taskType: TaskType) { + runCatching { + future?.get() + } + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/TrackerModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/TrackerModule.kt new file mode 100644 index 0000000000..00f8a10780 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/TrackerModule.kt @@ -0,0 +1,30 @@ +package com.bugsnag.android + +import com.bugsnag.android.internal.dag.ConfigModule +import com.bugsnag.android.internal.dag.DependencyModule + +/** + * A dependency module which constructs objects that track launch/session related information + * in Bugsnag. + */ +internal class TrackerModule( + configModule: ConfigModule, + storageModule: StorageModule, + client: Client, + bgTaskService: BackgroundTaskService, + callbackState: CallbackState +) : DependencyModule { + + private val config = configModule.config + + val launchCrashTracker = LaunchCrashTracker(config) + + val sessionTracker = SessionTracker( + config, + callbackState, + client, + storageModule.sessionStore, + config.logger, + bgTaskService + ) +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt index 29ccd71a19..0a7b59448e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/UserStore.kt @@ -12,7 +12,7 @@ import java.util.concurrent.atomic.AtomicReference internal class UserStore @JvmOverloads constructor( private val config: ImmutableConfig, private val deviceId: String?, - file: File = File(config.persistenceDirectory, "user-info"), + file: File = File(config.persistenceDirectory.value, "user-info"), private val sharedPrefMigrator: SharedPrefMigrator, private val logger: Logger ) { @@ -23,9 +23,7 @@ internal class UserStore @JvmOverloads constructor( init { try { - if (!file.exists()) { - file.createNewFile() - } + file.createNewFile() } catch (exc: IOException) { logger.w("Failed to created device ID file", exc) } diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 5c434a7d32..3a5b42c59d 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -47,7 +47,7 @@ data class ImmutableConfig( val maxBreadcrumbs: Int, val maxPersistedEvents: Int, val maxPersistedSessions: Int, - val persistenceDirectory: File, + val persistenceDirectory: Lazy, val sendLaunchCrashesSynchronously: Boolean, // results cached here to avoid unnecessary lookups in Client. @@ -128,7 +128,8 @@ internal fun convertToImmutableConfig( config: Configuration, buildUuid: String? = null, packageInfo: PackageInfo? = null, - appInfo: ApplicationInfo? = null + appInfo: ApplicationInfo? = null, + persistenceDir: Lazy = lazy { requireNotNull(config.persistenceDirectory) } ): ImmutableConfig { val errorTypes = when { config.autoDetectErrors -> config.enabledErrorTypes.copy() @@ -158,7 +159,7 @@ internal fun convertToImmutableConfig( maxPersistedEvents = config.maxPersistedEvents, maxPersistedSessions = config.maxPersistedSessions, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), - persistenceDirectory = config.persistenceDirectory!!, + persistenceDirectory = persistenceDir, sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, packageInfo = packageInfo, appInfo = appInfo @@ -214,11 +215,13 @@ internal fun sanitiseConfiguration( if (configuration.delivery == null) { configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!) } - - if (configuration.persistenceDirectory == null) { - configuration.persistenceDirectory = appContext.cacheDir - } - return convertToImmutableConfig(configuration, buildUuid, packageInfo, appInfo) + return convertToImmutableConfig( + configuration, + buildUuid, + packageInfo, + appInfo, + lazy { configuration.persistenceDirectory ?: appContext.cacheDir } + ) } internal const val RELEASE_STAGE_DEVELOPMENT = "development" diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt new file mode 100644 index 0000000000..1429989235 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt @@ -0,0 +1,18 @@ +package com.bugsnag.android.internal.dag + +import com.bugsnag.android.Configuration +import com.bugsnag.android.Connectivity +import com.bugsnag.android.internal.sanitiseConfiguration + +/** + * A dependency module which constructs the configuration object that is used to alter + * Bugsnag's default behaviour. + */ +internal class ConfigModule( + contextModule: ContextModule, + configuration: Configuration, + connectivity: Connectivity +) : DependencyModule { + + val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity) +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt new file mode 100644 index 0000000000..14ecd1bb94 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt @@ -0,0 +1,17 @@ +package com.bugsnag.android.internal.dag + +import android.content.Context + +/** + * A dependency module which accesses the application context object, falling back to the supplied + * context if it is the base context. + */ +internal class ContextModule( + appContext: Context +) : DependencyModule { + + val ctx: Context = when (appContext.applicationContext) { + null -> appContext + else -> appContext.applicationContext + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt new file mode 100644 index 0000000000..82e604f5fe --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt @@ -0,0 +1,29 @@ +package com.bugsnag.android.internal.dag + +import com.bugsnag.android.BackgroundTaskService +import com.bugsnag.android.TaskType + +/** + * A collection of related objects which are used to inject dependencies. This is somewhat + * analogous to Dagger's concept of modules - although this implementation is much simpler. + */ +internal interface DependencyModule { + + /** + * Blocks until all dependencies in the module have been constructed. This provides the option + * for modules to construct objects in a background thread, then have a user block on another + * thread until all the objects have been constructed. + */ + fun resolveDependencies(bgTaskService: BackgroundTaskService, taskType: TaskType) = Unit +} + +/** + * Creates a [Future] for loading objects on the IO thread. + */ +internal fun loadDepModuleIoObjects(bgTaskService: BackgroundTaskService, action: () -> Unit) = + runCatching { + bgTaskService.submitTask( + TaskType.IO, + Runnable(action) + ) + }.getOrNull() diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt new file mode 100644 index 0000000000..8537aa489a --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt @@ -0,0 +1,15 @@ +package com.bugsnag.android.internal.dag + +import com.bugsnag.android.getActivityManager +import com.bugsnag.android.getStorageManager + +/** + * A dependency module which provides a reference to Android system services. + */ +internal class SystemServiceModule( + contextModule: ContextModule +) : DependencyModule { + + val storageManager = contextModule.ctx.getStorageManager() + val activityManager = contextModule.ctx.getActivityManager() +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java index a3af0e1cff..facadde7bf 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ClientFacadeTest.java @@ -11,7 +11,6 @@ import com.bugsnag.android.internal.StateObserver; import android.content.Context; -import android.os.storage.StorageManager; import androidx.annotation.NonNull; @@ -64,27 +63,15 @@ public class ClientFacadeTest { @Mock EventStore eventStore; - @Mock - SessionStore sessionStore; - @Mock SystemBroadcastReceiver systemBroadcastReceiver; @Mock SessionTracker sessionTracker; - @Mock - ActivityBreadcrumbCollector activityBreadcrumbCollector; - - @Mock - SessionLifecycleCallback sessionLifecycleCallback; - @Mock Connectivity connectivity; - @Mock - StorageManager storageManager; - @Mock DeliveryDelegate deliveryDelegate; @@ -124,13 +111,9 @@ public void setUp() { appDataCollector, breadcrumbState, eventStore, - sessionStore, systemBroadcastReceiver, sessionTracker, - activityBreadcrumbCollector, - sessionLifecycleCallback, connectivity, - storageManager, logger, deliveryDelegate, lastRunInfoStore, diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt index 7eb173d81a..697d8317bd 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ImmutableConfigTest.kt @@ -179,7 +179,7 @@ internal class ImmutableConfigTest { assertEquals("development", config.releaseStage) assertEquals(55, config.versionCode) assertNotNull(config.delivery) - assertEquals(cacheDir, config.persistenceDirectory) + assertEquals(cacheDir, config.persistenceDirectory.value) } @Test @@ -201,6 +201,6 @@ internal class ImmutableConfigTest { assertEquals("production", config.releaseStage) assertEquals(55, config.versionCode) assertNotNull(config.delivery) - assertEquals(cacheDir, config.persistenceDirectory) + assertEquals(cacheDir, config.persistenceDirectory.value) } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt index b0e03023f6..a1b1f819ec 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/NativeInterfaceApiTest.kt @@ -75,7 +75,7 @@ internal class NativeInterfaceApiTest { @Test fun getNativeReportPathPersistenceDirectory() { val customDir = Files.createTempDirectory("custom").toFile() - `when`(immutableConfig.persistenceDirectory).thenReturn(customDir) + `when`(immutableConfig.persistenceDirectory).thenReturn(lazy { customDir }) val observed = NativeInterface.getNativeReportPath() assertEquals("${customDir.absolutePath}/bugsnag-native", observed) } diff --git a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java index 58bccc8f1b..0161992b05 100644 --- a/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java +++ b/bugsnag-plugin-react-native/src/test/java/com/bugsnag/android/TestData.java @@ -2,6 +2,10 @@ import com.bugsnag.android.internal.ImmutableConfig; +import kotlin.LazyKt; +import kotlin.jvm.functions.Function0; + +import java.io.File; import java.io.IOException; import java.nio.file.Files; import java.util.Collections; @@ -33,7 +37,16 @@ static ImmutableConfig generateConfig() throws IOException { 22, 32, 32, - Files.createTempDirectory("foo").toFile(), + LazyKt.lazy(new Function0() { + @Override + public File invoke() { + try { + return Files.createTempDirectory("foo").toFile(); + } catch (IOException ignored) { + return null; + } + } + }), true, null, null diff --git a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt index c63caf5361..cdbb900291 100644 --- a/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt +++ b/features/fixtures/mazerunner/app/src/main/java/com/bugsnag/android/mazerunner/StartupANRBehaviour.kt @@ -2,6 +2,8 @@ package com.bugsnag.android.mazerunner import android.app.Application import android.content.Context +import android.os.Handler +import android.os.Looper import android.util.Log import com.bugsnag.android.Bugsnag import java.util.concurrent.TimeUnit @@ -20,23 +22,26 @@ fun Application.triggerStartupAnrIfRequired() { // we have to startup Bugsnag at this point Bugsnag.start(this) - thread { - // This is a dirty hack to work around the limitations of our current testing - // systems - where external key-events are pushed through our main thread (which we - // are pausing to test for ANRs) + // wait for Bugsnag's ANR handler to install first + Handler(Looper.getMainLooper()).post { + thread { + // This is a dirty hack to work around the limitations of our current testing + // systems - where external key-events are pushed through our main thread (which we + // are pausing to test for ANRs) - // if there is a startup delay, we assume we are testing ANRs and send a "BACK" - // key-press from the *system* in an attempt to cause an ANR - try { - Thread.sleep(TimeUnit.SECONDS.toMillis(1L)) - Runtime.getRuntime() - .exec("input keyevent 4") - .waitFor() - } catch (ex: Exception) { - Log.w("StartupANR", "Couldn't send keyevent for BACK", ex) + // if there is a startup delay, we assume we are testing ANRs and send a "BACK" + // key-press from the *system* in an attempt to cause an ANR + try { + Thread.sleep(TimeUnit.SECONDS.toMillis(1L)) + Runtime.getRuntime() + .exec("input keyevent 4") + .waitFor() + } catch (ex: Exception) { + Log.w("StartupANR", "Couldn't send keyevent for BACK", ex) + } } - } - Thread.sleep(TimeUnit.SECONDS.toMillis(startupDelay)) + Thread.sleep(TimeUnit.SECONDS.toMillis(startupDelay)) + } } }