package com.bugsnag.android; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.StateObserver; import com.bugsnag.android.internal.dag.ConfigModule; import com.bugsnag.android.internal.dag.ContextModule; import com.bugsnag.android.internal.dag.SystemServiceModule; import android.app.Application; import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import kotlin.Unit; import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; import java.io.File; import java.util.Collection; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.Callable; import java.util.concurrent.RejectedExecutionException; /** * A Bugsnag Client instance allows you to use Bugsnag in your Android app. * Typically you'd instead use the static access provided in the Bugsnag class. *

* Example usage: *

* Client client = new Client(this, "your-api-key"); * client.notify(new RuntimeException("something broke!")); * * @see Bugsnag */ @SuppressWarnings({"checkstyle:JavadocTagContinuationIndentation", "ConstantConditions"}) public class Client implements MetadataAware, CallbackAware, UserAware, FeatureFlagAware { final ImmutableConfig immutableConfig; final MetadataState metadataState; final FeatureFlagState featureFlagState; private final ContextState contextState; private final CallbackState callbackState; private final UserState userState; final Context appContext; @NonNull final DeviceDataCollector deviceDataCollector; @NonNull final AppDataCollector appDataCollector; @NonNull final BreadcrumbState breadcrumbState; @NonNull final MemoryTrimState memoryTrimState = new MemoryTrimState(); @NonNull protected final EventStore eventStore; final SessionTracker sessionTracker; final SystemBroadcastReceiver systemBroadcastReceiver; final Logger logger; final Connectivity connectivity; final DeliveryDelegate deliveryDelegate; final ClientObservable clientObservable; PluginClient pluginClient; final Notifier notifier; @Nullable final LastRunInfo lastRunInfo; final LastRunInfoStore lastRunInfoStore; final LaunchCrashTracker launchCrashTracker; final BackgroundTaskService bgTaskService = new BackgroundTaskService(); private final ExceptionHandler exceptionHandler; /** * Initialize a Bugsnag client * * @param androidContext an Android context, usually this */ public Client(@NonNull Context androidContext) { this(androidContext, Configuration.load(androidContext)); } /** * Initialize a Bugsnag client * * @param androidContext an Android context, usually this * @param apiKey your Bugsnag API key from your Bugsnag dashboard */ public Client(@NonNull Context androidContext, @NonNull String apiKey) { this(androidContext, Configuration.load(androidContext, apiKey)); } /** * Initialize a Bugsnag client * * @param androidContext an Android context, usually this * @param configuration a configuration for the Client */ public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) { ContextModule contextModule = new ContextModule(androidContext); appContext = contextModule.getCtx(); notifier = configuration.getNotifier(); connectivity = new ConnectivityCompat(appContext, new Function2() { @Override public Unit invoke(Boolean hasConnection, String networkState) { Map data = new HashMap<>(); data.put("hasConnection", hasConnection); data.put("networkState", networkState); leaveAutoBreadcrumb("Connectivity changed", BreadcrumbType.STATE, data); if (hasConnection) { eventStore.flushAsync(); sessionTracker.flushAsync(); } return null; } }); // set sensible defaults for delivery/project packages etc if not set ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity); immutableConfig = configModule.getConfig(); logger = immutableConfig.getLogger(); warnIfNotAppContext(androidContext); // setup storage as soon as possible final StorageModule storageModule = new StorageModule(appContext, immutableConfig, logger); // setup state trackers for bugsnag BugsnagStateModule bugsnagStateModule = new BugsnagStateModule( configModule, configuration); clientObservable = bugsnagStateModule.getClientObservable(); callbackState = bugsnagStateModule.getCallbackState(); breadcrumbState = bugsnagStateModule.getBreadcrumbState(); contextState = bugsnagStateModule.getContextState(); metadataState = bugsnagStateModule.getMetadataState(); featureFlagState = bugsnagStateModule.getFeatureFlagState(); // lookup system services final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule); // block until storage module has resolved everything storageModule.resolveDependencies(bgTaskService, TaskType.IO); // setup further state trackers and data collection TrackerModule trackerModule = new TrackerModule(configModule, storageModule, this, bgTaskService, callbackState); launchCrashTracker = trackerModule.getLaunchCrashTracker(); sessionTracker = trackerModule.getSessionTracker(); DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, configModule, systemServiceModule, trackerModule, bgTaskService, connectivity, storageModule.getDeviceId(), storageModule.getInternalDeviceId(), memoryTrimState); dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO); appDataCollector = dataCollectionModule.getAppDataCollector(); deviceDataCollector = dataCollectionModule.getDeviceDataCollector(); // load the device + user information userState = storageModule.getUserStore().load(configuration.getUser()); storageModule.getSharedPrefMigrator().deleteLegacyPrefs(); registerLifecycleCallbacks(); EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier, callbackState); eventStorageModule.resolveDependencies(bgTaskService, TaskType.IO); eventStore = eventStorageModule.getEventStore(); deliveryDelegate = new DeliveryDelegate(logger, eventStore, immutableConfig, callbackState, notifier, bgTaskService); // Install a default exception handler with this client exceptionHandler = new ExceptionHandler(this, logger); if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) { exceptionHandler.install(); } // load last run info lastRunInfoStore = storageModule.getLastRunInfoStore(); lastRunInfo = storageModule.getLastRunInfo(); // initialise plugins before attempting to flush any errors loadPlugins(configuration); // 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); logger.d("Bugsnag loaded"); } @VisibleForTesting Client( ImmutableConfig immutableConfig, MetadataState metadataState, ContextState contextState, CallbackState callbackState, UserState userState, FeatureFlagState featureFlagState, ClientObservable clientObservable, Context appContext, @NonNull DeviceDataCollector deviceDataCollector, @NonNull AppDataCollector appDataCollector, @NonNull BreadcrumbState breadcrumbState, @NonNull EventStore eventStore, SystemBroadcastReceiver systemBroadcastReceiver, SessionTracker sessionTracker, Connectivity connectivity, Logger logger, DeliveryDelegate deliveryDelegate, LastRunInfoStore lastRunInfoStore, LaunchCrashTracker launchCrashTracker, ExceptionHandler exceptionHandler, Notifier notifier ) { this.immutableConfig = immutableConfig; this.metadataState = metadataState; this.contextState = contextState; this.callbackState = callbackState; this.userState = userState; this.featureFlagState = featureFlagState; this.clientObservable = clientObservable; this.appContext = appContext; this.deviceDataCollector = deviceDataCollector; this.appDataCollector = appDataCollector; this.breadcrumbState = breadcrumbState; this.eventStore = eventStore; this.systemBroadcastReceiver = systemBroadcastReceiver; this.sessionTracker = sessionTracker; this.connectivity = connectivity; this.logger = logger; this.deliveryDelegate = deliveryDelegate; this.lastRunInfoStore = lastRunInfoStore; this.launchCrashTracker = launchCrashTracker; this.lastRunInfo = null; this.exceptionHandler = exceptionHandler; this.notifier = notifier; } void registerLifecycleCallbacks() { if (appContext instanceof Application) { Application application = (Application) appContext; SessionLifecycleCallback sessionCb = new SessionLifecycleCallback(sessionTracker); application.registerActivityLifecycleCallbacks(sessionCb); if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector( new Function2, Unit>() { @SuppressWarnings("unchecked") @Override public Unit invoke(String activity, Map metadata) { leaveBreadcrumb(activity, (Map) metadata, BreadcrumbType.STATE); return null; } } ); application.registerActivityLifecycleCallbacks(activityCb); } } } /** * Registers listeners for system events in the background. This offloads work from the main * thread that collects useful information from callbacks, but that don't need to be done * 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); } } /** * Load information about the last run, and reset the persisted information to the defaults. */ private void persistRunInfo(final LastRunInfo runInfo) { try { bgTaskService.submitTask(TaskType.IO, new Runnable() { @Override public void run() { lastRunInfoStore.persist(runInfo); } }); } catch (RejectedExecutionException exc) { logger.w("Failed to persist last run info", exc); } } private void loadPlugins(@NonNull final Configuration configuration) { NativeInterface.setClient(Client.this); Set userPlugins = configuration.getPlugins(); pluginClient = new PluginClient(userPlugins, immutableConfig, logger); pluginClient.loadPlugins(Client.this); } private void logNull(String property) { logger.e("Invalid null value supplied to client." + property + ", ignoring"); } private void registerComponentCallbacks() { appContext.registerComponentCallbacks(new ClientComponentCallbacks( deviceDataCollector, new Function2() { @Override public Unit invoke(String oldOrientation, String newOrientation) { Map data = new HashMap<>(); data.put("from", oldOrientation); data.put("to", newOrientation); leaveAutoBreadcrumb("Orientation changed", BreadcrumbType.STATE, data); clientObservable.postOrientationChange(newOrientation); return null; } }, new Function2() { @Override public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { leaveAutoBreadcrumb( "Trim Memory", BreadcrumbType.STATE, Collections.singletonMap( "trimLevel", memoryTrimState.getTrimLevelDescription() ) ); } memoryTrimState.emitObservableEvent(); return null; } } )); } void setupNdkPlugin() { if (!setupNdkDirectory()) { logger.w("Failed to setup NDK directory."); return; } String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath(); int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0; clientObservable.postNdkInstall(immutableConfig, lastRunInfoPath, crashes); syncInitialState(); clientObservable.postNdkDeliverPending(); } private boolean setupNdkDirectory() { try { return bgTaskService.submitTask(TaskType.IO, new Callable() { @Override public Boolean call() { File outFile = new File(NativeInterface.getNativeReportPath()); return outFile.exists() || outFile.mkdirs(); } }).get(); } catch (Throwable exc) { return false; } } void addObserver(StateObserver observer) { metadataState.addObserver(observer); breadcrumbState.addObserver(observer); sessionTracker.addObserver(observer); clientObservable.addObserver(observer); userState.addObserver(observer); contextState.addObserver(observer); deliveryDelegate.addObserver(observer); launchCrashTracker.addObserver(observer); memoryTrimState.addObserver(observer); featureFlagState.addObserver(observer); } void removeObserver(StateObserver observer) { metadataState.removeObserver(observer); breadcrumbState.removeObserver(observer); sessionTracker.removeObserver(observer); clientObservable.removeObserver(observer); userState.removeObserver(observer); contextState.removeObserver(observer); deliveryDelegate.removeObserver(observer); launchCrashTracker.removeObserver(observer); memoryTrimState.removeObserver(observer); featureFlagState.removeObserver(observer); } /** * Sends initial state values for Metadata/User/Context to any registered observers. */ void syncInitialState() { metadataState.emitObservableEvent(); contextState.emitObservableEvent(); userState.emitObservableEvent(); memoryTrimState.emitObservableEvent(); featureFlagState.emitObservableEvent(); } /** * Starts tracking a new session. You should disable automatic session tracking via * {@link Configuration#setAutoTrackSessions(boolean)} if you call this method. *

* You should call this at the appropriate time in your application when you wish to start a * session. Any subsequent errors which occur in your application will still be reported to * Bugsnag but will not count towards your application's * * stability score. This will start a new session even if there is already an existing * session; you should call {@link #resumeSession()} if you only want to start a session * when one doesn't already exist. * * @see #resumeSession() * @see #pauseSession() * @see Configuration#setAutoTrackSessions(boolean) */ public void startSession() { sessionTracker.startSession(false); } /** * Pauses tracking of a session. You should disable automatic session tracking via * {@link Configuration#setAutoTrackSessions(boolean)} if you call this method. *

* You should call this at the appropriate time in your application when you wish to pause a * session. Any subsequent errors which occur in your application will still be reported to * Bugsnag but will not count towards your application's * * stability score. This can be advantageous if, for example, you do not wish the * stability score to include crashes in a background service. * * @see #startSession() * @see #resumeSession() * @see Configuration#setAutoTrackSessions(boolean) */ public void pauseSession() { sessionTracker.pauseSession(); } /** * Resumes a session which has previously been paused, or starts a new session if none exists. * If a session has already been resumed or started and has not been paused, calling this * method will have no effect. You should disable automatic session tracking via * {@link Configuration#setAutoTrackSessions(boolean)} if you call this method. *

* It's important to note that sessions are stored in memory for the lifetime of the * application process and are not persisted on disk. Therefore calling this method on app * startup would start a new session, rather than continuing any previous session. *

* You should call this at the appropriate time in your application when you wish to resume * a previously started session. Any subsequent errors which occur in your application will * still be reported to Bugsnag but will not count towards your application's * * stability score. * * @see #startSession() * @see #pauseSession() * @see Configuration#setAutoTrackSessions(boolean) * * @return true if a previous session was resumed, false if a new session was started. */ public boolean resumeSession() { return sessionTracker.resumeSession(); } /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. * * In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ @Nullable public String getContext() { return contextState.getContext(); } /** * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts * represent what was happening in your application at the time an error occurs. * * In an android app the "context" is automatically set as the foreground Activity. * If you would like to set this value manually, you should alter this property. */ public void setContext(@Nullable String context) { contextState.setManualContext(context); } /** * Sets the user associated with the event. */ @Override public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) { userState.setUser(new User(id, email, name)); } /** * Returns the currently set User information. */ @NonNull @Override public User getUser() { return userState.getUser(); } /** * Add a "on error" callback, to execute code at the point where an error report is * captured in Bugsnag. * * You can use this to add or modify information attached to an Event * before it is sent to your dashboard. You can also return * false from any callback to prevent delivery. "on error" * callbacks do not run before reports generated in the event * of immediate app termination from crashes in C/C++ code. * * For example: * * Bugsnag.addOnError(new OnErrorCallback() { * public boolean run(Event event) { * event.setSeverity(Severity.INFO); * return true; * } * }) * * @param onError a callback to run before sending errors to Bugsnag * @see OnErrorCallback */ @Override public void addOnError(@NonNull OnErrorCallback onError) { if (onError != null) { callbackState.addOnError(onError); } else { logNull("addOnError"); } } /** * Removes a previously added "on error" callback * @param onError the callback to remove */ @Override public void removeOnError(@NonNull OnErrorCallback onError) { if (onError != null) { callbackState.removeOnError(onError); } else { logNull("removeOnError"); } } /** * Add an "on breadcrumb" callback, to execute code before every * breadcrumb captured by Bugsnag. * * You can use this to modify breadcrumbs before they are stored by Bugsnag. * You can also return false from any callback to ignore a breadcrumb. * * For example: * * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { * public boolean run(Breadcrumb breadcrumb) { * return false; // ignore the breadcrumb * } * }) * * @param onBreadcrumb a callback to run before a breadcrumb is captured * @see OnBreadcrumbCallback */ @Override public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { if (onBreadcrumb != null) { callbackState.addOnBreadcrumb(onBreadcrumb); } else { logNull("addOnBreadcrumb"); } } /** * Removes a previously added "on breadcrumb" callback * @param onBreadcrumb the callback to remove */ @Override public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { if (onBreadcrumb != null) { callbackState.removeOnBreadcrumb(onBreadcrumb); } else { logNull("removeOnBreadcrumb"); } } /** * Add an "on session" callback, to execute code before every * session captured by Bugsnag. * * You can use this to modify sessions before they are stored by Bugsnag. * You can also return false from any callback to ignore a session. * * For example: * * Bugsnag.onSession(new OnSessionCallback() { * public boolean run(Session session) { * return false; // ignore the session * } * }) * * @param onSession a callback to run before a session is captured * @see OnSessionCallback */ @Override public void addOnSession(@NonNull OnSessionCallback onSession) { if (onSession != null) { callbackState.addOnSession(onSession); } else { logNull("addOnSession"); } } /** * Removes a previously added "on session" callback * @param onSession the callback to remove */ @Override public void removeOnSession(@NonNull OnSessionCallback onSession) { if (onSession != null) { callbackState.removeOnSession(onSession); } else { logNull("removeOnSession"); } } /** * Notify Bugsnag of a handled exception * * @param exception the exception to send to Bugsnag */ public void notify(@NonNull Throwable exception) { notify(exception, null); } /** * Notify Bugsnag of a handled exception * * @param exc the exception to send to Bugsnag * @param onError callback invoked on the generated error report for * additional modification */ public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { if (exc != null) { if (immutableConfig.shouldDiscardError(exc)) { return; } SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); Metadata metadata = metadataState.getMetadata(); FeatureFlags featureFlags = featureFlagState.getFeatureFlags(); Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags, logger); populateAndNotifyAndroidEvent(event, onError); } else { logNull("notify"); } } /** * Caches an error then attempts to notify. * * Should only ever be called from the {@link ExceptionHandler}. */ void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, @SeverityReason.SeverityReasonType String severityReason, @Nullable String attributeValue) { SeverityReason handledState = SeverityReason.newInstance(severityReason, Severity.ERROR, attributeValue); Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata); Event event = new Event(exc, immutableConfig, handledState, data, featureFlagState.getFeatureFlags(), logger); populateAndNotifyAndroidEvent(event, null); // persist LastRunInfo so that on relaunch users can check the app crashed int consecutiveLaunchCrashes = lastRunInfo == null ? 0 : lastRunInfo.getConsecutiveLaunchCrashes(); boolean launching = launchCrashTracker.isLaunching(); if (launching) { consecutiveLaunchCrashes += 1; } LastRunInfo runInfo = new LastRunInfo(consecutiveLaunchCrashes, true, launching); persistRunInfo(runInfo); // suspend execution of any further background tasks, waiting for previously // submitted ones to complete. bgTaskService.shutdown(); } void populateAndNotifyAndroidEvent(@NonNull Event event, @Nullable OnErrorCallback onError) { // Capture the state of the app and device and attach diagnostics to the event event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime())); event.addMetadata("device", deviceDataCollector.getDeviceMetadata()); // add additional info that belongs in metadata // generate new object each time, as this can be mutated by end-users event.setApp(appDataCollector.generateAppWithState()); event.addMetadata("app", appDataCollector.getAppDataMetadata()); // Attach breadcrumbState to the event event.setBreadcrumbs(breadcrumbState.copy()); // Attach user info to the event User user = userState.getUser(); event.setUser(user.getId(), user.getEmail(), user.getName()); // Attach context to the event event.setContext(contextState.getContext()); notifyInternal(event, onError); } void notifyInternal(@NonNull Event event, @Nullable OnErrorCallback onError) { // set the redacted keys on the event as this // will not have been set for RN/Unity events Collection redactedKeys = metadataState.getMetadata().getRedactedKeys(); event.setRedactedKeys(redactedKeys); // get session for event Session currentSession = sessionTracker.getCurrentSession(); if (currentSession != null && (immutableConfig.getAutoTrackSessions() || !currentSession.isAutoCaptured())) { event.setSession(currentSession); } // Run on error tasks, don't notify if any return false if (!callbackState.runOnErrorTasks(event, logger) || (onError != null && !onError.onError(event))) { logger.d("Skipping notification - onError task returned false"); return; } // leave an error breadcrumb of this event - for the next event leaveErrorBreadcrumb(event); deliveryDelegate.deliver(event); } /** * Returns the current buffer of breadcrumbs that will be sent with captured events. This * ordered list represents the most recent breadcrumbs to be captured up to the limit * set in {@link Configuration#getMaxBreadcrumbs()}. * * The returned collection is readonly and mutating the list will cause no effect on the * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and * {@link Configuration#addOnBreadcrumb(OnBreadcrumbCallback)} instead. * * @return a list of collected breadcrumbs */ @NonNull public List getBreadcrumbs() { return breadcrumbState.copy(); } @NonNull AppDataCollector getAppDataCollector() { return appDataCollector; } @NonNull DeviceDataCollector getDeviceDataCollector() { return deviceDataCollector; } /** * Adds a map of multiple metadata key-value pairs to the specified section. */ @Override public void addMetadata(@NonNull String section, @NonNull Map value) { if (section != null && value != null) { metadataState.addMetadata(section, value); } else { logNull("addMetadata"); } } /** * Adds the specified key and value in the specified section. The value can be of * any primitive type or a collection such as a map, set or array. */ @Override public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) { if (section != null && key != null) { metadataState.addMetadata(section, key, value); } else { logNull("addMetadata"); } } /** * Removes all the data from the specified section. */ @Override public void clearMetadata(@NonNull String section) { if (section != null) { metadataState.clearMetadata(section); } else { logNull("clearMetadata"); } } /** * Removes data with the specified key from the specified section. */ @Override public void clearMetadata(@NonNull String section, @NonNull String key) { if (section != null && key != null) { metadataState.clearMetadata(section, key); } else { logNull("clearMetadata"); } } /** * Returns a map of data in the specified section. */ @Nullable @Override public Map getMetadata(@NonNull String section) { if (section != null) { return metadataState.getMetadata(section); } else { logNull("getMetadata"); return null; } } /** * Returns the value of the specified key in the specified section. */ @Override @Nullable public Object getMetadata(@NonNull String section, @NonNull String key) { if (section != null && key != null) { return metadataState.getMetadata(section, key); } else { logNull("getMetadata"); return null; } } // cast map to retain original signature until next major version bump, as this // method signature is used by Unity/React native @NonNull @SuppressWarnings({"unchecked", "rawtypes"}) Map getMetadata() { return (Map) metadataState.getMetadata().toMap(); } /** * Leave a "breadcrumb" log message, representing an action that occurred * in your app, to aid with debugging. * * @param message the log message to leave */ public void leaveBreadcrumb(@NonNull String message) { if (message != null) { breadcrumbState.add(new Breadcrumb(message, logger)); } else { logNull("leaveBreadcrumb"); } } /** * Leave a "breadcrumb" log message representing an action or event which * occurred in your app, to aid with debugging * @param message A short label * @param metadata Additional diagnostic information about the app environment * @param type A category for the breadcrumb */ public void leaveBreadcrumb(@NonNull String message, @NonNull Map metadata, @NonNull BreadcrumbType type) { if (message != null && type != null && metadata != null) { breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); } else { logNull("leaveBreadcrumb"); } } /** * Intended for internal use only - leaves a breadcrumb if the type is enabled for automatic * breadcrumbs. * * @param message A short label * @param type A category for the breadcrumb * @param metadata Additional diagnostic information about the app environment */ void leaveAutoBreadcrumb(@NonNull String message, @NonNull BreadcrumbType type, @NonNull Map metadata) { if (!immutableConfig.shouldDiscardBreadcrumb(type)) { breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); } } private void leaveErrorBreadcrumb(@NonNull Event event) { // Add a breadcrumb for this event occurring List errors = event.getErrors(); if (errors.size() > 0) { String errorClass = errors.get(0).getErrorClass(); String message = errors.get(0).getErrorMessage(); Map data = new HashMap<>(); data.put("errorClass", errorClass); data.put("message", message); data.put("unhandled", String.valueOf(event.isUnhandled())); data.put("severity", event.getSeverity().toString()); breadcrumbState.add(new Breadcrumb(errorClass, BreadcrumbType.ERROR, data, new Date(), logger)); } } /** * {@inheritDoc} */ @Override public void addFeatureFlag(@NonNull String name) { if (name != null) { featureFlagState.addFeatureFlag(name); } else { logNull("addFeatureFlag"); } } /** * {@inheritDoc} */ @Override public void addFeatureFlag(@NonNull String name, @Nullable String variant) { if (name != null) { featureFlagState.addFeatureFlag(name, variant); } else { logNull("addFeatureFlag"); } } /** * {@inheritDoc} */ @Override public void addFeatureFlags(@NonNull Iterable featureFlags) { if (featureFlags != null) { featureFlagState.addFeatureFlags(featureFlags); } else { logNull("addFeatureFlags"); } } /** * {@inheritDoc} */ @Override public void clearFeatureFlag(@NonNull String name) { if (name != null) { featureFlagState.clearFeatureFlag(name); } else { logNull("clearFeatureFlag"); } } /** * {@inheritDoc} */ @Override public void clearFeatureFlags() { featureFlagState.clearFeatureFlags(); } /** * Retrieves information about the last launch of the application, if it has been run before. * * For example, this allows checking whether the app crashed on its last launch, which could * be used to perform conditional behaviour to recover from crashes, such as clearing the * app data cache. */ @Nullable public LastRunInfo getLastRunInfo() { return lastRunInfo; } /** * Informs Bugsnag that the application has finished launching. Once this has been called * {@link AppWithState#isLaunching()} will always be false in any new error reports, * and synchronous delivery will not be attempted on the next launch for any fatal crashes. * * By default this method will be called after Bugsnag is initialized when * {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually * has precedence over the value supplied via the launchDurationMillis configuration option. */ public void markLaunchCompleted() { launchCrashTracker.markLaunchCompleted(); } SessionTracker getSessionTracker() { return sessionTracker; } @NonNull EventStore getEventStore() { return eventStore; } /** * Finalize by removing the receiver * * @throws Throwable if something goes wrong */ @SuppressWarnings("checkstyle:NoFinalizer") protected void finalize() throws Throwable { if (systemBroadcastReceiver != null) { try { ContextExtensionsKt.unregisterReceiverSafe(appContext, systemBroadcastReceiver, logger); } catch (IllegalArgumentException exception) { logger.w("Receiver not registered"); } } super.finalize(); } private void warnIfNotAppContext(Context androidContext) { if (!(androidContext instanceof Application)) { logger.w("You should initialize Bugsnag from the onCreate() callback of your " + "Application subclass, as this guarantees errors are captured as early " + "as possible. " + "If a custom Application subclass is not possible in your app then you " + "should suppress this warning by passing the Application context instead: " + "Bugsnag.start(context.getApplicationContext()). " + "For further info see: " + "https://docs.bugsnag.com/platforms/android/#basic-configuration"); } } ImmutableConfig getConfig() { return immutableConfig; } void setBinaryArch(String binaryArch) { getAppDataCollector().setBinaryArch(binaryArch); } Context getAppContext() { return appContext; } /** * Intended for internal use only - sets the code bundle id for React Native */ @Nullable String getCodeBundleId() { return appDataCollector.getCodeBundleId(); } /** * Intended for internal use only - sets the code bundle id for React Native */ void setCodeBundleId(@Nullable String codeBundleId) { appDataCollector.setCodeBundleId(codeBundleId); } void addRuntimeVersionInfo(@NonNull String key, @NonNull String value) { deviceDataCollector.addRuntimeVersionInfo(key, value); } @VisibleForTesting void close() { connectivity.unregisterForNetworkChanges(); bgTaskService.shutdown(); } Logger getLogger() { return logger; } /** * Retrieves an instantiated plugin of the given type, or null if none has been created */ @SuppressWarnings("rawtypes") @Nullable Plugin getPlugin(@NonNull Class clz) { return pluginClient.findPlugin(clz); } Notifier getNotifier() { return notifier; } MetadataState getMetadataState() { return metadataState; } FeatureFlagState getFeatureFlagState() { return featureFlagState; } ContextState getContextState() { return contextState; } void setAutoNotify(boolean autoNotify) { pluginClient.setAutoNotify(this, autoNotify); if (autoNotify) { exceptionHandler.install(); } else { exceptionHandler.uninstall(); } } void setAutoDetectAnrs(boolean autoDetectAnrs) { pluginClient.setAutoDetectAnrs(this, autoDetectAnrs); } }