diff --git a/app/build.gradle b/app/build.gradle index 40205d27be..4854f26844 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -268,7 +268,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.9.2" + def bugsnag_version = "5.9.4" 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/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt index e270d2f96a..f39d817a20 100644 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -6,7 +6,6 @@ import android.content.pm.ApplicationInfo import android.content.pm.PackageManager import android.os.Build import android.os.SystemClock -import java.util.HashMap /** * Collects various data on the application state @@ -31,13 +30,19 @@ internal class AppDataCollector( private val releaseStage = config.releaseStage private val versionName = config.appVersion ?: packageInfo?.versionName - fun generateApp(): App = App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) + fun generateApp(): App = + App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) - fun generateAppWithState(): AppWithState = AppWithState( - config, binaryArch, packageName, releaseStage, versionName, codeBundleId, - getDurationMs(), calculateDurationInForeground(), sessionTracker.isInForeground, - launchCrashTracker.isLaunching() - ) + fun generateAppWithState(): AppWithState { + val inForeground = sessionTracker.isInForeground + val durationInForeground = calculateDurationInForeground(inForeground) + + return AppWithState( + config, binaryArch, packageName, releaseStage, versionName, codeBundleId, + getDurationMs(), durationInForeground, inForeground, + launchCrashTracker.isLaunching() + ) + } fun getAppDataMetadata(): MutableMap { val map = HashMap() @@ -102,9 +107,21 @@ internal class AppDataCollector( * * @return the duration in ms */ - internal fun calculateDurationInForeground(): Long? { + internal fun calculateDurationInForeground(inForeground: Boolean? = sessionTracker.isInForeground): Long? { + if (inForeground == null) { + return null + } + val nowMs = System.currentTimeMillis() - return sessionTracker.getDurationInForegroundMs(nowMs) + var durationMs: Long = 0 + + val sessionStartTimeMs: Long = sessionTracker.lastEnteredForegroundMs + + if (inForeground && sessionStartTimeMs != 0L) { + durationMs = nowMs - sessionStartTimeMs + } + + return if (durationMs > 0) durationMs else 0 } /** diff --git a/app/src/main/java/com/bugsnag/android/Breadcrumb.java b/app/src/main/java/com/bugsnag/android/Breadcrumb.java index 698223a6c0..4da160a4ee 100644 --- a/app/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/app/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -94,6 +94,11 @@ public class Breadcrumb implements JsonStream.Streamable { return impl.getTimestamp(); } + @NonNull + String getStringTimestamp() { + return DateUtils.toIso8601(impl.getTimestamp()); + } + @Override public void toStream(@NonNull JsonStream stream) throws IOException { impl.toStream(stream); diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index 590bd54f3c..6ed0a6f7b6 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -19,6 +19,7 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import kotlin.Unit; +import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; import java.util.ArrayList; @@ -83,7 +84,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { final Logger logger; final DeliveryDelegate deliveryDelegate; - final ClientObservable clientObservable = new ClientObservable(); + final ClientObservable clientObservable; private PluginClient pluginClient; final Notifier notifier = new Notifier(); @@ -93,6 +94,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { final LastRunInfoStore lastRunInfoStore; final LaunchCrashTracker launchCrashTracker; final BackgroundTaskService bgTaskService = new BackgroundTaskService(); + private final ExceptionHandler exceptionHandler; /** * Initialize a Bugsnag client @@ -142,6 +144,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { immutableConfig = sanitiseConfiguration(appContext, configuration, connectivity); logger = immutableConfig.getLogger(); warnIfNotAppContext(androidContext); + clientObservable = new ClientObservable(); // Set up breadcrumbs callbackState = configuration.impl.callbackState.copy(); @@ -213,14 +216,16 @@ public class Client implements MetadataAware, CallbackAware, UserAware { immutableConfig, breadcrumbState, notifier, bgTaskService); // Install a default exception handler with this client + exceptionHandler = new ExceptionHandler(this, logger); if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) { - new ExceptionHandler(this, logger); + exceptionHandler.install(); } // register a receiver for automatic breadcrumbs systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService); registerOrientationChangeListener(); + registerMemoryTrimListener(); // load last run info lastRunInfoStore = new LastRunInfoStore(immutableConfig); @@ -249,6 +254,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { ContextState contextState, CallbackState callbackState, UserState userState, + ClientObservable clientObservable, Context appContext, @NonNull DeviceDataCollector deviceDataCollector, @NonNull AppDataCollector appDataCollector, @@ -264,13 +270,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware { Logger logger, DeliveryDelegate deliveryDelegate, LastRunInfoStore lastRunInfoStore, - LaunchCrashTracker launchCrashTracker + LaunchCrashTracker launchCrashTracker, + ExceptionHandler exceptionHandler ) { this.immutableConfig = immutableConfig; this.metadataState = metadataState; this.contextState = contextState; this.callbackState = callbackState; this.userState = userState; + this.clientObservable = clientObservable; this.appContext = appContext; this.deviceDataCollector = deviceDataCollector; this.appDataCollector = appDataCollector; @@ -288,6 +296,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { this.lastRunInfoStore = lastRunInfoStore; this.launchCrashTracker = launchCrashTracker; this.lastRunInfo = null; + this.exceptionHandler = exceptionHandler; } private LastRunInfo loadLastRunInfo() { @@ -350,6 +359,18 @@ public class Client implements MetadataAware, CallbackAware, UserAware { ContextExtensionsKt.registerReceiverSafe(appContext, receiver, configFilter, logger); } + private void registerMemoryTrimListener() { + appContext.registerComponentCallbacks(new ClientComponentCallbacks( + new Function1() { + @Override + public Unit invoke(Boolean isLowMemory) { + clientObservable.postMemoryTrimEvent(isLowMemory); + return null; + } + } + )); + } + void setupNdkPlugin() { String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath(); int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0; @@ -369,6 +390,17 @@ public class Client implements MetadataAware, CallbackAware, UserAware { launchCrashTracker.addObserver(observer); } + void unregisterObserver(Observer observer) { + metadataState.deleteObserver(observer); + breadcrumbState.deleteObserver(observer); + sessionTracker.deleteObserver(observer); + clientObservable.deleteObserver(observer); + userState.deleteObserver(observer); + contextState.deleteObserver(observer); + deliveryDelegate.deleteObserver(observer); + launchCrashTracker.deleteObserver(observer); + } + /** * Sends initial state values for Metadata/User/Context to any registered observers. */ @@ -990,13 +1022,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { @SuppressWarnings("rawtypes") @Nullable Plugin getPlugin(@NonNull Class clz) { - Set plugins = pluginClient.getPlugins(); - for (Plugin plugin : plugins) { - if (plugin.getClass().equals(clz)) { - return plugin; - } - } - return null; + return pluginClient.findPlugin(clz); } Notifier getNotifier() { @@ -1006,4 +1032,18 @@ public class Client implements MetadataAware, CallbackAware, UserAware { MetadataState getMetadataState() { return metadataState; } + + void setAutoNotify(boolean autoNotify) { + pluginClient.setAutoNotify(this, autoNotify); + + if (autoNotify) { + exceptionHandler.install(); + } else { + exceptionHandler.uninstall(); + } + } + + void setAutoDetectAnrs(boolean autoDetectAnrs) { + pluginClient.setAutoDetectAnrs(this, autoDetectAnrs); + } } diff --git a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt new file mode 100644 index 0000000000..74c2435598 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt @@ -0,0 +1,14 @@ +package com.bugsnag.android + +import android.content.ComponentCallbacks +import android.content.res.Configuration + +internal class ClientComponentCallbacks( + val callback: (Boolean) -> Unit +) : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) {} + + override fun onLowMemory() { + callback(true) + } +} diff --git a/app/src/main/java/com/bugsnag/android/ClientObservable.kt b/app/src/main/java/com/bugsnag/android/ClientObservable.kt index a654a54e24..88e97d745e 100644 --- a/app/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/app/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -6,6 +6,10 @@ internal class ClientObservable : BaseObservable() { notifyObservers(StateEvent.UpdateOrientation(orientation)) } + fun postMemoryTrimEvent(isLowMemory: Boolean) { + notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory)) + } + fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) { notifyObservers( StateEvent.Install( diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 98aecee197..2cd4795134 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -85,8 +85,7 @@ internal class DeviceDataCollector( fun getDeviceMetadata(): Map { val map = HashMap() - map["batteryLevel"] = getBatteryLevel() - map["charging"] = isCharging() + populateBatteryInfo(into = map) map["locationStatus"] = getLocationStatus() map["networkAccess"] = getNetworkAccess() map["brand"] = buildInfo.brand @@ -126,41 +125,31 @@ internal class DeviceDataCollector( private fun getScreenDensityDpi(): Int? = displayMetrics?.densityDpi /** - * Get the current battery charge level, eg 0.3 + * Populate the current Battery Info into the specified MutableMap */ - private fun getBatteryLevel(): Float? { + private fun populateBatteryInfo(into: MutableMap) { try { val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger) if (batteryStatus != null) { - return batteryStatus.getIntExtra( - "level", - -1 - ) / batteryStatus.getIntExtra("scale", -1).toFloat() - } - } catch (exception: Exception) { - logger.w("Could not get batteryLevel") - } - return null - } + val level = batteryStatus.getIntExtra("level", -1) + val scale = batteryStatus.getIntExtra("scale", -1) - /** - * Is the device currently charging/full battery? - */ - private fun isCharging(): Boolean? { - try { - val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED) - val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger) + if (level != -1 || scale != -1) { + val batteryLevel: Float = level.toFloat() / scale.toFloat() + into["batteryLevel"] = batteryLevel + } - if (batteryStatus != null) { val status = batteryStatus.getIntExtra("status", -1) - return status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL + val charging = + status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL + + into["charging"] = charging } } catch (exception: Exception) { - logger.w("Could not get charging status") + logger.w("Could not get battery status") } - return null } /** diff --git a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java index a10ee3f263..4e5f9beab9 100644 --- a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java @@ -23,9 +23,16 @@ class ExceptionHandler implements UncaughtExceptionHandler { this.client = client; this.logger = logger; this.originalHandler = Thread.getDefaultUncaughtExceptionHandler(); + } + + void install() { Thread.setDefaultUncaughtExceptionHandler(this); } + void uninstall() { + Thread.setDefaultUncaughtExceptionHandler(originalHandler); + } + @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); diff --git a/app/src/main/java/com/bugsnag/android/LibraryLoader.java b/app/src/main/java/com/bugsnag/android/LibraryLoader.java index f8dcb51a71..850a2f82c8 100644 --- a/app/src/main/java/com/bugsnag/android/LibraryLoader.java +++ b/app/src/main/java/com/bugsnag/android/LibraryLoader.java @@ -4,7 +4,8 @@ import java.util.concurrent.atomic.AtomicBoolean; class LibraryLoader { - private AtomicBoolean attemptedLoad = new AtomicBoolean(); + private final AtomicBoolean attemptedLoad = new AtomicBoolean(); + private boolean loaded = false; /** * Attempts to load a native library, returning false if the load was unsuccessful. @@ -21,6 +22,7 @@ class LibraryLoader { if (!attemptedLoad.getAndSet(true)) { try { System.loadLibrary(name); + loaded = true; return true; } catch (UnsatisfiedLinkError error) { client.notify(error, callback); @@ -28,4 +30,8 @@ class LibraryLoader { } return false; } + + boolean isLoaded() { + return loaded; + } } diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index 032f74bd3e..30e857b601 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -404,4 +404,24 @@ public class NativeInterface { public static Logger getLogger() { return getClient().getConfig().getLogger(); } + + /** + * Switches automatic error detection on/off after Bugsnag has initialized. + * This is required to support legacy functionality in Unity. + * + * @param autoNotify whether errors should be automatically detected. + */ + public static void setAutoNotify(boolean autoNotify) { + getClient().setAutoNotify(autoNotify); + } + + /** + * Switches automatic ANR detection on/off after Bugsnag has initialized. + * This is required to support legacy functionality in Unity. + * + * @param autoDetectAnrs whether ANRs should be automatically detected. + */ + public static void setAutoDetectAnrs(boolean autoDetectAnrs) { + getClient().setAutoDetectAnrs(autoDetectAnrs); + } } diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 339a01786b..6d02f69e7e 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.9.3", + var version: String = "5.9.4", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/PluginClient.kt b/app/src/main/java/com/bugsnag/android/PluginClient.kt index 8d3671ee42..d4b4f93127 100644 --- a/app/src/main/java/com/bugsnag/android/PluginClient.kt +++ b/app/src/main/java/com/bugsnag/android/PluginClient.kt @@ -2,11 +2,21 @@ package com.bugsnag.android internal class PluginClient( userPlugins: Set, - immutableConfig: ImmutableConfig, + private val immutableConfig: ImmutableConfig, private val logger: Logger ) { - protected val plugins: Set + companion object { + private const val NDK_PLUGIN = "com.bugsnag.android.NdkPlugin" + private const val ANR_PLUGIN = "com.bugsnag.android.AnrPlugin" + private const val RN_PLUGIN = "com.bugsnag.android.BugsnagReactNativePlugin" + } + + private val plugins: Set + + private val ndkPlugin = instantiatePlugin(NDK_PLUGIN) + private val anrPlugin = instantiatePlugin(ANR_PLUGIN) + private val rnPlugin = instantiatePlugin(RN_PLUGIN) init { val set = mutableSetOf() @@ -14,13 +24,9 @@ internal class PluginClient( // instantiate ANR + NDK plugins by reflection as bugsnag-android-core has no // direct dependency on the artefacts - if (immutableConfig.enabledErrorTypes.ndkCrashes) { - instantiatePlugin("com.bugsnag.android.NdkPlugin")?.let { set.add(it) } - } - if (immutableConfig.enabledErrorTypes.anrs) { - instantiatePlugin("com.bugsnag.android.AnrPlugin")?.let { set.add(it) } - } - instantiatePlugin("com.bugsnag.android.BugsnagReactNativePlugin")?.let { set.add(it) } + ndkPlugin?.let(set::add) + anrPlugin?.let(set::add) + rnPlugin?.let(set::add) plugins = set.toSet() } @@ -37,11 +43,51 @@ internal class PluginClient( } } - fun loadPlugins(client: Client) = plugins.forEach { - try { - it.load(client) - } catch (exc: Throwable) { - logger.e("Failed to load plugin $it, continuing with initialisation.", exc) + fun loadPlugins(client: Client) { + plugins.forEach { plugin -> + try { + loadPluginInternal(plugin, client) + } catch (exc: Throwable) { + logger.e("Failed to load plugin $plugin, continuing with initialisation.", exc) + } + } + } + + fun setAutoNotify(client: Client, autoNotify: Boolean) { + setAutoDetectAnrs(client, autoNotify) + + if (autoNotify) { + ndkPlugin?.load(client) + } else { + ndkPlugin?.unload() + } + } + + fun setAutoDetectAnrs(client: Client, autoDetectAnrs: Boolean) { + if (autoDetectAnrs) { + anrPlugin?.load(client) + } else { + anrPlugin?.unload() + } + } + + fun findPlugin(clz: Class<*>): Plugin? = plugins.find { it.javaClass == clz } + + private fun loadPluginInternal(plugin: Plugin, client: Client) { + val name = plugin.javaClass.name + val errorTypes = immutableConfig.enabledErrorTypes + + // only initialize NDK/ANR plugins if automatic detection enabled + if (name == NDK_PLUGIN) { + if (errorTypes.ndkCrashes) { + plugin.load(client) + } + } else if (name == ANR_PLUGIN) { + if (errorTypes.anrs) { + plugin.load(client) + } + } else { + plugin.load(client) } } } diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index b1ef742274..c02fdd3b51 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -369,21 +369,8 @@ class SessionTracker extends BaseObservable { return foregroundDetector.isInForeground(); } - //FUTURE:SM This shouldnt be here - @Nullable - Long getDurationInForegroundMs(long nowMs) { - long durationMs = 0; - long sessionStartTimeMs = lastEnteredForegroundMs.get(); - - Boolean inForeground = isInForeground(); - - if (inForeground == null) { - return null; - } - if (inForeground && sessionStartTimeMs != 0) { - durationMs = nowMs - sessionStartTimeMs; - } - return durationMs > 0 ? durationMs : 0; + long getLastEnteredForegroundMs() { + return lastEnteredForegroundMs.get(); } @Nullable diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt index 8e53a58bfb..bc3c83e0ea 100644 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/app/src/main/java/com/bugsnag/android/StateEvent.kt @@ -42,4 +42,6 @@ sealed class StateEvent { class UpdateOrientation(val orientation: String?) : StateEvent() class UpdateUser(val user: User) : StateEvent() + + class UpdateMemoryTrimEvent(val isLowMemory: Boolean) : StateEvent() }