diff --git a/.idea/runConfigurations.xml b/.idea/runConfigurations.xml deleted file mode 100644 index e497da9998..0000000000 --- a/.idea/runConfigurations.xml +++ /dev/null @@ -1,13 +0,0 @@ - - - - - - \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/App.kt b/app/src/main/java/com/bugsnag/android/App.kt index bbdf02b4ea..e603b2926e 100644 --- a/app/src/main/java/com/bugsnag/android/App.kt +++ b/app/src/main/java/com/bugsnag/android/App.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException /** diff --git a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt index f39d817a20..e3f3a50762 100644 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -1,11 +1,14 @@ package com.bugsnag.android +import android.annotation.SuppressLint import android.app.ActivityManager +import android.app.Application import android.content.Context -import android.content.pm.ApplicationInfo import android.content.pm.PackageManager -import android.os.Build +import android.os.Build.VERSION +import android.os.Build.VERSION_CODES import android.os.SystemClock +import com.bugsnag.android.internal.ImmutableConfig /** * Collects various data on the application state @@ -22,13 +25,13 @@ internal class AppDataCollector( var codeBundleId: String? = null private val packageName: String = appContext.packageName - private var packageInfo = packageManager?.getPackageInfo(packageName, 0) - private var appInfo: ApplicationInfo? = packageManager?.getApplicationInfo(packageName, 0) + private val bgWorkRestricted = isBackgroundWorkRestricted() private var binaryArch: String? = null private val appName = getAppName() + private val processName = findProcessName() private val releaseStage = config.releaseStage - private val versionName = config.appVersion ?: packageInfo?.versionName + private val versionName = config.appVersion ?: config.packageInfo?.versionName fun generateApp(): App = App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) @@ -47,18 +50,19 @@ internal class AppDataCollector( fun getAppDataMetadata(): MutableMap { val map = HashMap() map["name"] = appName - map["activeScreen"] = getActiveScreenClass() + map["activeScreen"] = sessionTracker.contextActivity map["memoryUsage"] = getMemoryUsage() map["lowMemory"] = isLowMemory() - isBackgroundWorkRestricted()?.let { - map["backgroundWorkRestricted"] = it + bgWorkRestricted?.let { + map["backgroundWorkRestricted"] = bgWorkRestricted + } + processName?.let { + map["processName"] = it } return map } - fun getActiveScreenClass(): String? = sessionTracker.contextActivity - /** * Get the actual memory used by the VM (which may not be the total used * by the app in the case of NDK usage). @@ -73,7 +77,7 @@ internal class AppDataCollector( * https://developer.android.com/reference/android/app/ActivityManager#isBackgroundRestricted() */ private fun isBackgroundWorkRestricted(): Boolean? { - return if (activityManager == null || Build.VERSION.SDK_INT < Build.VERSION_CODES.P) { + return if (activityManager == null || VERSION.SDK_INT < VERSION_CODES.P) { null } else if (activityManager.isBackgroundRestricted) { true // only return non-null value if true to avoid noise in error reports @@ -129,7 +133,7 @@ internal class AppDataCollector( * AndroidManifest.xml */ private fun getAppName(): String? { - val copy = appInfo + val copy = config.appInfo return when { packageManager != null && copy != null -> { packageManager.getApplicationLabel(copy).toString() @@ -138,6 +142,31 @@ internal class AppDataCollector( } } + /** + * Finds the name of the current process, or null if this cannot be found. + */ + @SuppressLint("PrivateApi") + private fun findProcessName(): String? { + return runCatching { + when { + VERSION.SDK_INT >= VERSION_CODES.P -> { + Application.getProcessName() + } + else -> { + // see https://stackoverflow.com/questions/19631894 + val clz = Class.forName("android.app.ActivityThread") + val methodName = when { + VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2 -> "currentProcessName" + else -> "currentPackageName" + } + + val getProcessName = clz.getDeclaredMethod(methodName) + getProcessName.invoke(null) as String + } + } + }.getOrNull() + } + companion object { internal val startTimeMs = SystemClock.elapsedRealtime() diff --git a/app/src/main/java/com/bugsnag/android/AppWithState.kt b/app/src/main/java/com/bugsnag/android/AppWithState.kt index 173ce4e192..bf05c4af6b 100644 --- a/app/src/main/java/com/bugsnag/android/AppWithState.kt +++ b/app/src/main/java/com/bugsnag/android/AppWithState.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + /** * Stateful information set by the notifier about your app can be found on this class. These values * can be accessed and amended if necessary. diff --git a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt index 01bb3e17d3..4e763633f0 100644 --- a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt +++ b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt @@ -164,11 +164,20 @@ internal class BackgroundTaskService( // shutdown the IO executor last. errorExecutor.shutdown() sessionExecutor.shutdown() - errorExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) - sessionExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) + + errorExecutor.awaitTerminationSafe() + sessionExecutor.awaitTerminationSafe() // shutdown the IO executor last, waiting for any existing tasks to complete ioExecutor.shutdown() - ioExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) + ioExecutor.awaitTerminationSafe() + } + + private fun ThreadPoolExecutor.awaitTerminationSafe() { + try { + awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) + } catch (ignored: InterruptedException) { + // ignore interrupted exception as the JVM is shutting down + } } } diff --git a/app/src/main/java/com/bugsnag/android/BaseObservable.kt b/app/src/main/java/com/bugsnag/android/BaseObservable.kt index 1f61b03d9b..d9e8e36185 100644 --- a/app/src/main/java/com/bugsnag/android/BaseObservable.kt +++ b/app/src/main/java/com/bugsnag/android/BaseObservable.kt @@ -1,10 +1,46 @@ package com.bugsnag.android -import java.util.Observable +import com.bugsnag.android.internal.StateObserver +import java.util.concurrent.CopyOnWriteArrayList -internal open class BaseObservable : Observable() { - fun notifyObservers(event: StateEvent) { - setChanged() - super.notifyObservers(event) +internal open class BaseObservable { + + internal val observers = CopyOnWriteArrayList() + + /** + * Adds an observer that can react to [StateEvent] messages. + */ + fun addObserver(observer: StateObserver) { + observers.addIfAbsent(observer) + } + + /** + * Removes a previously added observer that reacts to [StateEvent] messages. + */ + fun removeObserver(observer: StateObserver) { + observers.remove(observer) } + + /** + * This method should be invoked when the notifier's state has changed. If an observer + * has been set, it will be notified of the [StateEvent] message so that it can react + * appropriately. If no observer has been set then this method will no-op. + */ + internal inline fun updateState(provider: () -> StateEvent) { + // optimization to avoid unnecessary iterator and StateEvent construction + if (observers.isEmpty()) { + return + } + + // construct the StateEvent object and notify observers + val event = provider() + observers.forEach { it.onStateChange(event) } + } + + /** + * An eager version of [updateState], which is intended primarily for use in Java code. + * If the event will occur very frequently, you should consider calling the lazy method + * instead. + */ + fun updateState(event: StateEvent) = updateState { event } } diff --git a/app/src/main/java/com/bugsnag/android/Breadcrumb.java b/app/src/main/java/com/bugsnag/android/Breadcrumb.java index 4da160a4ee..5d7ba6a78c 100644 --- a/app/src/main/java/com/bugsnag/android/Breadcrumb.java +++ b/app/src/main/java/com/bugsnag/android/Breadcrumb.java @@ -10,7 +10,8 @@ import java.util.Map; @SuppressWarnings("ConstantConditions") public class Breadcrumb implements JsonStream.Streamable { - private final BreadcrumbInternal impl; + // non-private to allow direct field access optimizations + final BreadcrumbInternal impl; private final Logger logger; Breadcrumb(@NonNull String message, @NonNull Logger logger) { @@ -36,7 +37,7 @@ public class Breadcrumb implements JsonStream.Streamable { */ public void setMessage(@NonNull String message) { if (message != null) { - impl.setMessage(message); + impl.message = message; } else { logNull("message"); } @@ -47,7 +48,7 @@ public class Breadcrumb implements JsonStream.Streamable { */ @NonNull public String getMessage() { - return impl.getMessage(); + return impl.message; } /** @@ -56,7 +57,7 @@ public class Breadcrumb implements JsonStream.Streamable { */ public void setType(@NonNull BreadcrumbType type) { if (type != null) { - impl.setType(type); + impl.type = type; } else { logNull("type"); } @@ -68,14 +69,14 @@ public class Breadcrumb implements JsonStream.Streamable { */ @NonNull public BreadcrumbType getType() { - return impl.getType(); + return impl.type; } /** * Sets diagnostic data relating to the breadcrumb */ public void setMetadata(@Nullable Map metadata) { - impl.setMetadata(metadata); + impl.metadata = metadata; } /** @@ -83,7 +84,7 @@ public class Breadcrumb implements JsonStream.Streamable { */ @Nullable public Map getMetadata() { - return impl.getMetadata(); + return impl.metadata; } /** @@ -91,12 +92,12 @@ public class Breadcrumb implements JsonStream.Streamable { */ @NonNull public Date getTimestamp() { - return impl.getTimestamp(); + return impl.timestamp; } @NonNull String getStringTimestamp() { - return DateUtils.toIso8601(impl.getTimestamp()); + return DateUtils.toIso8601(impl.timestamp); } @Override diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt index 0bc68fb349..49499b770d 100644 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt +++ b/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt @@ -9,11 +9,11 @@ import java.util.Date * attached to a crash to help diagnose what events lead to the error. */ internal class BreadcrumbInternal internal constructor( - var message: String, - var type: BreadcrumbType, - var metadata: MutableMap?, - val timestamp: Date = Date() -) : JsonStream.Streamable { + @JvmField var message: String, + @JvmField var type: BreadcrumbType, + @JvmField var metadata: MutableMap?, + @JvmField val timestamp: Date = Date() +) : JsonStream.Streamable { // JvmField allows direct field access optimizations internal constructor(message: String) : this( message, diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt index a4d8593731..331ca721a5 100644 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt @@ -1,55 +1,96 @@ package com.bugsnag.android import java.io.IOException -import java.util.Queue -import java.util.concurrent.ConcurrentLinkedQueue +import java.util.concurrent.atomic.AtomicInteger +/** + * Stores breadcrumbs added to the [Client] in a ring buffer. If the number of breadcrumbs exceeds + * the maximum configured limit then the oldest breadcrumb in the ring buffer will be overwritten. + * + * When the breadcrumbs are required for generation of an event a [List] is constructed and + * breadcrumbs added in the order of their addition. + */ internal class BreadcrumbState( - maxBreadcrumbs: Int, - val callbackState: CallbackState, - val logger: Logger + private val maxBreadcrumbs: Int, + private val callbackState: CallbackState, + private val logger: Logger ) : BaseObservable(), JsonStream.Streamable { - val store: Queue = ConcurrentLinkedQueue() + /* + * We use the `index` as both a pointer to the tail of our ring-buffer, and also as "cheat" + * semaphore. When the ring-buffer is being copied - the index is set to a negative number, + * which is an invalid array-index. By masking the `expected` value in a `compareAndSet` with + * `validIndexMask`: the CAS operation will only succeed if it wouldn't interrupt a concurrent + * `copy()` call. + */ + private val validIndexMask: Int = Int.MAX_VALUE - private val maxBreadcrumbs: Int - - init { - when { - maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs - else -> this.maxBreadcrumbs = 0 - } - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - pruneBreadcrumbs() - writer.beginArray() - store.forEach { it.toStream(writer) } - writer.endArray() - } + private val store = arrayOfNulls(maxBreadcrumbs) + private val index = AtomicInteger(0) fun add(breadcrumb: Breadcrumb) { - if (!callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { + if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { return } - store.add(breadcrumb) - pruneBreadcrumbs() - notifyObservers( + // store the breadcrumb in the ring buffer + val position = getBreadcrumbIndex() + store[position] = breadcrumb + + updateState { + // use direct field access to avoid overhead of accessor method StateEvent.AddBreadcrumb( - breadcrumb.message, - breadcrumb.type, - DateUtils.toIso8601(breadcrumb.timestamp), - breadcrumb.metadata ?: mutableMapOf() + breadcrumb.impl.message, + breadcrumb.impl.type, + DateUtils.toIso8601(breadcrumb.impl.timestamp), + breadcrumb.impl.metadata ?: mutableMapOf() ) - ) + } + } + + /** + * Retrieves the index in the ring buffer where the breadcrumb should be stored. + */ + private fun getBreadcrumbIndex(): Int { + while (true) { + val currentValue = index.get() and validIndexMask + val nextValue = (currentValue + 1) % maxBreadcrumbs + if (index.compareAndSet(currentValue, nextValue)) { + return currentValue + } + } } - private fun pruneBreadcrumbs() { - // Remove oldest breadcrumbState until new max size reached - while (store.size > maxBreadcrumbs) { - store.poll() + /** + * Creates a copy of the breadcrumbs in the order of their addition. + */ + fun copy(): List { + if (maxBreadcrumbs == 0) { + return emptyList() + } + + // Set a negative value that stops any other thread from adding a breadcrumb. + // This handles reentrancy by waiting here until the old value has been reset. + var tail = -1 + while (tail == -1) { + tail = index.getAndSet(-1) + } + + try { + val result = arrayOfNulls(maxBreadcrumbs) + store.copyInto(result, 0, tail, maxBreadcrumbs) + store.copyInto(result, maxBreadcrumbs - tail, 0, tail) + return result.filterNotNull() + } finally { + index.set(tail) } } + + @Throws(IOException::class) + override fun toStream(writer: JsonStream) { + val crumbs = copy() + writer.beginArray() + crumbs.forEach { it.toStream(writer) } + writer.endArray() + } } diff --git a/app/src/main/java/com/bugsnag/android/CallbackState.kt b/app/src/main/java/com/bugsnag/android/CallbackState.kt index cb7ae14d3c..734d26b18d 100644 --- a/app/src/main/java/com/bugsnag/android/CallbackState.kt +++ b/app/src/main/java/com/bugsnag/android/CallbackState.kt @@ -33,6 +33,10 @@ internal data class CallbackState( } fun runOnErrorTasks(event: Event, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onErrorTasks.isEmpty()) { + return true + } onErrorTasks.forEach { try { if (!it.onError(event)) { @@ -46,6 +50,10 @@ internal data class CallbackState( } fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onBreadcrumbTasks.isEmpty()) { + return true + } onBreadcrumbTasks.forEach { try { if (!it.onBreadcrumb(breadcrumb)) { @@ -59,6 +67,10 @@ internal data class CallbackState( } fun runOnSessionTasks(session: Session, logger: Logger): Boolean { + // optimization to avoid construction of iterator when no callbacks set + if (onSessionTasks.isEmpty()) { + return true + } onSessionTasks.forEach { try { if (!it.onSession(session)) { diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index 6ed0a6f7b6..205e42c165 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -2,14 +2,15 @@ package com.bugsnag.android; import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom; import static com.bugsnag.android.ContextExtensionsKt.getStorageManagerFrom; -import static com.bugsnag.android.ImmutableConfigKt.sanitiseConfiguration; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; +import static com.bugsnag.android.internal.ImmutableConfigKt.sanitiseConfiguration; + +import com.bugsnag.android.internal.ImmutableConfig; +import com.bugsnag.android.internal.StateObserver; import android.app.ActivityManager; import android.app.Application; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.res.Resources; import android.os.Environment; import android.os.storage.StorageManager; @@ -22,13 +23,11 @@ import kotlin.Unit; import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; -import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.List; import java.util.Map; -import java.util.Observer; import java.util.Set; import java.util.concurrent.RejectedExecutionException; @@ -72,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; @@ -152,9 +151,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware { breadcrumbState = new BreadcrumbState(maxBreadcrumbs, callbackState, logger); storageManager = getStorageManagerFrom(appContext); - contextState = new ContextState(); - contextState.setContext(configuration.getContext()); + + if (configuration.getContext() != null) { + contextState.setManualContext(configuration.getContext()); + } sessionStore = new SessionStore(immutableConfig, logger, null); sessionTracker = new SessionTracker(immutableConfig, callbackState, this, @@ -186,7 +187,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker); application.registerActivityLifecycleCallbacks(sessionLifecycleCallback); - if (immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.STATE)) { + if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector( new Function2, Unit>() { @SuppressWarnings("unchecked") @@ -221,12 +222,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware { exceptionHandler.install(); } - // register a receiver for automatic breadcrumbs - systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService); - - registerOrientationChangeListener(); - registerMemoryTrimListener(); - // load last run info lastRunInfoStore = new LastRunInfoStore(immutableConfig); lastRunInfo = loadLastRunInfo(); @@ -234,13 +229,16 @@ public class Client implements MetadataAware, CallbackAware, UserAware { // 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); @@ -299,6 +297,25 @@ public class Client implements MetadataAware, CallbackAware, UserAware { 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); @@ -340,10 +357,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware { return configuration.impl.metadataState.copy(copy); } - private void registerOrientationChangeListener() { - IntentFilter configFilter = new IntentFilter(); - configFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); - ConfigChangeReceiver receiver = new ConfigChangeReceiver(deviceDataCollector, + private void registerComponentCallbacks() { + appContext.registerComponentCallbacks(new ClientComponentCallbacks( + deviceDataCollector, new Function2() { @Override public Unit invoke(String oldOrientation, String newOrientation) { @@ -354,14 +370,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { clientObservable.postOrientationChange(newOrientation); return null; } - } - ); - ContextExtensionsKt.registerReceiverSafe(appContext, receiver, configFilter, logger); - } - - private void registerMemoryTrimListener() { - appContext.registerComponentCallbacks(new ClientComponentCallbacks( - new Function1() { + }, new Function1() { @Override public Unit invoke(Boolean isLowMemory) { clientObservable.postMemoryTrimEvent(isLowMemory); @@ -379,7 +388,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { clientObservable.postNdkDeliverPending(); } - void registerObserver(Observer observer) { + void addObserver(StateObserver observer) { metadataState.addObserver(observer); breadcrumbState.addObserver(observer); sessionTracker.addObserver(observer); @@ -390,15 +399,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware { launchCrashTracker.addObserver(observer); } - void unregisterObserver(Observer observer) { - metadataState.deleteObserver(observer); - breadcrumbState.deleteObserver(observer); - sessionTracker.deleteObserver(observer); - clientObservable.deleteObserver(observer); - userState.deleteObserver(observer); - contextState.deleteObserver(observer); - deliveryDelegate.deleteObserver(observer); - launchCrashTracker.deleteObserver(observer); + 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); } /** @@ -494,7 +503,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { * If you would like to set this value manually, you should alter this property. */ public void setContext(@Nullable String context) { - contextState.setContext(context); + contextState.setManualContext(context); } /** @@ -656,6 +665,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware { */ 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(); Event event = new Event(exc, immutableConfig, severityReason, metadata, logger); @@ -706,35 +718,19 @@ public class Client implements MetadataAware, CallbackAware, UserAware { event.addMetadata("app", appDataCollector.getAppDataMetadata()); // Attach breadcrumbState to the event - event.setBreadcrumbs(new ArrayList<>(breadcrumbState.getStore())); + event.setBreadcrumbs(breadcrumbState.copy()); // Attach user info to the event User user = userState.getUser(); event.setUser(user.getId(), user.getEmail(), user.getName()); - // Attach default context from active activity - if (Intrinsics.isEmpty(event.getContext())) { - String context = contextState.getContext(); - event.setContext(context != null ? context : appDataCollector.getActiveScreenClass()); - } + // Attach context to the event + event.setContext(contextState.getContext()); notifyInternal(event, onError); } void notifyInternal(@NonNull Event event, @Nullable OnErrorCallback onError) { - String type = event.getImpl().getSeverityReasonType(); - logger.d("Client#notifyInternal() - event captured by Client, type=" + type); - // Don't notify if this event class should be ignored - if (event.shouldDiscardClass()) { - logger.d("Skipping notification - should not notify for this class"); - return; - } - - if (!immutableConfig.shouldNotifyForReleaseStage()) { - logger.d("Skipping notification - should not notify for this release stage"); - return; - } - // set the redacted keys on the event as this // will not have been set for RN/Unity events Set redactedKeys = metadataState.getMetadata().getRedactedKeys(); @@ -773,7 +769,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { */ @NonNull public List getBreadcrumbs() { - return new ArrayList<>(breadcrumbState.getStore()); + return breadcrumbState.copy(); } @NonNull @@ -864,9 +860,12 @@ public class Client implements MetadataAware, CallbackAware, UserAware { } } + // 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 metadataState.getMetadata().toMap(); + return (Map) metadataState.getMetadata().toMap(); } /** @@ -911,7 +910,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { void leaveAutoBreadcrumb(@NonNull String message, @NonNull BreadcrumbType type, @NonNull Map metadata) { - if (immutableConfig.shouldRecordBreadcrumbType(type)) { + if (!immutableConfig.shouldDiscardBreadcrumb(type)) { breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); } } @@ -1033,6 +1032,10 @@ public class Client implements MetadataAware, CallbackAware, UserAware { return metadataState; } + ContextState getContextState() { + return contextState; + } + void setAutoNotify(boolean autoNotify) { pluginClient.setAutoNotify(this, autoNotify); diff --git a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt index 74c2435598..7095b50029 100644 --- a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt +++ b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt @@ -4,9 +4,19 @@ import android.content.ComponentCallbacks import android.content.res.Configuration internal class ClientComponentCallbacks( + private val deviceDataCollector: DeviceDataCollector, + private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit, val callback: (Boolean) -> Unit ) : ComponentCallbacks { - override fun onConfigurationChanged(newConfig: Configuration) {} + + override fun onConfigurationChanged(newConfig: Configuration) { + val oldOrientation = deviceDataCollector.getOrientationAsString() + + if (deviceDataCollector.updateOrientation(newConfig.orientation)) { + val newOrientation = deviceDataCollector.getOrientationAsString() + cb(oldOrientation, newOrientation) + } + } override fun onLowMemory() { callback(true) diff --git a/app/src/main/java/com/bugsnag/android/ClientObservable.kt b/app/src/main/java/com/bugsnag/android/ClientObservable.kt index 88e97d745e..8bd8e47522 100644 --- a/app/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/app/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -1,17 +1,23 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + internal class ClientObservable : BaseObservable() { fun postOrientationChange(orientation: String?) { - notifyObservers(StateEvent.UpdateOrientation(orientation)) + updateState { StateEvent.UpdateOrientation(orientation) } } fun postMemoryTrimEvent(isLowMemory: Boolean) { - notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory)) + updateState { StateEvent.UpdateMemoryTrimEvent(isLowMemory) } } - fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) { - notifyObservers( + fun postNdkInstall( + conf: ImmutableConfig, + lastRunInfoPath: String, + consecutiveLaunchCrashes: Int + ) { + updateState { StateEvent.Install( conf.apiKey, conf.enabledErrorTypes.ndkCrashes, @@ -21,10 +27,10 @@ internal class ClientObservable : BaseObservable() { lastRunInfoPath, consecutiveLaunchCrashes ) - ) + } } fun postNdkDeliverPending() { - notifyObservers(StateEvent.DeliverPending) + updateState { StateEvent.DeliverPending } } } diff --git a/app/src/main/java/com/bugsnag/android/ConfigChangeReceiver.kt b/app/src/main/java/com/bugsnag/android/ConfigChangeReceiver.kt deleted file mode 100644 index ba87ccfc9f..0000000000 --- a/app/src/main/java/com/bugsnag/android/ConfigChangeReceiver.kt +++ /dev/null @@ -1,22 +0,0 @@ -package com.bugsnag.android - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent - -internal class ConfigChangeReceiver( - private val deviceDataCollector: DeviceDataCollector, - private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit -) : BroadcastReceiver() { - - var orientation = deviceDataCollector.calculateOrientation() - - override fun onReceive(context: Context?, intent: Intent?) { - val newOrientation = deviceDataCollector.calculateOrientation() - - if (!newOrientation.equals(orientation)) { - cb(orientation, newOrientation) - orientation = newOrientation - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt index 9e60eb1262..e02e0ffdff 100644 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt @@ -37,19 +37,19 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS var context: String? = null - var redactedKeys: Set = metadataState.metadata.redactedKeys + var redactedKeys: Set + get() = metadataState.metadata.redactedKeys set(value) { metadataState.metadata.redactedKeys = value - field = value } var discardClasses: Set = emptySet() var enabledReleaseStages: Set? = null - var enabledBreadcrumbTypes: Set? = BreadcrumbType.values().toSet() + var enabledBreadcrumbTypes: Set? = null var projectPackages: Set = emptySet() var persistenceDirectory: File? = null - protected val plugins = mutableSetOf() + protected val plugins = HashSet() override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError) override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError) diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java index c4dc5a5a01..033a4d96e1 100644 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ b/app/src/main/java/com/bugsnag/android/Configuration.java @@ -4,6 +4,7 @@ import android.content.Context; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import androidx.annotation.VisibleForTesting; import java.io.File; import java.util.Locale; @@ -19,7 +20,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { private static final int MIN_BREADCRUMBS = 0; private static final int MAX_BREADCRUMBS = 100; - private static final String API_KEY_REGEX = "[A-Fa-f0-9]{32}"; + private static final int VALID_API_KEY_LEN = 32; private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0; final ConfigInternal impl; @@ -47,14 +48,29 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { } private void validateApiKey(String value) { - if (Intrinsics.isEmpty(value)) { - throw new IllegalArgumentException("No Bugsnag API Key set"); + if (isInvalidApiKey(value)) { + DebugLogger.INSTANCE.w("Invalid configuration. " + + "apiKey should be a 32-character hexademical string, got " + value); } + } - if (!value.matches(API_KEY_REGEX)) { - DebugLogger.INSTANCE.w(String.format("Invalid configuration. apiKey should be a " - + "32-character hexademical string, got \"%s\"", value)); + @VisibleForTesting + static boolean isInvalidApiKey(String apiKey) { + if (Intrinsics.isEmpty(apiKey)) { + throw new IllegalArgumentException("No Bugsnag API Key set"); + } + if (apiKey.length() != VALID_API_KEY_LEN) { + return true; + } + // check whether each character is hexadecimal (either a digit or a-f). + // this avoids using a regex to improve startup performance. + for (int k = 0; k < VALID_API_KEY_LEN; k++) { + char chr = apiKey.charAt(k); + if (!Character.isDigit(chr) && (chr < 'a' || chr > 'f')) { + return true; + } } + return false; } private void logNull(String property) { @@ -294,9 +310,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) { impl.setLaunchDurationMillis(launchDurationMillis); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option launchDurationMillis should be a positive long value." - + "Supplied value is %d", launchDurationMillis)); + + "Supplied value is " + launchDurationMillis); } } @@ -513,9 +529,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) { impl.setMaxBreadcrumbs(maxBreadcrumbs); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxBreadcrumbs should be an integer between 0-100. " - + "Supplied value is %d", maxBreadcrumbs)); + + "Supplied value is " + maxBreadcrumbs); } } @@ -539,9 +555,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { if (maxPersistedEvents >= 0) { impl.setMaxPersistedEvents(maxPersistedEvents); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxPersistedEvents should be a positive integer." - + "Supplied value is %d", maxPersistedEvents)); + + "Supplied value is " + maxPersistedEvents); } } @@ -565,9 +581,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware { if (maxPersistedSessions >= 0) { impl.setMaxPersistedSessions(maxPersistedSessions); } else { - getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " + getLogger().e("Invalid configuration value detected. " + "Option maxPersistedSessions should be a positive integer." - + "Supplied value is %d", maxPersistedSessions)); + + "Supplied value is " + maxPersistedSessions); } } diff --git a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt b/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt index 739a71cfb2..ad34e0b423 100644 --- a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt +++ b/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt @@ -61,6 +61,14 @@ internal class ConnectivityLegacy( private val changeReceiver = ConnectivityChangeReceiver(callback) + private val activeNetworkInfo: android.net.NetworkInfo? + get() = try { + cm.activeNetworkInfo + } catch (e: NullPointerException) { + // in some rare cases we get a remote NullPointerException via Parcel.readException + null + } + override fun registerForNetworkChanges() { val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) context.registerReceiverSafe(changeReceiver, intentFilter) @@ -69,11 +77,11 @@ internal class ConnectivityLegacy( override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver) override fun hasNetworkConnection(): Boolean { - return cm.activeNetworkInfo?.isConnectedOrConnecting ?: false + return activeNetworkInfo?.isConnectedOrConnecting ?: false } override fun retrieveNetworkAccessState(): String { - return when (cm.activeNetworkInfo?.type) { + return when (activeNetworkInfo?.type) { null -> "none" ConnectivityManager.TYPE_WIFI -> "wifi" ConnectivityManager.TYPE_ETHERNET -> "ethernet" diff --git a/app/src/main/java/com/bugsnag/android/ContextState.kt b/app/src/main/java/com/bugsnag/android/ContextState.kt index 47ca70cc3c..8c377b739c 100644 --- a/app/src/main/java/com/bugsnag/android/ContextState.kt +++ b/app/src/main/java/com/bugsnag/android/ContextState.kt @@ -1,13 +1,36 @@ package com.bugsnag.android -internal class ContextState(context: String? = null) : BaseObservable() { - var context = context - set(value) { - field = value +/** + * Tracks the current context and allows observers to be notified whenever it changes. + * + * The default behaviour is to track [SessionTracker.getContextActivity]. However, any value + * that the user sets via [Bugsnag.setContext] will override this and be returned instead. + */ +internal class ContextState : BaseObservable() { + + companion object { + private const val MANUAL = "__BUGSNAG_MANUAL_CONTEXT__" + } + + private var manualContext: String? = null + private var automaticContext: String? = null + + fun setManualContext(context: String?) { + manualContext = context + automaticContext = MANUAL + emitObservableEvent() + } + + fun setAutomaticContext(context: String?) { + if (automaticContext !== MANUAL) { + automaticContext = context emitObservableEvent() } + } - fun emitObservableEvent() = notifyObservers(StateEvent.UpdateContext(context)) + fun getContext(): String? { + return automaticContext.takeIf { it !== MANUAL } ?: manualContext + } - fun copy() = ContextState(context) + fun emitObservableEvent() = updateState { StateEvent.UpdateContext(getContext()) } } diff --git a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java b/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java index 7f3af2bdd4..bf2e5bf95a 100644 --- a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java +++ b/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java @@ -2,6 +2,8 @@ package com.bugsnag.android; import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -44,10 +46,10 @@ class DeliveryDelegate extends BaseObservable { if (session != null) { if (event.isUnhandled()) { event.setSession(session.incrementUnhandledAndCopy()); - notifyObservers(StateEvent.NotifyUnhandled.INSTANCE); + updateState(StateEvent.NotifyUnhandled.INSTANCE); } else { event.setSession(session.incrementHandledAndCopy()); - notifyObservers(StateEvent.NotifyHandled.INSTANCE); + updateState(StateEvent.NotifyHandled.INSTANCE); } } diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 2cd4795134..5181fa8303 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -16,13 +16,14 @@ import java.util.Locale import java.util.concurrent.Callable import java.util.concurrent.Future import java.util.concurrent.RejectedExecutionException +import java.util.concurrent.atomic.AtomicInteger import kotlin.math.max import kotlin.math.min internal class DeviceDataCollector( private val connectivity: Connectivity, private val appContext: Context, - private val resources: Resources?, + resources: Resources, private val deviceId: String?, private val buildInfo: DeviceBuildInfo, private val dataDirectory: File, @@ -31,7 +32,7 @@ internal class DeviceDataCollector( private val logger: Logger ) { - private val displayMetrics = resources?.displayMetrics + private val displayMetrics = resources.displayMetrics private val emulator = isEmulator() private val screenDensity = getScreenDensity() private val dpi = getScreenDensityDpi() @@ -40,6 +41,7 @@ internal class DeviceDataCollector( private val cpuAbi = getCpuAbi() private val runtimeVersions: MutableMap private val rootedFuture: Future? + private var orientation = AtomicInteger(resources.configuration.orientation) init { val map = mutableMapOf() @@ -79,7 +81,7 @@ internal class DeviceDataCollector( runtimeVersions.toMutableMap(), calculateFreeDisk(), calculateFreeMemory(), - calculateOrientation(), + getOrientationAsString(), Date(now) ) @@ -187,7 +189,7 @@ internal class DeviceDataCollector( return if (displayMetrics != null) { val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels) val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels) - String.format(Locale.US, "%dx%d", max, min) + "${max}x$min" } else { null } @@ -235,14 +237,23 @@ internal class DeviceDataCollector( } /** - * Get the device orientation, eg. "landscape" + * Get the current device orientation, eg. "landscape" */ - internal fun calculateOrientation() = when (resources?.configuration?.orientation) { + internal fun getOrientationAsString(): String? = when (orientation.get()) { ORIENTATION_LANDSCAPE -> "landscape" ORIENTATION_PORTRAIT -> "portrait" else -> null } + /** + * Called whenever the orientation is updated so that the device information is accurate. + * Currently this is only invoked by [ClientComponentCallbacks]. Returns true if the + * orientation has changed, otherwise false. + */ + internal fun updateOrientation(newOrientation: Int): Boolean { + return orientation.getAndSet(newOrientation) != newOrientation + } + fun addRuntimeVersionInfo(key: String, value: String) { runtimeVersions[key] = value } diff --git a/app/src/main/java/com/bugsnag/android/ErrorInternal.kt b/app/src/main/java/com/bugsnag/android/ErrorInternal.kt index 77a19659ef..6b247dd9ca 100644 --- a/app/src/main/java/com/bugsnag/android/ErrorInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ErrorInternal.kt @@ -15,8 +15,7 @@ internal class ErrorInternal @JvmOverloads internal constructor( .mapTo(mutableListOf()) { currentEx -> // Somehow it's possible for stackTrace to be null in rare cases val stacktrace = currentEx.stackTrace ?: arrayOf() - val trace = - Stacktrace.stacktraceFromJavaTrace(stacktrace, projectPackages, logger) + val trace = Stacktrace(stacktrace, projectPackages, logger) val errorInternal = ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace) diff --git a/app/src/main/java/com/bugsnag/android/Event.java b/app/src/main/java/com/bugsnag/android/Event.java index 1f43ca9869..31fb88c074 100644 --- a/app/src/main/java/com/bugsnag/android/Event.java +++ b/app/src/main/java/com/bugsnag/android/Event.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; diff --git a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt index 1dce6f8a1f..6d9ff766c9 100644 --- a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt +++ b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt @@ -1,7 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File -import java.util.Locale import java.util.UUID /** @@ -27,15 +27,7 @@ internal data class EventFilenameInfo( * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" */ fun encode(): String { - return String.format( - Locale.US, - "%d_%s_%s_%s_%s.json", - timestamp, - apiKey, - serializeErrorTypeHeader(errorTypes), - uuid, - suffix - ) + return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json" } fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH diff --git a/app/src/main/java/com/bugsnag/android/EventInternal.kt b/app/src/main/java/com/bugsnag/android/EventInternal.kt index f367444efd..8195a2025c 100644 --- a/app/src/main/java/com/bugsnag/android/EventInternal.kt +++ b/app/src/main/java/com/bugsnag/android/EventInternal.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException internal class EventInternal @JvmOverloads internal constructor( diff --git a/app/src/main/java/com/bugsnag/android/EventPayload.kt b/app/src/main/java/com/bugsnag/android/EventPayload.kt index 9294d9ac0e..f0f70e83f9 100644 --- a/app/src/main/java/com/bugsnag/android/EventPayload.kt +++ b/app/src/main/java/com/bugsnag/android/EventPayload.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File import java.io.IOException diff --git a/app/src/main/java/com/bugsnag/android/EventStore.java b/app/src/main/java/com/bugsnag/android/EventStore.java index 4e5d235e37..d22d8ddb24 100644 --- a/app/src/main/java/com/bugsnag/android/EventStore.java +++ b/app/src/main/java/com/bugsnag/android/EventStore.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -148,8 +150,8 @@ class EventStore extends FileStore { void flushReports(Collection storedReports) { if (!storedReports.isEmpty()) { - logger.i(String.format(Locale.US, - "Sending %d saved error(s) to Bugsnag", storedReports.size())); + int size = storedReports.size(); + logger.i("Sending " + size + " saved error(s) to Bugsnag"); for (File eventFile : storedReports) { flushEventFile(eventFile); @@ -200,14 +202,12 @@ class EventStore extends FileStore { String getFilename(Object object) { EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromEvent(object, null, config); - String encodedInfo = eventInfo.encode(); - return String.format(Locale.US, "%s", encodedInfo); + return eventInfo.encode(); } String getNdkFilename(Object object, String apiKey) { EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromEvent(object, apiKey, config); - String encodedInfo = eventInfo.encode(); - return String.format(Locale.US, "%s", encodedInfo); + return eventInfo.encode(); } } diff --git a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java index 4e5f9beab9..057f69b72c 100644 --- a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java @@ -35,6 +35,9 @@ class ExceptionHandler implements UncaughtExceptionHandler { @Override public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { + if (client.getConfig().shouldDiscardError(throwable)) { + return; + } boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); // Notify any subscribed clients of the uncaught exception diff --git a/app/src/main/java/com/bugsnag/android/FileStore.java b/app/src/main/java/com/bugsnag/android/FileStore.java index ee3cdd180e..4aae1e30e8 100644 --- a/app/src/main/java/com/bugsnag/android/FileStore.java +++ b/app/src/main/java/com/bugsnag/android/FileStore.java @@ -104,8 +104,7 @@ abstract class FileStore { out.close(); } } catch (Exception exception) { - logger.w(String.format("Failed to close unsent payload writer (%s) ", - filename), exception); + logger.w("Failed to close unsent payload writer: " + filename, exception); } lock.unlock(); } @@ -130,7 +129,7 @@ abstract class FileStore { Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); stream = new JsonStream(out); stream.value(streamable); - logger.i(String.format("Saved unsent payload to disk (%s) ", filename)); + logger.i("Saved unsent payload to disk: '" + filename + '\''); return filename; } catch (FileNotFoundException exc) { logger.w("Ignoring FileNotFoundException - unable to create file", exc); @@ -168,8 +167,8 @@ abstract class FileStore { File oldestFile = files.get(k); if (!queuedFiles.contains(oldestFile)) { - logger.w(String.format("Discarding oldest error as stored " - + "error limit reached (%s)", oldestFile.getPath())); + logger.w("Discarding oldest error as stored " + + "error limit reached: '" + oldestFile.getPath() + '\''); deleteStoredFiles(Collections.singleton(oldestFile)); files.remove(k); k--; diff --git a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java b/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java index 53ab5a66fa..b299ac201c 100644 --- a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java +++ b/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java @@ -3,6 +3,8 @@ package com.bugsnag.android; import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR; import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION; +import com.bugsnag.android.internal.ImmutableConfig; + import android.annotation.SuppressLint; import android.content.Context; import android.os.Build; diff --git a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index d79fb69a47..770cb65a92 100644 --- a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File import java.util.concurrent.locks.ReentrantReadWriteLock import kotlin.concurrent.withLock diff --git a/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt b/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt index 30774ac663..018b1788fe 100644 --- a/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt +++ b/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.util.concurrent.RejectedExecutionException import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.TimeUnit @@ -34,7 +35,7 @@ internal class LaunchCrashTracker @JvmOverloads constructor( fun markLaunchCompleted() { executor.shutdown() launching.set(false) - notifyObservers(StateEvent.UpdateIsLaunching(false)) + updateState { StateEvent.UpdateIsLaunching(false) } logger.d("App launch period marked as complete") } diff --git a/app/src/main/java/com/bugsnag/android/Metadata.kt b/app/src/main/java/com/bugsnag/android/Metadata.kt index a2e07ae2d1..4dc8a51668 100644 --- a/app/src/main/java/com/bugsnag/android/Metadata.kt +++ b/app/src/main/java/com/bugsnag/android/Metadata.kt @@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap * Diagnostic information is presented on your Bugsnag dashboard in tabs. */ internal data class Metadata @JvmOverloads constructor( - internal val store: ConcurrentHashMap = ConcurrentHashMap() + internal val store: MutableMap> = ConcurrentHashMap() ) : JsonStream.Streamable, MetadataAware { val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer() @@ -38,12 +38,9 @@ internal data class Metadata @JvmOverloads constructor( if (value == null) { clearMetadata(section, key) } else { - var tab = store[section] - if (tab !is MutableMap<*, *>) { - tab = ConcurrentHashMap() - store[section] = tab - } - insertValue(tab as MutableMap, key, value) + val tab = store[section] ?: ConcurrentHashMap() + store[section] = tab + insertValue(tab, key, value) } } @@ -52,7 +49,7 @@ internal data class Metadata @JvmOverloads constructor( // only merge if both the existing and new value are maps val existingValue = map[key] - if (obj is MutableMap<*, *> && existingValue is MutableMap<*, *>) { + if (existingValue != null && obj is Map<*, *>) { val maps = listOf(existingValue as Map, newValue as Map) obj = mergeMaps(maps) } @@ -65,49 +62,41 @@ internal data class Metadata @JvmOverloads constructor( override fun clearMetadata(section: String, key: String) { val tab = store[section] + tab?.remove(key) - if (tab is MutableMap<*, *>) { - tab.remove(key) - - if (tab.isEmpty()) { - store.remove(section) - } + if (tab.isNullOrEmpty()) { + store.remove(section) } } override fun getMetadata(section: String): Map? { - return store[section] as (Map?) + return store[section] } override fun getMetadata(section: String, key: String): Any? { - return when (val tab = store[section]) { - is Map<*, *> -> (tab as Map?)!![key] - else -> tab - } + return getMetadata(section)?.get(key) } - fun toMap(): ConcurrentHashMap { - val hashMap = ConcurrentHashMap(store) + fun toMap(): MutableMap> { + val copy = ConcurrentHashMap(store) // deep copy each section store.entries.forEach { - if (it.value is ConcurrentHashMap<*, *>) { - hashMap[it.key] = ConcurrentHashMap(it.value as ConcurrentHashMap<*, *>) - } + copy[it.key] = ConcurrentHashMap(it.value) } - return hashMap + return copy } companion object { fun merge(vararg data: Metadata): Metadata { val stores = data.map { it.toMap() } val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys } - val newMeta = Metadata(mergeMaps(stores)) + val newMeta = Metadata(mergeMaps(stores) as MutableMap>) newMeta.redactedKeys = redactKeys.toSet() return newMeta } - internal fun mergeMaps(data: List>): ConcurrentHashMap { + internal fun mergeMaps(data: List>): MutableMap { val keys = data.flatMap { it.keys }.toSet() val result = ConcurrentHashMap() @@ -120,7 +109,7 @@ internal data class Metadata @JvmOverloads constructor( } private fun getMergeValue( - result: ConcurrentHashMap, + result: MutableMap, key: String, map: Map ) { diff --git a/app/src/main/java/com/bugsnag/android/MetadataState.kt b/app/src/main/java/com/bugsnag/android/MetadataState.kt index d89035b635..d95177a5a4 100644 --- a/app/src/main/java/com/bugsnag/android/MetadataState.kt +++ b/app/src/main/java/com/bugsnag/android/MetadataState.kt @@ -28,8 +28,8 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : private fun notifyClear(section: String, key: String?) { when (key) { - null -> notifyObservers(StateEvent.ClearMetadataSection(section)) - else -> notifyObservers(StateEvent.ClearMetadataValue(section, key)) + null -> updateState { StateEvent.ClearMetadataSection(section) } + else -> updateState { StateEvent.ClearMetadataValue(section, key) } } } @@ -55,13 +55,13 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) : private fun notifyMetadataAdded(section: String, key: String, value: Any?) { when (value) { null -> notifyClear(section, key) - else -> notifyObservers(AddMetadata(section, key, metadata.getMetadata(section, key))) + else -> updateState { AddMetadata(section, key, metadata.getMetadata(section, key)) } } } private fun notifyMetadataAdded(section: String, value: Map) { value.entries.forEach { - notifyObservers(AddMetadata(section, it.key, metadata.getMetadata(it.key))) + updateState { AddMetadata(section, it.key, metadata.getMetadata(it.key)) } } } } diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index 30e857b601..56a000d90c 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import android.annotation.SuppressLint; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -325,7 +327,7 @@ public class NativeInterface { ImmutableConfig config = client.getConfig(); if (releaseStage == null || releaseStage.length() == 0 - || config.shouldNotifyForReleaseStage()) { + || !config.shouldDiscardByReleaseStage()) { EventStore eventStore = client.getEventStore(); String filename = eventStore.getNdkFilename(payload, apiKey); @@ -368,6 +370,9 @@ public class NativeInterface { @NonNull final String message, @NonNull final Severity severity, @NonNull final StackTraceElement[] stacktrace) { + if (getClient().getConfig().shouldDiscardError(name)) { + return; + } Throwable exc = new RuntimeException(); exc.setStackTrace(stacktrace); diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 6d02f69e7e..5302719044 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.9.4", + var version: String = "5.10.1", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/PluginClient.kt b/app/src/main/java/com/bugsnag/android/PluginClient.kt index d4b4f93127..01f268bd39 100644 --- a/app/src/main/java/com/bugsnag/android/PluginClient.kt +++ b/app/src/main/java/com/bugsnag/android/PluginClient.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig + internal class PluginClient( userPlugins: Set, private val immutableConfig: ImmutableConfig, diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java index c3e9f35bc9..66bfa46f55 100644 --- a/app/src/main/java/com/bugsnag/android/SessionStore.java +++ b/app/src/main/java/com/bugsnag/android/SessionStore.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -45,10 +47,7 @@ class SessionStore extends FileStore { @NonNull @Override String getFilename(Object object) { - return String.format(Locale.US, - "%s%d_v2.json", - UUID.randomUUID().toString(), - System.currentTimeMillis()); + return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json"; } } diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index c02fdd3b51..93ec02155a 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -1,5 +1,7 @@ package com.bugsnag.android; +import com.bugsnag.android.internal.ImmutableConfig; + import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; @@ -79,6 +81,9 @@ class SessionTracker extends BaseObservable { @VisibleForTesting Session startNewSession(@NonNull Date date, @Nullable User user, boolean autoCaptured) { + if (client.getConfig().shouldDiscardSession(autoCaptured)) { + return null; + } String id = UUID.randomUUID().toString(); Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger); currentSession.set(session); @@ -87,6 +92,9 @@ class SessionTracker extends BaseObservable { } Session startSession(boolean autoCaptured) { + if (client.getConfig().shouldDiscardSession(autoCaptured)) { + return null; + } return startNewSession(new Date(), client.getUser(), autoCaptured); } @@ -95,7 +103,7 @@ class SessionTracker extends BaseObservable { if (session != null) { session.isPaused.set(true); - notifyObservers(StateEvent.PauseSession.INSTANCE); + updateState(StateEvent.PauseSession.INSTANCE); } } @@ -116,10 +124,10 @@ class SessionTracker extends BaseObservable { return resumed; } - private void notifySessionStartObserver(Session session) { - String startedAt = DateUtils.toIso8601(session.getStartedAt()); - notifyObservers(new StateEvent.StartSession(session.getId(), startedAt, - session.getHandledCount(), session.getUnhandledCount())); + private void notifySessionStartObserver(final Session session) { + final String startedAt = DateUtils.toIso8601(session.getStartedAt()); + updateState(new StateEvent.StartSession(session.getId(), startedAt, + session.getHandledCount(), session.getUnhandledCount())); } /** @@ -137,13 +145,16 @@ class SessionTracker extends BaseObservable { Session registerExistingSession(@Nullable Date date, @Nullable String sessionId, @Nullable User user, int unhandledCount, int handledCount) { + if (client.getConfig().shouldDiscardSession(false)) { + return null; + } Session session = null; if (date != null && sessionId != null) { session = new Session(sessionId, date, user, unhandledCount, handledCount, client.getNotifier(), logger); notifySessionStartObserver(session); } else { - notifyObservers(StateEvent.PauseSession.INSTANCE); + updateState(StateEvent.PauseSession.INSTANCE); } currentSession.set(session); return session; @@ -157,18 +168,12 @@ class SessionTracker extends BaseObservable { */ private void trackSessionIfNeeded(final Session session) { logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client"); - - boolean notifyForRelease = configuration.shouldNotifyForReleaseStage(); - session.setApp(client.getAppDataCollector().generateApp()); session.setDevice(client.getDeviceDataCollector().generateDevice()); boolean deliverSession = callbackState.runOnSessionTasks(session, logger); - if (deliverSession && notifyForRelease - && (configuration.getAutoTrackSessions() || !session.isAutoCaptured()) - && session.isTracked().compareAndSet(false, true)) { + if (deliverSession && session.isTracked().compareAndSet(false, true)) { notifySessionStartObserver(session); - flushAsync(); flushInMemorySession(session); } @@ -355,13 +360,14 @@ class SessionTracker extends BaseObservable { lastExitedForegroundMs.set(nowMs); } } + client.getContextState().setAutomaticContext(getContextActivity()); notifyNdkInForeground(); } private void notifyNdkInForeground() { Boolean inForeground = isInForeground(); - boolean foreground = inForeground != null ? inForeground : false; - notifyObservers(new StateEvent.UpdateInForeground(foreground, getContextActivity())); + final boolean foreground = inForeground != null ? inForeground : false; + updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity())); } @Nullable diff --git a/app/src/main/java/com/bugsnag/android/SeverityReason.java b/app/src/main/java/com/bugsnag/android/SeverityReason.java index 1a472d8224..d445e6fd95 100644 --- a/app/src/main/java/com/bugsnag/android/SeverityReason.java +++ b/app/src/main/java/com/bugsnag/android/SeverityReason.java @@ -69,8 +69,7 @@ final class SeverityReason implements JsonStream.Streamable { case REASON_LOG: return new SeverityReason(severityReasonType, severity, false, attrVal); default: - String msg = String.format("Invalid argument '%s' for severityReason", - severityReasonType); + String msg = "Invalid argument for severityReason: '" + severityReasonType + '\''; throw new IllegalArgumentException(msg); } } diff --git a/app/src/main/java/com/bugsnag/android/Stacktrace.kt b/app/src/main/java/com/bugsnag/android/Stacktrace.kt index a3deebc005..d7ea3f6b49 100644 --- a/app/src/main/java/com/bugsnag/android/Stacktrace.kt +++ b/app/src/main/java/com/bugsnag/android/Stacktrace.kt @@ -20,43 +20,9 @@ internal class Stacktrace : JsonStream.Streamable { * not. */ fun inProject(className: String, projectPackages: Collection): Boolean? { - for (packageName in projectPackages) { - if (className.startsWith(packageName)) { - return true - } - } - return null - } - - fun stacktraceFromJavaTrace( - stacktrace: Array, - projectPackages: Collection, - logger: Logger - ): Stacktrace { - val frames = stacktrace.mapNotNull { serializeStackframe(it, projectPackages, logger) } - return Stacktrace(frames) - } - - private fun serializeStackframe( - el: StackTraceElement, - projectPackages: Collection, - logger: Logger - ): Stackframe? { - try { - val methodName = when { - el.className.isNotEmpty() -> el.className + "." + el.methodName - else -> el.methodName - } - - return Stackframe( - methodName, - if (el.fileName == null) "Unknown" else el.fileName, - el.lineNumber, - inProject(el.className, projectPackages) - ) - } catch (lineEx: Exception) { - logger.w("Failed to serialize stacktrace", lineEx) - return null + return when { + projectPackages.any { className.startsWith(it) } -> true + else -> null } } } @@ -67,13 +33,53 @@ internal class Stacktrace : JsonStream.Streamable { trace = limitTraceLength(frames) } - private fun limitTraceLength(frames: List): List { + constructor( + stacktrace: Array, + projectPackages: Collection, + logger: Logger + ) { + val frames = limitTraceLength(stacktrace) + trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) } + } + + private fun limitTraceLength(frames: Array): Array { + return when { + frames.size >= STACKTRACE_TRIM_LENGTH -> frames.sliceArray(0 until STACKTRACE_TRIM_LENGTH) + else -> frames + } + } + + private fun limitTraceLength(frames: List): List { return when { frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH) else -> frames } } + private fun serializeStackframe( + el: StackTraceElement, + projectPackages: Collection, + logger: Logger + ): Stackframe? { + try { + val className = el.className + val methodName = when { + className.isNotEmpty() -> className + "." + el.methodName + else -> el.methodName + } + + return Stackframe( + methodName, + el.fileName ?: "Unknown", + el.lineNumber, + inProject(className, projectPackages) + ) + } catch (lineEx: Exception) { + logger.w("Failed to serialize stacktrace", lineEx) + return null + } + } + @Throws(IOException::class) override fun toStream(writer: JsonStream) { writer.beginArray() diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt index bc3c83e0ea..a94a0301fb 100644 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/app/src/main/java/com/bugsnag/android/StateEvent.kt @@ -1,47 +1,66 @@ package com.bugsnag.android -sealed class StateEvent { +sealed class StateEvent { // JvmField allows direct field access optimizations + class Install( - val apiKey: String, - val autoDetectNdkCrashes: Boolean, - val appVersion: String?, - val buildUuid: String?, - val releaseStage: String?, - val lastRunInfoPath: String, - val consecutiveLaunchCrashes: Int + @JvmField val apiKey: String, + @JvmField val autoDetectNdkCrashes: Boolean, + @JvmField val appVersion: String?, + @JvmField val buildUuid: String?, + @JvmField val releaseStage: String?, + @JvmField val lastRunInfoPath: String, + @JvmField val consecutiveLaunchCrashes: Int ) : StateEvent() object DeliverPending : StateEvent() - class AddMetadata(val section: String, val key: String?, val value: Any?) : StateEvent() - class ClearMetadataSection(val section: String) : StateEvent() - class ClearMetadataValue(val section: String, val key: String?) : StateEvent() + class AddMetadata( + @JvmField val section: String, + @JvmField val key: String?, + @JvmField val value: Any? + ) : StateEvent() + + class ClearMetadataSection(@JvmField val section: String) : StateEvent() + + class ClearMetadataValue( + @JvmField val section: String, + @JvmField val key: String? + ) : StateEvent() class AddBreadcrumb( - val message: String, - val type: BreadcrumbType, - val timestamp: String, - val metadata: MutableMap + @JvmField val message: String, + @JvmField val type: BreadcrumbType, + @JvmField val timestamp: String, + @JvmField val metadata: MutableMap ) : StateEvent() object NotifyHandled : StateEvent() + object NotifyUnhandled : StateEvent() object PauseSession : StateEvent() + class StartSession( - val id: String, - val startedAt: String, - val handledCount: Int, + @JvmField val id: String, + @JvmField val startedAt: String, + @JvmField val handledCount: Int, val unhandledCount: Int ) : StateEvent() - class UpdateContext(val context: String?) : StateEvent() - class UpdateInForeground(val inForeground: Boolean, val contextActivity: String?) : StateEvent() - class UpdateLastRunInfo(val consecutiveLaunchCrashes: Int) : StateEvent() - class UpdateIsLaunching(val isLaunching: Boolean) : StateEvent() - class UpdateOrientation(val orientation: String?) : StateEvent() + class UpdateContext(@JvmField val context: String?) : StateEvent() + + class UpdateInForeground( + @JvmField val inForeground: Boolean, + val contextActivity: String? + ) : StateEvent() + + class UpdateLastRunInfo(@JvmField val consecutiveLaunchCrashes: Int) : StateEvent() + + class UpdateIsLaunching(@JvmField val isLaunching: Boolean) : StateEvent() + + class UpdateOrientation(@JvmField val orientation: String?) : StateEvent() - class UpdateUser(val user: User) : StateEvent() + class UpdateUser(@JvmField val user: User) : StateEvent() - class UpdateMemoryTrimEvent(val isLowMemory: Boolean) : StateEvent() + class UpdateMemoryTrimEvent(@JvmField val isLowMemory: Boolean) : StateEvent() } diff --git a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java b/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java deleted file mode 100644 index 0b0f9bc5cd..0000000000 --- a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.java +++ /dev/null @@ -1,191 +0,0 @@ -package com.bugsnag.android; - -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", String.format("%s: %s", 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<>(); - if (client.getConfig().shouldRecordBreadcrumbType(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 (client.getConfig().shouldRecordBreadcrumbType(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 (client.getConfig().shouldRecordBreadcrumbType(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/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt b/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt new file mode 100644 index 0000000000..ac8e309ccb --- /dev/null +++ b/app/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/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt index 8839badaec..ea27eb92a8 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadState.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.IOException /** @@ -11,7 +12,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc sendThreads: ThreadSendPolicy, projectPackages: Collection, logger: Logger, - currentThread: java.lang.Thread = java.lang.Thread.currentThread(), + currentThread: java.lang.Thread? = null, stackTraces: MutableMap>? = null ) : JsonStream.Streamable { @@ -30,7 +31,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc threads = when { recordThreads -> captureThreadTrace( stackTraces ?: java.lang.Thread.getAllStackTraces(), - currentThread, + currentThread ?: java.lang.Thread.currentThread(), exc, isUnhandled, projectPackages, @@ -64,7 +65,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc val trace = stackTraces[thread] if (trace != null) { - val stacktrace = Stacktrace.stacktraceFromJavaTrace(trace, projectPackages, logger) + val stacktrace = Stacktrace(trace, projectPackages, logger) val errorThread = thread.id == currentThreadId Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger) } else { diff --git a/app/src/main/java/com/bugsnag/android/UserState.kt b/app/src/main/java/com/bugsnag/android/UserState.kt index 8f408b7156..16da106515 100644 --- a/app/src/main/java/com/bugsnag/android/UserState.kt +++ b/app/src/main/java/com/bugsnag/android/UserState.kt @@ -7,5 +7,5 @@ internal class UserState(user: User) : BaseObservable() { emitObservableEvent() } - fun emitObservableEvent() = notifyObservers(StateEvent.UpdateUser(user)) + fun emitObservableEvent() = updateState { StateEvent.UpdateUser(user) } } diff --git a/app/src/main/java/com/bugsnag/android/UserStore.kt b/app/src/main/java/com/bugsnag/android/UserStore.kt index 30540ca41c..29ccd71a19 100644 --- a/app/src/main/java/com/bugsnag/android/UserStore.kt +++ b/app/src/main/java/com/bugsnag/android/UserStore.kt @@ -1,5 +1,7 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig +import com.bugsnag.android.internal.StateObserver import java.io.File import java.io.IOException import java.util.concurrent.atomic.AtomicReference @@ -55,11 +57,13 @@ internal class UserStore @JvmOverloads constructor( else -> UserState(User(deviceId, null, null)) } - userState.addObserver { _, arg -> - if (arg is StateEvent.UpdateUser) { - save(arg.user) + userState.addObserver( + StateObserver { event -> + if (event is StateEvent.UpdateUser) { + save(event.user) + } } - } + ) return userState } diff --git a/app/src/main/java/com/bugsnag/android/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt similarity index 58% rename from app/src/main/java/com/bugsnag/android/ImmutableConfig.kt rename to app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index d4d0753c2e..5c434a7d32 100644 --- a/app/src/main/java/com/bugsnag/android/ImmutableConfig.kt +++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -1,11 +1,30 @@ -package com.bugsnag.android +package com.bugsnag.android.internal import android.content.Context import android.content.pm.ApplicationInfo +import android.content.pm.PackageInfo import android.content.pm.PackageManager +import androidx.annotation.VisibleForTesting +import com.bugsnag.android.BreadcrumbType +import com.bugsnag.android.Configuration +import com.bugsnag.android.Connectivity +import com.bugsnag.android.DebugLogger +import com.bugsnag.android.DefaultDelivery +import com.bugsnag.android.Delivery +import com.bugsnag.android.DeliveryParams +import com.bugsnag.android.EndpointConfiguration +import com.bugsnag.android.ErrorTypes +import com.bugsnag.android.EventPayload +import com.bugsnag.android.Logger +import com.bugsnag.android.ManifestConfigLoader +import com.bugsnag.android.NoopLogger +import com.bugsnag.android.ThreadSendPolicy +import com.bugsnag.android.errorApiHeaders +import com.bugsnag.android.safeUnrollCauses +import com.bugsnag.android.sessionApiHeaders import java.io.File -internal data class ImmutableConfig( +data class ImmutableConfig( val apiKey: String, val autoDetectErrors: Boolean, val enabledErrorTypes: ErrorTypes, @@ -29,21 +48,12 @@ internal data class ImmutableConfig( val maxPersistedEvents: Int, val maxPersistedSessions: Int, val persistenceDirectory: File, - val sendLaunchCrashesSynchronously: Boolean -) { + val sendLaunchCrashesSynchronously: Boolean, - /** - * Checks if the given release stage should be notified or not - * - * @return true if the release state should be notified else false - */ - @JvmName("shouldNotifyForReleaseStage") - internal fun shouldNotifyForReleaseStage() = - enabledReleaseStages == null || enabledReleaseStages.contains(releaseStage) - - @JvmName("shouldRecordBreadcrumbType") - internal fun shouldRecordBreadcrumbType(type: BreadcrumbType) = - enabledBreadcrumbTypes == null || enabledBreadcrumbTypes.contains(type) + // results cached here to avoid unnecessary lookups in Client. + val packageInfo: PackageInfo?, + val appInfo: ApplicationInfo? +) { @JvmName("getErrorApiDeliveryParams") internal fun getErrorApiDeliveryParams(payload: EventPayload) = @@ -52,11 +62,73 @@ internal data class ImmutableConfig( @JvmName("getSessionApiDeliveryParams") internal fun getSessionApiDeliveryParams() = DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey)) + + /** + * Returns whether the given throwable should be discarded + * based on the automatic data capture settings in [Configuration]. + */ + fun shouldDiscardError(exc: Throwable): Boolean { + return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc) + } + + /** + * Returns whether the given error should be discarded + * based on the automatic data capture settings in [Configuration]. + */ + fun shouldDiscardError(errorClass: String?): Boolean { + return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(errorClass) + } + + /** + * Returns whether a session should be discarded based on the + * automatic data capture settings in [Configuration]. + */ + fun shouldDiscardSession(autoCaptured: Boolean): Boolean { + return shouldDiscardByReleaseStage() || (autoCaptured && !autoTrackSessions) + } + + /** + * Returns whether breadcrumbs with the given type should be discarded or not. + */ + fun shouldDiscardBreadcrumb(type: BreadcrumbType): Boolean { + return enabledBreadcrumbTypes != null && !enabledBreadcrumbTypes.contains(type) + } + + /** + * Returns whether errors/sessions should be discarded or not based on the enabled + * release stages. + */ + fun shouldDiscardByReleaseStage(): Boolean { + return enabledReleaseStages != null && !enabledReleaseStages.contains(releaseStage) + } + + /** + * Returns whether errors with the given errorClass should be discarded or not. + */ + @VisibleForTesting + internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean { + return discardClasses.contains(errorClass) + } + + /** + * Returns whether errors should be discarded or not based on the errorClass, as deduced + * by the Throwable's class name. + */ + @VisibleForTesting + internal fun shouldDiscardByErrorClass(exc: Throwable): Boolean { + return exc.safeUnrollCauses().any { throwable -> + val errorClass = throwable.javaClass.name + shouldDiscardByErrorClass(errorClass) + } + } } +@JvmOverloads internal fun convertToImmutableConfig( config: Configuration, - buildUuid: String? = null + buildUuid: String? = null, + packageInfo: PackageInfo? = null, + appInfo: ApplicationInfo? = null ): ImmutableConfig { val errorTypes = when { config.autoDetectErrors -> config.enabledErrorTypes.copy() @@ -87,7 +159,9 @@ internal fun convertToImmutableConfig( maxPersistedSessions = config.maxPersistedSessions, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), persistenceDirectory = config.persistenceDirectory!!, - sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously + sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, + packageInfo = packageInfo, + appInfo = appInfo ) } @@ -144,7 +218,7 @@ internal fun sanitiseConfiguration( if (configuration.persistenceDirectory == null) { configuration.persistenceDirectory = appContext.cacheDir } - return convertToImmutableConfig(configuration, buildUuid) + return convertToImmutableConfig(configuration, buildUuid, packageInfo, appInfo) } internal const val RELEASE_STAGE_DEVELOPMENT = "development" diff --git a/app/src/main/java/com/bugsnag/android/internal/StateObserver.java b/app/src/main/java/com/bugsnag/android/internal/StateObserver.java new file mode 100644 index 0000000000..d9230e21e1 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/StateObserver.java @@ -0,0 +1,14 @@ +package com.bugsnag.android.internal; + +import com.bugsnag.android.StateEvent; + +import androidx.annotation.NonNull; + +public interface StateObserver { + + /** + * This is called whenever the notifier's state is altered, so that observers can react + * appropriately. This is intended for internal use only. + */ + void onStateChange(@NonNull StateEvent event); +} diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch new file mode 100644 index 0000000000..25c19fd4c7 --- /dev/null +++ b/patches/Bugsnag.patch @@ -0,0 +1,22 @@ +From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001 +From: M66B +Date: Sat, 15 May 2021 22:07:24 +0200 +Subject: [PATCH] Bugsnag failure on I/O error + +--- + app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +- + 1 file changed, 1 insertion(+), 1 deletion(-) + +diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +index a7995164cb4e..5620f0bacd80 100644 +--- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt ++++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +@@ -64,7 +64,7 @@ internal class DefaultDelivery( + return DeliveryStatus.UNDELIVERED + } catch (exception: IOException) { + logger.w("IOException encountered in request", exception) +- return DeliveryStatus.UNDELIVERED ++ return DeliveryStatus.FAILURE + } catch (exception: Exception) { + logger.w("Unexpected error delivering payload", exception) + return DeliveryStatus.FAILURE