diff --git a/CHANGELOG.md b/CHANGELOG.md index f88cf1fafc..db31db94e9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## 5.9.4 (2021-05-26) + +* Unity: add methods for setting autoNotify and autoDetectAnrs + [#1233](https://github.com/bugsnag/bugsnag-android/pull/1233) + +* Including bugsnag.h in C++ code will no longer cause writable-strings warnings + [1260](https://github.com/bugsnag/bugsnag-android/pull/1260) + +* Small performance improvements to device and app state collection + [1258](https://github.com/bugsnag/bugsnag-android/pull/1258) + +* NDK: lowMemory attribute is now reported as expected + [1262](https://github.com/bugsnag/bugsnag-android/pull/1262) + +* Don't include loglog.so in ndk plugin builds performed on Linux + [1263](https://github.com/bugsnag/bugsnag-android/pull/1263) + ## 5.9.3 (2021-05-18) * Avoid unnecessary collection of Thread stacktraces diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java new file mode 100644 index 0000000000..3d0804b030 --- /dev/null +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java @@ -0,0 +1,62 @@ +package com.bugsnag.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.bugsnag.android.ObserverInterfaceTest.BugsnagTestObserver; + +import android.content.ComponentCallbacks; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Heavily mocked test to ensure that onLowMemory events are distributed to Client Observers + */ +@RunWith(MockitoJUnitRunner.class) +public class MemoryTrimTest { + + @Spy + Context context = ApplicationProvider.getApplicationContext(); + + @Captor + ArgumentCaptor componentCallbacksCaptor; + + @Test + public void onLowMemoryEvent() { + when(context.getApplicationContext()).thenReturn(context); + Client client = new Client(context, BugsnagTestUtils.generateConfiguration()); + + // capture the registered ComponentCallbacks + verify(context, times(1)).registerComponentCallbacks(componentCallbacksCaptor.capture()); + + BugsnagTestObserver observer = new BugsnagTestObserver(); + client.registerObserver(observer); + + ComponentCallbacks callbacks = componentCallbacksCaptor.getValue(); + callbacks.onLowMemory(); + + assertEquals(1, observer.observed.size()); + Object observedEvent = observer.observed.get(0); + + assertTrue( + "observed event should be UpdateMemoryTrimEvent", + observedEvent instanceof StateEvent.UpdateMemoryTrimEvent + ); + + assertTrue( + "observed event should be marked isLowMemory", + ((StateEvent.UpdateMemoryTrimEvent) observedEvent).isLowMemory() + ); + } + +} diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index 4b1315f565..1adc99f61f 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -207,7 +207,7 @@ private T findMessageInQueue(Class argClass) { } static class BugsnagTestObserver implements Observer { - private final ArrayList observed; + final ArrayList observed; BugsnagTestObserver() { observed = new ArrayList<>(4); diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt index e270d2f96a..f39d817a20 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java index 698223a6c0..4da160a4ee 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -94,6 +94,11 @@ public Date getTimestamp() { 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/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 590bd54f3c..6ed0a6f7b6 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 @@ -19,6 +19,7 @@ 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 Unit invoke(Boolean hasConnection, String networkState) { 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 Unit invoke(String activity, Map metadata) { 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 Unit invoke(String activity, Map metadata) { ContextState contextState, CallbackState callbackState, UserState userState, + ClientObservable clientObservable, Context appContext, @NonNull DeviceDataCollector deviceDataCollector, @NonNull AppDataCollector appDataCollector, @@ -264,13 +270,15 @@ public Unit invoke(String activity, Map metadata) { 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 Unit invoke(String activity, Map metadata) { this.lastRunInfoStore = lastRunInfoStore; this.launchCrashTracker = launchCrashTracker; this.lastRunInfo = null; + this.exceptionHandler = exceptionHandler; } private LastRunInfo loadLastRunInfo() { @@ -350,6 +359,18 @@ public Unit invoke(String oldOrientation, String newOrientation) { 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 @@ void registerObserver(Observer observer) { 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 @@ Logger getLogger() { @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 @@ Notifier getNotifier() { 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/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt new file mode 100644 index 0000000000..74c2435598 --- /dev/null +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt index a654a54e24..88e97d745e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 98aecee197..2cd4795134 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 @@ -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/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java index a10ee3f263..4e5f9beab9 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/LibraryLoader.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/LibraryLoader.java index f8dcb51a71..850a2f82c8 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/LibraryLoader.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/LibraryLoader.java @@ -4,7 +4,8 @@ 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 @@ boolean loadLibrary(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); @@ -28,4 +30,8 @@ boolean loadLibrary(String name, Client client, OnErrorCallback callback) { } return false; } + + boolean isLoaded() { + return loaded; + } } 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 032f74bd3e..30e857b601 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 @@ -404,4 +404,24 @@ public static Event createEvent(@Nullable Throwable exc, 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/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt index 339a01786b..6d02f69e7e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Notifier.kt +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt index 8d3671ee42..d4b4f93127 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/PluginClient.kt +++ b/bugsnag-android-core/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/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java index b1ef742274..c02fdd3b51 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SessionTracker.java @@ -369,21 +369,8 @@ Boolean isInForeground() { 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/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt index 8e53a58bfb..bc3c83e0ea 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/bugsnag-android-core/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() } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt new file mode 100644 index 0000000000..1a3d483ee0 --- /dev/null +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/AppDataCollectorForegroundTest.kt @@ -0,0 +1,92 @@ +package com.bugsnag.android + +import android.content.Context +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull +import org.junit.Assert.assertTrue +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mock +import org.mockito.Mockito.`when` +import org.mockito.Mockito.times +import org.mockito.Mockito.verify +import org.mockito.junit.MockitoJUnitRunner + +/** + * Test the contract for `AppWithState.inForeground` and `AppWithState.durationInForeground` as + * produced by `AppDataCollector.generateAppWithState` + */ +@RunWith(MockitoJUnitRunner::class) +class AppDataCollectorForegroundTest { + + @Mock + internal lateinit var appContext: Context + + @Mock + internal lateinit var sessionTracker: SessionTracker + + @Mock + internal lateinit var launchCrashTracker: LaunchCrashTracker + + private lateinit var appDataCollector: AppDataCollector + + @Before + fun setUp() { + `when`(appContext.packageName).thenReturn("test.package.name") + + appDataCollector = AppDataCollector( + appContext, + null, + BugsnagTestUtils.generateImmutableConfig(), + sessionTracker, + null, + launchCrashTracker, + NoopLogger + ) + } + + /** + * When the app is detected "in the foreground" both properties should be populated as such + */ + @Test + fun durationInForeground() { + val time = System.currentTimeMillis() + val lastForegroundTime = time - 1000L + + `when`(sessionTracker.isInForeground).thenReturn(true) + `when`(sessionTracker.lastEnteredForegroundMs).thenReturn(lastForegroundTime) + + val appWithState = appDataCollector.generateAppWithState() + assertEquals(true, appWithState.inForeground) + // allow for 20ms of error + assertTrue( + "Unexpected durationInForeground: ${appWithState.durationInForeground}", + appWithState.durationInForeground in 1000L..1020L + ) + + verify(sessionTracker, times(1)).isInForeground + } + + /** + * When the app is detected "in the background", the durationInForeground should be zero (0) + */ + @Test + fun inBackground() { + `when`(sessionTracker.isInForeground).thenReturn(false) + val appWithState = appDataCollector.generateAppWithState() + assertEquals(false, appWithState.inForeground) + assertEquals(0L, appWithState.durationInForeground) + } + + /** + * When the foreground detection is not possible, both properties should be `null` + */ + @Test + fun foregroundNotAvailable() { + `when`(sessionTracker.isInForeground).thenReturn(null) + val appWithState = appDataCollector.generateAppWithState() + assertNull(appWithState.inForeground) + assertNull(appWithState.durationInForeground) + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbFacadeTest.java b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbFacadeTest.java index 5b967a8f59..6dc00640fd 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbFacadeTest.java +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/BreadcrumbFacadeTest.java @@ -75,4 +75,9 @@ public void metadataValid() { public void dateValid() { assertEquals(new Date(0).getTime(), crumb.getTimestamp().getTime()); } + + @Test + public void stringDateValid() { + assertEquals("1970-01-01T00:00:00.000Z", crumb.getStringTimestamp()); + } } 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 b4e54fc241..052bea11b0 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 @@ -8,7 +8,6 @@ import static org.mockito.Mockito.when; import android.content.Context; -import android.content.SharedPreferences; import android.os.storage.StorageManager; import androidx.annotation.NonNull; @@ -23,6 +22,8 @@ import java.util.Collections; import java.util.HashMap; import java.util.Map; +import java.util.Observable; +import java.util.Observer; @SuppressWarnings("ConstantConditions") @RunWith(MockitoJUnitRunner.class) @@ -43,6 +44,9 @@ public class ClientFacadeTest { @Mock UserState userState; + @Mock + ClientObservable clientObservable; + @Mock Context appContext; @@ -73,9 +77,6 @@ public class ClientFacadeTest { @Mock SessionLifecycleCallback sessionLifecycleCallback; - @Mock - SharedPreferences sharedPrefs; - @Mock Connectivity connectivity; @@ -97,6 +98,9 @@ public class ClientFacadeTest { @Mock LaunchCrashTracker launchCrashTracker; + @Mock + ExceptionHandler exceptionHandler; + private Client client; private InterceptingLogger logger; @@ -112,6 +116,7 @@ public void setUp() { contextState, callbackState, userState, + clientObservable, appContext, deviceDataCollector, appDataCollector, @@ -127,7 +132,8 @@ public void setUp() { logger, deliveryDelegate, lastRunInfoStore, - launchCrashTracker + launchCrashTracker, + exceptionHandler ); // required fields for generating an event @@ -482,4 +488,44 @@ public void markLaunchCompleted() { client.markLaunchCompleted(); verify(launchCrashTracker, times(1)).markLaunchCompleted(); } + + + @Test + public void registerObserver() { + Observer observer = new Observer() { + @Override + public void update(Observable observable, Object arg) { + } + }; + client.registerObserver(observer); + + verify(metadataState, times(1)).addObserver(observer); + verify(breadcrumbState, times(1)).addObserver(observer); + verify(sessionTracker, times(1)).addObserver(observer); + verify(clientObservable, times(1)).addObserver(observer); + verify(userState, times(1)).addObserver(observer); + verify(contextState, times(1)).addObserver(observer); + verify(deliveryDelegate, times(1)).addObserver(observer); + verify(launchCrashTracker, times(1)).addObserver(observer); + } + + @Test + public void unregisterObserver() { + Observer observer = new Observer() { + @Override + public void update(Observable observable, Object arg) { + } + }; + client.unregisterObserver(observer); + + verify(metadataState, times(1)).deleteObserver(observer); + verify(breadcrumbState, times(1)).deleteObserver(observer); + verify(sessionTracker, times(1)).deleteObserver(observer); + verify(clientObservable, times(1)).deleteObserver(observer); + verify(userState, times(1)).deleteObserver(observer); + verify(contextState, times(1)).deleteObserver(observer); + verify(deliveryDelegate, times(1)).deleteObserver(observer); + verify(launchCrashTracker, times(1)).deleteObserver(observer); + } + } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt index db41e4cced..1ba84aa619 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/ExceptionHandlerTest.kt @@ -35,7 +35,13 @@ internal class ExceptionHandlerTest { @Test fun handlerInstalled() { val exceptionHandler = ExceptionHandler(client, NoopLogger) + assertSame(originalHandler, Thread.getDefaultUncaughtExceptionHandler()) + + exceptionHandler.install() assertSame(exceptionHandler, Thread.getDefaultUncaughtExceptionHandler()) + + exceptionHandler.uninstall() + assertSame(originalHandler, Thread.getDefaultUncaughtExceptionHandler()) } @Test diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/LibraryLoaderTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/LibraryLoaderTest.kt index 970f4d60f4..8b3d8dbb97 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/LibraryLoaderTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/LibraryLoaderTest.kt @@ -20,6 +20,7 @@ class LibraryLoaderTest { val libraryLoader = LibraryLoader() val loaded = libraryLoader.loadLibrary("foo", client) { true } assertFalse(loaded) + assertFalse(libraryLoader.isLoaded) verify(client, times(1)).notify(any(), any()) } @@ -28,10 +29,12 @@ class LibraryLoaderTest { val libraryLoader = LibraryLoader() var loaded = libraryLoader.loadLibrary("foo", client) { true } assertFalse(loaded) + assertFalse(libraryLoader.isLoaded) // duplicate calls only invoke System.loadLibrary once loaded = libraryLoader.loadLibrary("foo", client) { true } assertFalse(loaded) + assertFalse(libraryLoader.isLoaded) verify(client, times(1)).notify(any(), any()) } } 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 68d736e535..8a63a710d8 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 @@ -223,4 +223,16 @@ internal class NativeInterfaceApiTest { NativeInterface.notify("SIGPIPE", "SIGSEGV 11", Severity.ERROR, arrayOf()) verify(client, times(1)).notify(any(), any()) } + + @Test + fun autoDetectAnrs() { + NativeInterface.setAutoDetectAnrs(true) + verify(client, times(1)).setAutoDetectAnrs(true) + } + + @Test + fun autoNotify() { + NativeInterface.setAutoNotify(true) + verify(client, times(1)).setAutoNotify(true) + } } diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/PluginClientTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/PluginClientTest.kt index 6ebce1c8fd..44ffeb1618 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/PluginClientTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/PluginClientTest.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import org.junit.Assert.assertEquals +import org.junit.Assert.assertNull import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -24,4 +26,11 @@ class PluginClientTest { pluginClient.loadPlugins(client) Mockito.verify(plugin, times(1)).load(client) } + + @Test + fun findPlugin() { + val pluginClient = PluginClient(setOf(plugin), config, NoopLogger) + assertNull(pluginClient.findPlugin(String::class.java)) + assertEquals(plugin, pluginClient.findPlugin(plugin::class.java)) + } } diff --git a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt index 68b7c83444..f4bc87c22e 100644 --- a/bugsnag-plugin-android-anr/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-anr/src/main/CMakeLists.txt @@ -12,17 +12,10 @@ add_library( # Specifies the name of the library. include_directories(jni) -find_library( # Defines the name of the path variable that stores the - # location of the NDK library. - log-lib - # Specifies the name of the NDK library that - # CMake needs to locate. - log ) - target_link_libraries( # Specifies the target library. bugsnag-plugin-android-anr # Links the log library to the target library. - ${log-lib}) + log) set_target_properties(bugsnag-plugin-android-anr PROPERTIES diff --git a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt index 9858689943..2091b2ba49 100644 --- a/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt +++ b/bugsnag-plugin-android-anr/src/main/java/com/bugsnag/android/AnrPlugin.kt @@ -2,6 +2,7 @@ package com.bugsnag.android import android.os.Handler import android.os.Looper +import java.util.concurrent.atomic.AtomicBoolean internal class AnrPlugin : Plugin { @@ -20,7 +21,8 @@ internal class AnrPlugin : Plugin { } } - private val loader = LibraryLoader() + private val libraryLoader = LibraryLoader() + private val oneTimeSetupPerformed = AtomicBoolean(false) private lateinit var client: Client private val collector = AnrDetailsCollector() @@ -37,31 +39,14 @@ internal class AnrPlugin : Plugin { } override fun load(client: Client) { - val loaded = loader.loadLibrary("bugsnag-plugin-android-anr", client) { - val error = it.errors[0] - error.errorClass = "AnrLinkError" - error.errorMessage = LOAD_ERR_MSG - true - } + this.client = client - if (loaded) { - @Suppress("UNCHECKED_CAST") - val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? - if (clz != null) { - val ndkPlugin = client.getPlugin(clz) - if (ndkPlugin != null) { - val method = ndkPlugin.javaClass.getMethod("getUnwindStackFunction") - @Suppress("UNCHECKED_CAST") - val function = method.invoke(ndkPlugin) as Long - setUnwindFunction(function) - } - } - - // this must be run from the main thread as the SIGQUIT is sent to the main thread, - // and if the handler is installed on a background thread instead we receive no signal + if (!oneTimeSetupPerformed.getAndSet(true)) { + performOneTimeSetup(client) + } + if (libraryLoader.isLoaded) { Handler(Looper.getMainLooper()).post( Runnable { - this.client = client enableAnrReporting() client.logger.i("Initialised ANR Plugin") } @@ -71,7 +56,32 @@ internal class AnrPlugin : Plugin { } } - override fun unload() = disableAnrReporting() + private fun performOneTimeSetup(client: Client) { + libraryLoader.loadLibrary("bugsnag-plugin-android-anr", client) { + val error = it.errors[0] + error.errorClass = "AnrLinkError" + error.errorMessage = LOAD_ERR_MSG + true + } + @Suppress("UNCHECKED_CAST") + val clz = loadClass("com.bugsnag.android.NdkPlugin") as Class? + if (clz != null) { + val ndkPlugin = client.getPlugin(clz) + if (ndkPlugin != null) { + val method = ndkPlugin.javaClass.getMethod("getUnwindStackFunction") + + @Suppress("UNCHECKED_CAST") + val function = method.invoke(ndkPlugin) as Long + setUnwindFunction(function) + } + } + } + + override fun unload() { + if (libraryLoader.isLoaded) { + disableAnrReporting() + } + } /** * Notifies bugsnag that an ANR has occurred, by generating an Error report and populating it diff --git a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt index fd72ab9efe..f087e78c1b 100644 --- a/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt +++ b/bugsnag-plugin-android-ndk/src/main/CMakeLists.txt @@ -33,19 +33,10 @@ include_directories( target_include_directories(bugsnag-ndk PRIVATE ${BUGSNAG_DIR}/assets/include) -find_library( # Defines the name of the path variable that stores the - # location of the NDK library. - log-lib - - # Specifies the name of the NDK library that - # CMake needs to locate. - log ) - target_link_libraries( # Specifies the target library. bugsnag-ndk - # Links the log library to the target library. - ${log-lib}) + log) set_target_properties(bugsnag-ndk PROPERTIES diff --git a/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h b/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h index 1676ec7f1b..754ad091b8 100644 --- a/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h +++ b/bugsnag-plugin-android-ndk/src/main/assets/include/bugsnag.h @@ -16,29 +16,38 @@ typedef bool (*bsg_on_error)(void *); * @param env The JNI environment to use when using convenience methods */ void bugsnag_start(JNIEnv *env); + /** * Sends an error report to Bugsnag * @param name The name of the error * @param message The error message * @param severity The severity of the error */ -void bugsnag_notify(char *name, char *message, bugsnag_severity severity); -void bugsnag_notify_env(JNIEnv *env, char *name, char *message, +void bugsnag_notify(const char *name, const char *message, + bugsnag_severity severity); + +void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, bugsnag_severity severity); + /** * Set the current user * @param id The identifier of the user * @param email The user's email * @param name The user's name */ -void bugsnag_set_user(char *id, char *email, char *name); -void bugsnag_set_user_env(JNIEnv *env, char *id, char *email, char *name); +void bugsnag_set_user(const char *id, const char *email, const char *name); + +void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, + const char *name); + /** * Leave a breadcrumb, indicating an event of significance which will be logged * in subsequent error reports */ -void bugsnag_leave_breadcrumb(char *message, bugsnag_breadcrumb_type type); -void bugsnag_leave_breadcrumb_env(JNIEnv *env, char *message, +void bugsnag_leave_breadcrumb(const char *message, + bugsnag_breadcrumb_type type); + +void bugsnag_leave_breadcrumb_env(JNIEnv *env, const char *message, bugsnag_breadcrumb_type type); /** diff --git a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h index daf69f4b20..fceea2bc2f 100644 --- a/bugsnag-plugin-android-ndk/src/main/assets/include/event.h +++ b/bugsnag-plugin-android-ndk/src/main/assets/include/event.h @@ -91,7 +91,7 @@ char *bugsnag_event_get_api_key(void *event_ptr); * @param event_ptr a pointer to the event supplied in an on_error callback * @param value the new event api key value, which cannot be NULL */ -void bugsnag_event_set_api_key(void *event_ptr, char *value); +void bugsnag_event_set_api_key(void *event_ptr, const char *value); /* Accessors for event.context */ @@ -107,7 +107,7 @@ char *bugsnag_event_get_context(void *event_ptr); * @param event_ptr a pointer to the event supplied in an on_error callback * @param value the new event context value, which can be NULL */ -void bugsnag_event_set_context(void *event_ptr, char *value); +void bugsnag_event_set_context(void *event_ptr, const char *value); /* Accessors for event.app */ @@ -135,10 +135,11 @@ char *bugsnag_app_get_binary_arch(void *event_ptr); * @param event_ptr - a pointer to the bugsnag event * @param value - the new value for the binary_arch field (nullable) */ -void bugsnag_app_set_binary_arch(void *event_ptr, char *value); +void bugsnag_app_set_binary_arch(void *event_ptr, const char *value); char *bugsnag_app_get_build_uuid(void *event_ptr); -void bugsnag_app_set_build_uuid(void *event_ptr, char *value); + +void bugsnag_app_set_build_uuid(void *event_ptr, const char *value); time_t bugsnag_app_get_duration(void *event_ptr); void bugsnag_app_set_duration(void *event_ptr, time_t value); @@ -147,7 +148,8 @@ time_t bugsnag_app_get_duration_in_foreground(void *event_ptr); void bugsnag_app_set_duration_in_foreground(void *event_ptr, time_t value); char *bugsnag_app_get_id(void *event_ptr); -void bugsnag_app_set_id(void *event_ptr, char *value); + +void bugsnag_app_set_id(void *event_ptr, const char *value); bool bugsnag_app_get_in_foreground(void *event_ptr); void bugsnag_app_set_in_foreground(void *event_ptr, bool value); @@ -156,13 +158,16 @@ bool bugsnag_app_get_is_launching(void *event_ptr); void bugsnag_app_set_is_launching(void *event_ptr, bool value); char *bugsnag_app_get_release_stage(void *event_ptr); -void bugsnag_app_set_release_stage(void *event_ptr, char *value); + +void bugsnag_app_set_release_stage(void *event_ptr, const char *value); char *bugsnag_app_get_type(void *event_ptr); -void bugsnag_app_set_type(void *event_ptr, char *value); + +void bugsnag_app_set_type(void *event_ptr, const char *value); char *bugsnag_app_get_version(void *event_ptr); -void bugsnag_app_set_version(void *event_ptr, char *value); + +void bugsnag_app_set_version(void *event_ptr, const char *value); int bugsnag_app_get_version_code(void *event_ptr); void bugsnag_app_set_version_code(void *event_ptr, int value); @@ -173,42 +178,52 @@ bool bugsnag_device_get_jailbroken(void *event_ptr); void bugsnag_device_set_jailbroken(void *event_ptr, bool value); char *bugsnag_device_get_id(void *event_ptr); -void bugsnag_device_set_id(void *event_ptr, char *value); + +void bugsnag_device_set_id(void *event_ptr, const char *value); char *bugsnag_device_get_locale(void *event_ptr); -void bugsnag_device_set_locale(void *event_ptr, char *value); + +void bugsnag_device_set_locale(void *event_ptr, const char *value); char *bugsnag_device_get_manufacturer(void *event_ptr); -void bugsnag_device_set_manufacturer(void *event_ptr, char *value); + +void bugsnag_device_set_manufacturer(void *event_ptr, const char *value); char *bugsnag_device_get_model(void *event_ptr); -void bugsnag_device_set_model(void *event_ptr, char *value); + +void bugsnag_device_set_model(void *event_ptr, const char *value); char *bugsnag_device_get_os_version(void *event_ptr); -void bugsnag_device_set_os_version(void *event_ptr, char *value); + +void bugsnag_device_set_os_version(void *event_ptr, const char *value); long bugsnag_device_get_total_memory(void *event_ptr); void bugsnag_device_set_total_memory(void *event_ptr, long value); char *bugsnag_device_get_orientation(void *event_ptr); -void bugsnag_device_set_orientation(void *event_ptr, char *value); + +void bugsnag_device_set_orientation(void *event_ptr, const char *value); time_t bugsnag_device_get_time(void *event_ptr); void bugsnag_device_set_time(void *event_ptr, time_t value); char *bugsnag_device_get_os_name(void *event_ptr); -void bugsnag_device_set_os_name(void *event_ptr, char *value); + +void bugsnag_device_set_os_name(void *event_ptr, const char *value); /* Accessors for event.error */ char *bugsnag_error_get_error_class(void *event_ptr); -void bugsnag_error_set_error_class(void *event_ptr, char *value); + +void bugsnag_error_set_error_class(void *event_ptr, const char *value); char *bugsnag_error_get_error_message(void *event_ptr); -void bugsnag_error_set_error_message(void *event_ptr, char *value); + +void bugsnag_error_set_error_message(void *event_ptr, const char *value); char *bugsnag_error_get_error_type(void *event_ptr); -void bugsnag_error_set_error_type(void *event_ptr, char *value); + +void bugsnag_error_set_error_type(void *event_ptr, const char *value); /* Accessors for event.user */ @@ -225,7 +240,9 @@ void bugsnag_error_set_error_type(void *event_ptr, char *value); * @return the user in the event, represented as a struct */ bugsnag_user bugsnag_event_get_user(void *event_ptr); -void bugsnag_event_set_user(void *event_ptr, char *id, char *email, char *name); + +void bugsnag_event_set_user(void *event_ptr, const char *id, const char *email, + const char *name); /* Accessors for event.severity */ @@ -263,19 +280,24 @@ void bugsnag_event_set_unhandled(void *event_ptr, bool value); /* Accessors for event.groupingHash */ char *bugsnag_event_get_grouping_hash(void *event_ptr); -void bugsnag_event_set_grouping_hash(void *event_ptr, char *value); + +void bugsnag_event_set_grouping_hash(void *event_ptr, const char *value); /* Accessors for event.metadata */ -void bugsnag_event_add_metadata_double(void *event_ptr, char *section, - char *name, double value); -void bugsnag_event_add_metadata_string(void *event_ptr, char *section, - char *name, char *value); -void bugsnag_event_add_metadata_bool(void *event_ptr, char *section, char *name, - bool value); +void bugsnag_event_add_metadata_double(void *event_ptr, const char *section, + const char *name, double value); + +void bugsnag_event_add_metadata_string(void *event_ptr, const char *section, + const char *name, const char *value); -void bugsnag_event_clear_metadata_section(void *event_ptr, char *section); -void bugsnag_event_clear_metadata(void *event_ptr, char *section, char *name); +void bugsnag_event_add_metadata_bool(void *event_ptr, const char *section, + const char *name, bool value); + +void bugsnag_event_clear_metadata_section(void *event_ptr, const char *section); + +void bugsnag_event_clear_metadata(void *event_ptr, const char *section, + const char *name); /** * Retrieves the metadata type for a given section and key in this event. @@ -299,8 +321,9 @@ void bugsnag_event_clear_metadata(void *event_ptr, char *section, char *name); * @return the type of the metadata, or BSG_METADATA_NONE_VALUE if no value * exists */ -bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr, char *section, - char *name); +bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr, + const char *section, + const char *name); /** * Retrieves the metadata value for a given section and key in this event. @@ -319,16 +342,19 @@ bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr, char *section, * @param name - the metadata section name * @param value - the value to set on the given key/name */ -double bugsnag_event_get_metadata_double(void *event_ptr, char *section, - char *name); -char *bugsnag_event_get_metadata_string(void *event_ptr, char *section, - char *name); -bool bugsnag_event_get_metadata_bool(void *event_ptr, char *section, - char *name); +double bugsnag_event_get_metadata_double(void *event_ptr, const char *section, + const char *name); + +char *bugsnag_event_get_metadata_string(void *event_ptr, const char *section, + const char *name); + +bool bugsnag_event_get_metadata_bool(void *event_ptr, const char *section, + const char *name); /* Accessors for event.error.stacktrace */ int bugsnag_event_get_stacktrace_size(void *event_ptr); + bugsnag_stackframe *bugsnag_event_get_stackframe(void *event_ptr, int index); #ifdef __cplusplus diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt index 4c8434db40..918ddacc54 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/NdkPlugin.kt @@ -1,6 +1,7 @@ package com.bugsnag.android import com.bugsnag.android.ndk.NativeBridge +import java.util.concurrent.atomic.AtomicBoolean internal class NdkPlugin : Plugin { @@ -9,12 +10,14 @@ internal class NdkPlugin : Plugin { "not report NDK errors. See https://docs.bugsnag.com/platforms/android/ndk-link-errors" } - private val loader = LibraryLoader() + private val libraryLoader = LibraryLoader() + private val oneTimeSetupPerformed = AtomicBoolean(false) private external fun enableCrashReporting() private external fun disableCrashReporting() private var nativeBridge: NativeBridge? = null + private var client: Client? = null private fun initNativeBridge(client: Client): NativeBridge { val nativeBridge = NativeBridge() @@ -24,23 +27,39 @@ internal class NdkPlugin : Plugin { } override fun load(client: Client) { - val loaded = loader.loadLibrary("bugsnag-ndk", client) { + this.client = client + + if (!oneTimeSetupPerformed.getAndSet(true)) { + performOneTimeSetup(client) + } + if (libraryLoader.isLoaded) { + enableCrashReporting() + client.logger.i("Initialised NDK Plugin") + } + } + + private fun performOneTimeSetup(client: Client) { + libraryLoader.loadLibrary("bugsnag-ndk", client) { val error = it.errors[0] error.errorClass = "NdkLinkError" error.errorMessage = LOAD_ERR_MSG true } - - if (loaded) { + if (libraryLoader.isLoaded) { nativeBridge = initNativeBridge(client) - enableCrashReporting() - client.logger.i("Initialised NDK Plugin") } else { client.logger.e(LOAD_ERR_MSG) } } - override fun unload() = disableCrashReporting() + override fun unload() { + if (libraryLoader.isLoaded) { + disableCrashReporting() + nativeBridge?.let { bridge -> + client?.unregisterObserver(bridge) + } + } + } fun getUnwindStackFunction(): Long { val bridge = nativeBridge diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index 7b66d68eb9..1e908c5386 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt @@ -80,6 +80,7 @@ class NativeBridge : Observer { external fun updateUserEmail(newValue: String) external fun updateUserName(newValue: String) external fun getUnwindStackFunction(): Long + external fun updateLowMemory(newValue: Boolean) /** * Creates a new native bridge for interacting with native components. @@ -134,6 +135,7 @@ class NativeBridge : Observer { updateUserName(makeSafe(msg.user.name ?: "")) updateUserEmail(makeSafe(msg.user.email ?: "")) } + is StateEvent.UpdateMemoryTrimEvent -> updateLowMemory(msg.isLowMemory) } } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c index bb375f64ee..e36aac8404 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/bugsnag.c @@ -18,13 +18,17 @@ void bugsnag_set_binary_arch(JNIEnv *env); void bugsnag_start(JNIEnv *env) { bsg_global_jni_env = env; } -void bugsnag_notify_env(JNIEnv *env, char *name, char *message, +void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, bugsnag_severity severity); -void bugsnag_set_user_env(JNIEnv *env, char *id, char *email, char *name); -void bugsnag_leave_breadcrumb_env(JNIEnv *env, char *message, + +void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, + const char *name); + +void bugsnag_leave_breadcrumb_env(JNIEnv *env, const char *message, bugsnag_breadcrumb_type type); -void bugsnag_notify(char *name, char *message, bugsnag_severity severity) { +void bugsnag_notify(const char *name, const char *message, + bugsnag_severity severity) { if (bsg_global_jni_env != NULL) { bugsnag_notify_env(bsg_global_jni_env, name, message, severity); } else { @@ -32,7 +36,7 @@ void bugsnag_notify(char *name, char *message, bugsnag_severity severity) { } } -void bugsnag_set_user(char *id, char *email, char *name) { +void bugsnag_set_user(const char *id, const char *email, const char *name) { if (bsg_global_jni_env != NULL) { bugsnag_set_user_env(bsg_global_jni_env, id, email, name); } else { @@ -41,7 +45,8 @@ void bugsnag_set_user(char *id, char *email, char *name) { } } -void bugsnag_leave_breadcrumb(char *message, bugsnag_breadcrumb_type type) { +void bugsnag_leave_breadcrumb(const char *message, + bugsnag_breadcrumb_type type) { if (bsg_global_jni_env != NULL) { bugsnag_leave_breadcrumb_env(bsg_global_jni_env, message, type); } else { @@ -106,7 +111,7 @@ void bsg_populate_notify_stacktrace(JNIEnv *env, bugsnag_stackframe *stacktrace, } } -void bugsnag_notify_env(JNIEnv *env, char *name, char *message, +void bugsnag_notify_env(JNIEnv *env, const char *name, const char *message, bugsnag_severity severity) { jclass interface_class = NULL; jmethodID notify_method = NULL; @@ -241,7 +246,8 @@ void bugsnag_set_binary_arch(JNIEnv *env) { bsg_safe_delete_local_ref(env, interface_class); } -void bugsnag_set_user_env(JNIEnv *env, char *id, char *email, char *name) { +void bugsnag_set_user_env(JNIEnv *env, const char *id, const char *email, + const char *name) { // lookup com/bugsnag/android/NativeInterface jclass interface_class = NULL; jmethodID set_user_method = NULL; @@ -303,7 +309,7 @@ jfieldID bsg_parse_jcrumb_type(JNIEnv *env, bugsnag_breadcrumb_type type, } } -void bugsnag_leave_breadcrumb_env(JNIEnv *env, char *message, +void bugsnag_leave_breadcrumb_env(JNIEnv *env, const char *message, bugsnag_breadcrumb_type type) { jclass interface_class = NULL; jmethodID leave_breadcrumb_method = NULL; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/event.c b/bugsnag-plugin-android-ndk/src/main/jni/event.c index ac04b40788..155948eabc 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/event.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/event.c @@ -61,25 +61,26 @@ void bsg_add_metadata_value_bool(bugsnag_metadata *metadata, } } -void bugsnag_event_add_metadata_double(void *event_ptr, char *section, - char *name, double value) { +void bugsnag_event_add_metadata_double(void *event_ptr, const char *section, + const char *name, double value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_add_metadata_value_double(&event->metadata, section, name, value); } -void bugsnag_event_add_metadata_string(void *event_ptr, char *section, - char *name, char *value) { +void bugsnag_event_add_metadata_string(void *event_ptr, const char *section, + const char *name, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_add_metadata_value_str(&event->metadata, section, name, value); } -void bugsnag_event_add_metadata_bool(void *event_ptr, char *section, char *name, - bool value) { +void bugsnag_event_add_metadata_bool(void *event_ptr, const char *section, + const char *name, bool value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_add_metadata_value_bool(&event->metadata, section, name, value); } -void bugsnag_event_clear_metadata(void *event_ptr, char *section, char *name) { +void bugsnag_event_clear_metadata(void *event_ptr, const char *section, + const char *name) { bugsnag_event *event = (bugsnag_event *)event_ptr; for (int i = 0; i < event->metadata.value_count; ++i) { if (strcmp(event->metadata.values[i].section, section) == 0 && @@ -95,7 +96,8 @@ void bugsnag_event_clear_metadata(void *event_ptr, char *section, char *name) { } } -void bugsnag_event_clear_metadata_section(void *event_ptr, char *section) { +void bugsnag_event_clear_metadata_section(void *event_ptr, + const char *section) { bugsnag_event *event = (bugsnag_event *)event_ptr; for (int i = 0; i < event->metadata.value_count; ++i) { if (strcmp(event->metadata.values[i].section, section) == 0) { @@ -104,8 +106,9 @@ void bugsnag_event_clear_metadata_section(void *event_ptr, char *section) { } } -bsg_metadata_value bugsnag_get_metadata_value(void *event_ptr, char *section, - char *name) { +bsg_metadata_value bugsnag_get_metadata_value(void *event_ptr, + const char *section, + const char *name) { bugsnag_event *event = (bugsnag_event *)event_ptr; for (int k = 0; k < event->metadata.value_count; ++k) { @@ -119,13 +122,14 @@ bsg_metadata_value bugsnag_get_metadata_value(void *event_ptr, char *section, return data; } -bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr, char *section, - char *name) { +bugsnag_metadata_type bugsnag_event_has_metadata(void *event_ptr, + const char *section, + const char *name) { return bugsnag_get_metadata_value(event_ptr, section, name).type; } -double bugsnag_event_get_metadata_double(void *event_ptr, char *section, - char *name) { +double bugsnag_event_get_metadata_double(void *event_ptr, const char *section, + const char *name) { bsg_metadata_value value = bugsnag_get_metadata_value(event_ptr, section, name); @@ -135,8 +139,8 @@ double bugsnag_event_get_metadata_double(void *event_ptr, char *section, return 0.0; } -char *bugsnag_event_get_metadata_string(void *event_ptr, char *section, - char *name) { +char *bugsnag_event_get_metadata_string(void *event_ptr, const char *section, + const char *name) { bugsnag_event *event = (bugsnag_event *)event_ptr; for (int k = 0; k < event->metadata.value_count; ++k) { @@ -148,8 +152,8 @@ char *bugsnag_event_get_metadata_string(void *event_ptr, char *section, return NULL; } -bool bugsnag_event_get_metadata_bool(void *event_ptr, char *section, - char *name) { +bool bugsnag_event_get_metadata_bool(void *event_ptr, const char *section, + const char *name) { bsg_metadata_value value = bugsnag_get_metadata_value(event_ptr, section, name); @@ -174,7 +178,7 @@ char *bugsnag_event_get_api_key(void *event_ptr) { return event->api_key; } -void bugsnag_event_set_api_key(void *event_ptr, char *value) { +void bugsnag_event_set_api_key(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->api_key, value, sizeof(event->api_key)); } @@ -184,13 +188,13 @@ char *bugsnag_event_get_context(void *event_ptr) { return event->context; } -void bugsnag_event_set_context(void *event_ptr, char *value) { +void bugsnag_event_set_context(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->context, value, sizeof(event->context)); } -void bugsnag_event_set_user(void *event_ptr, char *id, char *email, - char *name) { +void bugsnag_event_set_user(void *event_ptr, const char *id, const char *email, + const char *name) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->user.id, id, sizeof(event->user.id)); bsg_strncpy_safe(event->user.email, email, sizeof(event->user.email)); @@ -227,7 +231,7 @@ char *bugsnag_app_get_binary_arch(void *event_ptr) { return event->app.binary_arch; } -void bugsnag_app_set_binary_arch(void *event_ptr, char *value) { +void bugsnag_app_set_binary_arch(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.binary_arch, value, sizeof(event->app.binary_arch)); @@ -238,7 +242,7 @@ char *bugsnag_app_get_build_uuid(void *event_ptr) { return event->app.build_uuid; } -void bugsnag_app_set_build_uuid(void *event_ptr, char *value) { +void bugsnag_app_set_build_uuid(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.build_uuid, value, sizeof(event->app.build_uuid)); } @@ -248,7 +252,7 @@ char *bugsnag_app_get_id(void *event_ptr) { return event->app.id; } -void bugsnag_app_set_id(void *event_ptr, char *value) { +void bugsnag_app_set_id(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.id, value, sizeof(event->app.id)); } @@ -258,7 +262,7 @@ char *bugsnag_app_get_release_stage(void *event_ptr) { return event->app.release_stage; } -void bugsnag_app_set_release_stage(void *event_ptr, char *value) { +void bugsnag_app_set_release_stage(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.release_stage, value, sizeof(event->app.release_stage)); @@ -269,7 +273,7 @@ char *bugsnag_app_get_type(void *event_ptr) { return event->app.type; } -void bugsnag_app_set_type(void *event_ptr, char *value) { +void bugsnag_app_set_type(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.type, value, sizeof(event->app.type)); } @@ -279,7 +283,7 @@ char *bugsnag_app_get_version(void *event_ptr) { return event->app.version; } -void bugsnag_app_set_version(void *event_ptr, char *value) { +void bugsnag_app_set_version(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->app.version, value, sizeof(event->app.version)); } @@ -351,7 +355,7 @@ char *bugsnag_device_get_id(void *event_ptr) { return event->device.id; } -void bugsnag_device_set_id(void *event_ptr, char *value) { +void bugsnag_device_set_id(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.id, value, sizeof(event->device.id)); } @@ -361,7 +365,7 @@ char *bugsnag_device_get_locale(void *event_ptr) { return event->device.locale; } -void bugsnag_device_set_locale(void *event_ptr, char *value) { +void bugsnag_device_set_locale(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.locale, value, sizeof(event->device.locale)); } @@ -371,7 +375,7 @@ char *bugsnag_device_get_manufacturer(void *event_ptr) { return event->device.manufacturer; } -void bugsnag_device_set_manufacturer(void *event_ptr, char *value) { +void bugsnag_device_set_manufacturer(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.manufacturer, value, sizeof(event->device.manufacturer)); @@ -382,7 +386,7 @@ char *bugsnag_device_get_model(void *event_ptr) { return event->device.model; } -void bugsnag_device_set_model(void *event_ptr, char *value) { +void bugsnag_device_set_model(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.model, value, sizeof(event->device.model)); } @@ -392,7 +396,7 @@ char *bugsnag_device_get_os_version(void *event_ptr) { return event->device.os_version; } -void bugsnag_device_set_os_version(void *event_ptr, char *value) { +void bugsnag_device_set_os_version(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.os_version, value, sizeof(event->device.os_version)); @@ -413,7 +417,7 @@ char *bugsnag_device_get_orientation(void *event_ptr) { return event->device.orientation; } -void bugsnag_device_set_orientation(void *event_ptr, char *value) { +void bugsnag_device_set_orientation(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.orientation, value, sizeof(event->device.orientation)); @@ -434,7 +438,7 @@ char *bugsnag_device_get_os_name(void *event_ptr) { return event->device.os_name; } -void bugsnag_device_set_os_name(void *event_ptr, char *value) { +void bugsnag_device_set_os_name(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->device.os_name, value, sizeof(event->device.os_name)); } @@ -444,7 +448,7 @@ char *bugsnag_error_get_error_class(void *event_ptr) { return event->error.errorClass; } -void bugsnag_error_set_error_class(void *event_ptr, char *value) { +void bugsnag_error_set_error_class(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->error.errorClass, value, sizeof(event->error.errorClass)); @@ -455,7 +459,7 @@ char *bugsnag_error_get_error_message(void *event_ptr) { return event->error.errorMessage; } -void bugsnag_error_set_error_message(void *event_ptr, char *value) { +void bugsnag_error_set_error_message(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->error.errorMessage, value, sizeof(event->error.errorMessage)); @@ -466,7 +470,7 @@ char *bugsnag_error_get_error_type(void *event_ptr) { return event->error.type; } -void bugsnag_error_set_error_type(void *event_ptr, char *value) { +void bugsnag_error_set_error_type(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->error.type, value, sizeof(event->error.type)); } @@ -501,7 +505,7 @@ char *bugsnag_event_get_grouping_hash(void *event_ptr) { return event->grouping_hash; } -void bugsnag_event_set_grouping_hash(void *event_ptr, char *value) { +void bugsnag_event_set_grouping_hash(void *event_ptr, const char *value) { bugsnag_event *event = (bugsnag_event *)event_ptr; bsg_strncpy_safe(event->grouping_hash, value, sizeof(event->grouping_hash)); } diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c index cd74319aec..f096497885 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.c @@ -3,9 +3,9 @@ #include #include -void bsg_strcpy(char *dst, char *src) { bsg_strncpy(dst, src, INT_MAX); } +void bsg_strcpy(char *dst, const char *src) { bsg_strncpy(dst, src, INT_MAX); } -void bsg_strncpy(char *dst, char *src, size_t len) { +void bsg_strncpy(char *dst, const char *src, size_t len) { int i = 0; while (i <= len) { char current = src[i]; diff --git a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h index 8b23fd64c0..1879fdbfd7 100644 --- a/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h +++ b/bugsnag-plugin-android-ndk/src/main/jni/utils/string.h @@ -11,7 +11,7 @@ extern "C" { /** * Copy the contents of src to dst where src is null-terminated */ -void bsg_strcpy(char *dst, char *src) __asyncsafe; +void bsg_strcpy(char *dst, const char *src) __asyncsafe; /** * Return the length of a string @@ -21,7 +21,7 @@ size_t bsg_strlen(const char *str) __asyncsafe; /** * Copy a maximum number of bytes from src to dst */ -void bsg_strncpy(char *dst, char *src, size_t len) __asyncsafe; +void bsg_strncpy(char *dst, const char *src, size_t len) __asyncsafe; /** * Copy a string from src to dst, null padding the rest diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp index d2f8574060..020393b2ea 100644 --- a/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/cpp/cxx-scenarios.cpp @@ -353,4 +353,14 @@ Java_com_bugsnag_android_mazerunner_scenarios_CXXMarkLaunchCompletedScenario_cra jobject thiz) { abort(); } + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_UnhandledNdkAutoNotifyTrueScenario_crash(JNIEnv *env) { + abort(); +} + +JNIEXPORT void JNICALL +Java_com_bugsnag_android_mazerunner_scenarios_UnhandledNdkAutoNotifyFalseScenario_crash(JNIEnv *env) { + abort(); +} } diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyFalseScenario.kt b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyFalseScenario.kt new file mode 100644 index 0000000000..72befbf5ea --- /dev/null +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyFalseScenario.kt @@ -0,0 +1,30 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.mazerunner.getZeroEventsLogMessages +import com.bugsnag.android.setAutoNotify + +class UnhandledNdkAutoNotifyFalseScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + init { + System.loadLibrary("cxx-scenarios") + } + + external fun crash() + + override fun startScenario() { + super.startScenario() + setAutoNotify(Bugsnag.getClient(), false) + crash() + } + + override fun getInterceptedLogMessages(): List { + return getZeroEventsLogMessages(startBugsnagOnly) + } +} diff --git a/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyTrueScenario.kt b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyTrueScenario.kt new file mode 100644 index 0000000000..dd96cdc219 --- /dev/null +++ b/features/fixtures/mazerunner/cxx-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledNdkAutoNotifyTrueScenario.kt @@ -0,0 +1,26 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.setAutoNotify + +class UnhandledNdkAutoNotifyTrueScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + init { + System.loadLibrary("cxx-scenarios") + } + + external fun crash() + + override fun startScenario() { + super.startScenario() + setAutoNotify(Bugsnag.getClient(), false) + setAutoNotify(Bugsnag.getClient(), true) + crash() + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml index 60cddc13d2..e63cd6f4c7 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml +++ b/features/fixtures/mazerunner/jvm-scenarios/detekt-baseline.xml @@ -4,6 +4,8 @@ MagicNumber:AnrHelper.kt$1000 MagicNumber:AnrHelper.kt$<no name provided>$60000 + MagicNumber:AutoDetectAnrsFalseScenario.kt$AutoDetectAnrsFalseScenario$100000 + MagicNumber:AutoDetectAnrsTrueScenario.kt$AutoDetectAnrsTrueScenario$100000 MagicNumber:BugsnagInitScenario.kt$BugsnagInitScenario$25 MagicNumber:HandledKotlinSmokeScenario.kt$HandledKotlinSmokeScenario$999 MagicNumber:JvmAnrSleepScenario.kt$JvmAnrSleepScenario$100000 diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt index 5dfe6f851d..b2a5b205d3 100644 --- a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/TestHarnessHooks.kt @@ -88,3 +88,11 @@ fun generateEvent(client: Client): Event { event.device = generateDeviceWithState() return event } + +fun setAutoNotify(client: Client, enabled: Boolean) { + client.setAutoNotify(enabled) +} + +fun setAutoDetectAnrs(client: Client, enabled: Boolean) { + client.setAutoDetectAnrs(enabled) +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsFalseScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsFalseScenario.kt new file mode 100644 index 0000000000..5b6b5d0859 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsFalseScenario.kt @@ -0,0 +1,37 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.mazerunner.getZeroEventsLogMessages +import com.bugsnag.android.setAutoDetectAnrs + +internal class AutoDetectAnrsFalseScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + init { + config.enabledErrorTypes.anrs = true + } + + override fun startScenario() { + super.startScenario() + setAutoDetectAnrs(Bugsnag.getClient(), false) + + val main = Handler(Looper.getMainLooper()) + main.postDelayed( + Runnable { + Thread.sleep(100000) + }, + 1 + ) // A moment of delay so there is something to 'tap' onscreen + } + + override fun getInterceptedLogMessages(): List { + return getZeroEventsLogMessages(startBugsnagOnly) + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsTrueScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsTrueScenario.kt new file mode 100644 index 0000000000..fbebf2532b --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/AutoDetectAnrsTrueScenario.kt @@ -0,0 +1,33 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import android.os.Handler +import android.os.Looper +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.setAutoDetectAnrs + +internal class AutoDetectAnrsTrueScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + init { + config.enabledErrorTypes.anrs = true + } + + override fun startScenario() { + super.startScenario() + setAutoDetectAnrs(Bugsnag.getClient(), false) + setAutoDetectAnrs(Bugsnag.getClient(), true) + + val main = Handler(Looper.getMainLooper()) + main.postDelayed( + Runnable { + Thread.sleep(100000) + }, + 1 + ) // A moment of delay so there is something to 'tap' onscreen + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledJvmAutoNotifyFalseScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledJvmAutoNotifyFalseScenario.kt new file mode 100644 index 0000000000..2700b3790f --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/HandledJvmAutoNotifyFalseScenario.kt @@ -0,0 +1,19 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.setAutoNotify + +class HandledJvmAutoNotifyFalseScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + override fun startScenario() { + super.startScenario() + setAutoNotify(Bugsnag.getClient(), false) + Bugsnag.notify(generateException()) + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyFalseScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyFalseScenario.kt new file mode 100644 index 0000000000..dd7c9e5941 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyFalseScenario.kt @@ -0,0 +1,24 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.mazerunner.getZeroEventsLogMessages +import com.bugsnag.android.setAutoNotify + +class UnhandledJvmAutoNotifyFalseScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + override fun startScenario() { + super.startScenario() + setAutoNotify(Bugsnag.getClient(), false) + throw generateException() + } + + override fun getInterceptedLogMessages(): List { + return getZeroEventsLogMessages(startBugsnagOnly) + } +} diff --git a/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyTrueScenario.kt b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyTrueScenario.kt new file mode 100644 index 0000000000..195a7626c8 --- /dev/null +++ b/features/fixtures/mazerunner/jvm-scenarios/src/main/java/com/bugsnag/android/mazerunner/scenarios/UnhandledJvmAutoNotifyTrueScenario.kt @@ -0,0 +1,20 @@ +package com.bugsnag.android.mazerunner.scenarios + +import android.content.Context +import com.bugsnag.android.Bugsnag +import com.bugsnag.android.Configuration +import com.bugsnag.android.setAutoNotify + +class UnhandledJvmAutoNotifyTrueScenario( + config: Configuration, + context: Context, + eventMetadata: String? +) : Scenario(config, context, eventMetadata) { + + override fun startScenario() { + super.startScenario() + setAutoNotify(Bugsnag.getClient(), false) + setAutoNotify(Bugsnag.getClient(), true) + throw generateException() + } +} diff --git a/features/full_tests/batch_1/auto_notify.feature b/features/full_tests/batch_1/auto_notify.feature new file mode 100644 index 0000000000..163a83b528 --- /dev/null +++ b/features/full_tests/batch_1/auto_notify.feature @@ -0,0 +1,67 @@ +Feature: Switching automatic error detection on/off for Unity + +Scenario: Handled JVM exceptions are captured with autoNotify=false + When I run "HandledJvmAutoNotifyFalseScenario" + Then I wait to receive an error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the exception "message" equals "HandledJvmAutoNotifyFalseScenario" + +Scenario: JVM exception not captured with autoNotify=false + When I run "UnhandledJvmAutoNotifyFalseScenario" and relaunch the app + And I configure Bugsnag for "UnhandledJvmAutoNotifyFalseScenario" + Then Bugsnag confirms it has no errors to send + +Scenario: NDK signal not captured with autoNotify=false + When I run "UnhandledNdkAutoNotifyFalseScenario" and relaunch the app + And I configure Bugsnag for "UnhandledNdkAutoNotifyFalseScenario" + Then Bugsnag confirms it has no errors to send + +@skip_android_8_1 +Scenario: ANR not captured with autoDetectAnrs=false + When I run "AutoDetectAnrsFalseScenario" and relaunch the app + And I configure Bugsnag for "AutoDetectAnrsFalseScenario" + Then Bugsnag confirms it has no errors to send + +Scenario: JVM exception captured with autoNotify reenabled + When I run "UnhandledJvmAutoNotifyTrueScenario" and relaunch the app + And I configure Bugsnag for "UnhandledJvmAutoNotifyTrueScenario" + Then I wait to receive an error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the error payload field "events" is an array with 1 elements + And the exception "errorClass" equals "java.lang.RuntimeException" + And the exception "message" equals "UnhandledJvmAutoNotifyTrueScenario" + And the exception "type" equals "android" + And the event "unhandled" is true + And the event "severity" equals "error" + +Scenario: NDK signal captured with autoNotify reenabled + When I run "UnhandledNdkAutoNotifyTrueScenario" and relaunch the app + And I configure Bugsnag for "UnhandledNdkAutoNotifyTrueScenario" + Then I wait to receive an error + And the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier + And the error payload field "events" is an array with 1 elements + And the exception "message" equals "Abort program" + And the exception "type" equals "c" + And the event "unhandled" is true + And the event "severity" equals "error" + And the event "severityReason.type" equals "signal" + And the event "severityReason.unhandledOverridden" is false + +# PLAT-6620 +# @skip_android_8_1 +# Scenario: ANR captured with autoDetectAnrs reenabled +# When I run "AutoDetectAnrsTrueScenario" +# And I wait for 2 seconds +# And I tap the screen 3 times +# And I wait for 4 seconds +# And I clear any error dialogue +# And I wait to receive an error +# Then the error is valid for the error reporting API version "4.0" for the "Android Bugsnag Notifier" notifier +# And the error payload field "events" is an array with 1 elements +# And the exception "errorClass" equals "ANR" +# And the exception "message" starts with " Input dispatching timed out" +# And the exception "type" equals "android" +# And the event "unhandled" is true +# And the event "severity" equals "error" +# And the event "severityReason.type" equals "anrError" +# And the event "severityReason.unhandledOverridden" is false diff --git a/gradle.properties b/gradle.properties index ec4f054e0f..1be5d4a9a1 100644 --- a/gradle.properties +++ b/gradle.properties @@ -11,7 +11,7 @@ org.gradle.jvmargs=-Xmx4096m # This option should only be used with decoupled projects. More details, visit # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects org.gradle.parallel=true -VERSION_NAME=5.9.3 +VERSION_NAME=5.9.4 GROUP=com.bugsnag POM_SCM_URL=https://github.com/bugsnag/bugsnag-android POM_SCM_CONNECTION=scm:git@github.com:bugsnag/bugsnag-android.git