diff --git a/app/build.gradle b/app/build.gradle index b26827dee1..123bf3d55d 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -291,7 +291,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.9.4" + def bugsnag_version = "5.11.0" def biweekly_version = "0.6.6" def relinker_version = "1.4.3" def markwon_version = "4.6.2" diff --git a/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt new file mode 100644 index 0000000000..50377ad59c --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt @@ -0,0 +1,36 @@ +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 + return configuration.impl.metadataState.copy(metadata = orig.copy()) + } +} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java b/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java new file mode 100644 index 0000000000..22969f4b59 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java @@ -0,0 +1,52 @@ +package com.bugsnag.android; + +import android.os.Build; +import android.os.StrictMode.OnThreadViolationListener; +import android.os.strictmode.Violation; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +/** + * Sends an error report to Bugsnag for each StrictMode thread policy violation that occurs in + * your app. + *

+ * You should use this class by instantiating Bugsnag in the normal way and then set the + * StrictMode policy with + * {@link android.os.StrictMode.ThreadPolicy.Builder#penaltyListener + * (Executor, OnThreadViolationListener)}. + * This functionality is only supported on API 28+. + */ +@RequiresApi(api = Build.VERSION_CODES.P) +public class BugsnagThreadViolationListener implements OnThreadViolationListener { + + private final Client client; + private final OnThreadViolationListener listener; + + public BugsnagThreadViolationListener() { + this(Bugsnag.getClient(), null); + } + + public BugsnagThreadViolationListener(@NonNull Client client) { + this(client, null); + } + + public BugsnagThreadViolationListener(@NonNull Client client, + @Nullable OnThreadViolationListener listener) { + this.client = client; + this.listener = listener; + } + + @Override + public void onThreadViolation(@NonNull Violation violation) { + if (client != null) { + client.notify(violation, new StrictModeOnErrorCallback( + "StrictMode policy violation detected: ThreadPolicy" + )); + } + if (listener != null) { + listener.onThreadViolation(violation); + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java b/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java new file mode 100644 index 0000000000..74faed3e18 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java @@ -0,0 +1,52 @@ +package com.bugsnag.android; + +import android.os.Build; +import android.os.StrictMode.OnVmViolationListener; +import android.os.strictmode.Violation; + +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; +import androidx.annotation.RequiresApi; + +/** + * Sends an error report to Bugsnag for each StrictMode VM policy violation that occurs in + * your app. + *

+ * You should use this class by instantiating Bugsnag in the normal way and then set the + * StrictMode policy with + * {@link android.os.StrictMode.VmPolicy.Builder#penaltyListener + * (Executor, OnVmViolationListener)}. + * This functionality is only supported on API 28+. + */ +@RequiresApi(api = Build.VERSION_CODES.P) +public class BugsnagVmViolationListener implements OnVmViolationListener { + + private final Client client; + private final OnVmViolationListener listener; + + public BugsnagVmViolationListener() { + this(Bugsnag.getClient(), null); + } + + public BugsnagVmViolationListener(@NonNull Client client) { + this(client, null); + } + + public BugsnagVmViolationListener(@NonNull Client client, + @Nullable OnVmViolationListener listener) { + this.client = client; + this.listener = listener; + } + + @Override + public void onVmViolation(@NonNull Violation violation) { + if (client != null) { + client.notify(violation, new StrictModeOnErrorCallback( + "StrictMode policy violation detected: VmPolicy" + )); + } + if (listener != null) { + listener.onVmViolation(violation); + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index 205e42c165..bdc21e2e38 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/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; @@ -23,12 +19,14 @@ import kotlin.Unit; import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; +import java.io.File; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; +import java.util.concurrent.Callable; import java.util.concurrent.RejectedExecutionException; /** @@ -67,24 +65,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 +111,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware { * @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 +130,53 @@ public class Client implements MetadataAware, CallbackAware, UserAware { }); // 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, 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()); + dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO); + 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); + registerLifecycleCallbacks(); - 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; - } - - 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 class Client implements MetadataAware, CallbackAware, UserAware { } // 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 class Client implements MetadataAware, CallbackAware, UserAware { @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 class Client implements MetadataAware, CallbackAware, UserAware { 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 class Client implements MetadataAware, CallbackAware, UserAware { 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 class Client implements MetadataAware, CallbackAware, UserAware { } } - 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,17 @@ public class Client implements MetadataAware, CallbackAware, UserAware { } } - private void loadPlugins(@NonNull Configuration configuration) { - NativeInterface.setClient(this); + private void loadPlugins(@NonNull final Configuration configuration) { + NativeInterface.setClient(Client.this); Set userPlugins = configuration.getPlugins(); pluginClient = new PluginClient(userPlugins, immutableConfig, logger); - pluginClient.loadPlugins(this); + pluginClient.loadPlugins(Client.this); } 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, @@ -381,6 +348,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware { } void setupNdkPlugin() { + if (!setupNdkDirectory()) { + logger.w("Failed to setup NDK directory."); + return; + } + String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath(); int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0; clientObservable.postNdkInstall(immutableConfig, lastRunInfoPath, crashes); @@ -388,6 +360,20 @@ public class Client implements MetadataAware, CallbackAware, UserAware { clientObservable.postNdkDeliverPending(); } + private boolean setupNdkDirectory() { + try { + return bgTaskService.submitTask(TaskType.IO, new Callable() { + @Override + public Boolean call() { + File outFile = new File(NativeInterface.getNativeReportPath()); + return outFile.exists() || outFile.mkdirs(); + } + }).get(); + } catch (Throwable exc) { + return false; + } + } + void addObserver(StateObserver observer) { metadataState.addObserver(observer); breadcrumbState.addObserver(observer); diff --git a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt new file mode 100644 index 0000000000..89b1b4f234 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt @@ -0,0 +1,58 @@ +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 + +/** + * 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 future { + AppDataCollector( + ctx, + ctx.packageManager, + cfg, + trackerModule.sessionTracker, + systemServiceModule.activityManager, + trackerModule.launchCrashTracker, + logger + ) + } + + private val rootDetector by future { + RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo) + } + + val deviceDataCollector by future { + DeviceDataCollector( + connectivity, + ctx, + ctx.resources, + deviceId, + deviceBuildInfo, + dataDir, + rootDetector, + bgTaskService, + logger + ) + } +} diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt index 5620f0bacd..5587ba7491 100644 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/app/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 } @@ -64,7 +66,7 @@ internal class DefaultDelivery( return DeliveryStatus.UNDELIVERED } catch (exception: IOException) { logger.w("IOException encountered in request", exception) - return DeliveryStatus.FAILURE + return DeliveryStatus.UNDELIVERED } catch (exception: Exception) { logger.w("Unexpected error delivering payload", exception) return DeliveryStatus.FAILURE diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 5181fa8303..44c4aa2900 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/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/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt b/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt index 5581acabf5..b9db53c296 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt +++ b/app/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/app/src/main/java/com/bugsnag/android/Event.java b/app/src/main/java/com/bugsnag/android/Event.java index 31fb88c074..fe68e04585 100644 --- a/app/src/main/java/com/bugsnag/android/Event.java +++ b/app/src/main/java/com/bugsnag/android/Event.java @@ -320,6 +320,10 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware { impl.updateSeverityInternal(severity); } + protected void updateSeverityReason(@NonNull @SeverityReason.SeverityReasonType String reason) { + impl.updateSeverityReason(reason); + } + void setApp(@NonNull AppWithState app) { impl.setApp(app); } diff --git a/app/src/main/java/com/bugsnag/android/EventInternal.kt b/app/src/main/java/com/bugsnag/android/EventInternal.kt index 8195a2025c..06ad16bc84 100644 --- a/app/src/main/java/com/bugsnag/android/EventInternal.kt +++ b/app/src/main/java/com/bugsnag/android/EventInternal.kt @@ -130,12 +130,21 @@ internal class EventInternal @JvmOverloads internal constructor( } protected fun updateSeverityInternal(severity: Severity) { - severityReason = SeverityReason.newInstance( + severityReason = SeverityReason( severityReason.severityReasonType, severity, + severityReason.unhandled, + severityReason.attributeValue + ) + } + + protected fun updateSeverityReason(@SeverityReason.SeverityReasonType reason: String) { + severityReason = SeverityReason( + reason, + severityReason.currentSeverity, + severityReason.unhandled, severityReason.attributeValue ) - this.severity = severity } fun getSeverityReasonType(): String = severityReason.severityReasonType diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt new file mode 100644 index 0000000000..790fd9e45b --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt @@ -0,0 +1,38 @@ +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 + +/** + * 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 future { + InternalReportDelegate( + contextModule.ctx, + cfg.logger, + cfg, + systemServiceModule.storageManager, + dataCollectionModule.appDataCollector, + dataCollectionModule.deviceDataCollector, + trackerModule.sessionTracker, + notifier, + bgTaskService + ) + } + + val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate) } +} diff --git a/app/src/main/java/com/bugsnag/android/EventStore.java b/app/src/main/java/com/bugsnag/android/EventStore.java index d22d8ddb24..e95fc6b1d4 100644 --- a/app/src/main/java/com/bugsnag/android/EventStore.java +++ b/app/src/main/java/com/bugsnag/android/EventStore.java @@ -11,7 +11,6 @@ import java.util.Collection; 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 @@ class EventStore extends FileStore { 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/app/src/main/java/com/bugsnag/android/FileStore.java b/app/src/main/java/com/bugsnag/android/FileStore.java index 4aae1e30e8..cbafdac431 100644 --- a/app/src/main/java/com/bugsnag/android/FileStore.java +++ b/app/src/main/java/com/bugsnag/android/FileStore.java @@ -63,13 +63,7 @@ abstract class FileStore { */ 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/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index 770cb65a92..4e6f125f7f 100644 --- a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/app/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/app/src/main/java/com/bugsnag/android/LibraryLoader.java b/app/src/main/java/com/bugsnag/android/LibraryLoader.java index 850a2f82c8..6d8f216b71 100644 --- a/app/src/main/java/com/bugsnag/android/LibraryLoader.java +++ b/app/src/main/java/com/bugsnag/android/LibraryLoader.java @@ -18,17 +18,29 @@ class LibraryLoader { * @param callback an OnErrorCallback * @return true if the library was loaded, false if not */ - boolean loadLibrary(String name, Client client, OnErrorCallback callback) { + boolean loadLibrary(final String name, final Client client, final OnErrorCallback callback) { + try { + client.bgTaskService.submitTask(TaskType.IO, new Runnable() { + @Override + public void run() { + loadLibInternal(name, client, callback); + } + }).get(); + return loaded; + } catch (Throwable exc) { + return false; + } + } + + void loadLibInternal(String name, Client client, OnErrorCallback callback) { if (!attemptedLoad.getAndSet(true)) { try { System.loadLibrary(name); loaded = true; - return true; } catch (UnsatisfiedLinkError error) { client.notify(error, callback); } } - return false; } boolean isLoaded() { diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index 56a000d90c..cb845ae389 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -56,7 +56,7 @@ public class NativeInterface { @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/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 5302719044..946d6df62d 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.10.1", + var version: String = "5.11.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java index 66bfa46f55..0d84d8a677 100644 --- a/app/src/main/java/com/bugsnag/android/SessionStore.java +++ b/app/src/main/java/com/bugsnag/android/SessionStore.java @@ -7,7 +7,6 @@ import androidx.annotation.Nullable; import java.io.File; import java.util.Comparator; -import java.util.Locale; import java.util.UUID; /** @@ -37,7 +36,7 @@ class SessionStore extends FileStore { 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/app/src/main/java/com/bugsnag/android/StorageModule.kt b/app/src/main/java/com/bugsnag/android/StorageModule.kt new file mode 100644 index 0000000000..2d4bff14ee --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/StorageModule.kt @@ -0,0 +1,47 @@ +package com.bugsnag.android + +import android.content.Context +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.dag.DependencyModule + +/** + * A dependency module which constructs the objects that store information to disk in Bugsnag. + */ +internal class StorageModule( + appContext: Context, + immutableConfig: ImmutableConfig, + logger: Logger +) : DependencyModule() { + + val sharedPrefMigrator by future { SharedPrefMigrator(appContext) } + + private val deviceIdStore by future { + DeviceIdStore( + appContext, + sharedPrefMigrator = sharedPrefMigrator, + logger = logger + ) + } + + val deviceId by future { deviceIdStore.loadDeviceId() } + + val userStore by future { + UserStore( + immutableConfig, + deviceId, + sharedPrefMigrator = sharedPrefMigrator, + logger = logger + ) + } + + val lastRunInfoStore by future { LastRunInfoStore(immutableConfig) } + + val sessionStore by future { SessionStore(immutableConfig, logger, null) } + + val lastRunInfo by future { + val info = lastRunInfoStore.load() + val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false) + lastRunInfoStore.persist(currentRunInfo) + info + } +} diff --git a/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt b/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt new file mode 100644 index 0000000000..4e2413889a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt @@ -0,0 +1,11 @@ +package com.bugsnag.android + +internal class StrictModeOnErrorCallback(private val errMsg: String) : OnErrorCallback { + override fun onError(event: Event): Boolean { + event.updateSeverityInternal(Severity.INFO) + event.updateSeverityReason(SeverityReason.REASON_STRICT_MODE) + val error = event.errors.firstOrNull() + error?.errorMessage = errMsg + return true + } +} diff --git a/app/src/main/java/com/bugsnag/android/TrackerModule.kt b/app/src/main/java/com/bugsnag/android/TrackerModule.kt new file mode 100644 index 0000000000..a293539f28 --- /dev/null +++ b/app/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/app/src/main/java/com/bugsnag/android/UserStore.kt b/app/src/main/java/com/bugsnag/android/UserStore.kt index 29ccd71a19..0a7b59448e 100644 --- a/app/src/main/java/com/bugsnag/android/UserStore.kt +++ b/app/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/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 5c434a7d32..3a5b42c59d 100644 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/app/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/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt new file mode 100644 index 0000000000..e327c8ee70 --- /dev/null +++ b/app/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/app/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt new file mode 100644 index 0000000000..9ece4ff604 --- /dev/null +++ b/app/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/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt new file mode 100644 index 0000000000..f766b0dd2c --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt @@ -0,0 +1,37 @@ +package com.bugsnag.android.internal.dag + +import com.bugsnag.android.BackgroundTaskService +import com.bugsnag.android.TaskType + +internal abstract class DependencyModule { + + private val properties = mutableListOf>() + + /** + * Creates a new [Lazy] property that is marked as an object that should be resolved off the + * main thread when [resolveDependencies] is called. + */ + fun future(initializer: () -> T): Lazy { + val lazy = lazy { + initializer() + } + properties.add(lazy) + return lazy + } + + /** + * 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) { + kotlin.runCatching { + bgTaskService.submitTask( + taskType, + Runnable { + properties.forEach { it.value } + } + ).get() + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt new file mode 100644 index 0000000000..eef01cbc63 --- /dev/null +++ b/app/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() +}