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