Updated Bugsnag to 5.10.1

pull/201/head
M66B 4 years ago
parent 45db3c29b5
commit d1d70d321f

@ -1,13 +0,0 @@
<?xml version="1.0" encoding="UTF-8"?>
<project version="4">
<component name="RunConfigurationProducerService">
<option name="ignoredProducers">
<set>
<option value="com.android.tools.idea.compose.preview.runconfiguration.ComposePreviewRunConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.AllInPackageGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestClassGradleConfigurationProducer" />
<option value="org.jetbrains.plugins.gradle.execution.test.runner.TestMethodGradleConfigurationProducer" />
</set>
</option>
</component>
</project>

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException import java.io.IOException
/** /**

@ -1,11 +1,14 @@
package com.bugsnag.android package com.bugsnag.android
import android.annotation.SuppressLint
import android.app.ActivityManager import android.app.ActivityManager
import android.app.Application
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager 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 android.os.SystemClock
import com.bugsnag.android.internal.ImmutableConfig
/** /**
* Collects various data on the application state * Collects various data on the application state
@ -22,13 +25,13 @@ internal class AppDataCollector(
var codeBundleId: String? = null var codeBundleId: String? = null
private val packageName: String = appContext.packageName private val packageName: String = appContext.packageName
private var packageInfo = packageManager?.getPackageInfo(packageName, 0) private val bgWorkRestricted = isBackgroundWorkRestricted()
private var appInfo: ApplicationInfo? = packageManager?.getApplicationInfo(packageName, 0)
private var binaryArch: String? = null private var binaryArch: String? = null
private val appName = getAppName() private val appName = getAppName()
private val processName = findProcessName()
private val releaseStage = config.releaseStage private val releaseStage = config.releaseStage
private val versionName = config.appVersion ?: packageInfo?.versionName private val versionName = config.appVersion ?: config.packageInfo?.versionName
fun generateApp(): App = fun generateApp(): App =
App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId)
@ -47,18 +50,19 @@ internal class AppDataCollector(
fun getAppDataMetadata(): MutableMap<String, Any?> { fun getAppDataMetadata(): MutableMap<String, Any?> {
val map = HashMap<String, Any?>() val map = HashMap<String, Any?>()
map["name"] = appName map["name"] = appName
map["activeScreen"] = getActiveScreenClass() map["activeScreen"] = sessionTracker.contextActivity
map["memoryUsage"] = getMemoryUsage() map["memoryUsage"] = getMemoryUsage()
map["lowMemory"] = isLowMemory() map["lowMemory"] = isLowMemory()
isBackgroundWorkRestricted()?.let { bgWorkRestricted?.let {
map["backgroundWorkRestricted"] = it map["backgroundWorkRestricted"] = bgWorkRestricted
}
processName?.let {
map["processName"] = it
} }
return map return map
} }
fun getActiveScreenClass(): String? = sessionTracker.contextActivity
/** /**
* Get the actual memory used by the VM (which may not be the total used * Get the actual memory used by the VM (which may not be the total used
* by the app in the case of NDK usage). * 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() * https://developer.android.com/reference/android/app/ActivityManager#isBackgroundRestricted()
*/ */
private fun isBackgroundWorkRestricted(): Boolean? { 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 null
} else if (activityManager.isBackgroundRestricted) { } else if (activityManager.isBackgroundRestricted) {
true // only return non-null value if true to avoid noise in error reports true // only return non-null value if true to avoid noise in error reports
@ -129,7 +133,7 @@ internal class AppDataCollector(
* AndroidManifest.xml * AndroidManifest.xml
*/ */
private fun getAppName(): String? { private fun getAppName(): String? {
val copy = appInfo val copy = config.appInfo
return when { return when {
packageManager != null && copy != null -> { packageManager != null && copy != null -> {
packageManager.getApplicationLabel(copy).toString() 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 { companion object {
internal val startTimeMs = SystemClock.elapsedRealtime() internal val startTimeMs = SystemClock.elapsedRealtime()

@ -1,5 +1,7 @@
package com.bugsnag.android 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 * Stateful information set by the notifier about your app can be found on this class. These values
* can be accessed and amended if necessary. * can be accessed and amended if necessary.

@ -164,11 +164,20 @@ internal class BackgroundTaskService(
// shutdown the IO executor last. // shutdown the IO executor last.
errorExecutor.shutdown() errorExecutor.shutdown()
sessionExecutor.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 // shutdown the IO executor last, waiting for any existing tasks to complete
ioExecutor.shutdown() 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
}
} }
} }

@ -1,10 +1,46 @@
package com.bugsnag.android package com.bugsnag.android
import java.util.Observable import com.bugsnag.android.internal.StateObserver
import java.util.concurrent.CopyOnWriteArrayList
internal open class BaseObservable : Observable() { internal open class BaseObservable {
fun notifyObservers(event: StateEvent) {
setChanged() internal val observers = CopyOnWriteArrayList<StateObserver>()
super.notifyObservers(event)
/**
* 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 }
} }

@ -10,7 +10,8 @@ import java.util.Map;
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
public class Breadcrumb implements JsonStream.Streamable { 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; private final Logger logger;
Breadcrumb(@NonNull String message, @NonNull 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) { public void setMessage(@NonNull String message) {
if (message != null) { if (message != null) {
impl.setMessage(message); impl.message = message;
} else { } else {
logNull("message"); logNull("message");
} }
@ -47,7 +48,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/ */
@NonNull @NonNull
public String getMessage() { 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) { public void setType(@NonNull BreadcrumbType type) {
if (type != null) { if (type != null) {
impl.setType(type); impl.type = type;
} else { } else {
logNull("type"); logNull("type");
} }
@ -68,14 +69,14 @@ public class Breadcrumb implements JsonStream.Streamable {
*/ */
@NonNull @NonNull
public BreadcrumbType getType() { public BreadcrumbType getType() {
return impl.getType(); return impl.type;
} }
/** /**
* Sets diagnostic data relating to the breadcrumb * Sets diagnostic data relating to the breadcrumb
*/ */
public void setMetadata(@Nullable Map<String, Object> metadata) { public void setMetadata(@Nullable Map<String, Object> metadata) {
impl.setMetadata(metadata); impl.metadata = metadata;
} }
/** /**
@ -83,7 +84,7 @@ public class Breadcrumb implements JsonStream.Streamable {
*/ */
@Nullable @Nullable
public Map<String, Object> getMetadata() { public Map<String, Object> getMetadata() {
return impl.getMetadata(); return impl.metadata;
} }
/** /**
@ -91,12 +92,12 @@ public class Breadcrumb implements JsonStream.Streamable {
*/ */
@NonNull @NonNull
public Date getTimestamp() { public Date getTimestamp() {
return impl.getTimestamp(); return impl.timestamp;
} }
@NonNull @NonNull
String getStringTimestamp() { String getStringTimestamp() {
return DateUtils.toIso8601(impl.getTimestamp()); return DateUtils.toIso8601(impl.timestamp);
} }
@Override @Override

@ -9,11 +9,11 @@ import java.util.Date
* attached to a crash to help diagnose what events lead to the error. * attached to a crash to help diagnose what events lead to the error.
*/ */
internal class BreadcrumbInternal internal constructor( internal class BreadcrumbInternal internal constructor(
var message: String, @JvmField var message: String,
var type: BreadcrumbType, @JvmField var type: BreadcrumbType,
var metadata: MutableMap<String, Any?>?, @JvmField var metadata: MutableMap<String, Any?>?,
val timestamp: Date = Date() @JvmField val timestamp: Date = Date()
) : JsonStream.Streamable { ) : JsonStream.Streamable { // JvmField allows direct field access optimizations
internal constructor(message: String) : this( internal constructor(message: String) : this(
message, message,

@ -1,55 +1,96 @@
package com.bugsnag.android package com.bugsnag.android
import java.io.IOException import java.io.IOException
import java.util.Queue import java.util.concurrent.atomic.AtomicInteger
import java.util.concurrent.ConcurrentLinkedQueue
/**
* 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( internal class BreadcrumbState(
maxBreadcrumbs: Int, private val maxBreadcrumbs: Int,
val callbackState: CallbackState, private val callbackState: CallbackState,
val logger: Logger private val logger: Logger
) : BaseObservable(), JsonStream.Streamable { ) : BaseObservable(), JsonStream.Streamable {
val store: Queue<Breadcrumb> = 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 private val store = arrayOfNulls<Breadcrumb?>(maxBreadcrumbs)
private val index = AtomicInteger(0)
init { fun add(breadcrumb: Breadcrumb) {
when { if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) {
maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs return
else -> this.maxBreadcrumbs = 0 }
// 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.impl.message,
breadcrumb.impl.type,
DateUtils.toIso8601(breadcrumb.impl.timestamp),
breadcrumb.impl.metadata ?: mutableMapOf()
)
} }
} }
@Throws(IOException::class) /**
override fun toStream(writer: JsonStream) { * Retrieves the index in the ring buffer where the breadcrumb should be stored.
pruneBreadcrumbs() */
writer.beginArray() private fun getBreadcrumbIndex(): Int {
store.forEach { it.toStream(writer) } while (true) {
writer.endArray() val currentValue = index.get() and validIndexMask
val nextValue = (currentValue + 1) % maxBreadcrumbs
if (index.compareAndSet(currentValue, nextValue)) {
return currentValue
}
}
} }
fun add(breadcrumb: Breadcrumb) { /**
if (!callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) { * Creates a copy of the breadcrumbs in the order of their addition.
return */
fun copy(): List<Breadcrumb> {
if (maxBreadcrumbs == 0) {
return emptyList()
} }
store.add(breadcrumb) // Set a negative value that stops any other thread from adding a breadcrumb.
pruneBreadcrumbs() // This handles reentrancy by waiting here until the old value has been reset.
notifyObservers( var tail = -1
StateEvent.AddBreadcrumb( while (tail == -1) {
breadcrumb.message, tail = index.getAndSet(-1)
breadcrumb.type,
DateUtils.toIso8601(breadcrumb.timestamp),
breadcrumb.metadata ?: mutableMapOf()
)
)
} }
private fun pruneBreadcrumbs() { try {
// Remove oldest breadcrumbState until new max size reached val result = arrayOfNulls<Breadcrumb>(maxBreadcrumbs)
while (store.size > maxBreadcrumbs) { store.copyInto(result, 0, tail, maxBreadcrumbs)
store.poll() 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()
}
} }

@ -33,6 +33,10 @@ internal data class CallbackState(
} }
fun runOnErrorTasks(event: Event, logger: Logger): Boolean { fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onErrorTasks.isEmpty()) {
return true
}
onErrorTasks.forEach { onErrorTasks.forEach {
try { try {
if (!it.onError(event)) { if (!it.onError(event)) {
@ -46,6 +50,10 @@ internal data class CallbackState(
} }
fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean { fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onBreadcrumbTasks.isEmpty()) {
return true
}
onBreadcrumbTasks.forEach { onBreadcrumbTasks.forEach {
try { try {
if (!it.onBreadcrumb(breadcrumb)) { if (!it.onBreadcrumb(breadcrumb)) {
@ -59,6 +67,10 @@ internal data class CallbackState(
} }
fun runOnSessionTasks(session: Session, logger: Logger): Boolean { fun runOnSessionTasks(session: Session, logger: Logger): Boolean {
// optimization to avoid construction of iterator when no callbacks set
if (onSessionTasks.isEmpty()) {
return true
}
onSessionTasks.forEach { onSessionTasks.forEach {
try { try {
if (!it.onSession(session)) { if (!it.onSession(session)) {

@ -2,14 +2,15 @@ package com.bugsnag.android;
import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom; import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom;
import static com.bugsnag.android.ContextExtensionsKt.getStorageManagerFrom; 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.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.ActivityManager;
import android.app.Application; import android.app.Application;
import android.content.Context; import android.content.Context;
import android.content.Intent;
import android.content.IntentFilter;
import android.content.res.Resources; import android.content.res.Resources;
import android.os.Environment; import android.os.Environment;
import android.os.storage.StorageManager; import android.os.storage.StorageManager;
@ -22,13 +23,11 @@ import kotlin.Unit;
import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function1;
import kotlin.jvm.functions.Function2; import kotlin.jvm.functions.Function2;
import java.util.ArrayList;
import java.util.Collections; import java.util.Collections;
import java.util.Date; import java.util.Date;
import java.util.HashMap; import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Observer;
import java.util.Set; import java.util.Set;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
@ -72,11 +71,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
final SessionTracker sessionTracker; final SessionTracker sessionTracker;
private final SystemBroadcastReceiver systemBroadcastReceiver; final SystemBroadcastReceiver systemBroadcastReceiver;
private final ActivityBreadcrumbCollector activityBreadcrumbCollector; private final ActivityBreadcrumbCollector activityBreadcrumbCollector;
private final SessionLifecycleCallback sessionLifecycleCallback; private final SessionLifecycleCallback sessionLifecycleCallback;
private final Connectivity connectivity; final Connectivity connectivity;
@Nullable @Nullable
private final StorageManager storageManager; private final StorageManager storageManager;
@ -152,9 +151,11 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
breadcrumbState = new BreadcrumbState(maxBreadcrumbs, callbackState, logger); breadcrumbState = new BreadcrumbState(maxBreadcrumbs, callbackState, logger);
storageManager = getStorageManagerFrom(appContext); storageManager = getStorageManagerFrom(appContext);
contextState = new ContextState(); contextState = new ContextState();
contextState.setContext(configuration.getContext());
if (configuration.getContext() != null) {
contextState.setManualContext(configuration.getContext());
}
sessionStore = new SessionStore(immutableConfig, logger, null); sessionStore = new SessionStore(immutableConfig, logger, null);
sessionTracker = new SessionTracker(immutableConfig, callbackState, this, sessionTracker = new SessionTracker(immutableConfig, callbackState, this,
@ -186,7 +187,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker); sessionLifecycleCallback = new SessionLifecycleCallback(sessionTracker);
application.registerActivityLifecycleCallbacks(sessionLifecycleCallback); application.registerActivityLifecycleCallbacks(sessionLifecycleCallback);
if (immutableConfig.shouldRecordBreadcrumbType(BreadcrumbType.STATE)) { if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) {
this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector( this.activityBreadcrumbCollector = new ActivityBreadcrumbCollector(
new Function2<String, Map<String, ? extends Object>, Unit>() { new Function2<String, Map<String, ? extends Object>, Unit>() {
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
@ -221,12 +222,6 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
exceptionHandler.install(); exceptionHandler.install();
} }
// register a receiver for automatic breadcrumbs
systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService);
registerOrientationChangeListener();
registerMemoryTrimListener();
// load last run info // load last run info
lastRunInfoStore = new LastRunInfoStore(immutableConfig); lastRunInfoStore = new LastRunInfoStore(immutableConfig);
lastRunInfo = loadLastRunInfo(); lastRunInfo = loadLastRunInfo();
@ -234,13 +229,16 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
// initialise plugins before attempting to flush any errors // initialise plugins before attempting to flush any errors
loadPlugins(configuration); loadPlugins(configuration);
connectivity.registerForNetworkChanges();
// Flush any on-disk errors and sessions // Flush any on-disk errors and sessions
eventStore.flushOnLaunch(); eventStore.flushOnLaunch();
eventStore.flushAsync(); eventStore.flushAsync();
sessionTracker.flushAsync(); sessionTracker.flushAsync();
// register listeners for system events in the background.
systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger);
registerComponentCallbacks();
registerListenersInBackground();
// leave auto breadcrumb // leave auto breadcrumb
Map<String, Object> data = Collections.emptyMap(); Map<String, Object> data = Collections.emptyMap();
leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data); leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data);
@ -299,6 +297,25 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
this.exceptionHandler = exceptionHandler; 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() { private LastRunInfo loadLastRunInfo() {
LastRunInfo lastRunInfo = lastRunInfoStore.load(); LastRunInfo lastRunInfo = lastRunInfoStore.load();
LastRunInfo currentRunInfo = new LastRunInfo(0, false, false); LastRunInfo currentRunInfo = new LastRunInfo(0, false, false);
@ -340,10 +357,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
return configuration.impl.metadataState.copy(copy); return configuration.impl.metadataState.copy(copy);
} }
private void registerOrientationChangeListener() { private void registerComponentCallbacks() {
IntentFilter configFilter = new IntentFilter(); appContext.registerComponentCallbacks(new ClientComponentCallbacks(
configFilter.addAction(Intent.ACTION_CONFIGURATION_CHANGED); deviceDataCollector,
ConfigChangeReceiver receiver = new ConfigChangeReceiver(deviceDataCollector,
new Function2<String, String, Unit>() { new Function2<String, String, Unit>() {
@Override @Override
public Unit invoke(String oldOrientation, String newOrientation) { public Unit invoke(String oldOrientation, String newOrientation) {
@ -354,14 +370,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
clientObservable.postOrientationChange(newOrientation); clientObservable.postOrientationChange(newOrientation);
return null; return null;
} }
} }, new Function1<Boolean, Unit>() {
);
ContextExtensionsKt.registerReceiverSafe(appContext, receiver, configFilter, logger);
}
private void registerMemoryTrimListener() {
appContext.registerComponentCallbacks(new ClientComponentCallbacks(
new Function1<Boolean, Unit>() {
@Override @Override
public Unit invoke(Boolean isLowMemory) { public Unit invoke(Boolean isLowMemory) {
clientObservable.postMemoryTrimEvent(isLowMemory); clientObservable.postMemoryTrimEvent(isLowMemory);
@ -379,7 +388,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
clientObservable.postNdkDeliverPending(); clientObservable.postNdkDeliverPending();
} }
void registerObserver(Observer observer) { void addObserver(StateObserver observer) {
metadataState.addObserver(observer); metadataState.addObserver(observer);
breadcrumbState.addObserver(observer); breadcrumbState.addObserver(observer);
sessionTracker.addObserver(observer); sessionTracker.addObserver(observer);
@ -390,15 +399,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
launchCrashTracker.addObserver(observer); launchCrashTracker.addObserver(observer);
} }
void unregisterObserver(Observer observer) { void removeObserver(StateObserver observer) {
metadataState.deleteObserver(observer); metadataState.removeObserver(observer);
breadcrumbState.deleteObserver(observer); breadcrumbState.removeObserver(observer);
sessionTracker.deleteObserver(observer); sessionTracker.removeObserver(observer);
clientObservable.deleteObserver(observer); clientObservable.removeObserver(observer);
userState.deleteObserver(observer); userState.removeObserver(observer);
contextState.deleteObserver(observer); contextState.removeObserver(observer);
deliveryDelegate.deleteObserver(observer); deliveryDelegate.removeObserver(observer);
launchCrashTracker.deleteObserver(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. * If you would like to set this value manually, you should alter this property.
*/ */
public void setContext(@Nullable String context) { 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) { public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) {
if (exc != null) { if (exc != null) {
if (immutableConfig.shouldDiscardError(exc)) {
return;
}
SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION);
Metadata metadata = metadataState.getMetadata(); Metadata metadata = metadataState.getMetadata();
Event event = new Event(exc, immutableConfig, severityReason, metadata, logger); 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()); event.addMetadata("app", appDataCollector.getAppDataMetadata());
// Attach breadcrumbState to the event // Attach breadcrumbState to the event
event.setBreadcrumbs(new ArrayList<>(breadcrumbState.getStore())); event.setBreadcrumbs(breadcrumbState.copy());
// Attach user info to the event // Attach user info to the event
User user = userState.getUser(); User user = userState.getUser();
event.setUser(user.getId(), user.getEmail(), user.getName()); event.setUser(user.getId(), user.getEmail(), user.getName());
// Attach default context from active activity // Attach context to the event
if (Intrinsics.isEmpty(event.getContext())) { event.setContext(contextState.getContext());
String context = contextState.getContext();
event.setContext(context != null ? context : appDataCollector.getActiveScreenClass());
}
notifyInternal(event, onError); notifyInternal(event, onError);
} }
void notifyInternal(@NonNull Event event, void notifyInternal(@NonNull Event event,
@Nullable OnErrorCallback onError) { @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 // set the redacted keys on the event as this
// will not have been set for RN/Unity events // will not have been set for RN/Unity events
Set<String> redactedKeys = metadataState.getMetadata().getRedactedKeys(); Set<String> redactedKeys = metadataState.getMetadata().getRedactedKeys();
@ -773,7 +769,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
*/ */
@NonNull @NonNull
public List<Breadcrumb> getBreadcrumbs() { public List<Breadcrumb> getBreadcrumbs() {
return new ArrayList<>(breadcrumbState.getStore()); return breadcrumbState.copy();
} }
@NonNull @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 @NonNull
@SuppressWarnings({"unchecked", "rawtypes"})
Map<String, Object> getMetadata() { Map<String, Object> 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, void leaveAutoBreadcrumb(@NonNull String message,
@NonNull BreadcrumbType type, @NonNull BreadcrumbType type,
@NonNull Map<String, Object> metadata) { @NonNull Map<String, Object> metadata) {
if (immutableConfig.shouldRecordBreadcrumbType(type)) { if (!immutableConfig.shouldDiscardBreadcrumb(type)) {
breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger));
} }
} }
@ -1033,6 +1032,10 @@ public class Client implements MetadataAware, CallbackAware, UserAware {
return metadataState; return metadataState;
} }
ContextState getContextState() {
return contextState;
}
void setAutoNotify(boolean autoNotify) { void setAutoNotify(boolean autoNotify) {
pluginClient.setAutoNotify(this, autoNotify); pluginClient.setAutoNotify(this, autoNotify);

@ -4,9 +4,19 @@ import android.content.ComponentCallbacks
import android.content.res.Configuration import android.content.res.Configuration
internal class ClientComponentCallbacks( internal class ClientComponentCallbacks(
private val deviceDataCollector: DeviceDataCollector,
private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit,
val callback: (Boolean) -> Unit val callback: (Boolean) -> Unit
) : ComponentCallbacks { ) : 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() { override fun onLowMemory() {
callback(true) callback(true)

@ -1,17 +1,23 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
internal class ClientObservable : BaseObservable() { internal class ClientObservable : BaseObservable() {
fun postOrientationChange(orientation: String?) { fun postOrientationChange(orientation: String?) {
notifyObservers(StateEvent.UpdateOrientation(orientation)) updateState { StateEvent.UpdateOrientation(orientation) }
} }
fun postMemoryTrimEvent(isLowMemory: Boolean) { fun postMemoryTrimEvent(isLowMemory: Boolean) {
notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory)) updateState { StateEvent.UpdateMemoryTrimEvent(isLowMemory) }
} }
fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) { fun postNdkInstall(
notifyObservers( conf: ImmutableConfig,
lastRunInfoPath: String,
consecutiveLaunchCrashes: Int
) {
updateState {
StateEvent.Install( StateEvent.Install(
conf.apiKey, conf.apiKey,
conf.enabledErrorTypes.ndkCrashes, conf.enabledErrorTypes.ndkCrashes,
@ -21,10 +27,10 @@ internal class ClientObservable : BaseObservable() {
lastRunInfoPath, lastRunInfoPath,
consecutiveLaunchCrashes consecutiveLaunchCrashes
) )
) }
} }
fun postNdkDeliverPending() { fun postNdkDeliverPending() {
notifyObservers(StateEvent.DeliverPending) updateState { StateEvent.DeliverPending }
} }
} }

@ -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
}
}
}

@ -37,19 +37,19 @@ internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var context: String? = null var context: String? = null
var redactedKeys: Set<String> = metadataState.metadata.redactedKeys var redactedKeys: Set<String>
get() = metadataState.metadata.redactedKeys
set(value) { set(value) {
metadataState.metadata.redactedKeys = value metadataState.metadata.redactedKeys = value
field = value
} }
var discardClasses: Set<String> = emptySet() var discardClasses: Set<String> = emptySet()
var enabledReleaseStages: Set<String>? = null var enabledReleaseStages: Set<String>? = null
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = BreadcrumbType.values().toSet() var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
var projectPackages: Set<String> = emptySet() var projectPackages: Set<String> = emptySet()
var persistenceDirectory: File? = null var persistenceDirectory: File? = null
protected val plugins = mutableSetOf<Plugin>() protected val plugins = HashSet<Plugin>()
override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError) override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError)
override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError) override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError)

@ -4,6 +4,7 @@ import android.content.Context;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File; import java.io.File;
import java.util.Locale; 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 MIN_BREADCRUMBS = 0;
private static final int MAX_BREADCRUMBS = 100; 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; private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
final ConfigInternal impl; final ConfigInternal impl;
@ -47,14 +48,29 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
} }
private void validateApiKey(String value) { private void validateApiKey(String value) {
if (Intrinsics.isEmpty(value)) { if (isInvalidApiKey(value)) {
throw new IllegalArgumentException("No Bugsnag API Key set"); DebugLogger.INSTANCE.w("Invalid configuration. "
+ "apiKey should be a 32-character hexademical string, got " + value);
}
} }
if (!value.matches(API_KEY_REGEX)) { @VisibleForTesting
DebugLogger.INSTANCE.w(String.format("Invalid configuration. apiKey should be a " static boolean isInvalidApiKey(String apiKey) {
+ "32-character hexademical string, got \"%s\"", value)); 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) { private void logNull(String property) {
@ -294,9 +310,9 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware {
if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) { if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) {
impl.setLaunchDurationMillis(launchDurationMillis); impl.setLaunchDurationMillis(launchDurationMillis);
} else { } 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." + "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) { if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
impl.setMaxBreadcrumbs(maxBreadcrumbs); impl.setMaxBreadcrumbs(maxBreadcrumbs);
} else { } 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. " + "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) { if (maxPersistedEvents >= 0) {
impl.setMaxPersistedEvents(maxPersistedEvents); impl.setMaxPersistedEvents(maxPersistedEvents);
} else { } else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " getLogger().e("Invalid configuration value detected. "
+ "Option maxPersistedEvents should be a positive integer." + "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) { if (maxPersistedSessions >= 0) {
impl.setMaxPersistedSessions(maxPersistedSessions); impl.setMaxPersistedSessions(maxPersistedSessions);
} else { } else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. " getLogger().e("Invalid configuration value detected. "
+ "Option maxPersistedSessions should be a positive integer." + "Option maxPersistedSessions should be a positive integer."
+ "Supplied value is %d", maxPersistedSessions)); + "Supplied value is " + maxPersistedSessions);
} }
} }

@ -61,6 +61,14 @@ internal class ConnectivityLegacy(
private val changeReceiver = ConnectivityChangeReceiver(callback) 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() { override fun registerForNetworkChanges() {
val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION) val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiverSafe(changeReceiver, intentFilter) context.registerReceiverSafe(changeReceiver, intentFilter)
@ -69,11 +77,11 @@ internal class ConnectivityLegacy(
override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver) override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver)
override fun hasNetworkConnection(): Boolean { override fun hasNetworkConnection(): Boolean {
return cm.activeNetworkInfo?.isConnectedOrConnecting ?: false return activeNetworkInfo?.isConnectedOrConnecting ?: false
} }
override fun retrieveNetworkAccessState(): String { override fun retrieveNetworkAccessState(): String {
return when (cm.activeNetworkInfo?.type) { return when (activeNetworkInfo?.type) {
null -> "none" null -> "none"
ConnectivityManager.TYPE_WIFI -> "wifi" ConnectivityManager.TYPE_WIFI -> "wifi"
ConnectivityManager.TYPE_ETHERNET -> "ethernet" ConnectivityManager.TYPE_ETHERNET -> "ethernet"

@ -1,13 +1,36 @@
package com.bugsnag.android package com.bugsnag.android
internal class ContextState(context: String? = null) : BaseObservable() { /**
var context = context * Tracks the current context and allows observers to be notified whenever it changes.
set(value) { *
field = value * 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() emitObservableEvent()
} }
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateContext(context)) fun setAutomaticContext(context: String?) {
if (automaticContext !== MANUAL) {
automaticContext = context
emitObservableEvent()
}
}
fun getContext(): String? {
return automaticContext.takeIf { it !== MANUAL } ?: manualContext
}
fun copy() = ContextState(context) fun emitObservableEvent() = updateState { StateEvent.UpdateContext(getContext()) }
} }

@ -2,6 +2,8 @@ package com.bugsnag.android;
import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION; import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -44,10 +46,10 @@ class DeliveryDelegate extends BaseObservable {
if (session != null) { if (session != null) {
if (event.isUnhandled()) { if (event.isUnhandled()) {
event.setSession(session.incrementUnhandledAndCopy()); event.setSession(session.incrementUnhandledAndCopy());
notifyObservers(StateEvent.NotifyUnhandled.INSTANCE); updateState(StateEvent.NotifyUnhandled.INSTANCE);
} else { } else {
event.setSession(session.incrementHandledAndCopy()); event.setSession(session.incrementHandledAndCopy());
notifyObservers(StateEvent.NotifyHandled.INSTANCE); updateState(StateEvent.NotifyHandled.INSTANCE);
} }
} }

@ -16,13 +16,14 @@ import java.util.Locale
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.Future import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.atomic.AtomicInteger
import kotlin.math.max import kotlin.math.max
import kotlin.math.min import kotlin.math.min
internal class DeviceDataCollector( internal class DeviceDataCollector(
private val connectivity: Connectivity, private val connectivity: Connectivity,
private val appContext: Context, private val appContext: Context,
private val resources: Resources?, resources: Resources,
private val deviceId: String?, private val deviceId: String?,
private val buildInfo: DeviceBuildInfo, private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File, private val dataDirectory: File,
@ -31,7 +32,7 @@ internal class DeviceDataCollector(
private val logger: Logger private val logger: Logger
) { ) {
private val displayMetrics = resources?.displayMetrics private val displayMetrics = resources.displayMetrics
private val emulator = isEmulator() private val emulator = isEmulator()
private val screenDensity = getScreenDensity() private val screenDensity = getScreenDensity()
private val dpi = getScreenDensityDpi() private val dpi = getScreenDensityDpi()
@ -40,6 +41,7 @@ internal class DeviceDataCollector(
private val cpuAbi = getCpuAbi() private val cpuAbi = getCpuAbi()
private val runtimeVersions: MutableMap<String, Any> private val runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>? private val rootedFuture: Future<Boolean>?
private var orientation = AtomicInteger(resources.configuration.orientation)
init { init {
val map = mutableMapOf<String, Any>() val map = mutableMapOf<String, Any>()
@ -79,7 +81,7 @@ internal class DeviceDataCollector(
runtimeVersions.toMutableMap(), runtimeVersions.toMutableMap(),
calculateFreeDisk(), calculateFreeDisk(),
calculateFreeMemory(), calculateFreeMemory(),
calculateOrientation(), getOrientationAsString(),
Date(now) Date(now)
) )
@ -187,7 +189,7 @@ internal class DeviceDataCollector(
return if (displayMetrics != null) { return if (displayMetrics != null) {
val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels) val max = max(displayMetrics.widthPixels, displayMetrics.heightPixels)
val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels) val min = min(displayMetrics.widthPixels, displayMetrics.heightPixels)
String.format(Locale.US, "%dx%d", max, min) "${max}x$min"
} else { } else {
null 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_LANDSCAPE -> "landscape"
ORIENTATION_PORTRAIT -> "portrait" ORIENTATION_PORTRAIT -> "portrait"
else -> null 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) { fun addRuntimeVersionInfo(key: String, value: String) {
runtimeVersions[key] = value runtimeVersions[key] = value
} }

@ -15,8 +15,7 @@ internal class ErrorInternal @JvmOverloads internal constructor(
.mapTo(mutableListOf()) { currentEx -> .mapTo(mutableListOf()) { currentEx ->
// Somehow it's possible for stackTrace to be null in rare cases // Somehow it's possible for stackTrace to be null in rare cases
val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>() val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>()
val trace = val trace = Stacktrace(stacktrace, projectPackages, logger)
Stacktrace.stacktraceFromJavaTrace(stacktrace, projectPackages, logger)
val errorInternal = val errorInternal =
ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace) ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace)

@ -1,5 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;

@ -1,7 +1,7 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File import java.io.File
import java.util.Locale
import java.util.UUID import java.util.UUID
/** /**
@ -27,15 +27,7 @@ internal data class EventFilenameInfo(
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json" * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
*/ */
fun encode(): String { fun encode(): String {
return String.format( return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
Locale.US,
"%d_%s_%s_%s_%s.json",
timestamp,
apiKey,
serializeErrorTypeHeader(errorTypes),
uuid,
suffix
)
} }
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException import java.io.IOException
internal class EventInternal @JvmOverloads internal constructor( internal class EventInternal @JvmOverloads internal constructor(

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException

@ -1,5 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -148,8 +150,8 @@ class EventStore extends FileStore {
void flushReports(Collection<File> storedReports) { void flushReports(Collection<File> storedReports) {
if (!storedReports.isEmpty()) { if (!storedReports.isEmpty()) {
logger.i(String.format(Locale.US, int size = storedReports.size();
"Sending %d saved error(s) to Bugsnag", storedReports.size())); logger.i("Sending " + size + " saved error(s) to Bugsnag");
for (File eventFile : storedReports) { for (File eventFile : storedReports) {
flushEventFile(eventFile); flushEventFile(eventFile);
@ -200,14 +202,12 @@ class EventStore extends FileStore {
String getFilename(Object object) { String getFilename(Object object) {
EventFilenameInfo eventInfo EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, null, config); = EventFilenameInfo.Companion.fromEvent(object, null, config);
String encodedInfo = eventInfo.encode(); return eventInfo.encode();
return String.format(Locale.US, "%s", encodedInfo);
} }
String getNdkFilename(Object object, String apiKey) { String getNdkFilename(Object object, String apiKey) {
EventFilenameInfo eventInfo EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, apiKey, config); = EventFilenameInfo.Companion.fromEvent(object, apiKey, config);
String encodedInfo = eventInfo.encode(); return eventInfo.encode();
return String.format(Locale.US, "%s", encodedInfo);
} }
} }

@ -35,6 +35,9 @@ class ExceptionHandler implements UncaughtExceptionHandler {
@Override @Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) { public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
if (client.getConfig().shouldDiscardError(throwable)) {
return;
}
boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable); boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable);
// Notify any subscribed clients of the uncaught exception // Notify any subscribed clients of the uncaught exception

@ -104,8 +104,7 @@ abstract class FileStore {
out.close(); out.close();
} }
} catch (Exception exception) { } catch (Exception exception) {
logger.w(String.format("Failed to close unsent payload writer (%s) ", logger.w("Failed to close unsent payload writer: " + filename, exception);
filename), exception);
} }
lock.unlock(); lock.unlock();
} }
@ -130,7 +129,7 @@ abstract class FileStore {
Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
stream = new JsonStream(out); stream = new JsonStream(out);
stream.value(streamable); stream.value(streamable);
logger.i(String.format("Saved unsent payload to disk (%s) ", filename)); logger.i("Saved unsent payload to disk: '" + filename + '\'');
return filename; return filename;
} catch (FileNotFoundException exc) { } catch (FileNotFoundException exc) {
logger.w("Ignoring FileNotFoundException - unable to create file", exc); logger.w("Ignoring FileNotFoundException - unable to create file", exc);
@ -168,8 +167,8 @@ abstract class FileStore {
File oldestFile = files.get(k); File oldestFile = files.get(k);
if (!queuedFiles.contains(oldestFile)) { if (!queuedFiles.contains(oldestFile)) {
logger.w(String.format("Discarding oldest error as stored " logger.w("Discarding oldest error as stored "
+ "error limit reached (%s)", oldestFile.getPath())); + "error limit reached: '" + oldestFile.getPath() + '\'');
deleteStoredFiles(Collections.singleton(oldestFile)); deleteStoredFiles(Collections.singleton(oldestFile));
files.remove(k); files.remove(k);
k--; k--;

@ -3,6 +3,8 @@ package com.bugsnag.android;
import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR; import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR;
import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION; import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
import com.bugsnag.android.internal.ImmutableConfig;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
import android.os.Build; import android.os.Build;

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock import kotlin.concurrent.withLock

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.util.concurrent.RejectedExecutionException import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledThreadPoolExecutor import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit import java.util.concurrent.TimeUnit
@ -34,7 +35,7 @@ internal class LaunchCrashTracker @JvmOverloads constructor(
fun markLaunchCompleted() { fun markLaunchCompleted() {
executor.shutdown() executor.shutdown()
launching.set(false) launching.set(false)
notifyObservers(StateEvent.UpdateIsLaunching(false)) updateState { StateEvent.UpdateIsLaunching(false) }
logger.d("App launch period marked as complete") logger.d("App launch period marked as complete")
} }

@ -12,7 +12,7 @@ import java.util.concurrent.ConcurrentHashMap
* Diagnostic information is presented on your Bugsnag dashboard in tabs. * Diagnostic information is presented on your Bugsnag dashboard in tabs.
*/ */
internal data class Metadata @JvmOverloads constructor( internal data class Metadata @JvmOverloads constructor(
internal val store: ConcurrentHashMap<String, Any> = ConcurrentHashMap() internal val store: MutableMap<String, MutableMap<String, Any>> = ConcurrentHashMap()
) : JsonStream.Streamable, MetadataAware { ) : JsonStream.Streamable, MetadataAware {
val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer() val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer()
@ -38,12 +38,9 @@ internal data class Metadata @JvmOverloads constructor(
if (value == null) { if (value == null) {
clearMetadata(section, key) clearMetadata(section, key)
} else { } else {
var tab = store[section] val tab = store[section] ?: ConcurrentHashMap()
if (tab !is MutableMap<*, *>) {
tab = ConcurrentHashMap<Any, Any>()
store[section] = tab store[section] = tab
} insertValue(tab, key, value)
insertValue(tab as MutableMap<String, Any>, key, value)
} }
} }
@ -52,7 +49,7 @@ internal data class Metadata @JvmOverloads constructor(
// only merge if both the existing and new value are maps // only merge if both the existing and new value are maps
val existingValue = map[key] val existingValue = map[key]
if (obj is MutableMap<*, *> && existingValue is MutableMap<*, *>) { if (existingValue != null && obj is Map<*, *>) {
val maps = listOf(existingValue as Map<String, Any>, newValue as Map<String, Any>) val maps = listOf(existingValue as Map<String, Any>, newValue as Map<String, Any>)
obj = mergeMaps(maps) obj = mergeMaps(maps)
} }
@ -65,49 +62,41 @@ internal data class Metadata @JvmOverloads constructor(
override fun clearMetadata(section: String, key: String) { override fun clearMetadata(section: String, key: String) {
val tab = store[section] val tab = store[section]
tab?.remove(key)
if (tab is MutableMap<*, *>) { if (tab.isNullOrEmpty()) {
tab.remove(key)
if (tab.isEmpty()) {
store.remove(section) store.remove(section)
} }
} }
}
override fun getMetadata(section: String): Map<String, Any>? { override fun getMetadata(section: String): Map<String, Any>? {
return store[section] as (Map<String, Any>?) return store[section]
} }
override fun getMetadata(section: String, key: String): Any? { override fun getMetadata(section: String, key: String): Any? {
return when (val tab = store[section]) { return getMetadata(section)?.get(key)
is Map<*, *> -> (tab as Map<String, Any>?)!![key]
else -> tab
}
} }
fun toMap(): ConcurrentHashMap<String, Any> { fun toMap(): MutableMap<String, MutableMap<String, Any>> {
val hashMap = ConcurrentHashMap(store) val copy = ConcurrentHashMap(store)
// deep copy each section // deep copy each section
store.entries.forEach { store.entries.forEach {
if (it.value is ConcurrentHashMap<*, *>) { copy[it.key] = ConcurrentHashMap(it.value)
hashMap[it.key] = ConcurrentHashMap(it.value as ConcurrentHashMap<*, *>)
}
} }
return hashMap return copy
} }
companion object { companion object {
fun merge(vararg data: Metadata): Metadata { fun merge(vararg data: Metadata): Metadata {
val stores = data.map { it.toMap() } val stores = data.map { it.toMap() }
val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys } val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys }
val newMeta = Metadata(mergeMaps(stores)) val newMeta = Metadata(mergeMaps(stores) as MutableMap<String, MutableMap<String, Any>>)
newMeta.redactedKeys = redactKeys.toSet() newMeta.redactedKeys = redactKeys.toSet()
return newMeta return newMeta
} }
internal fun mergeMaps(data: List<Map<String, Any>>): ConcurrentHashMap<String, Any> { internal fun mergeMaps(data: List<Map<String, Any>>): MutableMap<String, Any> {
val keys = data.flatMap { it.keys }.toSet() val keys = data.flatMap { it.keys }.toSet()
val result = ConcurrentHashMap<String, Any>() val result = ConcurrentHashMap<String, Any>()
@ -120,7 +109,7 @@ internal data class Metadata @JvmOverloads constructor(
} }
private fun getMergeValue( private fun getMergeValue(
result: ConcurrentHashMap<String, Any>, result: MutableMap<String, Any>,
key: String, key: String,
map: Map<String, Any> map: Map<String, Any>
) { ) {

@ -28,8 +28,8 @@ internal data class MetadataState(val metadata: Metadata = Metadata()) :
private fun notifyClear(section: String, key: String?) { private fun notifyClear(section: String, key: String?) {
when (key) { when (key) {
null -> notifyObservers(StateEvent.ClearMetadataSection(section)) null -> updateState { StateEvent.ClearMetadataSection(section) }
else -> notifyObservers(StateEvent.ClearMetadataValue(section, key)) 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?) { private fun notifyMetadataAdded(section: String, key: String, value: Any?) {
when (value) { when (value) {
null -> notifyClear(section, key) 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<String, Any?>) { private fun notifyMetadataAdded(section: String, value: Map<String, Any?>) {
value.entries.forEach { value.entries.forEach {
notifyObservers(AddMetadata(section, it.key, metadata.getMetadata(it.key))) updateState { AddMetadata(section, it.key, metadata.getMetadata(it.key)) }
} }
} }
} }

@ -1,5 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -325,7 +327,7 @@ public class NativeInterface {
ImmutableConfig config = client.getConfig(); ImmutableConfig config = client.getConfig();
if (releaseStage == null if (releaseStage == null
|| releaseStage.length() == 0 || releaseStage.length() == 0
|| config.shouldNotifyForReleaseStage()) { || !config.shouldDiscardByReleaseStage()) {
EventStore eventStore = client.getEventStore(); EventStore eventStore = client.getEventStore();
String filename = eventStore.getNdkFilename(payload, apiKey); String filename = eventStore.getNdkFilename(payload, apiKey);
@ -368,6 +370,9 @@ public class NativeInterface {
@NonNull final String message, @NonNull final String message,
@NonNull final Severity severity, @NonNull final Severity severity,
@NonNull final StackTraceElement[] stacktrace) { @NonNull final StackTraceElement[] stacktrace) {
if (getClient().getConfig().shouldDiscardError(name)) {
return;
}
Throwable exc = new RuntimeException(); Throwable exc = new RuntimeException();
exc.setStackTrace(stacktrace); exc.setStackTrace(stacktrace);

@ -7,7 +7,7 @@ import java.io.IOException
*/ */
class Notifier @JvmOverloads constructor( class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier", var name: String = "Android Bugsnag Notifier",
var version: String = "5.9.4", var version: String = "5.10.1",
var url: String = "https://bugsnag.com" var url: String = "https://bugsnag.com"
) : JsonStream.Streamable { ) : JsonStream.Streamable {

@ -1,5 +1,7 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
internal class PluginClient( internal class PluginClient(
userPlugins: Set<Plugin>, userPlugins: Set<Plugin>,
private val immutableConfig: ImmutableConfig, private val immutableConfig: ImmutableConfig,

@ -1,5 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -45,10 +47,7 @@ class SessionStore extends FileStore {
@NonNull @NonNull
@Override @Override
String getFilename(Object object) { String getFilename(Object object) {
return String.format(Locale.US, return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json";
"%s%d_v2.json",
UUID.randomUUID().toString(),
System.currentTimeMillis());
} }
} }

@ -1,5 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.ImmutableConfig;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting; import androidx.annotation.VisibleForTesting;
@ -79,6 +81,9 @@ class SessionTracker extends BaseObservable {
@VisibleForTesting @VisibleForTesting
Session startNewSession(@NonNull Date date, @Nullable User user, Session startNewSession(@NonNull Date date, @Nullable User user,
boolean autoCaptured) { boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return null;
}
String id = UUID.randomUUID().toString(); String id = UUID.randomUUID().toString();
Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger); Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger);
currentSession.set(session); currentSession.set(session);
@ -87,6 +92,9 @@ class SessionTracker extends BaseObservable {
} }
Session startSession(boolean autoCaptured) { Session startSession(boolean autoCaptured) {
if (client.getConfig().shouldDiscardSession(autoCaptured)) {
return null;
}
return startNewSession(new Date(), client.getUser(), autoCaptured); return startNewSession(new Date(), client.getUser(), autoCaptured);
} }
@ -95,7 +103,7 @@ class SessionTracker extends BaseObservable {
if (session != null) { if (session != null) {
session.isPaused.set(true); session.isPaused.set(true);
notifyObservers(StateEvent.PauseSession.INSTANCE); updateState(StateEvent.PauseSession.INSTANCE);
} }
} }
@ -116,9 +124,9 @@ class SessionTracker extends BaseObservable {
return resumed; return resumed;
} }
private void notifySessionStartObserver(Session session) { private void notifySessionStartObserver(final Session session) {
String startedAt = DateUtils.toIso8601(session.getStartedAt()); final String startedAt = DateUtils.toIso8601(session.getStartedAt());
notifyObservers(new StateEvent.StartSession(session.getId(), startedAt, updateState(new StateEvent.StartSession(session.getId(), startedAt,
session.getHandledCount(), session.getUnhandledCount())); session.getHandledCount(), session.getUnhandledCount()));
} }
@ -137,13 +145,16 @@ class SessionTracker extends BaseObservable {
Session registerExistingSession(@Nullable Date date, @Nullable String sessionId, Session registerExistingSession(@Nullable Date date, @Nullable String sessionId,
@Nullable User user, int unhandledCount, @Nullable User user, int unhandledCount,
int handledCount) { int handledCount) {
if (client.getConfig().shouldDiscardSession(false)) {
return null;
}
Session session = null; Session session = null;
if (date != null && sessionId != null) { if (date != null && sessionId != null) {
session = new Session(sessionId, date, user, unhandledCount, handledCount, session = new Session(sessionId, date, user, unhandledCount, handledCount,
client.getNotifier(), logger); client.getNotifier(), logger);
notifySessionStartObserver(session); notifySessionStartObserver(session);
} else { } else {
notifyObservers(StateEvent.PauseSession.INSTANCE); updateState(StateEvent.PauseSession.INSTANCE);
} }
currentSession.set(session); currentSession.set(session);
return session; return session;
@ -157,18 +168,12 @@ class SessionTracker extends BaseObservable {
*/ */
private void trackSessionIfNeeded(final Session session) { private void trackSessionIfNeeded(final Session session) {
logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client"); logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client");
boolean notifyForRelease = configuration.shouldNotifyForReleaseStage();
session.setApp(client.getAppDataCollector().generateApp()); session.setApp(client.getAppDataCollector().generateApp());
session.setDevice(client.getDeviceDataCollector().generateDevice()); session.setDevice(client.getDeviceDataCollector().generateDevice());
boolean deliverSession = callbackState.runOnSessionTasks(session, logger); boolean deliverSession = callbackState.runOnSessionTasks(session, logger);
if (deliverSession && notifyForRelease if (deliverSession && session.isTracked().compareAndSet(false, true)) {
&& (configuration.getAutoTrackSessions() || !session.isAutoCaptured())
&& session.isTracked().compareAndSet(false, true)) {
notifySessionStartObserver(session); notifySessionStartObserver(session);
flushAsync(); flushAsync();
flushInMemorySession(session); flushInMemorySession(session);
} }
@ -355,13 +360,14 @@ class SessionTracker extends BaseObservable {
lastExitedForegroundMs.set(nowMs); lastExitedForegroundMs.set(nowMs);
} }
} }
client.getContextState().setAutomaticContext(getContextActivity());
notifyNdkInForeground(); notifyNdkInForeground();
} }
private void notifyNdkInForeground() { private void notifyNdkInForeground() {
Boolean inForeground = isInForeground(); Boolean inForeground = isInForeground();
boolean foreground = inForeground != null ? inForeground : false; final boolean foreground = inForeground != null ? inForeground : false;
notifyObservers(new StateEvent.UpdateInForeground(foreground, getContextActivity())); updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity()));
} }
@Nullable @Nullable

@ -69,8 +69,7 @@ final class SeverityReason implements JsonStream.Streamable {
case REASON_LOG: case REASON_LOG:
return new SeverityReason(severityReasonType, severity, false, attrVal); return new SeverityReason(severityReasonType, severity, false, attrVal);
default: default:
String msg = String.format("Invalid argument '%s' for severityReason", String msg = "Invalid argument for severityReason: '" + severityReasonType + '\'';
severityReasonType);
throw new IllegalArgumentException(msg); throw new IllegalArgumentException(msg);
} }
} }

@ -20,21 +20,40 @@ internal class Stacktrace : JsonStream.Streamable {
* not. * not.
*/ */
fun inProject(className: String, projectPackages: Collection<String>): Boolean? { fun inProject(className: String, projectPackages: Collection<String>): Boolean? {
for (packageName in projectPackages) { return when {
if (className.startsWith(packageName)) { projectPackages.any { className.startsWith(it) } -> true
return true else -> null
} }
} }
return null
} }
fun stacktraceFromJavaTrace( val trace: List<Stackframe>
constructor(frames: List<Stackframe>) {
trace = limitTraceLength(frames)
}
constructor(
stacktrace: Array<StackTraceElement>, stacktrace: Array<StackTraceElement>,
projectPackages: Collection<String>, projectPackages: Collection<String>,
logger: Logger logger: Logger
): Stacktrace { ) {
val frames = stacktrace.mapNotNull { serializeStackframe(it, projectPackages, logger) } val frames = limitTraceLength(stacktrace)
return Stacktrace(frames) trace = frames.mapNotNull { serializeStackframe(it, projectPackages, logger) }
}
private fun limitTraceLength(frames: Array<StackTraceElement>): Array<StackTraceElement> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.sliceArray(0 until STACKTRACE_TRIM_LENGTH)
else -> frames
}
}
private fun limitTraceLength(frames: List<Stackframe>): List<Stackframe> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH)
else -> frames
}
} }
private fun serializeStackframe( private fun serializeStackframe(
@ -43,36 +62,23 @@ internal class Stacktrace : JsonStream.Streamable {
logger: Logger logger: Logger
): Stackframe? { ): Stackframe? {
try { try {
val className = el.className
val methodName = when { val methodName = when {
el.className.isNotEmpty() -> el.className + "." + el.methodName className.isNotEmpty() -> className + "." + el.methodName
else -> el.methodName else -> el.methodName
} }
return Stackframe( return Stackframe(
methodName, methodName,
if (el.fileName == null) "Unknown" else el.fileName, el.fileName ?: "Unknown",
el.lineNumber, el.lineNumber,
inProject(el.className, projectPackages) inProject(className, projectPackages)
) )
} catch (lineEx: Exception) { } catch (lineEx: Exception) {
logger.w("Failed to serialize stacktrace", lineEx) logger.w("Failed to serialize stacktrace", lineEx)
return null return null
} }
} }
}
val trace: List<Stackframe>
constructor(frames: List<Stackframe>) {
trace = limitTraceLength(frames)
}
private fun <T> limitTraceLength(frames: List<T>): List<T> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH)
else -> frames
}
}
@Throws(IOException::class) @Throws(IOException::class)
override fun toStream(writer: JsonStream) { override fun toStream(writer: JsonStream) {

@ -1,47 +1,66 @@
package com.bugsnag.android package com.bugsnag.android
sealed class StateEvent { sealed class StateEvent { // JvmField allows direct field access optimizations
class Install( class Install(
val apiKey: String, @JvmField val apiKey: String,
val autoDetectNdkCrashes: Boolean, @JvmField val autoDetectNdkCrashes: Boolean,
val appVersion: String?, @JvmField val appVersion: String?,
val buildUuid: String?, @JvmField val buildUuid: String?,
val releaseStage: String?, @JvmField val releaseStage: String?,
val lastRunInfoPath: String, @JvmField val lastRunInfoPath: String,
val consecutiveLaunchCrashes: Int @JvmField val consecutiveLaunchCrashes: Int
) : StateEvent() ) : StateEvent()
object DeliverPending : StateEvent() object DeliverPending : StateEvent()
class AddMetadata(val section: String, val key: String?, val value: Any?) : StateEvent() class AddMetadata(
class ClearMetadataSection(val section: String) : StateEvent() @JvmField val section: String,
class ClearMetadataValue(val section: String, val key: String?) : StateEvent() @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( class AddBreadcrumb(
val message: String, @JvmField val message: String,
val type: BreadcrumbType, @JvmField val type: BreadcrumbType,
val timestamp: String, @JvmField val timestamp: String,
val metadata: MutableMap<String, Any?> @JvmField val metadata: MutableMap<String, Any?>
) : StateEvent() ) : StateEvent()
object NotifyHandled : StateEvent() object NotifyHandled : StateEvent()
object NotifyUnhandled : StateEvent() object NotifyUnhandled : StateEvent()
object PauseSession : StateEvent() object PauseSession : StateEvent()
class StartSession( class StartSession(
val id: String, @JvmField val id: String,
val startedAt: String, @JvmField val startedAt: String,
val handledCount: Int, @JvmField val handledCount: Int,
val unhandledCount: Int val unhandledCount: Int
) : StateEvent() ) : StateEvent()
class UpdateContext(val context: String?) : StateEvent() class UpdateContext(@JvmField val context: String?) : StateEvent()
class UpdateInForeground(val inForeground: Boolean, val contextActivity: String?) : StateEvent()
class UpdateLastRunInfo(val consecutiveLaunchCrashes: Int) : StateEvent() class UpdateInForeground(
class UpdateIsLaunching(val isLaunching: Boolean) : StateEvent() @JvmField val inForeground: Boolean,
class UpdateOrientation(val orientation: String?) : StateEvent() 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()
} }

@ -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<String, BreadcrumbType> 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<String, Object> 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<String, BreadcrumbType> buildActions() {
Map<String, BreadcrumbType> 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<String, BreadcrumbType> 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;
}
}

@ -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<String, BreadcrumbType> = buildActions()
override fun onReceive(context: Context, intent: Intent) {
try {
val meta: MutableMap<String, Any> = 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<String, Any>,
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<String, BreadcrumbType> {
val actions: MutableMap<String, BreadcrumbType> = 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
}
}

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException import java.io.IOException
/** /**
@ -11,7 +12,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
sendThreads: ThreadSendPolicy, sendThreads: ThreadSendPolicy,
projectPackages: Collection<String>, projectPackages: Collection<String>,
logger: Logger, logger: Logger,
currentThread: java.lang.Thread = java.lang.Thread.currentThread(), currentThread: java.lang.Thread? = null,
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>? = null stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>? = null
) : JsonStream.Streamable { ) : JsonStream.Streamable {
@ -30,7 +31,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
threads = when { threads = when {
recordThreads -> captureThreadTrace( recordThreads -> captureThreadTrace(
stackTraces ?: java.lang.Thread.getAllStackTraces(), stackTraces ?: java.lang.Thread.getAllStackTraces(),
currentThread, currentThread ?: java.lang.Thread.currentThread(),
exc, exc,
isUnhandled, isUnhandled,
projectPackages, projectPackages,
@ -64,7 +65,7 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
val trace = stackTraces[thread] val trace = stackTraces[thread]
if (trace != null) { if (trace != null) {
val stacktrace = Stacktrace.stacktraceFromJavaTrace(trace, projectPackages, logger) val stacktrace = Stacktrace(trace, projectPackages, logger)
val errorThread = thread.id == currentThreadId val errorThread = thread.id == currentThreadId
Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger) Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger)
} else { } else {

@ -7,5 +7,5 @@ internal class UserState(user: User) : BaseObservable() {
emitObservableEvent() emitObservableEvent()
} }
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateUser(user)) fun emitObservableEvent() = updateState { StateEvent.UpdateUser(user) }
} }

@ -1,5 +1,7 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.StateObserver
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
@ -55,11 +57,13 @@ internal class UserStore @JvmOverloads constructor(
else -> UserState(User(deviceId, null, null)) else -> UserState(User(deviceId, null, null))
} }
userState.addObserver { _, arg -> userState.addObserver(
if (arg is StateEvent.UpdateUser) { StateObserver { event ->
save(arg.user) if (event is StateEvent.UpdateUser) {
save(event.user)
} }
} }
)
return userState return userState
} }

@ -1,11 +1,30 @@
package com.bugsnag.android package com.bugsnag.android.internal
import android.content.Context import android.content.Context
import android.content.pm.ApplicationInfo import android.content.pm.ApplicationInfo
import android.content.pm.PackageInfo
import android.content.pm.PackageManager 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 import java.io.File
internal data class ImmutableConfig( data class ImmutableConfig(
val apiKey: String, val apiKey: String,
val autoDetectErrors: Boolean, val autoDetectErrors: Boolean,
val enabledErrorTypes: ErrorTypes, val enabledErrorTypes: ErrorTypes,
@ -29,21 +48,12 @@ internal data class ImmutableConfig(
val maxPersistedEvents: Int, val maxPersistedEvents: Int,
val maxPersistedSessions: Int, val maxPersistedSessions: Int,
val persistenceDirectory: File, val persistenceDirectory: File,
val sendLaunchCrashesSynchronously: Boolean val sendLaunchCrashesSynchronously: Boolean,
) {
/** // results cached here to avoid unnecessary lookups in Client.
* Checks if the given release stage should be notified or not val packageInfo: PackageInfo?,
* val appInfo: ApplicationInfo?
* @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)
@JvmName("getErrorApiDeliveryParams") @JvmName("getErrorApiDeliveryParams")
internal fun getErrorApiDeliveryParams(payload: EventPayload) = internal fun getErrorApiDeliveryParams(payload: EventPayload) =
@ -52,11 +62,73 @@ internal data class ImmutableConfig(
@JvmName("getSessionApiDeliveryParams") @JvmName("getSessionApiDeliveryParams")
internal fun getSessionApiDeliveryParams() = internal fun getSessionApiDeliveryParams() =
DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey)) 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( internal fun convertToImmutableConfig(
config: Configuration, config: Configuration,
buildUuid: String? = null buildUuid: String? = null,
packageInfo: PackageInfo? = null,
appInfo: ApplicationInfo? = null
): ImmutableConfig { ): ImmutableConfig {
val errorTypes = when { val errorTypes = when {
config.autoDetectErrors -> config.enabledErrorTypes.copy() config.autoDetectErrors -> config.enabledErrorTypes.copy()
@ -87,7 +159,9 @@ internal fun convertToImmutableConfig(
maxPersistedSessions = config.maxPersistedSessions, maxPersistedSessions = config.maxPersistedSessions,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
persistenceDirectory = config.persistenceDirectory!!, persistenceDirectory = config.persistenceDirectory!!,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
packageInfo = packageInfo,
appInfo = appInfo
) )
} }
@ -144,7 +218,7 @@ internal fun sanitiseConfiguration(
if (configuration.persistenceDirectory == null) { if (configuration.persistenceDirectory == null) {
configuration.persistenceDirectory = appContext.cacheDir configuration.persistenceDirectory = appContext.cacheDir
} }
return convertToImmutableConfig(configuration, buildUuid) return convertToImmutableConfig(configuration, buildUuid, packageInfo, appInfo)
} }
internal const val RELEASE_STAGE_DEVELOPMENT = "development" internal const val RELEASE_STAGE_DEVELOPMENT = "development"

@ -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);
}

@ -0,0 +1,22 @@
From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001
From: M66B <M66B@users.noreply.github.com>
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
Loading…
Cancel
Save