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