diff --git a/CHANGELOG.md b/CHANGELOG.md index 6231fbe644..9f27645ae0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,9 @@ * Avoid unnecessary BroadcastReceiver registration for monitoring device orientation [#1303](https://github.com/bugsnag/bugsnag-android/pull/1303) +* Register system callbacks on background thread + [#1292](https://github.com/bugsnag/bugsnag-android/pull/1292) + ## 5.9.5 (2021-06-25) * Unity: Properly handle ANRs after multiple calls to autoNotify and autoDetectAnrs 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 index 9d6cfe6cf7..8d8799b65d 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java @@ -36,6 +36,9 @@ public void onLowMemoryEvent() { when(context.getApplicationContext()).thenReturn(context); Client client = new Client(context, BugsnagTestUtils.generateConfiguration()); + // block until observer is registered + client.bgTaskService.shutdown(); + // capture the registered ComponentCallbacks verify(context, times(1)).registerComponentCallbacks(componentCallbacksCaptor.capture()); 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 915e170738..8907151d87 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 @@ -71,11 +71,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware { final SessionTracker sessionTracker; - private final SystemBroadcastReceiver systemBroadcastReceiver; + final SystemBroadcastReceiver systemBroadcastReceiver; private final ActivityBreadcrumbCollector activityBreadcrumbCollector; private final SessionLifecycleCallback sessionLifecycleCallback; - private final Connectivity connectivity; + final Connectivity connectivity; @Nullable private final StorageManager storageManager; @@ -220,10 +220,6 @@ public Unit invoke(String activity, Map metadata) { exceptionHandler.install(); } - // register a receiver for automatic breadcrumbs - systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService); - registerComponentCallbacks(); - // load last run info lastRunInfoStore = new LastRunInfoStore(immutableConfig); lastRunInfo = loadLastRunInfo(); @@ -231,13 +227,16 @@ public Unit invoke(String activity, Map metadata) { // initialise plugins before attempting to flush any errors loadPlugins(configuration); - connectivity.registerForNetworkChanges(); - // Flush any on-disk errors and sessions eventStore.flushOnLaunch(); eventStore.flushAsync(); sessionTracker.flushAsync(); + // register listeners for system events in the background. + systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger); + registerComponentCallbacks(); + registerListenersInBackground(); + // leave auto breadcrumb Map data = Collections.emptyMap(); leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data); @@ -296,6 +295,25 @@ public Unit invoke(String activity, Map metadata) { this.exceptionHandler = exceptionHandler; } + /** + * Registers listeners for system events in the background. This offloads work from the main + * thread that collects useful information from callbacks, but that don't need to be done + * immediately on client construction. + */ + void registerListenersInBackground() { + try { + bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() { + @Override + public void run() { + connectivity.registerForNetworkChanges(); + SystemBroadcastReceiver.register(appContext, systemBroadcastReceiver, logger); + } + }); + } catch (RejectedExecutionException ex) { + logger.w("Failed to register for system events", ex); + } + } + private LastRunInfo loadLastRunInfo() { LastRunInfo lastRunInfo = lastRunInfoStore.load(); LastRunInfo currentRunInfo = new LastRunInfo(0, false, false); diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java deleted file mode 100644 index 44092a0358..0000000000 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java +++ /dev/null @@ -1,196 +0,0 @@ -package com.bugsnag.android; - -import com.bugsnag.android.internal.ImmutableConfig; - -import android.content.BroadcastReceiver; -import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; -import android.os.Bundle; - -import androidx.annotation.NonNull; - -import java.util.HashMap; -import java.util.Map; -import java.util.concurrent.RejectedExecutionException; - -/** - * Used to automatically create breadcrumbs for system events - * Broadcast actions and categories can be found in text files in the android folder - * e.g. ~/Library/Android/sdk/platforms/android-9/data/broadcast_actions.txt - * See http://stackoverflow.com/a/27601497 - */ -class SystemBroadcastReceiver extends BroadcastReceiver { - - private static final String INTENT_ACTION_KEY = "Intent Action"; - - private final Client client; - private final Logger logger; - private final Map actions; - - SystemBroadcastReceiver(@NonNull Client client, Logger logger) { - this.client = client; - this.logger = logger; - this.actions = buildActions(); - } - - static SystemBroadcastReceiver register(final Client client, - final Logger logger, - BackgroundTaskService bgTaskService) { - final SystemBroadcastReceiver receiver = new SystemBroadcastReceiver(client, logger); - if (receiver.getActions().size() > 0) { - try { - bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() { - @Override - public void run() { - IntentFilter intentFilter = receiver.getIntentFilter(); - Context context = client.appContext; - ContextExtensionsKt.registerReceiverSafe(context, - receiver, intentFilter, logger); - } - }); - } catch (RejectedExecutionException ex) { - logger.w("Failed to register for automatic breadcrumb broadcasts", ex); - } - return receiver; - } else { - return null; - } - } - - @Override - public void onReceive(@NonNull Context context, @NonNull Intent intent) { - try { - Map meta = new HashMap<>(); - String fullAction = intent.getAction(); - - if (fullAction == null) { - return; - } - - String shortAction = shortenActionNameIfNeeded(fullAction); - meta.put(INTENT_ACTION_KEY, fullAction); // always add the Intent Action - - Bundle extras = intent.getExtras(); - if (extras != null) { - for (String key : extras.keySet()) { - Object valObj = extras.get(key); - if (valObj == null) { - continue; - } - - String val = valObj.toString(); - - if (isAndroidKey(key)) { // shorten the Intent action - meta.put("Extra", shortAction + ": " + val); - } else { - meta.put(key, val); - } - } - } - BreadcrumbType type = actions.get(fullAction); - - if (type == null) { - type = BreadcrumbType.STATE; - } - client.leaveBreadcrumb(shortAction, meta, type); - - } catch (Exception ex) { - logger.w("Failed to leave breadcrumb in SystemBroadcastReceiver: " - + ex.getMessage()); - } - } - - private static boolean isAndroidKey(@NonNull String actionName) { - return actionName.startsWith("android."); - } - - @NonNull - static String shortenActionNameIfNeeded(@NonNull String action) { - if (isAndroidKey(action)) { - return action.substring(action.lastIndexOf(".") + 1); - } else { - return action; - } - } - - /** - * Builds a map of intent actions and their breadcrumb type (if enabled). - * - * Noisy breadcrumbs are omitted, along with anything that involves a state change. - * @return the action map - */ - @NonNull - private Map buildActions() { - - Map actions = new HashMap<>(); - ImmutableConfig config = client.getConfig(); - - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.USER)) { - actions.put("android.appwidget.action.APPWIDGET_DELETED", BreadcrumbType.USER); - actions.put("android.appwidget.action.APPWIDGET_DISABLED", BreadcrumbType.USER); - actions.put("android.appwidget.action.APPWIDGET_ENABLED", BreadcrumbType.USER); - actions.put("android.intent.action.CAMERA_BUTTON", BreadcrumbType.USER); - actions.put("android.intent.action.CLOSE_SYSTEM_DIALOGS", BreadcrumbType.USER); - actions.put("android.intent.action.DOCK_EVENT", BreadcrumbType.USER); - } - - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { - actions.put("android.appwidget.action.APPWIDGET_HOST_RESTORED", BreadcrumbType.STATE); - actions.put("android.appwidget.action.APPWIDGET_RESTORED", BreadcrumbType.STATE); - actions.put("android.appwidget.action.APPWIDGET_UPDATE", BreadcrumbType.STATE); - actions.put("android.appwidget.action.APPWIDGET_UPDATE_OPTIONS", BreadcrumbType.STATE); - actions.put("android.intent.action.ACTION_POWER_CONNECTED", BreadcrumbType.STATE); - actions.put("android.intent.action.ACTION_POWER_DISCONNECTED", BreadcrumbType.STATE); - actions.put("android.intent.action.ACTION_SHUTDOWN", BreadcrumbType.STATE); - actions.put("android.intent.action.AIRPLANE_MODE", BreadcrumbType.STATE); - actions.put("android.intent.action.BATTERY_LOW", BreadcrumbType.STATE); - actions.put("android.intent.action.BATTERY_OKAY", BreadcrumbType.STATE); - actions.put("android.intent.action.BOOT_COMPLETED", BreadcrumbType.STATE); - actions.put("android.intent.action.CONFIGURATION_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.CONTENT_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.DATE_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.DEVICE_STORAGE_LOW", BreadcrumbType.STATE); - actions.put("android.intent.action.DEVICE_STORAGE_OK", BreadcrumbType.STATE); - actions.put("android.intent.action.INPUT_METHOD_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.LOCALE_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.REBOOT", BreadcrumbType.STATE); - actions.put("android.intent.action.SCREEN_OFF", BreadcrumbType.STATE); - actions.put("android.intent.action.SCREEN_ON", BreadcrumbType.STATE); - actions.put("android.intent.action.TIMEZONE_CHANGED", BreadcrumbType.STATE); - actions.put("android.intent.action.TIME_SET", BreadcrumbType.STATE); - actions.put("android.os.action.DEVICE_IDLE_MODE_CHANGED", BreadcrumbType.STATE); - actions.put("android.os.action.POWER_SAVE_MODE_CHANGED", BreadcrumbType.STATE); - } - - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.NAVIGATION)) { - actions.put("android.intent.action.DREAMING_STARTED", BreadcrumbType.NAVIGATION); - actions.put("android.intent.action.DREAMING_STOPPED", BreadcrumbType.NAVIGATION); - } - - return actions; - } - - /** - * @return the enabled actions - */ - public Map getActions() { - return actions; - } - - /** - * Creates a new Intent filter with all the intents to record breadcrumbs for - * - * @return The intent filter - */ - @NonNull - public IntentFilter getIntentFilter() { - IntentFilter filter = new IntentFilter(); - - for (String action : actions.keySet()) { - filter.addAction(action); - } - return filter; - } - -} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt new file mode 100644 index 0000000000..ac8e309ccb --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt @@ -0,0 +1,130 @@ +package com.bugsnag.android + +import android.content.BroadcastReceiver +import android.content.Context +import android.content.Intent +import android.content.IntentFilter +import java.util.HashMap + +/** + * Used to automatically create breadcrumbs for system events + * Broadcast actions and categories can be found in text files in the android folder + * e.g. ~/Library/Android/sdk/platforms/android-9/data/broadcast_actions.txt + * See http://stackoverflow.com/a/27601497 + */ +internal class SystemBroadcastReceiver( + private val client: Client, + private val logger: Logger +) : BroadcastReceiver() { + + companion object { + private const val INTENT_ACTION_KEY = "Intent Action" + + @JvmStatic + fun register(ctx: Context, receiver: SystemBroadcastReceiver, logger: Logger) { + if (receiver.actions.isNotEmpty()) { + val filter = IntentFilter() + receiver.actions.keys.forEach(filter::addAction) + ctx.registerReceiverSafe(receiver, filter, logger) + } + } + + fun isAndroidKey(actionName: String): Boolean { + return actionName.startsWith("android.") + } + + fun shortenActionNameIfNeeded(action: String): String { + return if (isAndroidKey(action)) { + action.substringAfterLast('.') + } else { + action + } + } + } + + val actions: Map = buildActions() + + override fun onReceive(context: Context, intent: Intent) { + try { + val meta: MutableMap = HashMap() + val fullAction = intent.action ?: return + val shortAction = shortenActionNameIfNeeded(fullAction) + meta[INTENT_ACTION_KEY] = fullAction // always add the Intent Action + addExtrasToMetadata(intent, meta, shortAction) + + val type = actions[fullAction] ?: BreadcrumbType.STATE + client.leaveBreadcrumb(shortAction, meta, type) + } catch (ex: Exception) { + logger.w("Failed to leave breadcrumb in SystemBroadcastReceiver: ${ex.message}") + } + } + + private fun addExtrasToMetadata( + intent: Intent, + meta: MutableMap, + shortAction: String + ) { + val extras = intent.extras + extras?.keySet()?.forEach { key -> + val valObj = extras[key] ?: return@forEach + val strVal = valObj.toString() + if (isAndroidKey(key)) { // shorten the Intent action + meta["Extra"] = "$shortAction: $strVal" + } else { + meta[key] = strVal + } + } + } + + /** + * Builds a map of intent actions and their breadcrumb type (if enabled). + * + * Noisy breadcrumbs are omitted, along with anything that involves a state change. + * @return the action map + */ + private fun buildActions(): Map { + val actions: MutableMap = HashMap() + val config = client.config + + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.USER)) { + actions["android.appwidget.action.APPWIDGET_DELETED"] = BreadcrumbType.USER + actions["android.appwidget.action.APPWIDGET_DISABLED"] = BreadcrumbType.USER + actions["android.appwidget.action.APPWIDGET_ENABLED"] = BreadcrumbType.USER + actions["android.intent.action.CAMERA_BUTTON"] = BreadcrumbType.USER + actions["android.intent.action.CLOSE_SYSTEM_DIALOGS"] = BreadcrumbType.USER + actions["android.intent.action.DOCK_EVENT"] = BreadcrumbType.USER + } + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { + actions["android.appwidget.action.APPWIDGET_HOST_RESTORED"] = BreadcrumbType.STATE + actions["android.appwidget.action.APPWIDGET_RESTORED"] = BreadcrumbType.STATE + actions["android.appwidget.action.APPWIDGET_UPDATE"] = BreadcrumbType.STATE + actions["android.appwidget.action.APPWIDGET_UPDATE_OPTIONS"] = BreadcrumbType.STATE + actions["android.intent.action.ACTION_POWER_CONNECTED"] = BreadcrumbType.STATE + actions["android.intent.action.ACTION_POWER_DISCONNECTED"] = BreadcrumbType.STATE + actions["android.intent.action.ACTION_SHUTDOWN"] = BreadcrumbType.STATE + actions["android.intent.action.AIRPLANE_MODE"] = BreadcrumbType.STATE + actions["android.intent.action.BATTERY_LOW"] = BreadcrumbType.STATE + actions["android.intent.action.BATTERY_OKAY"] = BreadcrumbType.STATE + actions["android.intent.action.BOOT_COMPLETED"] = BreadcrumbType.STATE + actions["android.intent.action.CONFIGURATION_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.CONTENT_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.DATE_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.DEVICE_STORAGE_LOW"] = BreadcrumbType.STATE + actions["android.intent.action.DEVICE_STORAGE_OK"] = BreadcrumbType.STATE + actions["android.intent.action.INPUT_METHOD_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.LOCALE_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.REBOOT"] = BreadcrumbType.STATE + actions["android.intent.action.SCREEN_OFF"] = BreadcrumbType.STATE + actions["android.intent.action.SCREEN_ON"] = BreadcrumbType.STATE + actions["android.intent.action.TIMEZONE_CHANGED"] = BreadcrumbType.STATE + actions["android.intent.action.TIME_SET"] = BreadcrumbType.STATE + actions["android.os.action.DEVICE_IDLE_MODE_CHANGED"] = BreadcrumbType.STATE + actions["android.os.action.POWER_SAVE_MODE_CHANGED"] = BreadcrumbType.STATE + } + if (!config.shouldDiscardBreadcrumb(BreadcrumbType.NAVIGATION)) { + actions["android.intent.action.DREAMING_STARTED"] = BreadcrumbType.NAVIGATION + actions["android.intent.action.DREAMING_STOPPED"] = BreadcrumbType.NAVIGATION + } + return actions + } +} diff --git a/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt index d9d249589b..6eabd1e56e 100644 --- a/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt +++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/SystemBroadcastReceiverTest.kt @@ -3,7 +3,7 @@ package com.bugsnag.android import android.content.Context import android.content.Intent import android.os.Bundle -import com.bugsnag.android.SystemBroadcastReceiver.shortenActionNameIfNeeded +import com.bugsnag.android.SystemBroadcastReceiver.Companion.shortenActionNameIfNeeded import com.bugsnag.android.internal.ImmutableConfig import org.junit.Assert.assertEquals import org.junit.Test @@ -96,7 +96,7 @@ class SystemBroadcastReceiverTest { } private fun getConfig(breadcrumbTypes: Set): ImmutableConfig { - var config = BugsnagTestUtils.generateConfiguration() + val config = BugsnagTestUtils.generateConfiguration() config.enabledBreadcrumbTypes = breadcrumbTypes return BugsnagTestUtils.convert(config) }