Build Bugsnag inline

This commit is contained in:
M66B 2021-05-15 22:03:05 +02:00
parent 779de80459
commit c49509cfcb
100 changed files with 10890 additions and 0 deletions

View File

@ -1,5 +1,6 @@
apply plugin: 'com.android.application'
apply plugin: 'com.bugsnag.android.gradle'
apply plugin: 'kotlin-android'
def getVersionCode = { -> return 1591 }
def getReleaseName = { -> return "\"Pteropelyx\"" }
@ -406,6 +407,7 @@ dependencies {
implementation("com.bugsnag:bugsnag-android:$bugsnag_version") {
//exclude group: "com.bugsnag", module: "bugsnag-plugin-android-anr"
exclude group: "com.bugsnag", module: "bugsnag-plugin-android-ndk"
exclude group: "com.bugsnag", module: "bugsnag-android-core"
}
// https://github.com/mangstadt/biweekly

View File

@ -0,0 +1,49 @@
package com.bugsnag.android
import android.app.Activity
import android.app.Application
import android.os.Bundle
internal class ActivityBreadcrumbCollector(
private val cb: (message: String, method: Map<String, Any>) -> Unit
) : Application.ActivityLifecycleCallbacks {
var prevState: String? = null
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) =
leaveBreadcrumb(getActivityName(activity), "onCreate()", savedInstanceState != null)
override fun onActivityStarted(activity: Activity) =
leaveBreadcrumb(getActivityName(activity), "onStart()")
override fun onActivityResumed(activity: Activity) =
leaveBreadcrumb(getActivityName(activity), "onResume()")
override fun onActivityPaused(activity: Activity) =
leaveBreadcrumb(getActivityName(activity), "onPause()")
override fun onActivityStopped(activity: Activity) =
leaveBreadcrumb(getActivityName(activity), "onStop()")
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) =
leaveBreadcrumb(getActivityName(activity), "onSaveInstanceState()")
override fun onActivityDestroyed(activity: Activity) =
leaveBreadcrumb(getActivityName(activity), "onDestroy()")
private fun getActivityName(activity: Activity) = activity.javaClass.simpleName
private fun leaveBreadcrumb(activityName: String, lifecycleCallback: String, hasBundle: Boolean? = null) {
val metadata = mutableMapOf<String, Any>()
if (hasBundle != null) {
metadata["hasBundle"] = hasBundle
}
val previousVal = prevState
if (previousVal != null) {
metadata["previous"] = previousVal
}
cb("$activityName#$lifecycleCallback", metadata)
prevState = lifecycleCallback
}
}

View File

@ -0,0 +1,86 @@
package com.bugsnag.android
import java.io.IOException
/**
* Stateless information set by the notifier about your app can be found on this class. These values
* can be accessed and amended if necessary.
*/
open class App internal constructor(
/**
* The architecture of the running application binary
*/
var binaryArch: String?,
/**
* The package name of the application
*/
var id: String?,
/**
* The release stage set in [Configuration.releaseStage]
*/
var releaseStage: String?,
/**
* The version of the application set in [Configuration.version]
*/
var version: String?,
/**
The revision ID from the manifest (React Native apps only)
*/
var codeBundleId: String?,
/**
* The unique identifier for the build of the application set in [Configuration.buildUuid]
*/
var buildUuid: String?,
/**
* The application type set in [Configuration#version]
*/
var type: String?,
/**
* The version code of the application set in [Configuration.versionCode]
*/
var versionCode: Number?
) : JsonStream.Streamable {
internal constructor(
config: ImmutableConfig,
binaryArch: String?,
id: String?,
releaseStage: String?,
version: String?,
codeBundleId: String?
) : this(
binaryArch,
id,
releaseStage,
version,
codeBundleId,
config.buildUuid,
config.appType,
config.versionCode
)
internal open fun serialiseFields(writer: JsonStream) {
writer.name("binaryArch").value(binaryArch)
writer.name("buildUUID").value(buildUuid)
writer.name("codeBundleId").value(codeBundleId)
writer.name("id").value(id)
writer.name("releaseStage").value(releaseStage)
writer.name("type").value(type)
writer.name("version").value(version)
writer.name("versionCode").value(versionCode)
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
serialiseFields(writer)
writer.endObject()
}
}

View File

@ -0,0 +1,133 @@
package com.bugsnag.android
import android.app.ActivityManager
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import android.os.Build
import android.os.SystemClock
import java.util.HashMap
/**
* Collects various data on the application state
*/
internal class AppDataCollector(
appContext: Context,
private val packageManager: PackageManager?,
private val config: ImmutableConfig,
private val sessionTracker: SessionTracker,
private val activityManager: ActivityManager?,
private val launchCrashTracker: LaunchCrashTracker,
private val logger: Logger
) {
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 var binaryArch: String? = null
private val appName = getAppName()
private val releaseStage = config.releaseStage
private val versionName = config.appVersion ?: packageInfo?.versionName
fun generateApp(): App = App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId)
fun generateAppWithState(): AppWithState = AppWithState(
config, binaryArch, packageName, releaseStage, versionName, codeBundleId,
getDurationMs(), calculateDurationInForeground(), sessionTracker.isInForeground,
launchCrashTracker.isLaunching()
)
fun getAppDataMetadata(): MutableMap<String, Any?> {
val map = HashMap<String, Any?>()
map["name"] = appName
map["activeScreen"] = getActiveScreenClass()
map["memoryUsage"] = getMemoryUsage()
map["lowMemory"] = isLowMemory()
isBackgroundWorkRestricted()?.let {
map["backgroundWorkRestricted"] = 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).
*/
private fun getMemoryUsage(): Long {
val runtime = Runtime.getRuntime()
return runtime.totalMemory() - runtime.freeMemory()
}
/**
* Checks whether the user has restricted the amount of work this app can do in the background.
* 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) {
null
} else if (activityManager.isBackgroundRestricted) {
true // only return non-null value if true to avoid noise in error reports
} else {
null
}
}
/**
* Check if the device is currently running low on memory.
*/
private fun isLowMemory(): Boolean? {
try {
if (activityManager != null) {
val memInfo = ActivityManager.MemoryInfo()
activityManager.getMemoryInfo(memInfo)
return memInfo.lowMemory
}
} catch (exception: Exception) {
logger.w("Could not check lowMemory status")
}
return null
}
fun setBinaryArch(binaryArch: String) {
this.binaryArch = binaryArch
}
/**
* Calculates the duration the app has been in the foreground
*
* @return the duration in ms
*/
internal fun calculateDurationInForeground(): Long? {
val nowMs = System.currentTimeMillis()
return sessionTracker.getDurationInForegroundMs(nowMs)
}
/**
* The name of the running Android app, from android:label in
* AndroidManifest.xml
*/
private fun getAppName(): String? {
val copy = appInfo
return when {
packageManager != null && copy != null -> {
packageManager.getApplicationLabel(copy).toString()
}
else -> null
}
}
companion object {
internal val startTimeMs = SystemClock.elapsedRealtime()
/**
* Get the time in milliseconds since Bugsnag was initialized, which is a
* good approximation for how long the app has been running.
*/
fun getDurationMs(): Long = SystemClock.elapsedRealtime() - startTimeMs
}
}

View File

@ -0,0 +1,72 @@
package com.bugsnag.android
/**
* Stateful information set by the notifier about your app can be found on this class. These values
* can be accessed and amended if necessary.
*/
class AppWithState(
binaryArch: String?,
id: String?,
releaseStage: String?,
version: String?,
codeBundleId: String?,
buildUuid: String?,
type: String?,
versionCode: Number?,
/**
* The number of milliseconds the application was running before the event occurred
*/
var duration: Number?,
/**
* The number of milliseconds the application was running in the foreground before the
* event occurred
*/
var durationInForeground: Number?,
/**
* Whether the application was in the foreground when the event occurred
*/
var inForeground: Boolean?,
/**
* Whether the application was launching when the event occurred
*/
var isLaunching: Boolean?
) : App(binaryArch, id, releaseStage, version, codeBundleId, buildUuid, type, versionCode) {
internal constructor(
config: ImmutableConfig,
binaryArch: String?,
id: String?,
releaseStage: String?,
version: String?,
codeBundleId: String?,
duration: Number?,
durationInForeground: Number?,
inForeground: Boolean?,
isLaunching: Boolean?
) : this(
binaryArch,
id,
releaseStage,
version,
codeBundleId,
config.buildUuid,
config.appType,
config.versionCode,
duration,
durationInForeground,
inForeground,
isLaunching
)
override fun serialiseFields(writer: JsonStream) {
super.serialiseFields(writer)
writer.name("duration").value(duration)
writer.name("durationInForeground").value(durationInForeground)
writer.name("inForeground").value(inForeground)
writer.name("isLaunching").value(isLaunching)
}
}

View File

@ -0,0 +1,174 @@
package com.bugsnag.android
import androidx.annotation.VisibleForTesting
import java.util.concurrent.BlockingQueue
import java.util.concurrent.Callable
import java.util.concurrent.Executors
import java.util.concurrent.Future
import java.util.concurrent.LinkedBlockingQueue
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ThreadFactory
import java.util.concurrent.ThreadPoolExecutor
import java.util.concurrent.TimeUnit
/**
* The type of task which is being submitted. This determines which execution queue
* the task will be added to.
*/
internal enum class TaskType {
/**
* A task that sends an error request. Any filesystem operations
* that persist/delete errors must be submitted using this type.
*/
ERROR_REQUEST,
/**
* A task that sends a session request. Any filesystem operations
* that persist/delete sessions must be submitted using this type.
*/
SESSION_REQUEST,
/**
* A task that performs I/O, such as reading a file on disk. This should NOT include operations
* related to error/session storage - use [ERROR_REQUEST] or [SESSION_REQUEST] instead.
*/
IO,
/**
* A task that sends an internal error report to Bugsnag.
*/
INTERNAL_REPORT,
/**
* Any other task that needs to run in the background. These will typically be
* short-lived operations that take <100ms, such as registering a
* [android.content.BroadcastReceiver].
*/
DEFAULT
}
private const val SHUTDOWN_WAIT_MS = 1500L
// these values have been loosely adapted from android.os.AsyncTask over the years.
private const val THREAD_POOL_SIZE = 1
private const val KEEP_ALIVE_SECS = 30L
private const val TASK_QUEUE_SIZE = 128
internal fun createExecutor(name: String, keepAlive: Boolean): ThreadPoolExecutor {
val queue: BlockingQueue<Runnable> = LinkedBlockingQueue(TASK_QUEUE_SIZE)
val threadFactory = ThreadFactory { Thread(it, name) }
// certain executors (error/session/io) should always keep their threads alive, but others
// are less important so are allowed a pool size of 0 that expands on demand.
val coreSize = when {
keepAlive -> THREAD_POOL_SIZE
else -> 0
}
return ThreadPoolExecutor(
coreSize,
THREAD_POOL_SIZE,
KEEP_ALIVE_SECS,
TimeUnit.SECONDS,
queue,
threadFactory
)
}
/**
* Provides a service for submitting lengthy tasks to run on background threads.
*
* A [TaskType] must be submitted with each task, which routes it to the appropriate executor.
* Setting the correct [TaskType] is critical as it can be used to enforce thread confinement.
* It also avoids short-running operations being held up by long-running operations submitted
* to the same executor.
*/
internal class BackgroundTaskService(
// these executors must remain single-threaded - the SDK makes assumptions
// about synchronization based on this.
@VisibleForTesting
internal val errorExecutor: ThreadPoolExecutor = createExecutor(
"Bugsnag Error thread",
true
),
@VisibleForTesting
internal val sessionExecutor: ThreadPoolExecutor = createExecutor(
"Bugsnag Session thread",
true
),
@VisibleForTesting
internal val ioExecutor: ThreadPoolExecutor = createExecutor(
"Bugsnag IO thread",
true
),
@VisibleForTesting
internal val internalReportExecutor: ThreadPoolExecutor = createExecutor(
"Bugsnag Internal Report thread",
false
),
@VisibleForTesting
internal val defaultExecutor: ThreadPoolExecutor = createExecutor(
"Bugsnag Default thread",
false
)
) {
/**
* Submits a task for execution on a single-threaded executor. It is guaranteed that tasks
* with the same [TaskType] are executed in the order of submission.
*
* The caller is responsible for catching and handling
* [java.util.concurrent.RejectedExecutionException] if the executor is saturated.
*
* On process termination the service will attempt to wait for previously submitted jobs
* with the task type [TaskType.ERROR_REQUEST], [TaskType.SESSION_REQUEST] and [TaskType.IO].
* This is a best-effort attempt - no guarantee can be made that the operations will complete.
*/
@Throws(RejectedExecutionException::class)
fun submitTask(taskType: TaskType, runnable: Runnable): Future<*> {
return submitTask(taskType, Executors.callable(runnable))
}
/**
* @see [submitTask]
*/
@Throws(RejectedExecutionException::class)
fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> {
return when (taskType) {
TaskType.ERROR_REQUEST -> errorExecutor.submit(callable)
TaskType.SESSION_REQUEST -> sessionExecutor.submit(callable)
TaskType.IO -> ioExecutor.submit(callable)
TaskType.INTERNAL_REPORT -> internalReportExecutor.submit(callable)
TaskType.DEFAULT -> defaultExecutor.submit(callable)
}
}
/**
* Notifies the background service that the process is about to terminate. This causes it to
* shutdown submission of tasks to executors, while allowing for in-flight tasks
* to be completed within a reasonable grace period.
*/
fun shutdown() {
// don't wait for existing tasks to complete for these executors, as they are
// less essential
internalReportExecutor.shutdownNow()
defaultExecutor.shutdownNow()
// shutdown the error/session executors first, waiting for existing tasks to complete.
// If a request fails it may perform IO to persist the payload for delivery next launch,
// which would submit tasks to the IO executor - therefore it's critical to
// shutdown the IO executor last.
errorExecutor.shutdown()
sessionExecutor.shutdown()
errorExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
sessionExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
// shutdown the IO executor last, waiting for any existing tasks to complete
ioExecutor.shutdown()
ioExecutor.awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)
}
}

View File

@ -0,0 +1,10 @@
package com.bugsnag.android
import java.util.Observable
internal open class BaseObservable : Observable() {
fun notifyObservers(event: StateEvent) {
setChanged()
super.notifyObservers(event)
}
}

View File

@ -0,0 +1,101 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
@SuppressWarnings("ConstantConditions")
public class Breadcrumb implements JsonStream.Streamable {
private final BreadcrumbInternal impl;
private final Logger logger;
Breadcrumb(@NonNull String message, @NonNull Logger logger) {
this.impl = new BreadcrumbInternal(message);
this.logger = logger;
}
Breadcrumb(@NonNull String message,
@NonNull BreadcrumbType type,
@Nullable Map<String, Object> metadata,
@NonNull Date timestamp,
@NonNull Logger logger) {
this.impl = new BreadcrumbInternal(message, type, metadata, timestamp);
this.logger = logger;
}
private void logNull(String property) {
logger.e("Invalid null value supplied to breadcrumb." + property + ", ignoring");
}
/**
* Sets the description of the breadcrumb
*/
public void setMessage(@NonNull String message) {
if (message != null) {
impl.setMessage(message);
} else {
logNull("message");
}
}
/**
* Gets the description of the breadcrumb
*/
@NonNull
public String getMessage() {
return impl.getMessage();
}
/**
* Sets the type of breadcrumb left - one of those enabled in
* {@link Configuration#getEnabledBreadcrumbTypes()}
*/
public void setType(@NonNull BreadcrumbType type) {
if (type != null) {
impl.setType(type);
} else {
logNull("type");
}
}
/**
* Gets the type of breadcrumb left - one of those enabled in
* {@link Configuration#getEnabledBreadcrumbTypes()}
*/
@NonNull
public BreadcrumbType getType() {
return impl.getType();
}
/**
* Sets diagnostic data relating to the breadcrumb
*/
public void setMetadata(@Nullable Map<String, Object> metadata) {
impl.setMetadata(metadata);
}
/**
* Gets diagnostic data relating to the breadcrumb
*/
@Nullable
public Map<String, Object> getMetadata() {
return impl.getMetadata();
}
/**
* The timestamp that the breadcrumb was left
*/
@NonNull
public Date getTimestamp() {
return impl.getTimestamp();
}
@Override
public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream);
}
}

View File

@ -0,0 +1,35 @@
package com.bugsnag.android
import java.io.IOException
import java.util.Date
/**
* In order to understand what happened in your application before each crash, it can be helpful
* to leave short log statements that we call breadcrumbs. Breadcrumbs are
* 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 {
internal constructor(message: String) : this(
message,
BreadcrumbType.MANUAL,
mutableMapOf(),
Date()
)
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("timestamp").value(DateUtils.toIso8601(timestamp))
writer.name("name").value(message)
writer.name("type").value(type.toString())
writer.name("metaData")
writer.value(metadata, true)
writer.endObject()
}
}

View File

@ -0,0 +1,55 @@
package com.bugsnag.android
import java.io.IOException
import java.util.Queue
import java.util.concurrent.ConcurrentLinkedQueue
internal class BreadcrumbState(
maxBreadcrumbs: Int,
val callbackState: CallbackState,
val logger: Logger
) : BaseObservable(), JsonStream.Streamable {
val store: Queue<Breadcrumb> = ConcurrentLinkedQueue()
private val maxBreadcrumbs: Int
init {
when {
maxBreadcrumbs > 0 -> this.maxBreadcrumbs = maxBreadcrumbs
else -> this.maxBreadcrumbs = 0
}
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
pruneBreadcrumbs()
writer.beginArray()
store.forEach { it.toStream(writer) }
writer.endArray()
}
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()
}
}
}

View File

@ -0,0 +1,41 @@
package com.bugsnag.android
/**
* Recognized types of breadcrumbs
*/
enum class BreadcrumbType(private val type: String) {
/**
* An error was sent to Bugsnag (internal use only)
*/
ERROR("error"),
/**
* A log message
*/
LOG("log"),
/**
* A manual invocation of `leaveBreadcrumb` (default)
*/
MANUAL("manual"),
/**
* A navigation event, such as a window opening or closing
*/
NAVIGATION("navigation"),
/**
* A background process such as a database query
*/
PROCESS("process"),
/**
* A network request
*/
REQUEST("request"),
/**
* A change in application state, such as launch or memory warning
*/
STATE("state"),
/**
* A user action, such as tapping a button
*/
USER("user");
override fun toString() = type
}

View File

@ -0,0 +1,419 @@
package com.bugsnag.android;
import android.annotation.SuppressLint;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Set;
/**
* Static access to a Bugsnag Client, the easiest way to use Bugsnag in your Android app.
* For example:
* <p>
* Bugsnag.start(this, "your-api-key");
* Bugsnag.notify(new RuntimeException("something broke!"));
*
* @see Client
*/
@SuppressWarnings("checkstyle:JavadocTagContinuationIndentation")
public final class Bugsnag {
private static final Object lock = new Object();
@SuppressLint("StaticFieldLeak")
static Client client;
private Bugsnag() {
}
/**
* Initialize the static Bugsnag client
*
* @param androidContext an Android context, usually <code>this</code>
*/
@NonNull
public static Client start(@NonNull Context androidContext) {
return start(androidContext, Configuration.load(androidContext));
}
/**
* Initialize the static Bugsnag client
*
* @param androidContext an Android context, usually <code>this</code>
* @param apiKey your Bugsnag API key from your Bugsnag dashboard
*/
@NonNull
public static Client start(@NonNull Context androidContext, @NonNull String apiKey) {
return start(androidContext, Configuration.load(androidContext, apiKey));
}
/**
* Initialize the static Bugsnag client
*
* @param androidContext an Android context, usually <code>this</code>
* @param config a configuration for the Client
*/
@NonNull
public static Client start(@NonNull Context androidContext, @NonNull Configuration config) {
synchronized (lock) {
if (client == null) {
client = new Client(androidContext, config);
} else {
logClientInitWarning();
}
}
return client;
}
private static void logClientInitWarning() {
getClient().logger.w("Multiple Bugsnag.start calls detected. Ignoring.");
}
/**
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
* represent what was happening in your application at the time an error occurs.
*
* In an android app the "context" is automatically set as the foreground Activity.
* If you would like to set this value manually, you should alter this property.
*/
@Nullable public static String getContext() {
return getClient().getContext();
}
/**
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
* represent what was happening in your application at the time an error occurs.
*
* In an android app the "context" is automatically set as the foreground Activity.
* If you would like to set this value manually, you should alter this property.
*/
public static void setContext(@Nullable final String context) {
getClient().setContext(context);
}
/**
* Sets the user associated with the event.
*/
public static void setUser(@Nullable final String id,
@Nullable final String email,
@Nullable final String name) {
getClient().setUser(id, email, name);
}
/**
* Returns the currently set User information.
*/
@NonNull
public static User getUser() {
return getClient().getUser();
}
/**
* Add a "on error" callback, to execute code at the point where an error report is
* captured in Bugsnag.
*
* You can use this to add or modify information attached to an Event
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to prevent delivery. "on error"
* callbacks do not run before reports generated in the event
* of immediate app termination from crashes in C/C++ code.
*
* For example:
*
* Bugsnag.addOnError(new OnErrorCallback() {
* public boolean run(Event event) {
* event.setSeverity(Severity.INFO);
* return true;
* }
* })
*
* @param onError a callback to run before sending errors to Bugsnag
* @see OnErrorCallback
*/
public static void addOnError(@NonNull OnErrorCallback onError) {
getClient().addOnError(onError);
}
/**
* Removes a previously added "on error" callback
* @param onError the callback to remove
*/
public static void removeOnError(@NonNull OnErrorCallback onError) {
getClient().removeOnError(onError);
}
/**
* Add an "on breadcrumb" callback, to execute code before every
* breadcrumb captured by Bugsnag.
*
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
*
* For example:
*
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
* public boolean run(Breadcrumb breadcrumb) {
* return false; // ignore the breadcrumb
* }
* })
*
* @param onBreadcrumb a callback to run before a breadcrumb is captured
* @see OnBreadcrumbCallback
*/
public static void addOnBreadcrumb(@NonNull final OnBreadcrumbCallback onBreadcrumb) {
getClient().addOnBreadcrumb(onBreadcrumb);
}
/**
* Removes a previously added "on breadcrumb" callback
* @param onBreadcrumb the callback to remove
*/
public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
getClient().removeOnBreadcrumb(onBreadcrumb);
}
/**
* Add an "on session" callback, to execute code before every
* session captured by Bugsnag.
*
* You can use this to modify sessions before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a session.
*
* For example:
*
* Bugsnag.onSession(new OnSessionCallback() {
* public boolean run(Session session) {
* return false; // ignore the session
* }
* })
*
* @param onSession a callback to run before a session is captured
* @see OnSessionCallback
*/
public static void addOnSession(@NonNull OnSessionCallback onSession) {
getClient().addOnSession(onSession);
}
/**
* Removes a previously added "on session" callback
* @param onSession the callback to remove
*/
public static void removeOnSession(@NonNull OnSessionCallback onSession) {
getClient().removeOnSession(onSession);
}
/**
* Notify Bugsnag of a handled exception
*
* @param exception the exception to send to Bugsnag
*/
public static void notify(@NonNull final Throwable exception) {
getClient().notify(exception);
}
/**
* Notify Bugsnag of a handled exception
*
* @param exception the exception to send to Bugsnag
* @param onError callback invoked on the generated error report for
* additional modification
*/
public static void notify(@NonNull final Throwable exception,
@Nullable final OnErrorCallback onError) {
getClient().notify(exception, onError);
}
/**
* Adds a map of multiple metadata key-value pairs to the specified section.
*/
public static void addMetadata(@NonNull String section, @NonNull Map<String, ?> value) {
getClient().addMetadata(section, value);
}
/**
* Adds the specified key and value in the specified section. The value can be of
* any primitive type or a collection such as a map, set or array.
*/
public static void addMetadata(@NonNull String section, @NonNull String key,
@Nullable Object value) {
getClient().addMetadata(section, key, value);
}
/**
* Removes all the data from the specified section.
*/
public static void clearMetadata(@NonNull String section) {
getClient().clearMetadata(section);
}
/**
* Removes data with the specified key from the specified section.
*/
public static void clearMetadata(@NonNull String section, @NonNull String key) {
getClient().clearMetadata(section, key);
}
/**
* Returns a map of data in the specified section.
*/
@Nullable
public static Map<String, Object> getMetadata(@NonNull String section) {
return getClient().getMetadata(section);
}
/**
* Returns the value of the specified key in the specified section.
*/
@Nullable
public static Object getMetadata(@NonNull String section, @NonNull String key) {
return getClient().getMetadata(section, key);
}
/**
* Leave a "breadcrumb" log message, representing an action that occurred
* in your app, to aid with debugging.
*
* @param message the log message to leave
*/
public static void leaveBreadcrumb(@NonNull String message) {
getClient().leaveBreadcrumb(message);
}
/**
* Leave a "breadcrumb" log message representing an action or event which
* occurred in your app, to aid with debugging
* @param message A short label
* @param metadata Additional diagnostic information about the app environment
* @param type A category for the breadcrumb
*/
public static void leaveBreadcrumb(@NonNull String message,
@NonNull Map<String, Object> metadata,
@NonNull BreadcrumbType type) {
getClient().leaveBreadcrumb(message, metadata, type);
}
/**
* Starts tracking a new session. You should disable automatic session tracking via
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
* <p/>
* You should call this at the appropriate time in your application when you wish to start a
* session. Any subsequent errors which occur in your application will still be reported to
* Bugsnag but will not count towards your application's
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
* stability score</a>. This will start a new session even if there is already an existing
* session; you should call {@link #resumeSession()} if you only want to start a session
* when one doesn't already exist.
*
* @see #resumeSession()
* @see #pauseSession()
* @see Configuration#setAutoTrackSessions(boolean)
*/
public static void startSession() {
getClient().startSession();
}
/**
* Resumes a session which has previously been paused, or starts a new session if none exists.
* If a session has already been resumed or started and has not been paused, calling this
* method will have no effect. You should disable automatic session tracking via
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
* <p/>
* It's important to note that sessions are stored in memory for the lifetime of the
* application process and are not persisted on disk. Therefore calling this method on app
* startup would start a new session, rather than continuing any previous session.
* <p/>
* You should call this at the appropriate time in your application when you wish to resume
* a previously started session. Any subsequent errors which occur in your application will
* still be reported to Bugsnag but will not count towards your application's
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
* stability score</a>.
*
* @see #startSession()
* @see #pauseSession()
* @see Configuration#setAutoTrackSessions(boolean)
*
* @return true if a previous session was resumed, false if a new session was started.
*/
public static boolean resumeSession() {
return getClient().resumeSession();
}
/**
* Pauses tracking of a session. You should disable automatic session tracking via
* {@link Configuration#setAutoTrackSessions(boolean)} if you call this method.
* <p/>
* You should call this at the appropriate time in your application when you wish to pause a
* session. Any subsequent errors which occur in your application will still be reported to
* Bugsnag but will not count towards your application's
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
* stability score</a>. This can be advantageous if, for example, you do not wish the
* stability score to include crashes in a background service.
*
* @see #startSession()
* @see #resumeSession()
* @see Configuration#setAutoTrackSessions(boolean)
*/
public static void pauseSession() {
getClient().pauseSession();
}
/**
* Returns the current buffer of breadcrumbs that will be sent with captured events. This
* ordered list represents the most recent breadcrumbs to be captured up to the limit
* set in {@link Configuration#getMaxBreadcrumbs()}.
*
* The returned collection is readonly and mutating the list will cause no effect on the
* Client's state. If you wish to alter the breadcrumbs collected by the Client then you should
* use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and
* {@link Configuration#addOnBreadcrumb(OnBreadcrumbCallback)} instead.
*
* @return a list of collected breadcrumbs
*/
@NonNull
public static List<Breadcrumb> getBreadcrumbs() {
return getClient().getBreadcrumbs();
}
/**
* Retrieves information about the last launch of the application, if it has been run before.
*
* For example, this allows checking whether the app crashed on its last launch, which could
* be used to perform conditional behaviour to recover from crashes, such as clearing the
* app data cache.
*/
@Nullable
public static LastRunInfo getLastRunInfo() {
return getClient().getLastRunInfo();
}
/**
* Informs Bugsnag that the application has finished launching. Once this has been called
* {@link AppWithState#isLaunching()} will always be false in any new error reports,
* and synchronous delivery will not be attempted on the next launch for any fatal crashes.
*
* By default this method will be called after Bugsnag is initialized when
* {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually
* has precedence over the value supplied via the launchDurationMillis configuration option.
*/
public static void markLaunchCompleted() {
getClient().markLaunchCompleted();
}
/**
* Get the current Bugsnag Client instance.
*/
@NonNull
public static Client getClient() {
if (client == null) {
throw new IllegalStateException("You must call Bugsnag.start before any"
+ " other Bugsnag methods");
}
return client;
}
}

View File

@ -0,0 +1,10 @@
package com.bugsnag.android
internal interface CallbackAware {
fun addOnError(onError: OnErrorCallback)
fun removeOnError(onError: OnErrorCallback)
fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback)
fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback)
fun addOnSession(onSession: OnSessionCallback)
fun removeOnSession(onSession: OnSessionCallback)
}

View File

@ -0,0 +1,79 @@
package com.bugsnag.android
import java.util.concurrent.ConcurrentLinkedQueue
internal data class CallbackState(
val onErrorTasks: MutableCollection<OnErrorCallback> = ConcurrentLinkedQueue<OnErrorCallback>(),
val onBreadcrumbTasks: MutableCollection<OnBreadcrumbCallback> = ConcurrentLinkedQueue<OnBreadcrumbCallback>(),
val onSessionTasks: MutableCollection<OnSessionCallback> = ConcurrentLinkedQueue()
) : CallbackAware {
override fun addOnError(onError: OnErrorCallback) {
onErrorTasks.add(onError)
}
override fun removeOnError(onError: OnErrorCallback) {
onErrorTasks.remove(onError)
}
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
onBreadcrumbTasks.add(onBreadcrumb)
}
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
onBreadcrumbTasks.remove(onBreadcrumb)
}
override fun addOnSession(onSession: OnSessionCallback) {
onSessionTasks.add(onSession)
}
override fun removeOnSession(onSession: OnSessionCallback) {
onSessionTasks.remove(onSession)
}
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
onErrorTasks.forEach {
try {
if (!it.onError(event)) {
return false
}
} catch (ex: Throwable) {
logger.w("OnBreadcrumbCallback threw an Exception", ex)
}
}
return true
}
fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean {
onBreadcrumbTasks.forEach {
try {
if (!it.onBreadcrumb(breadcrumb)) {
return false
}
} catch (ex: Throwable) {
logger.w("OnBreadcrumbCallback threw an Exception", ex)
}
}
return true
}
fun runOnSessionTasks(session: Session, logger: Logger): Boolean {
onSessionTasks.forEach {
try {
if (!it.onSession(session)) {
return false
}
} catch (ex: Throwable) {
logger.w("OnSessionCallback threw an Exception", ex)
}
}
return true
}
fun copy() = this.copy(
onErrorTasks = onErrorTasks,
onBreadcrumbTasks = onBreadcrumbTasks,
onSessionTasks = onSessionTasks
)
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,26 @@
package com.bugsnag.android
internal class ClientObservable : BaseObservable() {
fun postOrientationChange(orientation: String?) {
notifyObservers(StateEvent.UpdateOrientation(orientation))
}
fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) {
notifyObservers(
StateEvent.Install(
conf.apiKey,
conf.enabledErrorTypes.ndkCrashes,
conf.appVersion,
conf.buildUuid,
conf.releaseStage,
lastRunInfoPath,
consecutiveLaunchCrashes
)
)
}
fun postNdkDeliverPending() {
notifyObservers(StateEvent.DeliverPending)
}
}

View File

@ -0,0 +1,19 @@
package com.bugsnag.android;
import androidx.annotation.Nullable;
import java.util.Collection;
class CollectionUtils {
static <T> boolean containsNullElements(@Nullable Collection<T> data) {
if (data == null) {
return true;
}
for (T datum : data) {
if (datum == null) {
return true;
}
}
return false;
}
}

View File

@ -0,0 +1,22 @@
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
}
}
}

View File

@ -0,0 +1,95 @@
package com.bugsnag.android
import android.content.Context
import java.io.File
internal class ConfigInternal(var apiKey: String) : CallbackAware, MetadataAware, UserAware {
private var user = User()
@JvmField
internal val callbackState: CallbackState = CallbackState()
@JvmField
internal val metadataState: MetadataState = MetadataState()
var appVersion: String? = null
var versionCode: Int? = 0
var releaseStage: String? = null
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
var persistUser: Boolean = false
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS
var autoTrackSessions: Boolean = true
var sendLaunchCrashesSynchronously: Boolean = true
var enabledErrorTypes: ErrorTypes = ErrorTypes()
var autoDetectErrors: Boolean = true
var appType: String? = "android"
var logger: Logger? = DebugLogger
set(value) {
field = value ?: NoopLogger
}
var delivery: Delivery? = null
var endpoints: EndpointConfiguration = EndpointConfiguration()
var maxBreadcrumbs: Int = DEFAULT_MAX_BREADCRUMBS
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var context: String? = null
var redactedKeys: Set<String> = 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 projectPackages: Set<String> = emptySet()
var persistenceDirectory: File? = null
protected val plugins = mutableSetOf<Plugin>()
override fun addOnError(onError: OnErrorCallback) = callbackState.addOnError(onError)
override fun removeOnError(onError: OnErrorCallback) = callbackState.removeOnError(onError)
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) =
callbackState.addOnBreadcrumb(onBreadcrumb)
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) =
callbackState.removeOnBreadcrumb(onBreadcrumb)
override fun addOnSession(onSession: OnSessionCallback) = callbackState.addOnSession(onSession)
override fun removeOnSession(onSession: OnSessionCallback) = callbackState.removeOnSession(onSession)
override fun addMetadata(section: String, value: Map<String, Any?>) =
metadataState.addMetadata(section, value)
override fun addMetadata(section: String, key: String, value: Any?) =
metadataState.addMetadata(section, key, value)
override fun clearMetadata(section: String) = metadataState.clearMetadata(section)
override fun clearMetadata(section: String, key: String) = metadataState.clearMetadata(section, key)
override fun getMetadata(section: String) = metadataState.getMetadata(section)
override fun getMetadata(section: String, key: String) = metadataState.getMetadata(section, key)
override fun getUser(): User = user
override fun setUser(id: String?, email: String?, name: String?) {
user = User(id, email, name)
}
fun addPlugin(plugin: Plugin) {
plugins.add(plugin)
}
companion object {
private const val DEFAULT_MAX_BREADCRUMBS = 25
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
@JvmStatic
fun load(context: Context): Configuration = load(context, null)
@JvmStatic
protected fun load(context: Context, apiKey: String?): Configuration {
return ManifestConfigLoader().load(context, apiKey)
}
}
}

View File

@ -0,0 +1,968 @@
package com.bugsnag.android;
import android.content.Context;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.util.Locale;
import java.util.Map;
import java.util.Set;
/**
* User-specified configuration storage object, contains information
* specified at the client level, api-key and endpoint configuration.
*/
@SuppressWarnings("ConstantConditions") // suppress warning about making redundant null checks
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 long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
final ConfigInternal impl;
/**
* Constructs a new Configuration object with default values.
*/
public Configuration(@NonNull String apiKey) {
validateApiKey(apiKey);
impl = new ConfigInternal(apiKey);
}
/**
* Loads a Configuration object from values supplied as meta-data elements in your
* AndroidManifest.
*/
@NonNull
public static Configuration load(@NonNull Context context) {
return ConfigInternal.load(context);
}
@NonNull
static Configuration load(@NonNull Context context, @NonNull String apiKey) {
return ConfigInternal.load(context, apiKey);
}
private void validateApiKey(String value) {
if (Intrinsics.isEmpty(value)) {
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));
}
}
private void logNull(String property) {
getLogger().e("Invalid null value supplied to config." + property + ", ignoring");
}
/**
* Retrieves the API key used for events sent to Bugsnag.
*/
@NonNull
public String getApiKey() {
return impl.getApiKey();
}
/**
* Changes the API key used for events sent to Bugsnag.
*/
public void setApiKey(@NonNull String apiKey) {
validateApiKey(apiKey);
impl.setApiKey(apiKey);
}
/**
* Set the application version sent to Bugsnag. We'll automatically pull your app version
* from the versionName field in your AndroidManifest.xml file.
*/
@Nullable
public String getAppVersion() {
return impl.getAppVersion();
}
/**
* Set the application version sent to Bugsnag. We'll automatically pull your app version
* from the versionName field in your AndroidManifest.xml file.
*/
public void setAppVersion(@Nullable String appVersion) {
impl.setAppVersion(appVersion);
}
/**
* We'll automatically pull your versionCode from the versionCode field
* in your AndroidManifest.xml file. If you'd like to override this you
* can set this property.
*/
@Nullable
public Integer getVersionCode() {
return impl.getVersionCode();
}
/**
* We'll automatically pull your versionCode from the versionCode field
* in your AndroidManifest.xml file. If you'd like to override this you
* can set this property.
*/
public void setVersionCode(@Nullable Integer versionCode) {
impl.setVersionCode(versionCode);
}
/**
* If you would like to distinguish between errors that happen in different stages of the
* application release process (development, production, etc) you can set the releaseStage
* that is reported to Bugsnag.
*
* If you are running a debug build, we'll automatically set this to "development",
* otherwise it is set to "production". You can control whether events are sent for
* specific release stages using the enabledReleaseStages option.
*/
@Nullable
public String getReleaseStage() {
return impl.getReleaseStage();
}
/**
* If you would like to distinguish between errors that happen in different stages of the
* application release process (development, production, etc) you can set the releaseStage
* that is reported to Bugsnag.
*
* If you are running a debug build, we'll automatically set this to "development",
* otherwise it is set to "production". You can control whether events are sent for
* specific release stages using the enabledReleaseStages option.
*/
public void setReleaseStage(@Nullable String releaseStage) {
impl.setReleaseStage(releaseStage);
}
/**
* Controls whether we should capture and serialize the state of all threads at the time
* of an error.
*
* By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to
* Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY
* to only do so for unhandled errors.
*/
@NonNull
public ThreadSendPolicy getSendThreads() {
return impl.getSendThreads();
}
/**
* Controls whether we should capture and serialize the state of all threads at the time
* of an error.
*
* By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to
* Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY
* to only do so for unhandled errors.
*/
public void setSendThreads(@NonNull ThreadSendPolicy sendThreads) {
if (sendThreads != null) {
impl.setSendThreads(sendThreads);
} else {
logNull("sendThreads");
}
}
/**
* Set whether or not Bugsnag should persist user information between application sessions.
*
* If enabled then any user information set will be re-used until the user information is
* removed manually by calling {@link Bugsnag#setUser(String, String, String)}
* with null arguments.
*/
public boolean getPersistUser() {
return impl.getPersistUser();
}
/**
* Set whether or not Bugsnag should persist user information between application sessions.
*
* If enabled then any user information set will be re-used until the user information is
* removed manually by calling {@link Bugsnag#setUser(String, String, String)}
* with null arguments.
*/
public void setPersistUser(boolean persistUser) {
impl.setPersistUser(persistUser);
}
/**
* Sets the directory where event and session JSON payloads should be persisted if a network
* request is not successful. If you use Bugsnag in multiple processes, then a unique
* persistenceDirectory <b>must</b> be configured for each process to prevent duplicate
* requests being made by each instantiation of Bugsnag.
* <p/>
* The persistenceDirectory also stores user information if {@link #getPersistUser()} has been
* set to true.
* <p/>
* By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}.
* <p/>
* If the persistenceDirectory is changed between application launches, no attempt will be made
* to deliver events or sessions cached in the previous location.
*/
@Nullable
public File getPersistenceDirectory() {
return impl.getPersistenceDirectory();
}
/**
* Sets the directory where event and session JSON payloads should be persisted if a network
* request is not successful. If you use Bugsnag in multiple processes, then a unique
* persistenceDirectory <b>must</b> be configured for each process to prevent duplicate
* requests being made by each instantiation of Bugsnag.
* <p/>
* The persistenceDirectory also stores user information if {@link #getPersistUser()} has been
* set to true.
* <p/>
* By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}.
* <p/>
* If the persistenceDirectory is changed between application launches, no attempt will be made
* to deliver events or sessions cached in the previous location.
*/
public void setPersistenceDirectory(@Nullable File directory) {
impl.setPersistenceDirectory(directory);
}
/**
* Deprecated. Use {@link #getLaunchDurationMillis()} instead.
*/
@Deprecated
public long getLaunchCrashThresholdMs() {
getLogger().w("The launchCrashThresholdMs configuration option is deprecated "
+ "and will be removed in a future release. Please use "
+ "launchDurationMillis instead.");
return getLaunchDurationMillis();
}
/**
* Deprecated. Use {@link #setLaunchDurationMillis(long)} instead.
*/
@Deprecated
public void setLaunchCrashThresholdMs(long launchCrashThresholdMs) {
getLogger().w("The launchCrashThresholdMs configuration option is deprecated "
+ "and will be removed in a future release. Please use "
+ "launchDurationMillis instead.");
setLaunchDurationMillis(launchCrashThresholdMs);
}
/**
* Sets whether or not Bugsnag should send crashes synchronously that occurred during
* the application's launch period. By default this behavior is enabled.
*
* See {@link #setLaunchDurationMillis(long)}
*/
public boolean getSendLaunchCrashesSynchronously() {
return impl.getSendLaunchCrashesSynchronously();
}
/**
* Sets whether or not Bugsnag should send crashes synchronously that occurred during
* the application's launch period. By default this behavior is enabled.
*
* See {@link #setLaunchDurationMillis(long)}
*/
public void setSendLaunchCrashesSynchronously(boolean sendLaunchCrashesSynchronously) {
impl.setSendLaunchCrashesSynchronously(sendLaunchCrashesSynchronously);
}
/**
* Sets the threshold in milliseconds for an uncaught error to be considered as a crash on
* launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent
* event synchronously.
*
* By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes
* as launch crashes until markLaunchCompleted() is called.
*/
public long getLaunchDurationMillis() {
return impl.getLaunchDurationMillis();
}
/**
* Sets the threshold in milliseconds for an uncaught error to be considered as a crash on
* launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent
* event synchronously.
*
* By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes
* as launch crashes until markLaunchCompleted() is called.
*/
public void setLaunchDurationMillis(long launchDurationMillis) {
if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) {
impl.setLaunchDurationMillis(launchDurationMillis);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
+ "Option launchDurationMillis should be a positive long value."
+ "Supplied value is %d", launchDurationMillis));
}
}
/**
* Sets whether or not Bugsnag should automatically capture and report User sessions whenever
* the app enters the foreground.
*
* By default this behavior is enabled.
*/
public boolean getAutoTrackSessions() {
return impl.getAutoTrackSessions();
}
/**
* Sets whether or not Bugsnag should automatically capture and report User sessions whenever
* the app enters the foreground.
*
* By default this behavior is enabled.
*/
public void setAutoTrackSessions(boolean autoTrackSessions) {
impl.setAutoTrackSessions(autoTrackSessions);
}
/**
* Bugsnag will automatically detect different types of error in your application.
* If you wish to control exactly which types are enabled, set this property.
*/
@NonNull
public ErrorTypes getEnabledErrorTypes() {
return impl.getEnabledErrorTypes();
}
/**
* Bugsnag will automatically detect different types of error in your application.
* If you wish to control exactly which types are enabled, set this property.
*/
public void setEnabledErrorTypes(@NonNull ErrorTypes enabledErrorTypes) {
if (enabledErrorTypes != null) {
impl.setEnabledErrorTypes(enabledErrorTypes);
} else {
logNull("enabledErrorTypes");
}
}
/**
* If you want to disable automatic detection of all errors, you can set this property to false.
* By default this property is true.
*
* Setting autoDetectErrors to false will disable all automatic errors, regardless of the
* error types enabled by enabledErrorTypes
*/
public boolean getAutoDetectErrors() {
return impl.getAutoDetectErrors();
}
/**
* If you want to disable automatic detection of all errors, you can set this property to false.
* By default this property is true.
*
* Setting autoDetectErrors to false will disable all automatic errors, regardless of the
* error types enabled by enabledErrorTypes
*/
public void setAutoDetectErrors(boolean autoDetectErrors) {
impl.setAutoDetectErrors(autoDetectErrors);
}
/**
* If your app's codebase contains different entry-points/processes, but reports to a single
* Bugsnag project, you might want to add information denoting the type of process the error
* came from.
*
* This information can be used in the dashboard to filter errors and to determine whether
* an error is limited to a subset of appTypes.
*
* By default, this value is set to 'android'.
*/
@Nullable
public String getAppType() {
return impl.getAppType();
}
/**
* If your app's codebase contains different entry-points/processes, but reports to a single
* Bugsnag project, you might want to add information denoting the type of process the error
* came from.
*
* This information can be used in the dashboard to filter errors and to determine whether
* an error is limited to a subset of appTypes.
*
* By default, this value is set to 'android'.
*/
public void setAppType(@Nullable String appType) {
impl.setAppType(appType);
}
/**
* By default, the notifier's log messages will be logged using android.util.Log
* with a "Bugsnag" tag unless the releaseStage is "production".
*
* To override this behavior, an alternative instance can be provided that implements the
* Logger interface.
*/
@Nullable
public Logger getLogger() {
return impl.getLogger();
}
/**
* By default, the notifier's log messages will be logged using android.util.Log
* with a "Bugsnag" tag unless the releaseStage is "production".
*
* To override this behavior, an alternative instance can be provided that implements the
* Logger interface.
*/
public void setLogger(@Nullable Logger logger) {
impl.setLogger(logger);
}
/**
* The Delivery implementation used to make network calls to the Bugsnag
* <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
* <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a>.
*
* This may be useful if you have requirements such as certificate pinning and rotation,
* which are not supported by the default implementation.
*
* To provide custom delivery functionality, create a class which implements the Delivery
* interface. Please note that request bodies must match the structure specified in the
* <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
* <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a> documentation.
*
* You can use the return type from the deliver functions to control the strategy for
* retrying the transmission at a later date.
*
* If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache
* the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED
* or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload
* and no further delivery will be attempted.
*/
@NonNull
public Delivery getDelivery() {
return impl.getDelivery();
}
/**
* The Delivery implementation used to make network calls to the Bugsnag
* <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
* <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a>.
*
* This may be useful if you have requirements such as certificate pinning and rotation,
* which are not supported by the default implementation.
*
* To provide custom delivery functionality, create a class which implements the Delivery
* interface. Please note that request bodies must match the structure specified in the
* <a href="https://docs.bugsnag.com/api/error-reporting/">Error Reporting</a> and
* <a href="https://docs.bugsnag.com/api/sessions/">Sessions API</a> documentation.
*
* You can use the return type from the deliver functions to control the strategy for
* retrying the transmission at a later date.
*
* If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache
* the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED
* or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload
* and no further delivery will be attempted.
*/
public void setDelivery(@NonNull Delivery delivery) {
if (delivery != null) {
impl.setDelivery(delivery);
} else {
logNull("delivery");
}
}
/**
* Set the endpoints to send data to. By default we'll send error reports to
* https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
* override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints.
*/
@NonNull
public EndpointConfiguration getEndpoints() {
return impl.getEndpoints();
}
/**
* Set the endpoints to send data to. By default we'll send error reports to
* https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
* override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints.
*/
public void setEndpoints(@NonNull EndpointConfiguration endpoints) {
if (endpoints != null) {
impl.setEndpoints(endpoints);
} else {
logNull("endpoints");
}
}
/**
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
* the oldest breadcrumbs will be deleted.
*
* By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100.
*/
public int getMaxBreadcrumbs() {
return impl.getMaxBreadcrumbs();
}
/**
* Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached,
* the oldest breadcrumbs will be deleted.
*
* By default, 25 breadcrumbs are stored: this can be amended up to a maximum of 100.
*/
public void setMaxBreadcrumbs(int maxBreadcrumbs) {
if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) {
impl.setMaxBreadcrumbs(maxBreadcrumbs);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
+ "Option maxBreadcrumbs should be an integer between 0-100. "
+ "Supplied value is %d", maxBreadcrumbs));
}
}
/**
* Sets the maximum number of persisted events which will be stored. Once the threshold is
* reached, the oldest event will be deleted.
*
* By default, 32 events are persisted.
*/
public int getMaxPersistedEvents() {
return impl.getMaxPersistedEvents();
}
/**
* Sets the maximum number of persisted events which will be stored. Once the threshold is
* reached, the oldest event will be deleted.
*
* By default, 32 events are persisted.
*/
public void setMaxPersistedEvents(int maxPersistedEvents) {
if (maxPersistedEvents >= 0) {
impl.setMaxPersistedEvents(maxPersistedEvents);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
+ "Option maxPersistedEvents should be a positive integer."
+ "Supplied value is %d", maxPersistedEvents));
}
}
/**
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
* reached, the oldest session will be deleted.
*
* By default, 128 sessions are persisted.
*/
public int getMaxPersistedSessions() {
return impl.getMaxPersistedSessions();
}
/**
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
* reached, the oldest session will be deleted.
*
* By default, 128 sessions are persisted.
*/
public void setMaxPersistedSessions(int maxPersistedSessions) {
if (maxPersistedSessions >= 0) {
impl.setMaxPersistedSessions(maxPersistedSessions);
} else {
getLogger().e(String.format(Locale.US, "Invalid configuration value detected. "
+ "Option maxPersistedSessions should be a positive integer."
+ "Supplied value is %d", maxPersistedSessions));
}
}
/**
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
* represent what was happening in your application at the time an error occurs.
*
* In an android app the "context" is automatically set as the foreground Activity.
* If you would like to set this value manually, you should alter this property.
*/
@Nullable
public String getContext() {
return impl.getContext();
}
/**
* Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts
* represent what was happening in your application at the time an error occurs.
*
* In an android app the "context" is automatically set as the foreground Activity.
* If you would like to set this value manually, you should alter this property.
*/
public void setContext(@Nullable String context) {
impl.setContext(context);
}
/**
* Sets which values should be removed from any Metadata objects before
* sending them to Bugsnag. Use this if you want to ensure you don't send
* sensitive data such as passwords, and credit card numbers to our
* servers. Any keys which contain these strings will be filtered.
*
* By default, redactedKeys is set to "password"
*/
@NonNull
public Set<String> getRedactedKeys() {
return impl.getRedactedKeys();
}
/**
* Sets which values should be removed from any Metadata objects before
* sending them to Bugsnag. Use this if you want to ensure you don't send
* sensitive data such as passwords, and credit card numbers to our
* servers. Any keys which contain these strings will be filtered.
*
* By default, redactedKeys is set to "password"
*/
public void setRedactedKeys(@NonNull Set<String> redactedKeys) {
if (CollectionUtils.containsNullElements(redactedKeys)) {
logNull("redactedKeys");
} else {
impl.setRedactedKeys(redactedKeys);
}
}
/**
* Allows you to specify the fully-qualified name of error classes that will be discarded
* before being sent to Bugsnag if they are detected. The notifier performs an exact
* match against the canonical class name.
*/
@NonNull
public Set<String> getDiscardClasses() {
return impl.getDiscardClasses();
}
/**
* Allows you to specify the fully-qualified name of error classes that will be discarded
* before being sent to Bugsnag if they are detected. The notifier performs an exact
* match against the canonical class name.
*/
public void setDiscardClasses(@NonNull Set<String> discardClasses) {
if (CollectionUtils.containsNullElements(discardClasses)) {
logNull("discardClasses");
} else {
impl.setDiscardClasses(discardClasses);
}
}
/**
* By default, Bugsnag will be notified of events that happen in any releaseStage.
* If you would like to change which release stages notify Bugsnag you can set this property.
*/
@Nullable
public Set<String> getEnabledReleaseStages() {
return impl.getEnabledReleaseStages();
}
/**
* By default, Bugsnag will be notified of events that happen in any releaseStage.
* If you would like to change which release stages notify Bugsnag you can set this property.
*/
public void setEnabledReleaseStages(@Nullable Set<String> enabledReleaseStages) {
impl.setEnabledReleaseStages(enabledReleaseStages);
}
/**
* By default we will automatically add breadcrumbs for common application events such as
* activity lifecycle events and system intents. To amend this behavior,
* override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an
* empty set.
*
* The following breadcrumb types can be enabled:
*
* - Captured errors: left when an error event is sent to the Bugsnag API.
* - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function.
* - Navigation changes: left for Activity Lifecycle events to track the user's journey in
* the app.
* - State changes: state breadcrumbs are left for system broadcast events. For example:
* battery warnings, airplane mode, etc.
* - User interaction: left when the user performs certain system operations.
*/
@Nullable
public Set<BreadcrumbType> getEnabledBreadcrumbTypes() {
return impl.getEnabledBreadcrumbTypes();
}
/**
* By default we will automatically add breadcrumbs for common application events such as
* activity lifecycle events and system intents. To amend this behavior,
* override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an
* empty set.
*
* The following breadcrumb types can be enabled:
*
* - Captured errors: left when an error event is sent to the Bugsnag API.
* - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function.
* - Navigation changes: left for Activity Lifecycle events to track the user's journey in
* the app.
* - State changes: state breadcrumbs are left for system broadcast events. For example:
* battery warnings, airplane mode, etc.
* - User interaction: left when the user performs certain system operations.
*/
public void setEnabledBreadcrumbTypes(@Nullable Set<BreadcrumbType> enabledBreadcrumbTypes) {
impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes);
}
/**
* Sets which package names Bugsnag should consider as a part of the
* running application. We mark stacktrace lines as in-project if they
* originate from any of these packages and this allows us to improve
* the visual display of the stacktrace on the dashboard.
*
* By default, projectPackages is set to be the package you called Bugsnag.start from.
*/
@NonNull
public Set<String> getProjectPackages() {
return impl.getProjectPackages();
}
/**
* Sets which package names Bugsnag should consider as a part of the
* running application. We mark stacktrace lines as in-project if they
* originate from any of these packages and this allows us to improve
* the visual display of the stacktrace on the dashboard.
*
* By default, projectPackages is set to be the package you called Bugsnag.start from.
*/
public void setProjectPackages(@NonNull Set<String> projectPackages) {
if (CollectionUtils.containsNullElements(projectPackages)) {
logNull("projectPackages");
} else {
impl.setProjectPackages(projectPackages);
}
}
/**
* Add a "on error" callback, to execute code at the point where an error report is
* captured in Bugsnag.
*
* You can use this to add or modify information attached to an Event
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to prevent delivery. "on error"
* callbacks do not run before reports generated in the event
* of immediate app termination from crashes in C/C++ code.
*
* For example:
*
* Bugsnag.addOnError(new OnErrorCallback() {
* public boolean run(Event event) {
* event.setSeverity(Severity.INFO);
* return true;
* }
* })
*
* @param onError a callback to run before sending errors to Bugsnag
* @see OnErrorCallback
*/
@Override
public void addOnError(@NonNull OnErrorCallback onError) {
if (onError != null) {
impl.addOnError(onError);
} else {
logNull("addOnError");
}
}
/**
* Removes a previously added "on error" callback
* @param onError the callback to remove
*/
@Override
public void removeOnError(@NonNull OnErrorCallback onError) {
if (onError != null) {
impl.removeOnError(onError);
} else {
logNull("removeOnError");
}
}
/**
* Add an "on breadcrumb" callback, to execute code before every
* breadcrumb captured by Bugsnag.
*
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
*
* For example:
*
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
* public boolean run(Breadcrumb breadcrumb) {
* return false; // ignore the breadcrumb
* }
* })
*
* @param onBreadcrumb a callback to run before a breadcrumb is captured
* @see OnBreadcrumbCallback
*/
@Override
public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
if (onBreadcrumb != null) {
impl.addOnBreadcrumb(onBreadcrumb);
} else {
logNull("addOnBreadcrumb");
}
}
/**
* Removes a previously added "on breadcrumb" callback
* @param onBreadcrumb the callback to remove
*/
@Override
public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
if (onBreadcrumb != null) {
impl.removeOnBreadcrumb(onBreadcrumb);
} else {
logNull("removeOnBreadcrumb");
}
}
/**
* Add an "on session" callback, to execute code before every
* session captured by Bugsnag.
*
* You can use this to modify sessions before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a session.
*
* For example:
*
* Bugsnag.onSession(new OnSessionCallback() {
* public boolean run(Session session) {
* return false; // ignore the session
* }
* })
*
* @param onSession a callback to run before a session is captured
* @see OnSessionCallback
*/
@Override
public void addOnSession(@NonNull OnSessionCallback onSession) {
if (onSession != null) {
impl.addOnSession(onSession);
} else {
logNull("addOnSession");
}
}
/**
* Removes a previously added "on session" callback
* @param onSession the callback to remove
*/
@Override
public void removeOnSession(@NonNull OnSessionCallback onSession) {
if (onSession != null) {
impl.removeOnSession(onSession);
} else {
logNull("removeOnSession");
}
}
/**
* Adds a map of multiple metadata key-value pairs to the specified section.
*/
@Override
public void addMetadata(@NonNull String section, @NonNull Map<String, ?> value) {
if (section != null && value != null) {
impl.addMetadata(section, value);
} else {
logNull("addMetadata");
}
}
/**
* Adds the specified key and value in the specified section. The value can be of
* any primitive type or a collection such as a map, set or array.
*/
@Override
public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) {
if (section != null && key != null) {
impl.addMetadata(section, key, value);
} else {
logNull("addMetadata");
}
}
/**
* Removes all the data from the specified section.
*/
@Override
public void clearMetadata(@NonNull String section) {
if (section != null) {
impl.clearMetadata(section);
} else {
logNull("clearMetadata");
}
}
/**
* Removes data with the specified key from the specified section.
*/
@Override
public void clearMetadata(@NonNull String section, @NonNull String key) {
if (section != null && key != null) {
impl.clearMetadata(section, key);
} else {
logNull("clearMetadata");
}
}
/**
* Returns a map of data in the specified section.
*/
@Nullable
@Override
public Map<String, Object> getMetadata(@NonNull String section) {
if (section != null) {
return impl.getMetadata(section);
} else {
logNull("getMetadata");
return null;
}
}
/**
* Returns the value of the specified key in the specified section.
*/
@Nullable
@Override
public Object getMetadata(@NonNull String section, @NonNull String key) {
if (section != null && key != null) {
return impl.getMetadata(section, key);
} else {
logNull("getMetadata");
return null;
}
}
/**
* Returns the currently set User information.
*/
@NonNull
@Override
public User getUser() {
return impl.getUser();
}
/**
* Sets the user associated with the event.
*/
@Override
public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
impl.setUser(id, email, name);
}
/**
* Adds a plugin which will be loaded when the bugsnag notifier is instantiated.
*/
public void addPlugin(@NonNull Plugin plugin) {
if (plugin != null) {
impl.addPlugin(plugin);
} else {
logNull("addPlugin");
}
}
Set<Plugin> getPlugins() {
return impl.getPlugins();
}
}

View File

@ -0,0 +1,127 @@
package com.bugsnag.android
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.net.ConnectivityManager
import android.net.Network
import android.net.NetworkCapabilities
import android.os.Build
import androidx.annotation.RequiresApi
internal typealias NetworkChangeCallback = (hasConnection: Boolean, networkState: String) -> Unit
internal interface Connectivity {
fun registerForNetworkChanges()
fun unregisterForNetworkChanges()
fun hasNetworkConnection(): Boolean
fun retrieveNetworkAccessState(): String
}
internal class ConnectivityCompat(
context: Context,
callback: NetworkChangeCallback?
) : Connectivity {
private val cm = context.getSystemService(Context.CONNECTIVITY_SERVICE) as ConnectivityManager
private val connectivity: Connectivity =
when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.N -> ConnectivityApi24(cm, callback)
else -> ConnectivityLegacy(context, cm, callback)
}
override fun registerForNetworkChanges() {
runCatching { connectivity.registerForNetworkChanges() }
}
override fun hasNetworkConnection(): Boolean {
val result = runCatching { connectivity.hasNetworkConnection() }
return result.getOrElse { true } // allow network requests to be made if state unknown
}
override fun unregisterForNetworkChanges() {
runCatching { connectivity.unregisterForNetworkChanges() }
}
override fun retrieveNetworkAccessState(): String {
val result = runCatching { connectivity.retrieveNetworkAccessState() }
return result.getOrElse { "unknown" }
}
}
@Suppress("DEPRECATION")
internal class ConnectivityLegacy(
private val context: Context,
private val cm: ConnectivityManager,
callback: NetworkChangeCallback?
) : Connectivity {
private val changeReceiver = ConnectivityChangeReceiver(callback)
override fun registerForNetworkChanges() {
val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
context.registerReceiverSafe(changeReceiver, intentFilter)
}
override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver)
override fun hasNetworkConnection(): Boolean {
return cm.activeNetworkInfo?.isConnectedOrConnecting ?: false
}
override fun retrieveNetworkAccessState(): String {
return when (cm.activeNetworkInfo?.type) {
null -> "none"
ConnectivityManager.TYPE_WIFI -> "wifi"
ConnectivityManager.TYPE_ETHERNET -> "ethernet"
else -> "cellular" // all other types are cellular in some form
}
}
private inner class ConnectivityChangeReceiver(private val cb: NetworkChangeCallback?) :
BroadcastReceiver() {
override fun onReceive(context: Context, intent: Intent) {
cb?.invoke(hasNetworkConnection(), retrieveNetworkAccessState())
}
}
}
@RequiresApi(Build.VERSION_CODES.N)
internal class ConnectivityApi24(
private val cm: ConnectivityManager,
callback: NetworkChangeCallback?
) : Connectivity {
private val networkCallback = ConnectivityTrackerCallback(callback)
override fun registerForNetworkChanges() = cm.registerDefaultNetworkCallback(networkCallback)
override fun unregisterForNetworkChanges() = cm.unregisterNetworkCallback(networkCallback)
override fun hasNetworkConnection() = cm.activeNetwork != null
override fun retrieveNetworkAccessState(): String {
val network = cm.activeNetwork
val capabilities = if (network != null) cm.getNetworkCapabilities(network) else null
return when {
capabilities == null -> "none"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_WIFI) -> "wifi"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_ETHERNET) -> "ethernet"
capabilities.hasTransport(NetworkCapabilities.TRANSPORT_CELLULAR) -> "cellular"
else -> "unknown"
}
}
private inner class ConnectivityTrackerCallback(private val cb: NetworkChangeCallback?) :
ConnectivityManager.NetworkCallback() {
override fun onUnavailable() {
super.onUnavailable()
cb?.invoke(false, retrieveNetworkAccessState())
}
override fun onAvailable(network: Network) {
super.onAvailable(network)
cb?.invoke(true, retrieveNetworkAccessState())
}
}
}

View File

@ -0,0 +1,47 @@
package com.bugsnag.android
import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.os.RemoteException
/**
* Calls [Context.registerReceiver] but swallows [SecurityException] and [RemoteException]
* to avoid terminating the process in rare cases where the registration is unsuccessful.
*/
internal fun Context.registerReceiverSafe(
receiver: BroadcastReceiver?,
filter: IntentFilter?,
logger: Logger? = null
): Intent? {
try {
return registerReceiver(receiver, filter)
} catch (exc: SecurityException) {
logger?.w("Failed to register receiver", exc)
} catch (exc: RemoteException) {
logger?.w("Failed to register receiver", exc)
} catch (exc: IllegalArgumentException) {
logger?.w("Failed to register receiver", exc)
}
return null
}
/**
* Calls [Context.unregisterReceiver] but swallows [SecurityException] and [RemoteException]
* to avoid terminating the process in rare cases where the registration is unsuccessful.
*/
internal fun Context.unregisterReceiverSafe(
receiver: BroadcastReceiver?,
logger: Logger? = null
) {
try {
unregisterReceiver(receiver)
} catch (exc: SecurityException) {
logger?.w("Failed to register receiver", exc)
} catch (exc: RemoteException) {
logger?.w("Failed to register receiver", exc)
} catch (exc: IllegalArgumentException) {
logger?.w("Failed to register receiver", exc)
}
}

View File

@ -0,0 +1,13 @@
package com.bugsnag.android
internal class ContextState(context: String? = null) : BaseObservable() {
var context = context
set(value) {
field = value
emitObservableEvent()
}
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateContext(context))
fun copy() = ContextState(context)
}

View File

@ -0,0 +1,42 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import java.text.DateFormat;
import java.text.ParseException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Locale;
import java.util.TimeZone;
class DateUtils {
// SimpleDateFormat isn't thread safe, cache one instance per thread as needed.
private static final ThreadLocal<DateFormat> iso8601Holder = new ThreadLocal<DateFormat>() {
@NonNull
@Override
protected DateFormat initialValue() {
TimeZone tz = TimeZone.getTimeZone("UTC");
DateFormat iso8601 = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US);
iso8601.setTimeZone(tz);
return iso8601;
}
};
@NonNull
static String toIso8601(@NonNull Date date) {
DateFormat dateFormat = iso8601Holder.get();
if (dateFormat == null) {
throw new IllegalStateException("Unable to find valid dateformatter");
}
return dateFormat.format(date);
}
@NonNull
static Date fromIso8601(@NonNull String date) {
try {
return iso8601Holder.get().parse(date);
} catch (ParseException exc) {
throw new IllegalArgumentException("Failed to parse timestamp", exc);
}
}
}

View File

@ -0,0 +1,40 @@
package com.bugsnag.android
import android.util.Log
internal object DebugLogger : Logger {
private const val TAG = "Bugsnag"
override fun e(msg: String) {
Log.e(TAG, msg)
}
override fun e(msg: String, throwable: Throwable) {
Log.e(TAG, msg, throwable)
}
override fun w(msg: String) {
Log.w(TAG, msg)
}
override fun w(msg: String, throwable: Throwable) {
Log.w(TAG, msg, throwable)
}
override fun i(msg: String) {
Log.i(TAG, msg)
}
override fun i(msg: String, throwable: Throwable) {
Log.i(TAG, msg, throwable)
}
override fun d(msg: String) {
Log.d(TAG, msg)
}
override fun d(msg: String, throwable: Throwable) {
Log.d(TAG, msg, throwable)
}
}

View File

@ -0,0 +1,134 @@
package com.bugsnag.android
import java.io.ByteArrayOutputStream
import java.io.IOException
import java.io.PrintWriter
import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL
/**
* Converts a [JsonStream.Streamable] into JSON, placing it in a [ByteArray]
*/
internal fun serializeJsonPayload(streamable: JsonStream.Streamable): ByteArray {
return ByteArrayOutputStream().use { baos ->
JsonStream(PrintWriter(baos).buffered()).use(streamable::toStream)
baos.toByteArray()
}
}
internal class DefaultDelivery(
private val connectivity: Connectivity?,
val logger: Logger
) : Delivery {
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
val status = deliver(deliveryParams.endpoint, payload, deliveryParams.headers)
logger.i("Session API request finished with status $status")
return status
}
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
val status = deliver(deliveryParams.endpoint, payload, deliveryParams.headers)
logger.i("Error API request finished with status $status")
return status
}
fun deliver(
urlString: String,
streamable: JsonStream.Streamable,
headers: Map<String, String?>
): DeliveryStatus {
if (connectivity != null && !connectivity.hasNetworkConnection()) {
return DeliveryStatus.UNDELIVERED
}
var conn: HttpURLConnection? = null
try {
val json = serializeJsonPayload(streamable)
conn = makeRequest(URL(urlString), json, headers)
// End the request, get the response code
val responseCode = conn.responseCode
val status = getDeliveryStatus(responseCode)
logRequestInfo(responseCode, conn, status)
return status
} catch (oom: OutOfMemoryError) {
// attempt to persist the payload on disk. This approach uses streams to write to a
// file, which takes less memory than serializing the payload into a ByteArray, and
// therefore has a reasonable chance of retaining the payload for future delivery.
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
return DeliveryStatus.UNDELIVERED
} catch (exception: IOException) {
logger.w("IOException encountered in request", exception)
return DeliveryStatus.UNDELIVERED
} catch (exception: Exception) {
logger.w("Unexpected error delivering payload", exception)
return DeliveryStatus.FAILURE
} finally {
conn?.disconnect()
}
}
private fun makeRequest(
url: URL,
json: ByteArray,
headers: Map<String, String?>
): HttpURLConnection {
val conn = url.openConnection() as HttpURLConnection
conn.doOutput = true
// avoids creating a buffer within HttpUrlConnection, see
// https://developer.android.com/reference/java/net/HttpURLConnection
conn.setFixedLengthStreamingMode(json.size)
// calculate the SHA-1 digest and add all other headers
computeSha1Digest(json)?.let { digest ->
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
}
headers.forEach { (key, value) ->
if (value != null) {
conn.addRequestProperty(key, value)
}
}
// write the JSON payload
conn.outputStream.use {
it.write(json)
}
return conn
}
private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
logger.i(
"Request completed with code $code, " +
"message: ${conn.responseMessage}, " +
"headers: ${conn.headerFields}"
)
conn.inputStream.bufferedReader().use {
logger.d("Received request response: ${it.readText()}")
}
if (status != DeliveryStatus.DELIVERED) {
conn.errorStream.bufferedReader().use {
logger.w("Request error details: ${it.readText()}")
}
}
}
internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus {
val unrecoverableCodes = IntRange(HTTP_BAD_REQUEST, 499).filter {
it != HTTP_CLIENT_TIMEOUT && it != 429
}
return when (responseCode) {
in HTTP_OK..299 -> DeliveryStatus.DELIVERED
in unrecoverableCodes -> DeliveryStatus.FAILURE
else -> DeliveryStatus.UNDELIVERED
}
}
}

View File

@ -0,0 +1,65 @@
package com.bugsnag.android
/**
* Implementations of this interface deliver Error Reports and Sessions captured to the Bugsnag API.
*
* A default [Delivery] implementation is provided as part of Bugsnag initialization,
* but you may wish to use your own implementation if you have requirements such
* as pinning SSL certificates, for example.
*
* Any custom implementation must be capable of sending
* [Error Reports](https://docs.bugsnag.com/api/error-reporting/)
* and [Sessions](https://docs.bugsnag.com/api/sessions/) as
* documented at [https://docs.bugsnag.com/api/](https://docs.bugsnag.com/api/)
*
* @see DefaultDelivery
*/
interface Delivery {
/**
* Posts an array of sessions to the Bugsnag Session Tracking API.
*
* This request must be delivered to the endpoint specified in [deliveryParams] with the given
* HTTP headers.
*
* You should return the [DeliveryStatus] which best matches the end-result of your delivery
* attempt. Bugsnag will use the return value to decide whether to delete the payload if it was
* cached on disk, or whether to reattempt delivery later on.
*
* For example, a 2xx status code will indicate success so you should return
* [DeliveryStatus.DELIVERED]. Most 4xx status codes would indicate an unrecoverable error, so
* the report should be dropped using [DeliveryStatus.FAILURE]. For all other scenarios,
* delivery should be attempted again later by using [DeliveryStatus.UNDELIVERED].
*
* See [https://docs.bugsnag.com/api/sessions/](https://docs.bugsnag.com/api/sessions/)
*
* @param payload The session tracking payload
* @param deliveryParams The delivery parameters to be used for this request
* @return the end-result of your delivery attempt
*/
fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus
/**
* Posts an Error Report to the Bugsnag Error Reporting API.
*
* This request must be delivered to the endpoint specified in [deliveryParams] with the given
* HTTP headers.
*
* You should return the [DeliveryStatus] which best matches the end-result of your delivery
* attempt. Bugsnag will use the return value to decide whether to delete the payload if it was
* cached on disk, or whether to reattempt delivery later on.
*
* For example, a 2xx status code will indicate success so you should return
* [DeliveryStatus.DELIVERED]. Most 4xx status codes would indicate an unrecoverable error, so
* the report should be dropped using [DeliveryStatus.FAILURE]. For all other scenarios,
* delivery should be attempted again later by using [DeliveryStatus.UNDELIVERED].
*
* See [https://docs.bugsnag.com/api/error-reporting/]
* (https://docs.bugsnag.com/api/error-reporting/)
*
* @param payload The error payload
* @param deliveryParams The delivery parameters to be used for this request
* @return the end-result of your delivery attempt
*/
fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus
}

View File

@ -0,0 +1,134 @@
package com.bugsnag.android;
import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
import androidx.annotation.NonNull;
import androidx.annotation.VisibleForTesting;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
class DeliveryDelegate extends BaseObservable {
final Logger logger;
private final EventStore eventStore;
private final ImmutableConfig immutableConfig;
final BreadcrumbState breadcrumbState;
private final Notifier notifier;
final BackgroundTaskService backgroundTaskService;
DeliveryDelegate(Logger logger,
EventStore eventStore,
ImmutableConfig immutableConfig,
BreadcrumbState breadcrumbState,
Notifier notifier,
BackgroundTaskService backgroundTaskService) {
this.logger = logger;
this.eventStore = eventStore;
this.immutableConfig = immutableConfig;
this.breadcrumbState = breadcrumbState;
this.notifier = notifier;
this.backgroundTaskService = backgroundTaskService;
}
void deliver(@NonNull Event event) {
logger.d("DeliveryDelegate#deliver() - event being stored/delivered by Client");
// Build the eventPayload
String apiKey = event.getApiKey();
EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig);
Session session = event.getSession();
if (session != null) {
if (event.isUnhandled()) {
event.setSession(session.incrementUnhandledAndCopy());
notifyObservers(StateEvent.NotifyUnhandled.INSTANCE);
} else {
event.setSession(session.incrementHandledAndCopy());
notifyObservers(StateEvent.NotifyHandled.INSTANCE);
}
}
if (event.getImpl().getOriginalUnhandled()) {
// should only send unhandled errors if they don't terminate the process (i.e. ANRs)
String severityReasonType = event.getImpl().getSeverityReasonType();
boolean promiseRejection = REASON_PROMISE_REJECTION.equals(severityReasonType);
boolean anr = event.getImpl().isAnr(event);
cacheEvent(event, anr || promiseRejection);
} else {
deliverPayloadAsync(event, eventPayload);
}
}
private void deliverPayloadAsync(@NonNull Event event, EventPayload eventPayload) {
final EventPayload finalEventPayload = eventPayload;
final Event finalEvent = event;
// Attempt to send the eventPayload in the background
try {
backgroundTaskService.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
deliverPayloadInternal(finalEventPayload, finalEvent);
}
});
} catch (RejectedExecutionException exception) {
cacheEvent(event, false);
logger.w("Exceeded max queue count, saving to disk to send later");
}
}
@VisibleForTesting
DeliveryStatus deliverPayloadInternal(@NonNull EventPayload payload, @NonNull Event event) {
logger.d("DeliveryDelegate#deliverPayloadInternal() - attempting event delivery");
DeliveryParams deliveryParams = immutableConfig.getErrorApiDeliveryParams(payload);
Delivery delivery = immutableConfig.getDelivery();
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);
switch (deliveryStatus) {
case DELIVERED:
logger.i("Sent 1 new event to Bugsnag");
leaveErrorBreadcrumb(event);
break;
case UNDELIVERED:
logger.w("Could not send event(s) to Bugsnag,"
+ " saving to disk to send later");
cacheEvent(event, false);
leaveErrorBreadcrumb(event);
break;
case FAILURE:
logger.w("Problem sending event to Bugsnag");
break;
default:
break;
}
return deliveryStatus;
}
private void cacheEvent(@NonNull Event event, boolean attemptSend) {
eventStore.write(event);
if (attemptSend) {
eventStore.flushAsync();
}
}
private void leaveErrorBreadcrumb(@NonNull Event event) {
// Add a breadcrumb for this event occurring
List<Error> errors = event.getErrors();
if (errors.size() > 0) {
String errorClass = errors.get(0).getErrorClass();
String message = errors.get(0).getErrorMessage();
Map<String, Object> data = new HashMap<>();
data.put("errorClass", errorClass);
data.put("message", message);
data.put("unhandled", String.valueOf(event.isUnhandled()));
data.put("severity", event.getSeverity().toString());
breadcrumbState.add(new Breadcrumb(errorClass,
BreadcrumbType.ERROR, data, new Date(), logger));
}
}
}

View File

@ -0,0 +1,82 @@
package com.bugsnag.android
import java.io.OutputStream
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.util.Date
private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version"
private const val HEADER_BUGSNAG_SENT_AT = "Bugsnag-Sent-At"
private const val HEADER_BUGSNAG_STACKTRACE_TYPES = "Bugsnag-Stacktrace-Types"
private const val HEADER_CONTENT_TYPE = "Content-Type"
internal const val HEADER_BUGSNAG_INTEGRITY = "Bugsnag-Integrity"
internal const val HEADER_API_KEY = "Bugsnag-Api-Key"
internal const val HEADER_INTERNAL_ERROR = "Bugsnag-Internal-Error"
/**
* Supplies the headers which must be used in any request sent to the Error Reporting API.
*
* @return the HTTP headers
*/
internal fun errorApiHeaders(payload: EventPayload): Map<String, String?> {
val mutableHeaders = mutableMapOf(
HEADER_API_PAYLOAD_VERSION to "4.0",
HEADER_API_KEY to (payload.apiKey ?: ""),
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date()),
HEADER_CONTENT_TYPE to "application/json"
)
val errorTypes = payload.getErrorTypes()
if (errorTypes.isNotEmpty()) {
mutableHeaders[HEADER_BUGSNAG_STACKTRACE_TYPES] = serializeErrorTypeHeader(errorTypes)
}
return mutableHeaders.toMap()
}
/**
* Serializes the error types to a comma delimited string
*/
internal fun serializeErrorTypeHeader(errorTypes: Set<ErrorType>): String {
return when {
errorTypes.isEmpty() -> ""
else ->
errorTypes
.map(ErrorType::desc)
.reduce { accumulator, str ->
"$accumulator,$str"
}
}
}
/**
* Supplies the headers which must be used in any request sent to the Session Tracking API.
*
* @return the HTTP headers
*/
internal fun sessionApiHeaders(apiKey: String): Map<String, String?> = mapOf(
HEADER_API_PAYLOAD_VERSION to "1.0",
HEADER_API_KEY to apiKey,
HEADER_CONTENT_TYPE to "application/json",
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
)
internal fun computeSha1Digest(payload: ByteArray): String? {
runCatching {
val shaDigest = MessageDigest.getInstance("SHA-1")
val builder = StringBuilder("sha1 ")
// Pipe the object through a no-op output stream
DigestOutputStream(NullOutputStream(), shaDigest).use { stream ->
stream.buffered().use { writer ->
writer.write(payload)
}
shaDigest.digest().forEach { byte ->
builder.append(String.format("%02x", byte))
}
}
return builder.toString()
}.getOrElse { return null }
}
internal class NullOutputStream : OutputStream() {
override fun write(b: Int) = Unit
}

View File

@ -0,0 +1,17 @@
package com.bugsnag.android
/**
* The parameters which should be used to deliver an Event/Session.
*/
class DeliveryParams(
/**
* The endpoint to which the payload should be sent
*/
val endpoint: String,
/**
* The HTTP headers which must be attached to the request
*/
val headers: Map<String, String?>
)

View File

@ -0,0 +1,23 @@
package com.bugsnag.android
/**
* Return value for the status of a payload delivery.
*/
enum class DeliveryStatus {
/**
* The payload was delivered successfully and can be deleted.
*/
DELIVERED,
/**
* The payload was not delivered but can be retried, e.g. when there was a loss of connectivity
*/
UNDELIVERED,
/**
*
* The payload was not delivered and should be deleted without attempting retry.
*/
FAILURE
}

View File

@ -0,0 +1,80 @@
package com.bugsnag.android
/**
* Stateless information set by the notifier about the device on which the event occurred can be
* found on this class. These values can be accessed and amended if necessary.
*/
open class Device internal constructor(
buildInfo: DeviceBuildInfo,
/**
* The Application Binary Interface used
*/
var cpuAbi: Array<String>?,
/**
* Whether the device has been jailbroken
*/
var jailbroken: Boolean?,
/**
* A UUID generated by Bugsnag and used for the individual application on a device
*/
var id: String?,
/**
* The IETF language tag of the locale used
*/
var locale: String?,
/**
* The total number of bytes of memory on the device
*/
var totalMemory: Long?,
/**
* A collection of names and their versions of the primary languages, frameworks or
* runtimes that the application is running on
*/
var runtimeVersions: MutableMap<String, Any>?
) : JsonStream.Streamable {
/**
* The manufacturer of the device used
*/
var manufacturer: String? = buildInfo.manufacturer
/**
* The model name of the device used
*/
var model: String? = buildInfo.model
/**
* The name of the operating system running on the device used
*/
var osName: String? = "android"
/**
* The version of the operating system running on the device used
*/
var osVersion: String? = buildInfo.osVersion
internal open fun serializeFields(writer: JsonStream) {
writer.name("cpuAbi").value(cpuAbi)
writer.name("jailbroken").value(jailbroken)
writer.name("id").value(id)
writer.name("locale").value(locale)
writer.name("manufacturer").value(manufacturer)
writer.name("model").value(model)
writer.name("osName").value(osName)
writer.name("osVersion").value(osVersion)
writer.name("runtimeVersions").value(runtimeVersions)
writer.name("totalMemory").value(totalMemory)
}
override fun toStream(writer: JsonStream) {
writer.beginObject()
serializeFields(writer)
writer.endObject()
}
}

View File

@ -0,0 +1,36 @@
package com.bugsnag.android
import android.os.Build
internal class DeviceBuildInfo(
val manufacturer: String?,
val model: String?,
val osVersion: String?,
val apiLevel: Int?,
val osBuild: String?,
val fingerprint: String?,
val tags: String?,
val brand: String?,
val cpuAbis: Array<String>?
) {
companion object {
fun defaultInfo(): DeviceBuildInfo {
@Suppress("DEPRECATION") val cpuABis = when {
Build.VERSION.SDK_INT >= Build.VERSION_CODES.LOLLIPOP -> Build.SUPPORTED_ABIS
else -> arrayOf(Build.CPU_ABI, Build.CPU_ABI2)
}
return DeviceBuildInfo(
Build.MANUFACTURER,
Build.MODEL,
Build.VERSION.RELEASE,
Build.VERSION.SDK_INT,
Build.DISPLAY,
Build.FINGERPRINT,
Build.TAGS,
Build.BRAND,
cpuABis
)
}
}
}

View File

@ -0,0 +1,260 @@
package com.bugsnag.android
import android.annotation.SuppressLint
import android.content.Context
import android.content.Intent
import android.content.IntentFilter
import android.content.res.Configuration.ORIENTATION_LANDSCAPE
import android.content.res.Configuration.ORIENTATION_PORTRAIT
import android.content.res.Resources
import android.os.BatteryManager
import android.provider.Settings
import java.io.File
import java.util.Date
import java.util.HashMap
import java.util.Locale
import java.util.concurrent.Callable
import java.util.concurrent.Future
import java.util.concurrent.RejectedExecutionException
import kotlin.math.max
import kotlin.math.min
internal class DeviceDataCollector(
private val connectivity: Connectivity,
private val appContext: Context,
private val resources: Resources?,
private val deviceId: String?,
private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File,
rootDetector: RootDetector,
bgTaskService: BackgroundTaskService,
private val logger: Logger
) {
private val displayMetrics = resources?.displayMetrics
private val emulator = isEmulator()
private val screenDensity = getScreenDensity()
private val dpi = getScreenDensityDpi()
private val screenResolution = getScreenResolution()
private val locale = Locale.getDefault().toString()
private val cpuAbi = getCpuAbi()
private val runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>?
init {
val map = mutableMapOf<String, Any>()
buildInfo.apiLevel?.let { map["androidApiLevel"] = it }
buildInfo.osBuild?.let { map["osBuild"] = it }
runtimeVersions = map
rootedFuture = try {
bgTaskService.submitTask(
TaskType.IO,
Callable {
rootDetector.isRooted()
}
)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to perform root detection checks", exc)
null
}
}
fun generateDevice() = Device(
buildInfo,
cpuAbi,
checkIsRooted(),
deviceId,
locale,
calculateTotalMemory(),
runtimeVersions.toMutableMap()
)
fun generateDeviceWithState(now: Long) = DeviceWithState(
buildInfo,
checkIsRooted(),
deviceId,
locale,
calculateTotalMemory(),
runtimeVersions.toMutableMap(),
calculateFreeDisk(),
calculateFreeMemory(),
calculateOrientation(),
Date(now)
)
fun getDeviceMetadata(): Map<String, Any?> {
val map = HashMap<String, Any?>()
map["batteryLevel"] = getBatteryLevel()
map["charging"] = isCharging()
map["locationStatus"] = getLocationStatus()
map["networkAccess"] = getNetworkAccess()
map["brand"] = buildInfo.brand
map["screenDensity"] = screenDensity
map["dpi"] = dpi
map["emulator"] = emulator
map["screenResolution"] = screenResolution
return map
}
private fun checkIsRooted(): Boolean {
return try {
rootedFuture != null && rootedFuture.get()
} catch (exc: Exception) {
false
}
}
/**
* Guesses whether the current device is an emulator or not, erring on the side of caution
*
* @return true if the current device is an emulator
*/
private // genymotion
fun isEmulator(): Boolean {
val fingerprint = buildInfo.fingerprint
return fingerprint != null && (
fingerprint.startsWith("unknown") ||
fingerprint.contains("generic") ||
fingerprint.contains("vbox")
)
}
/**
* The screen density of the current Android device in dpi, eg. 320
*/
private fun getScreenDensityDpi(): Int? = displayMetrics?.densityDpi
/**
* Get the current battery charge level, eg 0.3
*/
private fun getBatteryLevel(): Float? {
try {
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger)
if (batteryStatus != null) {
return batteryStatus.getIntExtra(
"level",
-1
) / batteryStatus.getIntExtra("scale", -1).toFloat()
}
} catch (exception: Exception) {
logger.w("Could not get batteryLevel")
}
return null
}
/**
* Is the device currently charging/full battery?
*/
private fun isCharging(): Boolean? {
try {
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger)
if (batteryStatus != null) {
val status = batteryStatus.getIntExtra("status", -1)
return status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
}
} catch (exception: Exception) {
logger.w("Could not get charging status")
}
return null
}
/**
* Get the current status of location services
*/
private fun getLocationStatus(): String? {
try {
val cr = appContext.contentResolver
@Suppress("DEPRECATION") val providersAllowed =
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
return when {
providersAllowed != null && providersAllowed.isNotEmpty() -> "allowed"
else -> "disallowed"
}
} catch (exception: Exception) {
logger.w("Could not get locationStatus")
}
return null
}
/**
* Get the current status of network access, eg "cellular"
*/
private fun getNetworkAccess(): String = connectivity.retrieveNetworkAccessState()
/**
* The screen density scaling factor of the current Android device
*/
private fun getScreenDensity(): Float? = displayMetrics?.density
/**
* The screen resolution of the current Android device in px, eg. 1920x1080
*/
private fun getScreenResolution(): String? {
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)
} else {
null
}
}
/**
* Gets information about the CPU / API
*/
fun getCpuAbi(): Array<String> = buildInfo.cpuAbis ?: emptyArray()
/**
* Get the usable disk space on internal storage's data directory
*/
@SuppressLint("UsableSpace")
fun calculateFreeDisk(): Long {
// for this specific case we want the currently usable space, not
// StorageManager#allocatableBytes() as the UsableSpace lint inspection suggests
return dataDirectory.usableSpace
}
/**
* Get the amount of memory remaining that the VM can allocate
*/
private fun calculateFreeMemory(): Long {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
return if (maxMemory != Long.MAX_VALUE) {
maxMemory - runtime.totalMemory() + runtime.freeMemory()
} else {
runtime.freeMemory()
}
}
/**
* Get the total memory available on the current Android device, in bytes
*/
private fun calculateTotalMemory(): Long {
val runtime = Runtime.getRuntime()
val maxMemory = runtime.maxMemory()
return when {
maxMemory != Long.MAX_VALUE -> maxMemory
else -> runtime.totalMemory()
}
}
/**
* Get the device orientation, eg. "landscape"
*/
internal fun calculateOrientation() = when (resources?.configuration?.orientation) {
ORIENTATION_LANDSCAPE -> "landscape"
ORIENTATION_PORTRAIT -> "portrait"
else -> null
}
fun addRuntimeVersionInfo(key: String, value: String) {
runtimeVersions[key] = value
}
}

View File

@ -0,0 +1,181 @@
package com.bugsnag.android
import android.content.Context
import android.util.JsonReader
import java.io.File
import java.io.IOException
import java.lang.Thread
import java.nio.channels.FileChannel
import java.nio.channels.FileLock
import java.nio.channels.OverlappingFileLockException
import java.util.UUID
/**
* This class is responsible for persisting and retrieving the device ID which uniquely
* identifies this device.
*
* This class is made multi-process safe through the use of a [FileLock], and thread safe
* through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
*/
internal class DeviceIdStore @JvmOverloads constructor(
context: Context,
private val file: File = File(context.filesDir, "device-id"),
private val sharedPrefMigrator: SharedPrefMigrator,
private val logger: Logger
) {
private val synchronizedStreamableStore: SynchronizedStreamableStore<DeviceId>
init {
try {
if (!file.exists()) {
file.createNewFile()
}
} catch (exc: Throwable) {
logger.w("Failed to created device ID file", exc)
}
this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
}
/**
* Loads the device ID from its file system location. Device IDs are UUIDs which are
* persisted on a per-install basis. This method is thread-safe and multi-process safe.
*
* If no device ID exists then the legacy value stored in [SharedPreferences] will
* be used. If no value is present then a random UUID will be generated and persisted.
*/
fun loadDeviceId(): String? {
return loadDeviceId {
when (val legacyDeviceId = sharedPrefMigrator.loadDeviceId()) {
null -> UUID.randomUUID()
else -> UUID.fromString(legacyDeviceId)
}
}
}
internal fun loadDeviceId(uuidProvider: () -> UUID): String? {
return try {
// optimistically read device ID without a lock - the majority of the time
// the device ID will already be present so no synchronization is required.
val deviceId = loadDeviceIdInternal()
if (deviceId?.id != null) {
deviceId.id
} else {
return persistNewDeviceUuid(uuidProvider)
}
} catch (exc: Throwable) {
logger.w("Failed to load device ID", exc)
null
}
}
/**
* Loads the device ID from the file.
*
* If the file has zero length it can't contain device ID, so reading will be skipped.
*/
private fun loadDeviceIdInternal(): DeviceId? {
if (file.length() > 0) {
try {
return synchronizedStreamableStore.load(DeviceId.Companion::fromReader)
} catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader
// on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590
logger.w("Failed to load device ID", exc)
}
}
return null
}
/**
* Write a new Device ID to the file.
*/
private fun persistNewDeviceUuid(uuidProvider: () -> UUID): String? {
return try {
// acquire a FileLock to prevent Clients in different processes writing
// to the same file concurrently
file.outputStream().channel.use { channel ->
persistNewDeviceIdWithLock(channel, uuidProvider)
}
} catch (exc: IOException) {
logger.w("Failed to persist device ID", exc)
null
}
}
private fun persistNewDeviceIdWithLock(
channel: FileChannel,
uuidProvider: () -> UUID
): String? {
val lock = waitForFileLock(channel) ?: return null
return try {
// read the device ID again as it could have changed
// between the last read and when the lock was acquired
val deviceId = loadDeviceIdInternal()
if (deviceId?.id != null) {
// the device ID changed between the last read
// and acquiring the lock, so return the generated value
deviceId.id
} else {
// generate a new device ID and persist it
val newId = DeviceId(uuidProvider().toString())
synchronizedStreamableStore.persist(newId)
newId.id
}
} finally {
lock.release()
}
}
/**
* Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown
* then the method will wait for 50ms then try again, for a maximum of 10 attempts.
*/
private fun waitForFileLock(channel: FileChannel): FileLock? {
repeat(MAX_FILE_LOCK_ATTEMPTS) {
try {
return channel.tryLock()
} catch (exc: OverlappingFileLockException) {
Thread.sleep(FILE_LOCK_WAIT_MS)
}
}
return null
}
companion object {
private const val MAX_FILE_LOCK_ATTEMPTS = 20
private const val FILE_LOCK_WAIT_MS = 25L
}
}
/**
* Serializes and deserializes the device ID to/from JSON.
*/
private class DeviceId(val id: String?) : JsonStream.Streamable {
override fun toStream(stream: JsonStream) {
with(stream) {
beginObject()
name(KEY_ID)
value(id)
endObject()
}
}
companion object : JsonReadable<DeviceId> {
private const val KEY_ID = "id"
override fun fromReader(reader: JsonReader): DeviceId {
var id: String? = null
with(reader) {
beginObject()
if (hasNext() && KEY_ID == nextName()) {
id = nextString()
}
}
return DeviceId(id)
}
}
}

View File

@ -0,0 +1,48 @@
package com.bugsnag.android
import java.util.Date
/**
* Stateful information set by the notifier about the device on which the event occurred can be
* found on this class. These values can be accessed and amended if necessary.
*/
class DeviceWithState internal constructor(
buildInfo: DeviceBuildInfo,
jailbroken: Boolean?,
id: String?,
locale: String?,
totalMemory: Long?,
runtimeVersions: MutableMap<String, Any>,
/**
* The number of free bytes of storage available on the device
*/
var freeDisk: Long?,
/**
* The number of free bytes of memory available on the device
*/
var freeMemory: Long?,
/**
* The orientation of the device when the event occurred: either portrait or landscape
*/
var orientation: String?,
/**
* The timestamp on the device when the event occurred
*/
var time: Date?
) : Device(buildInfo, buildInfo.cpuAbis, jailbroken, id, locale, totalMemory, runtimeVersions) {
override fun serializeFields(writer: JsonStream) {
super.serializeFields(writer)
writer.name("freeDisk").value(freeDisk)
writer.name("freeMemory").value(freeMemory)
writer.name("orientation").value(orientation)
if (time != null) {
writer.name("time").value(DateUtils.toIso8601(time!!))
}
}
}

View File

@ -0,0 +1,19 @@
package com.bugsnag.android
/**
* Set the endpoints to send data to. By default we'll send error reports to
* https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can
* override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints.
*/
class EndpointConfiguration(
/**
* Configures the endpoint to which events should be sent
*/
val notify: String = "https://notify.bugsnag.com",
/**
* Configures the endpoint to which sessions should be sent
*/
val sessions: String = "https://sessions.bugsnag.com"
)

View File

@ -0,0 +1,101 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.Collection;
import java.util.List;
/**
* An Error represents information extracted from a {@link Throwable}.
*/
@SuppressWarnings("ConstantConditions")
public class Error implements JsonStream.Streamable {
private final ErrorInternal impl;
private final Logger logger;
Error(@NonNull ErrorInternal impl,
@NonNull Logger logger) {
this.impl = impl;
this.logger = logger;
}
private void logNull(String property) {
logger.e("Invalid null value supplied to error." + property + ", ignoring");
}
/**
* Sets the fully-qualified class name of the {@link Throwable}
*/
public void setErrorClass(@NonNull String errorClass) {
if (errorClass != null) {
impl.setErrorClass(errorClass);
} else {
logNull("errorClass");
}
}
/**
* Gets the fully-qualified class name of the {@link Throwable}
*/
@NonNull
public String getErrorClass() {
return impl.getErrorClass();
}
/**
* The message string from the {@link Throwable}
*/
public void setErrorMessage(@Nullable String errorMessage) {
impl.setErrorMessage(errorMessage);
}
/**
* The message string from the {@link Throwable}
*/
@Nullable
public String getErrorMessage() {
return impl.getErrorMessage();
}
/**
* Sets the type of error based on the originating platform (intended for internal use only)
*/
public void setType(@NonNull ErrorType type) {
if (type != null) {
impl.setType(type);
} else {
logNull("type");
}
}
/**
* Sets the type of error based on the originating platform (intended for internal use only)
*/
@NonNull
public ErrorType getType() {
return impl.getType();
}
/**
* Gets a representation of the stacktrace
*/
@NonNull
public List<Stackframe> getStacktrace() {
return impl.getStacktrace();
}
@Override
public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream);
}
static List<Error> createError(@NonNull Throwable exc,
@NonNull Collection<String> projectPackages,
@NonNull Logger logger) {
return ErrorInternal.Companion.createError(exc, projectPackages, logger);
}
}

View File

@ -0,0 +1,36 @@
package com.bugsnag.android
internal class ErrorInternal @JvmOverloads internal constructor(
var errorClass: String,
var errorMessage: String?,
stacktrace: Stacktrace,
var type: ErrorType = ErrorType.ANDROID
) : JsonStream.Streamable {
val stacktrace: List<Stackframe> = stacktrace.trace
internal companion object {
fun createError(exc: Throwable, projectPackages: Collection<String>, logger: Logger): MutableList<Error> {
val errors = mutableListOf<ErrorInternal>()
var currentEx: Throwable? = exc
while (currentEx != null) {
// 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)
errors.add(ErrorInternal(currentEx.javaClass.name, currentEx.localizedMessage, trace))
currentEx = currentEx.cause
}
return errors.map { Error(it, logger) }.toMutableList()
}
}
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("errorClass").value(errorClass)
writer.name("message").value(errorMessage)
writer.name("type").value(type.desc)
writer.name("stacktrace").value(stacktrace)
writer.endObject()
}
}

View File

@ -0,0 +1,22 @@
package com.bugsnag.android
/**
* Represents the type of error captured
*/
enum class ErrorType(internal val desc: String) {
/**
* An error captured from Android's JVM layer
*/
ANDROID("android"),
/**
* An error captured from JavaScript
*/
REACTNATIVEJS("reactnativejs"),
/**
* An error captured from Android's C layer
*/
C("c")
}

View File

@ -0,0 +1,36 @@
package com.bugsnag.android
class ErrorTypes(
/**
* Sets whether [ANRs](https://developer.android.com/topic/performance/vitals/anr)
* should be reported to Bugsnag.
*
* If you wish to disable ANR detection, you should set this property to false.
*/
var anrs: Boolean = true,
/**
* Determines whether NDK crashes such as signals and exceptions should be reported by bugsnag.
*
* This flag is true by default.
*/
var ndkCrashes: Boolean = true,
/**
* Sets whether Bugsnag should automatically capture and report unhandled errors.
* By default, this value is true.
*/
var unhandledExceptions: Boolean = true,
/**
* Sets whether Bugsnag should automatically capture and report unhandled promise rejections.
* This only applies to React Native apps.
* By default, this value is true.
*/
var unhandledRejections: Boolean = true
) {
internal constructor(detectErrors: Boolean) : this(detectErrors, detectErrors, detectErrors, detectErrors)
internal fun copy() = ErrorTypes(anrs, ndkCrashes, unhandledExceptions, unhandledRejections)
}

View File

@ -0,0 +1,345 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* An Event object represents a Throwable captured by Bugsnag and is available as a parameter on
* an {@link OnErrorCallback}, where individual properties can be mutated before an error report is
* sent to Bugsnag's API.
*/
@SuppressWarnings("ConstantConditions")
public class Event implements JsonStream.Streamable, MetadataAware, UserAware {
private final EventInternal impl;
private final Logger logger;
Event(@Nullable Throwable originalError,
@NonNull ImmutableConfig config,
@NonNull SeverityReason severityReason,
@NonNull Logger logger) {
this(originalError, config, severityReason, new Metadata(), logger);
}
Event(@Nullable Throwable originalError,
@NonNull ImmutableConfig config,
@NonNull SeverityReason severityReason,
@NonNull Metadata metadata,
@NonNull Logger logger) {
this(new EventInternal(originalError, config, severityReason, metadata), logger);
}
Event(@NonNull EventInternal impl, @NonNull Logger logger) {
this.impl = impl;
this.logger = logger;
}
private void logNull(String property) {
logger.e("Invalid null value supplied to config." + property + ", ignoring");
}
/**
* The Throwable object that caused the event in your application.
*
* Manipulating this field does not affect the error information reported to the
* Bugsnag dashboard. Use {@link Event#getErrors()} to access and amend the representation of
* the error that will be sent.
*/
@Nullable
public Throwable getOriginalError() {
return impl.getOriginalError();
}
/**
* Information extracted from the {@link Throwable} that caused the event can be found in this
* field. The list contains at least one {@link Error} that represents the thrown object
* with subsequent elements in the list populated from {@link Throwable#getCause()}.
*
* A reference to the actual {@link Throwable} object that caused the event is available
* through {@link Event#getOriginalError()} ()}.
*/
@NonNull
public List<Error> getErrors() {
return impl.getErrors();
}
/**
* If thread state is being captured along with the event, this field will contain a
* list of {@link Thread} objects.
*/
@NonNull
public List<Thread> getThreads() {
return impl.getThreads();
}
/**
* A list of breadcrumbs leading up to the event. These values can be accessed and amended
* if necessary. See {@link Breadcrumb} for details of the data available.
*/
@NonNull
public List<Breadcrumb> getBreadcrumbs() {
return impl.getBreadcrumbs();
}
/**
* Information set by the notifier about your app can be found in this field. These values
* can be accessed and amended if necessary.
*/
@NonNull
public AppWithState getApp() {
return impl.getApp();
}
/**
* Information set by the notifier about your device can be found in this field. These values
* can be accessed and amended if necessary.
*/
@NonNull
public DeviceWithState getDevice() {
return impl.getDevice();
}
/**
* The API key used for events sent to Bugsnag. Even though the API key is set when Bugsnag
* is initialized, you may choose to send certain events to a different Bugsnag project.
*/
public void setApiKey(@NonNull String apiKey) {
if (apiKey != null) {
impl.setApiKey(apiKey);
} else {
logNull("apiKey");
}
}
/**
* The API key used for events sent to Bugsnag. Even though the API key is set when Bugsnag
* is initialized, you may choose to send certain events to a different Bugsnag project.
*/
@NonNull
public String getApiKey() {
return impl.getApiKey();
}
/**
* The severity of the event. By default, unhandled exceptions will be {@link Severity#ERROR}
* and handled exceptions sent with {@link Bugsnag#notify} {@link Severity#WARNING}.
*/
public void setSeverity(@NonNull Severity severity) {
if (severity != null) {
impl.setSeverity(severity);
} else {
logNull("severity");
}
}
/**
* The severity of the event. By default, unhandled exceptions will be {@link Severity#ERROR}
* and handled exceptions sent with {@link Bugsnag#notify} {@link Severity#WARNING}.
*/
@NonNull
public Severity getSeverity() {
return impl.getSeverity();
}
/**
* Set the grouping hash of the event to override the default grouping on the dashboard.
* All events with the same grouping hash will be grouped together into one error. This is an
* advanced usage of the library and mis-using it will cause your events not to group properly
* in your dashboard.
*
* As the name implies, this option accepts a hash of sorts.
*/
public void setGroupingHash(@Nullable String groupingHash) {
impl.setGroupingHash(groupingHash);
}
/**
* Set the grouping hash of the event to override the default grouping on the dashboard.
* All events with the same grouping hash will be grouped together into one error. This is an
* advanced usage of the library and mis-using it will cause your events not to group properly
* in your dashboard.
*
* As the name implies, this option accepts a hash of sorts.
*/
@Nullable
public String getGroupingHash() {
return impl.getGroupingHash();
}
/**
* Sets the context of the error. The context is a summary what what was occurring in the
* application at the time of the crash, if available, such as the visible activity.
*/
public void setContext(@Nullable String context) {
impl.setContext(context);
}
/**
* Returns the context of the error. The context is a summary what what was occurring in the
* application at the time of the crash, if available, such as the visible activity.
*/
@Nullable
public String getContext() {
return impl.getContext();
}
/**
* Sets the user associated with the event.
*/
@Override
public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
impl.setUser(id, email, name);
}
/**
* Returns the currently set User information.
*/
@Override
@NonNull
public User getUser() {
return impl.getUser();
}
/**
* Adds a map of multiple metadata key-value pairs to the specified section.
*/
@Override
public void addMetadata(@NonNull String section, @NonNull Map<String, ?> value) {
if (section != null && value != null) {
impl.addMetadata(section, value);
} else {
logNull("addMetadata");
}
}
/**
* Adds the specified key and value in the specified section. The value can be of
* any primitive type or a collection such as a map, set or array.
*/
@Override
public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) {
if (section != null && key != null) {
impl.addMetadata(section, key, value);
} else {
logNull("addMetadata");
}
}
/**
* Removes all the data from the specified section.
*/
@Override
public void clearMetadata(@NonNull String section) {
if (section != null) {
impl.clearMetadata(section);
} else {
logNull("clearMetadata");
}
}
/**
* Removes data with the specified key from the specified section.
*/
@Override
public void clearMetadata(@NonNull String section, @NonNull String key) {
if (section != null && key != null) {
impl.clearMetadata(section, key);
} else {
logNull("clearMetadata");
}
}
/**
* Returns a map of data in the specified section.
*/
@Override
@Nullable
public Map<String, Object> getMetadata(@NonNull String section) {
if (section != null) {
return impl.getMetadata(section);
} else {
logNull("getMetadata");
return null;
}
}
/**
* Returns the value of the specified key in the specified section.
*/
@Override
@Nullable
public Object getMetadata(@NonNull String section, @NonNull String key) {
if (section != null && key != null) {
return impl.getMetadata(section, key);
} else {
logNull("getMetadata");
return null;
}
}
@Override
public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream);
}
/**
* Whether the event was a crash (i.e. unhandled) or handled error in which the system
* continued running.
*
* Unhandled errors count towards your stability score. If you don't want certain errors
* to count towards your stability score, you can alter this property through an
* {@link OnErrorCallback}.
*/
public boolean isUnhandled() {
return impl.getUnhandled();
}
/**
* Whether the event was a crash (i.e. unhandled) or handled error in which the system
* continued running.
*
* Unhandled errors count towards your stability score. If you don't want certain errors
* to count towards your stability score, you can alter this property through an
* {@link OnErrorCallback}.
*/
public void setUnhandled(boolean unhandled) {
impl.setUnhandled(unhandled);
}
protected boolean shouldDiscardClass() {
return impl.shouldDiscardClass();
}
protected void updateSeverityInternal(@NonNull Severity severity) {
impl.updateSeverityInternal(severity);
}
void setApp(@NonNull AppWithState app) {
impl.setApp(app);
}
void setDevice(@NonNull DeviceWithState device) {
impl.setDevice(device);
}
void setBreadcrumbs(@NonNull List<Breadcrumb> breadcrumbs) {
impl.setBreadcrumbs(breadcrumbs);
}
@Nullable
Session getSession() {
return impl.session;
}
void setSession(@Nullable Session session) {
impl.session = session;
}
EventInternal getImpl() {
return impl;
}
}

View File

@ -0,0 +1,152 @@
package com.bugsnag.android
import java.io.File
import java.util.Locale
import java.util.UUID
/**
* Represents important information about an event which is encoded/decoded from a filename.
* Currently the following information is encoded:
*
* apiKey - as a user can decide to override the value on an Event
* uuid - to disambiguate stored error reports
* timestamp - to sort error reports by time of capture
* suffix - used to encode whether the app crashed on launch, or the report is not a JVM error
* errorTypes - a comma delimited string which contains the stackframe types in the error
*/
internal data class EventFilenameInfo(
val apiKey: String,
val uuid: String,
val timestamp: Long,
val suffix: String,
val errorTypes: Set<ErrorType>
) {
/**
* Generates a filename for the Event in the format
* "[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
)
}
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
internal companion object {
private const val STARTUP_CRASH = "startupcrash"
private const val NON_JVM_CRASH = "not-jvm"
@JvmOverloads
fun fromEvent(
obj: Any,
uuid: String = UUID.randomUUID().toString(),
apiKey: String?,
timestamp: Long = System.currentTimeMillis(),
config: ImmutableConfig,
isLaunching: Boolean? = null
): EventFilenameInfo {
val sanitizedApiKey = when {
obj is Event -> obj.apiKey
apiKey.isNullOrEmpty() -> config.apiKey
else -> apiKey
}
return EventFilenameInfo(
sanitizedApiKey,
uuid,
timestamp,
findSuffixForEvent(obj, isLaunching),
findErrorTypesForEvent(obj)
)
}
/**
* Reads event information from a filename.
*/
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
return EventFilenameInfo(
findApiKeyInFilename(file, config),
"", // ignore UUID field when reading from file as unused
-1, // ignore timestamp when reading from file as unused
findSuffixInFilename(file),
findErrorTypesInFilename(file)
)
}
/**
* Retrieves the api key encoded in the filename, or an empty string if this information
* is not encoded for the given event
*/
private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
val name = file.name.removeSuffix("_$STARTUP_CRASH.json")
val start = name.indexOf("_") + 1
val end = name.indexOf("_", start)
val apiKey = if (start == 0 || end == -1 || end <= start) {
null
} else {
name.substring(start, end)
}
return apiKey ?: config.apiKey
}
/**
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
private fun findErrorTypesInFilename(eventFile: File): Set<ErrorType> {
val name = eventFile.name
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
val start = name.lastIndexOf("_", end - 1) + 1
if (start < end) {
val encodedValues: List<String> = name.substring(start, end).split(",")
return ErrorType.values().filter {
encodedValues.contains(it.desc)
}.toSet()
}
return emptySet()
}
/**
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
private fun findSuffixInFilename(eventFile: File): String {
val name = eventFile.nameWithoutExtension
val suffix = name.substring(name.lastIndexOf("_") + 1)
return when (suffix) {
STARTUP_CRASH, NON_JVM_CRASH -> suffix
else -> ""
}
}
/**
* Retrieves the error types for the given event
*/
private fun findErrorTypesForEvent(obj: Any): Set<ErrorType> {
return when (obj) {
is Event -> obj.impl.getErrorTypesFromStackframes()
else -> setOf(ErrorType.C)
}
}
/**
* Calculates the suffix for the given event
*/
private fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
return when {
obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH
launching == true -> STARTUP_CRASH
else -> ""
}
}
}
}

View File

@ -0,0 +1,160 @@
package com.bugsnag.android
import java.io.IOException
internal class EventInternal @JvmOverloads internal constructor(
val originalError: Throwable? = null,
config: ImmutableConfig,
private var severityReason: SeverityReason,
data: Metadata = Metadata()
) : JsonStream.Streamable, MetadataAware, UserAware {
val metadata: Metadata = data.copy()
private val discardClasses: Set<String> = config.discardClasses.toSet()
private val projectPackages = config.projectPackages
@JvmField
internal var session: Session? = null
var severity: Severity
get() = severityReason.currentSeverity
set(value) {
severityReason.currentSeverity = value
}
var apiKey: String = config.apiKey
lateinit var app: AppWithState
lateinit var device: DeviceWithState
var breadcrumbs: MutableList<Breadcrumb> = mutableListOf()
var unhandled: Boolean
get() = severityReason.unhandled
set(value) {
severityReason.unhandled = value
}
val unhandledOverridden: Boolean
get() = severityReason.unhandledOverridden
val originalUnhandled: Boolean
get() = severityReason.originalUnhandled
var errors: MutableList<Error> = when (originalError) {
null -> mutableListOf()
else -> Error.createError(originalError, config.projectPackages, config.logger)
}
var threads: MutableList<Thread> = ThreadState(originalError, unhandled, config).threads
var groupingHash: String? = null
var context: String? = null
/**
* @return user information associated with this Event
*/
internal var _user = User(null, null, null)
protected fun shouldDiscardClass(): Boolean {
return when {
errors.isEmpty() -> true
else -> errors.any { discardClasses.contains(it.errorClass) }
}
}
protected fun isAnr(event: Event): Boolean {
val errors = event.errors
var errorClass: String? = null
if (errors.isNotEmpty()) {
val error = errors[0]
errorClass = error.errorClass
}
return "ANR" == errorClass
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
// Write error basics
writer.beginObject()
writer.name("context").value(context)
writer.name("metaData").value(metadata)
writer.name("severity").value(severity)
writer.name("severityReason").value(severityReason)
writer.name("unhandled").value(severityReason.unhandled)
// Write exception info
writer.name("exceptions")
writer.beginArray()
errors.forEach { writer.value(it) }
writer.endArray()
// Write project packages
writer.name("projectPackages")
writer.beginArray()
projectPackages.forEach { writer.value(it) }
writer.endArray()
// Write user info
writer.name("user").value(_user)
// Write diagnostics
writer.name("app").value(app)
writer.name("device").value(device)
writer.name("breadcrumbs").value(breadcrumbs)
writer.name("groupingHash").value(groupingHash)
writer.name("threads")
writer.beginArray()
threads.forEach { writer.value(it) }
writer.endArray()
if (session != null) {
val copy = Session.copySession(session)
writer.name("session").beginObject()
writer.name("id").value(copy.id)
writer.name("startedAt").value(DateUtils.toIso8601(copy.startedAt))
writer.name("events").beginObject()
writer.name("handled").value(copy.handledCount.toLong())
writer.name("unhandled").value(copy.unhandledCount.toLong())
writer.endObject()
writer.endObject()
}
writer.endObject()
}
internal fun getErrorTypesFromStackframes(): Set<ErrorType> {
val errorTypes = errors.mapNotNull(Error::getType).toSet()
val frameOverrideTypes = errors
.map { it.stacktrace }
.flatMap { it.mapNotNull(Stackframe::type) }
return errorTypes.plus(frameOverrideTypes)
}
protected fun updateSeverityInternal(severity: Severity) {
severityReason = SeverityReason.newInstance(
severityReason.severityReasonType,
severity,
severityReason.attributeValue
)
this.severity = severity
}
fun getSeverityReasonType(): String = severityReason.severityReasonType
override fun setUser(id: String?, email: String?, name: String?) {
_user = User(id, email, name)
}
override fun getUser() = _user
override fun addMetadata(section: String, value: Map<String, Any?>) = metadata.addMetadata(section, value)
override fun addMetadata(section: String, key: String, value: Any?) =
metadata.addMetadata(section, key, value)
override fun clearMetadata(section: String) = metadata.clearMetadata(section)
override fun clearMetadata(section: String, key: String) = metadata.clearMetadata(section, key)
override fun getMetadata(section: String) = metadata.getMetadata(section)
override fun getMetadata(section: String, key: String) = metadata.getMetadata(section, key)
}

View File

@ -0,0 +1,49 @@
package com.bugsnag.android
import java.io.File
import java.io.IOException
/**
* An error report payload.
*
* This payload contains an error report and identifies the source application
* using your API key.
*/
class EventPayload @JvmOverloads internal constructor(
var apiKey: String?,
val event: Event? = null,
internal val eventFile: File? = null,
notifier: Notifier,
private val config: ImmutableConfig
) : JsonStream.Streamable {
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
dependencies = notifier.dependencies.toMutableList()
}
internal fun getErrorTypes(): Set<ErrorType> {
return when {
event != null -> event.impl.getErrorTypesFromStackframes()
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes
else -> emptySet()
}
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("apiKey").value(apiKey)
writer.name("payloadVersion").value("4.0")
writer.name("notifier").value(notifier)
writer.name("events").beginArray()
when {
event != null -> writer.value(event)
eventFile != null -> writer.value(eventFile)
else -> Unit
}
writer.endArray()
writer.endObject()
}
}

View File

@ -0,0 +1,213 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.Locale;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;
/**
* Store and flush Event reports which couldn't be sent immediately due to
* lack of network connectivity.
*/
class EventStore extends FileStore {
private static final long LAUNCH_CRASH_TIMEOUT_MS = 2000;
private final ImmutableConfig config;
private final Delegate delegate;
private final Notifier notifier;
private final BackgroundTaskService bgTaskSevice;
final Logger logger;
static final Comparator<File> EVENT_COMPARATOR = new Comparator<File>() {
@Override
public int compare(File lhs, File rhs) {
if (lhs == null && rhs == null) {
return 0;
}
if (lhs == null) {
return 1;
}
if (rhs == null) {
return -1;
}
return lhs.compareTo(rhs);
}
};
EventStore(@NonNull ImmutableConfig config,
@NonNull Logger logger,
Notifier notifier,
BackgroundTaskService bgTaskSevice,
Delegate delegate) {
super(new File(config.getPersistenceDirectory(), "bugsnag-errors"),
config.getMaxPersistedEvents(),
EVENT_COMPARATOR,
logger,
delegate);
this.config = config;
this.logger = logger;
this.delegate = delegate;
this.notifier = notifier;
this.bgTaskSevice = bgTaskSevice;
}
/**
* Flush startup crashes synchronously on the main thread
*/
void flushOnLaunch() {
if (!config.getSendLaunchCrashesSynchronously()) {
return;
}
Future<?> future = null;
try {
future = bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
flushLaunchCrashReport();
}
});
} catch (RejectedExecutionException exc) {
logger.d("Failed to flush launch crash reports, continuing.", exc);
}
try {
if (future != null) {
future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS);
}
} catch (InterruptedException | ExecutionException | TimeoutException exc) {
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc);
}
}
void flushLaunchCrashReport() {
List<File> storedFiles = findStoredFiles();
File launchCrashReport = findLaunchCrashReport(storedFiles);
// cancel non-launch crash reports
if (launchCrashReport != null) {
storedFiles.remove(launchCrashReport);
}
cancelQueuedFiles(storedFiles);
if (launchCrashReport != null) {
logger.i("Attempting to send the most recent launch crash report");
flushReports(Collections.singletonList(launchCrashReport));
logger.i("Continuing with Bugsnag initialisation");
} else {
logger.d("No startupcrash events to flush to Bugsnag.");
}
}
@Nullable
File findLaunchCrashReport(Collection<File> storedFiles) {
List<File> launchCrashes = new ArrayList<>();
for (File file : storedFiles) {
EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config);
if (filenameInfo.isLaunchCrashReport()) {
launchCrashes.add(file);
}
}
// sort to get most recent timestamp
Collections.sort(launchCrashes, EVENT_COMPARATOR);
return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1);
}
/**
* Flush any on-disk errors to Bugsnag
*/
void flushAsync() {
try {
bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() {
@Override
public void run() {
List<File> storedFiles = findStoredFiles();
if (storedFiles.isEmpty()) {
logger.d("No regular events to flush to Bugsnag.");
}
flushReports(storedFiles);
}
});
} catch (RejectedExecutionException exception) {
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.");
}
}
void flushReports(Collection<File> storedReports) {
if (!storedReports.isEmpty()) {
logger.i(String.format(Locale.US,
"Sending %d saved error(s) to Bugsnag", storedReports.size()));
for (File eventFile : storedReports) {
flushEventFile(eventFile);
}
}
}
private void flushEventFile(File eventFile) {
try {
EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromFile(eventFile, config);
String apiKey = eventInfo.getApiKey();
EventPayload payload = new EventPayload(apiKey, null, eventFile, notifier, config);
DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload);
Delivery delivery = config.getDelivery();
DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams);
switch (deliveryStatus) {
case DELIVERED:
deleteStoredFiles(Collections.singleton(eventFile));
logger.i("Deleting sent error file " + eventFile.getName());
break;
case UNDELIVERED:
cancelQueuedFiles(Collections.singleton(eventFile));
logger.w("Could not send previously saved error(s)"
+ " to Bugsnag, will try again later");
break;
case FAILURE:
Exception exc = new RuntimeException("Failed to deliver event payload");
handleEventFlushFailure(exc, eventFile);
break;
default:
break;
}
} catch (Exception exception) {
handleEventFlushFailure(exception, eventFile);
}
}
private void handleEventFlushFailure(Exception exc, File eventFile) {
if (delegate != null) {
delegate.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization");
}
deleteStoredFiles(Collections.singleton(eventFile));
}
@NonNull
@Override
String getFilename(Object object) {
EventFilenameInfo eventInfo
= EventFilenameInfo.Companion.fromEvent(object, null, config);
String encodedInfo = eventInfo.encode();
return String.format(Locale.US, "%s", encodedInfo);
}
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);
}
}

View File

@ -0,0 +1,67 @@
package com.bugsnag.android;
import android.os.StrictMode;
import androidx.annotation.NonNull;
import java.lang.Thread;
import java.lang.Thread.UncaughtExceptionHandler;
/**
* Provides automatic notification hooks for unhandled exceptions.
*/
class ExceptionHandler implements UncaughtExceptionHandler {
private static final String STRICT_MODE_TAB = "StrictMode";
private static final String STRICT_MODE_KEY = "Violation";
private final UncaughtExceptionHandler originalHandler;
private final StrictModeHandler strictModeHandler = new StrictModeHandler();
private final Client client;
private final Logger logger;
ExceptionHandler(Client client, Logger logger) {
this.client = client;
this.logger = logger;
this.originalHandler = Thread.getDefaultUncaughtExceptionHandler();
Thread.setDefaultUncaughtExceptionHandler(this);
}
@Override
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
boolean strictModeThrowable = strictModeHandler.isStrictModeThrowable(throwable);
// Notify any subscribed clients of the uncaught exception
Metadata metadata = new Metadata();
String violationDesc = null;
if (strictModeThrowable) { // add strictmode policy violation to metadata
violationDesc = strictModeHandler.getViolationDescription(throwable.getMessage());
metadata = new Metadata();
metadata.addMetadata(STRICT_MODE_TAB, STRICT_MODE_KEY, violationDesc);
}
String severityReason = strictModeThrowable
? SeverityReason.REASON_STRICT_MODE : SeverityReason.REASON_UNHANDLED_EXCEPTION;
if (strictModeThrowable) { // writes to disk on main thread
StrictMode.ThreadPolicy originalThreadPolicy = StrictMode.getThreadPolicy();
StrictMode.setThreadPolicy(StrictMode.ThreadPolicy.LAX);
client.notifyUnhandledException(throwable,
metadata, severityReason, violationDesc);
StrictMode.setThreadPolicy(originalThreadPolicy);
} else {
client.notifyUnhandledException(throwable,
metadata, severityReason, null);
}
// Pass exception on to original exception handler
if (originalHandler != null) {
originalHandler.uncaughtException(thread, throwable);
} else {
System.err.printf("Exception in thread \"%s\" ", thread.getName());
logger.w("Exception", throwable);
}
}
}

View File

@ -0,0 +1,241 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedWriter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.OutputStreamWriter;
import java.io.Writer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import java.util.concurrent.ConcurrentSkipListSet;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
abstract class FileStore {
interface Delegate {
/**
* Invoked when an error report is not (de)serialized correctly
*
* @param exception the error encountered reading/delivering the file
* @param errorFile file which could not be (de)serialized correctly
* @param context the context used to group the exception
*/
void onErrorIOFailure(Exception exception, File errorFile, String context);
}
private final File storageDir;
private final int maxStoreCount;
private final Comparator<File> comparator;
private final Lock lock = new ReentrantLock();
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
private final Logger logger;
private final EventStore.Delegate delegate;
FileStore(@NonNull File storageDir,
int maxStoreCount,
Comparator<File> comparator,
Logger logger,
Delegate delegate) {
this.maxStoreCount = maxStoreCount;
this.comparator = comparator;
this.logger = logger;
this.delegate = delegate;
this.storageDir = storageDir;
isStorageDirValid(storageDir);
}
/**
* Checks whether the storage directory is a writable directory. If it is not,
* this method will attempt to create the directory.
*
* If the directory could not be created then an error will be logged.
*/
private boolean isStorageDirValid(@NonNull File storageDir) {
try {
if (!storageDir.isDirectory() || !storageDir.canWrite()) {
if (!storageDir.mkdirs()) {
this.logger.e("Could not prepare storage directory at "
+ storageDir.getAbsolutePath());
return false;
}
}
} catch (Exception exception) {
this.logger.e("Could not prepare file storage directory", exception);
return false;
}
return true;
}
void enqueueContentForDelivery(String content, String filename) {
if (!isStorageDirValid(storageDir)) {
return;
}
discardOldestFileIfNeeded();
lock.lock();
Writer out = null;
String filePath = new File(storageDir, filename).getAbsolutePath();
try {
FileOutputStream fos = new FileOutputStream(filePath);
out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8"));
out.write(content);
} catch (Exception exc) {
File eventFile = new File(filePath);
if (delegate != null) {
delegate.onErrorIOFailure(exc, eventFile, "NDK Crash report copy");
}
IOUtils.deleteFile(eventFile, logger);
} finally {
try {
if (out != null) {
out.close();
}
} catch (Exception exception) {
logger.w(String.format("Failed to close unsent payload writer (%s) ",
filename), exception);
}
lock.unlock();
}
}
@Nullable
String write(@NonNull JsonStream.Streamable streamable) {
if (!isStorageDirValid(storageDir)) {
return null;
}
if (maxStoreCount == 0) {
return null;
}
discardOldestFileIfNeeded();
String filename = new File(storageDir, getFilename(streamable)).getAbsolutePath();
JsonStream stream = null;
lock.lock();
try {
FileOutputStream fos = new FileOutputStream(filename);
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));
return filename;
} catch (FileNotFoundException exc) {
logger.w("Ignoring FileNotFoundException - unable to create file", exc);
} catch (Exception exc) {
File eventFile = new File(filename);
if (delegate != null) {
delegate.onErrorIOFailure(exc, eventFile, "Crash report serialization");
}
IOUtils.deleteFile(eventFile, logger);
} finally {
IOUtils.closeQuietly(stream);
lock.unlock();
}
return null;
}
void discardOldestFileIfNeeded() {
// Limit number of saved payloads to prevent disk space issues
if (isStorageDirValid(storageDir)) {
File[] listFiles = storageDir.listFiles();
if (listFiles == null) {
return;
}
List<File> files = new ArrayList<>(Arrays.asList(listFiles));
if (files.size() >= maxStoreCount) {
// Sort files then delete the first one (oldest timestamp)
Collections.sort(files, comparator);
for (int k = 0; k < files.size() && files.size() >= maxStoreCount; k++) {
File oldestFile = files.get(k);
if (!queuedFiles.contains(oldestFile)) {
logger.w(String.format("Discarding oldest error as stored "
+ "error limit reached (%s)", oldestFile.getPath()));
deleteStoredFiles(Collections.singleton(oldestFile));
files.remove(k);
k--;
}
}
}
}
}
@NonNull
abstract String getFilename(Object object);
List<File> findStoredFiles() {
lock.lock();
try {
List<File> files = new ArrayList<>();
if (isStorageDirValid(storageDir)) {
File[] values = storageDir.listFiles();
if (values != null) {
for (File value : values) {
// delete any tombstoned/empty files, as they contain no useful info
if (value.length() == 0) {
if (!value.delete()) {
value.deleteOnExit();
}
} else if (value.isFile() && !queuedFiles.contains(value)) {
files.add(value);
}
}
}
}
queuedFiles.addAll(files);
return files;
} finally {
lock.unlock();
}
}
void cancelQueuedFiles(Collection<File> files) {
lock.lock();
try {
if (files != null) {
queuedFiles.removeAll(files);
}
} finally {
lock.unlock();
}
}
void deleteStoredFiles(Collection<File> storedFiles) {
lock.lock();
try {
if (storedFiles != null) {
queuedFiles.removeAll(storedFiles);
for (File storedFile : storedFiles) {
if (!storedFile.delete()) {
storedFile.deleteOnExit();
}
}
}
} finally {
lock.unlock();
}
}
}

View File

@ -0,0 +1,73 @@
package com.bugsnag.android;
import android.app.ActivityManager;
import android.content.Context;
import android.os.Build;
import android.os.Process;
import androidx.annotation.Nullable;
import java.util.List;
class ForegroundDetector {
private final ActivityManager activityManager;
ForegroundDetector(Context context) {
this.activityManager =
(ActivityManager) context.getSystemService(Context.ACTIVITY_SERVICE);
}
/**
* Determines whether or not the application is in the foreground, by using the process'
* importance as a proxy.
* <p/>
* In the unlikely event that information about the process cannot be retrieved, this method
* will return null, and the 'inForeground' and 'durationInForeground' values will not be
* serialized in API calls.
*
* @return whether the application is in the foreground or not
*/
@Nullable
Boolean isInForeground() {
try {
ActivityManager.RunningAppProcessInfo info = getProcessInfo();
if (info != null) {
return info.importance
<= ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND;
} else {
return null;
}
} catch (RuntimeException exc) {
return null;
}
}
private ActivityManager.RunningAppProcessInfo getProcessInfo() {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
ActivityManager.RunningAppProcessInfo info =
new ActivityManager.RunningAppProcessInfo();
ActivityManager.getMyMemoryState(info);
return info;
} else {
return getProcessInfoPreApi16();
}
}
@Nullable
private ActivityManager.RunningAppProcessInfo getProcessInfoPreApi16() {
List<ActivityManager.RunningAppProcessInfo> appProcesses
= activityManager.getRunningAppProcesses();
if (appProcesses != null) {
int pid = Process.myPid();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (pid == appProcess.pid) {
return appProcess;
}
}
}
return null;
}
}

View File

@ -0,0 +1,55 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.Closeable;
import java.io.File;
import java.io.IOException;
import java.io.Reader;
import java.io.Writer;
import java.net.HttpURLConnection;
import java.net.URLConnection;
@SuppressWarnings("checkstyle:AbbreviationAsWordInName")
class IOUtils {
private static final int DEFAULT_BUFFER_SIZE = 1024 * 4;
private static final int EOF = -1;
static void closeQuietly(@Nullable final Closeable closeable) {
try {
if (closeable != null) {
closeable.close();
}
} catch (@NonNull final Exception ioe) {
// ignore
}
}
static int copy(@NonNull final Reader input,
@NonNull final Writer output) throws IOException {
char[] buffer = new char[DEFAULT_BUFFER_SIZE];
long count = 0;
int read;
while (EOF != (read = input.read(buffer))) {
output.write(buffer, 0, read);
count += read;
}
if (count > Integer.MAX_VALUE) {
return -1;
}
return (int) count;
}
static void deleteFile(File file, Logger logger) {
try {
if (!file.delete()) {
file.deleteOnExit();
}
} catch (Exception ex) {
logger.w("Failed to delete file", ex);
}
}
}

View File

@ -0,0 +1,151 @@
package com.bugsnag.android
import android.content.Context
import android.content.pm.ApplicationInfo
import android.content.pm.PackageManager
import java.io.File
internal data class ImmutableConfig(
val apiKey: String,
val autoDetectErrors: Boolean,
val enabledErrorTypes: ErrorTypes,
val autoTrackSessions: Boolean,
val sendThreads: ThreadSendPolicy,
val discardClasses: Collection<String>,
val enabledReleaseStages: Collection<String>?,
val projectPackages: Collection<String>,
val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
val releaseStage: String?,
val buildUuid: String?,
val appVersion: String?,
val versionCode: Int?,
val appType: String?,
val delivery: Delivery,
val endpoints: EndpointConfiguration,
val persistUser: Boolean,
val launchDurationMillis: Long,
val logger: Logger,
val maxBreadcrumbs: Int,
val maxPersistedEvents: Int,
val maxPersistedSessions: Int,
val persistenceDirectory: File,
val sendLaunchCrashesSynchronously: Boolean
) {
/**
* Checks if the given release stage should be notified or not
*
* @return true if the release state should be notified else false
*/
@JvmName("shouldNotifyForReleaseStage")
internal fun shouldNotifyForReleaseStage() =
enabledReleaseStages == null || enabledReleaseStages.contains(releaseStage)
@JvmName("shouldRecordBreadcrumbType")
internal fun shouldRecordBreadcrumbType(type: BreadcrumbType) =
enabledBreadcrumbTypes == null || enabledBreadcrumbTypes.contains(type)
@JvmName("getErrorApiDeliveryParams")
internal fun getErrorApiDeliveryParams(payload: EventPayload) =
DeliveryParams(endpoints.notify, errorApiHeaders(payload))
@JvmName("getSessionApiDeliveryParams")
internal fun getSessionApiDeliveryParams() =
DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey))
}
internal fun convertToImmutableConfig(
config: Configuration,
buildUuid: String? = null
): ImmutableConfig {
val errorTypes = when {
config.autoDetectErrors -> config.enabledErrorTypes.copy()
else -> ErrorTypes(false)
}
return ImmutableConfig(
apiKey = config.apiKey,
autoDetectErrors = config.autoDetectErrors,
enabledErrorTypes = errorTypes,
autoTrackSessions = config.autoTrackSessions,
sendThreads = config.sendThreads,
discardClasses = config.discardClasses.toSet(),
enabledReleaseStages = config.enabledReleaseStages?.toSet(),
projectPackages = config.projectPackages.toSet(),
releaseStage = config.releaseStage,
buildUuid = buildUuid,
appVersion = config.appVersion,
versionCode = config.versionCode,
appType = config.appType,
delivery = config.delivery,
endpoints = config.endpoints,
persistUser = config.persistUser,
launchDurationMillis = config.launchDurationMillis,
logger = config.logger!!,
maxBreadcrumbs = config.maxBreadcrumbs,
maxPersistedEvents = config.maxPersistedEvents,
maxPersistedSessions = config.maxPersistedSessions,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
persistenceDirectory = config.persistenceDirectory!!,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously
)
}
internal fun sanitiseConfiguration(
appContext: Context,
configuration: Configuration,
connectivity: Connectivity
): ImmutableConfig {
val packageName = appContext.packageName
val packageManager = appContext.packageManager
val packageInfo = runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull()
val appInfo = runCatching {
packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
}.getOrNull()
// populate releaseStage
if (configuration.releaseStage == null) {
configuration.releaseStage = when {
appInfo != null && (appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) -> RELEASE_STAGE_DEVELOPMENT
else -> RELEASE_STAGE_PRODUCTION
}
}
// if the user has set the releaseStage to production manually, disable logging
if (configuration.logger == null || configuration.logger == DebugLogger) {
val releaseStage = configuration.releaseStage
val loggingEnabled = RELEASE_STAGE_PRODUCTION != releaseStage
if (loggingEnabled) {
configuration.logger = DebugLogger
} else {
configuration.logger = NoopLogger
}
}
if (configuration.versionCode == null || configuration.versionCode == 0) {
@Suppress("DEPRECATION")
configuration.versionCode = packageInfo?.versionCode
}
// Set sensible defaults if project packages not already set
if (configuration.projectPackages.isEmpty()) {
configuration.projectPackages = setOf<String>(packageName)
}
// populate buildUUID from manifest
val buildUuid = appInfo?.metaData?.getString(ManifestConfigLoader.BUILD_UUID)
@Suppress("SENSELESS_COMPARISON")
if (configuration.delivery == null) {
configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!)
}
if (configuration.persistenceDirectory == null) {
configuration.persistenceDirectory = appContext.cacheDir
}
return convertToImmutableConfig(configuration, buildUuid)
}
internal const val RELEASE_STAGE_DEVELOPMENT = "development"
internal const val RELEASE_STAGE_PRODUCTION = "production"

View File

@ -0,0 +1,131 @@
package com.bugsnag.android;
import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR;
import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
import android.annotation.SuppressLint;
import android.content.Context;
import android.os.Build;
import android.os.storage.StorageManager;
import androidx.annotation.NonNull;
import java.io.File;
import java.io.IOException;
import java.util.Date;
import java.util.Map;
import java.util.concurrent.RejectedExecutionException;
class InternalReportDelegate implements EventStore.Delegate {
static final String INTERNAL_DIAGNOSTICS_TAB = "BugsnagDiagnostics";
final Logger logger;
final ImmutableConfig config;
final StorageManager storageManager;
final AppDataCollector appDataCollector;
final DeviceDataCollector deviceDataCollector;
final Context appContext;
final SessionTracker sessionTracker;
final Notifier notifier;
final BackgroundTaskService backgroundTaskService;
InternalReportDelegate(Context context,
Logger logger,
ImmutableConfig immutableConfig,
StorageManager storageManager,
AppDataCollector appDataCollector,
DeviceDataCollector deviceDataCollector,
SessionTracker sessionTracker,
Notifier notifier,
BackgroundTaskService backgroundTaskService) {
this.logger = logger;
this.config = immutableConfig;
this.storageManager = storageManager;
this.appDataCollector = appDataCollector;
this.deviceDataCollector = deviceDataCollector;
this.appContext = context;
this.sessionTracker = sessionTracker;
this.notifier = notifier;
this.backgroundTaskService = backgroundTaskService;
}
@Override
public void onErrorIOFailure(Exception exc, File errorFile, String context) {
// send an internal error to bugsnag with no cache
SeverityReason severityReason = SeverityReason.newInstance(REASON_UNHANDLED_EXCEPTION);
Event err = new Event(exc, config, severityReason, logger);
err.setContext(context);
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "canRead", errorFile.canRead());
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "canWrite", errorFile.canWrite());
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "exists", errorFile.exists());
@SuppressLint("UsableSpace") // storagemanager alternative API requires API 26
long usableSpace = appContext.getCacheDir().getUsableSpace();
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "usableSpace", usableSpace);
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "filename", errorFile.getName());
err.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "fileLength", errorFile.length());
recordStorageCacheBehavior(err);
reportInternalBugsnagError(err);
}
void recordStorageCacheBehavior(Event event) {
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
File cacheDir = appContext.getCacheDir();
File errDir = new File(cacheDir, "bugsnag-errors");
try {
boolean tombstone = storageManager.isCacheBehaviorTombstone(errDir);
boolean group = storageManager.isCacheBehaviorGroup(errDir);
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "cacheTombstone", tombstone);
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "cacheGroup", group);
} catch (IOException exc) {
logger.w("Failed to record cache behaviour, skipping diagnostics", exc);
}
}
}
/**
* Reports an event that occurred within the notifier to bugsnag. A lean event report will be
* generated and sent asynchronously with no callbacks, retry attempts, or writing to disk.
* This is intended for internal use only, and reports will not be visible to end-users.
*/
void reportInternalBugsnagError(@NonNull Event event) {
event.setApp(appDataCollector.generateAppWithState());
event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime()));
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierName", notifier.getName());
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierVersion", notifier.getVersion());
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "apiKey", config.getApiKey());
final EventPayload payload = new EventPayload(null, event, notifier, config);
try {
backgroundTaskService.submitTask(TaskType.INTERNAL_REPORT, new Runnable() {
@Override
public void run() {
try {
logger.d("InternalReportDelegate - sending internal event");
Delivery delivery = config.getDelivery();
DeliveryParams params = config.getErrorApiDeliveryParams(payload);
// can only modify headers if DefaultDelivery is in use
if (delivery instanceof DefaultDelivery) {
Map<String, String> headers = params.getHeaders();
headers.put(HEADER_INTERNAL_ERROR, "true");
headers.remove(DeliveryHeadersKt.HEADER_API_KEY);
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
defaultDelivery.deliver(params.getEndpoint(), payload, headers);
}
} catch (Exception exception) {
logger.w("Failed to report internal event to Bugsnag", exception);
}
}
});
} catch (RejectedExecutionException ignored) {
// drop internal report
}
}
}

View File

@ -0,0 +1,8 @@
package com.bugsnag.android;
class Intrinsics {
static boolean isEmpty(CharSequence str) {
return str == null || str.length() == 0;
}
}

View File

@ -0,0 +1,14 @@
package com.bugsnag.android
import android.util.JsonReader
/**
* Classes which implement this interface are capable of deserializing a JSON input.
*/
internal interface JsonReadable<T : JsonStream.Streamable> {
/**
* Constructs an object from a JSON input.
*/
fun fromReader(reader: JsonReader): T
}

View File

@ -0,0 +1,75 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bugsnag.android;
// last retrieved from gson-parent-2.8.5 on 17/01/2019
// https://github.com/google/gson/tree/gson-parent-2.8.5/gson/src/main/java/com/google/gson/stream
/**
* Lexical scoping elements within a JSON reader or writer.
*
* @author Jesse Wilson
* @since 1.6
*/
@SuppressWarnings("all")
final class JsonScope {
/**
* An array with no elements requires no separators or newlines before
* it is closed.
*/
static final int EMPTY_ARRAY = 1;
/**
* A array with at least one value requires a comma and newline before
* the next element.
*/
static final int NONEMPTY_ARRAY = 2;
/**
* An object with no name/value pairs requires no separators or newlines
* before it is closed.
*/
static final int EMPTY_OBJECT = 3;
/**
* An object whose most recent element is a key. The next element must
* be a value.
*/
static final int DANGLING_NAME = 4;
/**
* An object with at least one name/value pair requires a comma and
* newline before the next element.
*/
static final int NONEMPTY_OBJECT = 5;
/**
* No object or array has been started.
*/
static final int EMPTY_DOCUMENT = 6;
/**
* A document with at an array or object.
*/
static final int NONEMPTY_DOCUMENT = 7;
/**
* A document that's been closed and cannot be accessed.
*/
static final int CLOSED = 8;
}

View File

@ -0,0 +1,86 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.Reader;
import java.io.Writer;
public class JsonStream extends JsonWriter {
private final ObjectJsonStreamer objectJsonStreamer;
public interface Streamable {
void toStream(@NonNull JsonStream stream) throws IOException;
}
private final Writer out;
/**
* Constructs a JSONStream
*
* @param out the writer
*/
public JsonStream(@NonNull Writer out) {
super(out);
setSerializeNulls(false);
this.out = out;
objectJsonStreamer = new ObjectJsonStreamer();
}
// Allow chaining name().value()
@NonNull
public JsonStream name(@Nullable String name) throws IOException {
super.name(name);
return this;
}
/**
* Serialises an arbitrary object as JSON, handling primitive types as well as
* Collections, Maps, and arrays.
*/
public void value(@Nullable Object object, boolean shouldRedactKeys) throws IOException {
if (object instanceof Streamable) {
((Streamable) object).toStream(this);
} else {
objectJsonStreamer.objectToStream(object, this, shouldRedactKeys);
}
}
/**
* Serialises an arbitrary object as JSON, handling primitive types as well as
* Collections, Maps, and arrays.
*/
public void value(@Nullable Object object) throws IOException {
value(object, false);
}
/**
* Writes a File (its content) into the stream
*/
public void value(@NonNull File file) throws IOException {
if (file == null || file.length() <= 0) {
return;
}
super.flush();
beforeValue(); // add comma if in array
// Copy the file contents onto the stream
Reader input = null;
try {
FileInputStream fis = new FileInputStream(file);
input = new BufferedReader(new InputStreamReader(fis, "UTF-8"));
IOUtils.copy(input, out);
} finally {
IOUtils.closeQuietly(input);
}
out.flush();
}
}

View File

@ -0,0 +1,663 @@
/*
* Copyright (C) 2010 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.bugsnag.android;
// last retrieved from gson-parent-2.8.5 on 17/01/2019
// https://github.com/google/gson/tree/gson-parent-2.8.5/gson/src/main/java/com/google/gson/stream
import static com.bugsnag.android.JsonScope.DANGLING_NAME;
import static com.bugsnag.android.JsonScope.EMPTY_ARRAY;
import static com.bugsnag.android.JsonScope.EMPTY_DOCUMENT;
import static com.bugsnag.android.JsonScope.EMPTY_OBJECT;
import static com.bugsnag.android.JsonScope.NONEMPTY_ARRAY;
import static com.bugsnag.android.JsonScope.NONEMPTY_DOCUMENT;
import static com.bugsnag.android.JsonScope.NONEMPTY_OBJECT;
import java.io.Closeable;
import java.io.Flushable;
import java.io.IOException;
import java.io.Writer;
/**
* Writes a JSON (<a href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>)
* encoded value to a stream, one token at a time. The stream includes both
* literal values (strings, numbers, booleans and nulls) as well as the begin
* and end delimiters of objects and arrays.
*
* <h3>Encoding JSON</h3>
* To encode your data as JSON, create a new {@code JsonWriter}. Each JSON
* document must contain one top-level array or object. Call methods on the
* writer as you walk the structure's contents, nesting arrays and objects as
* necessary:
* <ul>
* <li>To write <strong>arrays</strong>, first call {@link #beginArray()}.
* Write each of the array's elements with the appropriate {@link #value}
* methods or by nesting other arrays and objects. Finally close the array
* using {@link #endArray()}.
* <li>To write <strong>objects</strong>, first call {@link #beginObject()}.
* Write each of the object's properties by alternating calls to
* {@link #name} with the property's value. Write property values with the
* appropriate {@link #value} method or by nesting other objects or arrays.
* Finally close the object using {@link #endObject()}.
* </ul>
*
* <h3>Example</h3>
* Suppose we'd like to encode a stream of messages such as the following: <pre> {@code
* [
* {
* "id": 912345678901,
* "text": "How do I stream JSON in Java?",
* "geo": null,
* "user": {
* "name": "json_newb",
* "followers_count": 41
* }
* },
* {
* "id": 912345678902,
* "text": "@json_newb just use JsonWriter!",
* "geo": [50.454722, -104.606667],
* "user": {
* "name": "jesse",
* "followers_count": 2
* }
* }
* ]}</pre>
* This code encodes the above structure: <pre> {@code
* public void writeJsonStream(OutputStream out, List<Message> messages) throws IOException {
* JsonWriter writer = new JsonWriter(new OutputStreamWriter(out, "UTF-8"));
* writer.setIndent(" ");
* writeMessagesArray(writer, messages);
* writer.close();
* }
*
* public void writeMessagesArray(JsonWriter writer, List<Message> messages) throws IOException {
* writer.beginArray();
* for (Message message : messages) {
* writeMessage(writer, message);
* }
* writer.endArray();
* }
*
* public void writeMessage(JsonWriter writer, Message message) throws IOException {
* writer.beginObject();
* writer.name("id").value(message.getId());
* writer.name("text").value(message.getText());
* if (message.getGeo() != null) {
* writer.name("geo");
* writeDoublesArray(writer, message.getGeo());
* } else {
* writer.name("geo").nullValue();
* }
* writer.name("user");
* writeUser(writer, message.getUser());
* writer.endObject();
* }
*
* public void writeUser(JsonWriter writer, User user) throws IOException {
* writer.beginObject();
* writer.name("name").value(user.getMessage());
* writer.name("followers_count").value(user.getFollowersCount());
* writer.endObject();
* }
*
* public void writeDoublesArray(JsonWriter writer, List<Double> doubles) throws IOException {
* writer.beginArray();
* for (Double value : doubles) {
* writer.value(value);
* }
* writer.endArray();
* }}</pre>
*
* <p>Each {@code JsonWriter} may be used to write a single JSON stream.
* Instances of this class are not thread safe. Calls that would result in a
* malformed JSON string will fail with an {@link IllegalStateException}.
*
* @author Jesse Wilson
* @since 1.6
*/
@SuppressWarnings("all")
class JsonWriter implements Closeable, Flushable {
/*
* From RFC 7159, "All Unicode characters may be placed within the
* quotation marks except for the characters that must be escaped:
* quotation mark, reverse solidus, and the control characters
* (U+0000 through U+001F)."
*
* We also escape '\u2028' and '\u2029', which JavaScript interprets as
* newline characters. This prevents eval() from failing with a syntax
* error. http://code.google.com/p/google-gson/issues/detail?id=341
*/
private static final String[] REPLACEMENT_CHARS;
private static final String[] HTML_SAFE_REPLACEMENT_CHARS;
static {
REPLACEMENT_CHARS = new String[128];
for (int i = 0; i <= 0x1f; i++) {
REPLACEMENT_CHARS[i] = String.format("\\u%04x", i);
}
REPLACEMENT_CHARS['"'] = "\\\"";
REPLACEMENT_CHARS['\\'] = "\\\\";
REPLACEMENT_CHARS['\t'] = "\\t";
REPLACEMENT_CHARS['\b'] = "\\b";
REPLACEMENT_CHARS['\n'] = "\\n";
REPLACEMENT_CHARS['\r'] = "\\r";
REPLACEMENT_CHARS['\f'] = "\\f";
HTML_SAFE_REPLACEMENT_CHARS = REPLACEMENT_CHARS.clone();
HTML_SAFE_REPLACEMENT_CHARS['<'] = "\\u003c";
HTML_SAFE_REPLACEMENT_CHARS['>'] = "\\u003e";
HTML_SAFE_REPLACEMENT_CHARS['&'] = "\\u0026";
HTML_SAFE_REPLACEMENT_CHARS['='] = "\\u003d";
HTML_SAFE_REPLACEMENT_CHARS['\''] = "\\u0027";
}
/** The output data, containing at most one top-level array or object. */
private final Writer out;
private int[] stack = new int[32];
private int stackSize = 0;
{
push(EMPTY_DOCUMENT);
}
/**
* A string containing a full set of spaces for a single level of
* indentation, or null for no pretty printing.
*/
private String indent;
/**
* The name/value separator; either ":" or ": ".
*/
private String separator = ":";
private boolean lenient;
private boolean htmlSafe;
private String deferredName;
private boolean serializeNulls = true;
/**
* Creates a new instance that writes a JSON-encoded stream to {@code out}.
* For best performance, ensure {@link Writer} is buffered; wrapping in
* {@link java.io.BufferedWriter BufferedWriter} if necessary.
*/
public JsonWriter(Writer out) {
if (out == null) {
throw new NullPointerException("out == null");
}
this.out = out;
}
/**
* Sets the indentation string to be repeated for each level of indentation
* in the encoded document. If {@code indent.isEmpty()} the encoded document
* will be compact. Otherwise the encoded document will be more
* human-readable.
*
* @param indent a string containing only whitespace.
*/
public final void setIndent(String indent) {
if (indent.length() == 0) {
this.indent = null;
this.separator = ":";
} else {
this.indent = indent;
this.separator = ": ";
}
}
/**
* Configure this writer to relax its syntax rules. By default, this writer
* only emits well-formed JSON as specified by <a
* href="http://www.ietf.org/rfc/rfc7159.txt">RFC 7159</a>. Setting the writer
* to lenient permits the following:
* <ul>
* <li>Top-level values of any type. With strict writing, the top-level
* value must be an object or an array.
* <li>Numbers may be {@link Double#isNaN() NaNs} or {@link
* Double#isInfinite() infinities}.
* </ul>
*/
public final void setLenient(boolean lenient) {
this.lenient = lenient;
}
/**
* Returns true if this writer has relaxed syntax rules.
*/
public boolean isLenient() {
return lenient;
}
/**
* Configure this writer to emit JSON that's safe for direct inclusion in HTML
* and XML documents. This escapes the HTML characters {@code <}, {@code >},
* {@code &} and {@code =} before writing them to the stream. Without this
* setting, your XML/HTML encoder should replace these characters with the
* corresponding escape sequences.
*/
public final void setHtmlSafe(boolean htmlSafe) {
this.htmlSafe = htmlSafe;
}
/**
* Returns true if this writer writes JSON that's safe for inclusion in HTML
* and XML documents.
*/
public final boolean isHtmlSafe() {
return htmlSafe;
}
/**
* Sets whether object members are serialized when their value is null.
* This has no impact on array elements. The default is true.
*/
public final void setSerializeNulls(boolean serializeNulls) {
this.serializeNulls = serializeNulls;
}
/**
* Returns true if object members are serialized when their value is null.
* This has no impact on array elements. The default is true.
*/
public final boolean getSerializeNulls() {
return serializeNulls;
}
/**
* Begins encoding a new array. Each call to this method must be paired with
* a call to {@link #endArray}.
*
* @return this writer.
*/
public JsonWriter beginArray() throws IOException {
writeDeferredName();
return open(EMPTY_ARRAY, "[");
}
/**
* Ends encoding the current array.
*
* @return this writer.
*/
public JsonWriter endArray() throws IOException {
return close(EMPTY_ARRAY, NONEMPTY_ARRAY, "]");
}
/**
* Begins encoding a new object. Each call to this method must be paired
* with a call to {@link #endObject}.
*
* @return this writer.
*/
public JsonWriter beginObject() throws IOException {
writeDeferredName();
return open(EMPTY_OBJECT, "{");
}
/**
* Ends encoding the current object.
*
* @return this writer.
*/
public JsonWriter endObject() throws IOException {
return close(EMPTY_OBJECT, NONEMPTY_OBJECT, "}");
}
/**
* Enters a new scope by appending any necessary whitespace and the given
* bracket.
*/
private JsonWriter open(int empty, String openBracket) throws IOException {
beforeValue();
push(empty);
out.write(openBracket);
return this;
}
/**
* Closes the current scope by appending any necessary whitespace and the
* given bracket.
*/
private JsonWriter close(int empty, int nonempty, String closeBracket)
throws IOException {
int context = peek();
if (context != nonempty && context != empty) {
throw new IllegalStateException("Nesting problem.");
}
if (deferredName != null) {
throw new IllegalStateException("Dangling name: " + deferredName);
}
stackSize--;
if (context == nonempty) {
newline();
}
out.write(closeBracket);
return this;
}
private void push(int newTop) {
if (stackSize == stack.length) {
int[] newStack = new int[stackSize * 2];
System.arraycopy(stack, 0, newStack, 0, stackSize);
stack = newStack;
}
stack[stackSize++] = newTop;
}
/**
* Returns the value on the top of the stack.
*/
private int peek() {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
return stack[stackSize - 1];
}
/**
* Replace the value on the top of the stack with the given value.
*/
private void replaceTop(int topOfStack) {
stack[stackSize - 1] = topOfStack;
}
/**
* Encodes the property name.
*
* @param name the name of the forthcoming value. May not be null.
* @return this writer.
*/
public JsonWriter name(String name) throws IOException {
if (name == null) {
throw new NullPointerException("name == null");
}
if (deferredName != null) {
throw new IllegalStateException();
}
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
deferredName = name;
return this;
}
private void writeDeferredName() throws IOException {
if (deferredName != null) {
beforeName();
string(deferredName);
deferredName = null;
}
}
/**
* Encodes {@code value}.
*
* @param value the literal string value, or null to encode a null literal.
* @return this writer.
*/
public JsonWriter value(String value) throws IOException {
if (value == null) {
return nullValue();
}
writeDeferredName();
beforeValue();
string(value);
return this;
}
/**
* Writes {@code value} directly to the writer without quoting or
* escaping.
*
* @param value the literal string value, or null to encode a null literal.
* @return this writer.
*/
public JsonWriter jsonValue(String value) throws IOException {
if (value == null) {
return nullValue();
}
writeDeferredName();
beforeValue();
out.append(value);
return this;
}
/**
* Encodes {@code null}.
*
* @return this writer.
*/
public JsonWriter nullValue() throws IOException {
if (deferredName != null) {
if (serializeNulls) {
writeDeferredName();
} else {
deferredName = null;
return this; // skip the name and the value
}
}
beforeValue();
out.write("null");
return this;
}
/**
* Encodes {@code value}.
*
* @return this writer.
*/
public JsonWriter value(boolean value) throws IOException {
writeDeferredName();
beforeValue();
out.write(value ? "true" : "false");
return this;
}
/**
* Encodes {@code value}.
*
* @return this writer.
*/
public JsonWriter value(Boolean value) throws IOException {
if (value == null) {
return nullValue();
}
writeDeferredName();
beforeValue();
out.write(value ? "true" : "false");
return this;
}
/**
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @return this writer.
*/
public JsonWriter value(double value) throws IOException {
writeDeferredName();
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
beforeValue();
out.append(Double.toString(value));
return this;
}
/**
* Encodes {@code value}.
*
* @return this writer.
*/
public JsonWriter value(long value) throws IOException {
writeDeferredName();
beforeValue();
out.write(Long.toString(value));
return this;
}
/**
* Encodes {@code value}.
*
* @param value a finite value. May not be {@link Double#isNaN() NaNs} or
* {@link Double#isInfinite() infinities}.
* @return this writer.
*/
public JsonWriter value(Number value) throws IOException {
if (value == null) {
return nullValue();
}
writeDeferredName();
String string = value.toString();
if (!lenient
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
throw new IllegalArgumentException("Numeric values must be finite, but was " + value);
}
beforeValue();
out.append(string);
return this;
}
/**
* Ensures all buffered data is written to the underlying {@link Writer}
* and flushes that writer.
*/
public void flush() throws IOException {
if (stackSize == 0) {
throw new IllegalStateException("JsonWriter is closed.");
}
out.flush();
}
/**
* Flushes and closes this writer and the underlying {@link Writer}.
*
* @throws IOException if the JSON document is incomplete.
*/
public void close() throws IOException {
out.close();
int size = stackSize;
if (size > 1 || size == 1 && stack[size - 1] != NONEMPTY_DOCUMENT) {
throw new IOException("Incomplete document");
}
stackSize = 0;
}
private void string(String value) throws IOException {
String[] replacements = htmlSafe ? HTML_SAFE_REPLACEMENT_CHARS : REPLACEMENT_CHARS;
out.write("\"");
int last = 0;
int length = value.length();
for (int i = 0; i < length; i++) {
char c = value.charAt(i);
String replacement;
if (c < 128) {
replacement = replacements[c];
if (replacement == null) {
continue;
}
} else if (c == '\u2028') {
replacement = "\\u2028";
} else if (c == '\u2029') {
replacement = "\\u2029";
} else {
continue;
}
if (last < i) {
out.write(value, last, i - last);
}
out.write(replacement);
last = i + 1;
}
if (last < length) {
out.write(value, last, length - last);
}
out.write("\"");
}
private void newline() throws IOException {
if (indent == null) {
return;
}
out.write("\n");
for (int i = 1, size = stackSize; i < size; i++) {
out.write(indent);
}
}
/**
* Inserts any necessary separators and whitespace before a name. Also
* adjusts the stack to expect the name's value.
*/
private void beforeName() throws IOException {
int context = peek();
if (context == NONEMPTY_OBJECT) { // first in object
out.write(',');
} else if (context != EMPTY_OBJECT) { // not in an object!
throw new IllegalStateException("Nesting problem.");
}
newline();
replaceTop(DANGLING_NAME);
}
/**
* Inserts any necessary separators and whitespace before a literal value,
* inline array, or inline object. Also adjusts the stack to expect either a
* closing bracket or another element.
*/
@SuppressWarnings("fallthrough")
void beforeValue() throws IOException {
switch (peek()) {
case NONEMPTY_DOCUMENT:
if (!lenient) {
throw new IllegalStateException(
"JSON must have only one top-level value.");
}
// fall-through
case EMPTY_DOCUMENT: // first in document
replaceTop(NONEMPTY_DOCUMENT);
break;
case EMPTY_ARRAY: // first in array
replaceTop(NONEMPTY_ARRAY);
newline();
break;
case NONEMPTY_ARRAY: // another in array
out.append(',');
newline();
break;
case DANGLING_NAME: // value for name
out.append(separator);
replaceTop(NONEMPTY_OBJECT);
break;
default:
throw new IllegalStateException("Nesting problem.");
}
}
}

View File

@ -0,0 +1,26 @@
package com.bugsnag.android
/**
* Provides information about the last launch of the application, if there was one.
*/
class LastRunInfo(
/**
* The number times the app has consecutively crashed during its launch period.
*/
val consecutiveLaunchCrashes: Int,
/**
* Whether the last app run ended with a crash, or was abnormally terminated by the system.
*/
val crashed: Boolean,
/**
* True if the previous app run ended with a crash during its launch period.
*/
val crashedDuringLaunch: Boolean
) {
override fun toString(): String {
return "LastRunInfo(consecutiveLaunchCrashes=$consecutiveLaunchCrashes, crashed=$crashed, crashedDuringLaunch=$crashedDuringLaunch)"
}
}

View File

@ -0,0 +1,95 @@
package com.bugsnag.android
import java.io.File
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
private const val KEY_VALUE_DELIMITER = "="
private const val KEY_CONSECUTIVE_LAUNCH_CRASHES = "consecutiveLaunchCrashes"
private const val KEY_CRASHED = "crashed"
private const val KEY_CRASHED_DURING_LAUNCH = "crashedDuringLaunch"
/**
* Persists/loads [LastRunInfo] on disk, which allows Bugsnag to determine
* whether the previous application launch crashed or not. This class is thread-safe.
*/
internal class LastRunInfoStore(config: ImmutableConfig) {
val file: File = File(config.persistenceDirectory, "last-run-info")
private val logger: Logger = config.logger
private val lock = ReentrantReadWriteLock()
fun persist(lastRunInfo: LastRunInfo) {
lock.writeLock().withLock {
try {
persistImpl(lastRunInfo)
} catch (exc: Throwable) {
logger.w("Unexpectedly failed to persist LastRunInfo.", exc)
}
}
}
private fun persistImpl(lastRunInfo: LastRunInfo) {
val text = KeyValueWriter().apply {
add(KEY_CONSECUTIVE_LAUNCH_CRASHES, lastRunInfo.consecutiveLaunchCrashes)
add(KEY_CRASHED, lastRunInfo.crashed)
add(KEY_CRASHED_DURING_LAUNCH, lastRunInfo.crashedDuringLaunch)
}.toString()
file.writeText(text)
logger.d("Persisted: $text")
}
fun load(): LastRunInfo? {
return lock.readLock().withLock {
try {
loadImpl()
} catch (exc: Throwable) {
logger.w("Unexpectedly failed to load LastRunInfo.", exc)
null
}
}
}
private fun loadImpl(): LastRunInfo? {
if (!file.exists()) {
return null
}
val lines = file.readText().split("\n").filter { it.isNotBlank() }
if (lines.size != 3) {
logger.w("Unexpected number of lines when loading LastRunInfo. Skipping load. $lines")
return null
}
return try {
val consecutiveLaunchCrashes = lines[0].asIntValue(KEY_CONSECUTIVE_LAUNCH_CRASHES)
val crashed = lines[1].asBooleanValue(KEY_CRASHED)
val crashedDuringLaunch = lines[2].asBooleanValue(KEY_CRASHED_DURING_LAUNCH)
val runInfo = LastRunInfo(consecutiveLaunchCrashes, crashed, crashedDuringLaunch)
logger.d("Loaded: $runInfo")
runInfo
} catch (exc: NumberFormatException) {
// unlikely case where information was serialized incorrectly
logger.w("Failed to read consecutiveLaunchCrashes from saved lastRunInfo", exc)
null
}
}
private fun String.asIntValue(key: String) =
substringAfter("$key$KEY_VALUE_DELIMITER").toInt()
private fun String.asBooleanValue(key: String) =
substringAfter("$key$KEY_VALUE_DELIMITER").toBoolean()
}
private class KeyValueWriter {
private val sb = StringBuilder()
fun add(key: String, value: Any) {
sb.appendln("$key$KEY_VALUE_DELIMITER$value")
}
override fun toString() = sb.toString()
}

View File

@ -0,0 +1,42 @@
package com.bugsnag.android
import java.util.concurrent.RejectedExecutionException
import java.util.concurrent.ScheduledThreadPoolExecutor
import java.util.concurrent.TimeUnit
import java.util.concurrent.atomic.AtomicBoolean
/**
* Tracks whether the app is currently in its launch period. This creates a timer of
* configuration.launchDurationMillis, after which which the launch period is considered
* complete. If this value is zero, then the user must manually call markLaunchCompleted().
*/
internal class LaunchCrashTracker @JvmOverloads constructor(
config: ImmutableConfig,
private val executor: ScheduledThreadPoolExecutor = ScheduledThreadPoolExecutor(1)
) : BaseObservable() {
private val launching = AtomicBoolean(true)
private val logger = config.logger
init {
val delay = config.launchDurationMillis
if (delay > 0) {
executor.executeExistingDelayedTasksAfterShutdownPolicy = false
try {
executor.schedule({ markLaunchCompleted() }, delay, TimeUnit.MILLISECONDS)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to schedule timer for LaunchCrashTracker", exc)
}
}
}
fun markLaunchCompleted() {
executor.shutdown()
launching.set(false)
notifyObservers(StateEvent.UpdateIsLaunching(false))
logger.d("App launch period marked as complete")
}
fun isLaunching() = launching.get()
}

View File

@ -0,0 +1,31 @@
package com.bugsnag.android;
import java.util.concurrent.atomic.AtomicBoolean;
class LibraryLoader {
private AtomicBoolean attemptedLoad = new AtomicBoolean();
/**
* Attempts to load a native library, returning false if the load was unsuccessful.
* <p>
* If a load was attempted and failed, an error report will be sent using the supplied client
* and OnErrorCallback.
*
* @param name the library name
* @param client the bugsnag client
* @param callback an OnErrorCallback
* @return true if the library was loaded, false if not
*/
boolean loadLibrary(String name, Client client, OnErrorCallback callback) {
if (!attemptedLoad.getAndSet(true)) {
try {
System.loadLibrary(name);
return true;
} catch (UnsatisfiedLinkError error) {
client.notify(error, callback);
}
}
return false;
}
}

View File

@ -0,0 +1,47 @@
package com.bugsnag.android
/**
* Logs internal messages from within the bugsnag notifier.
*/
interface Logger {
/**
* Logs a message at the error level.
*/
fun e(msg: String): Unit = Unit
/**
* Logs a message at the error level.
*/
fun e(msg: String, throwable: Throwable): Unit = Unit
/**
* Logs a message at the warning level.
*/
fun w(msg: String): Unit = Unit
/**
* Logs a message at the warning level.
*/
fun w(msg: String, throwable: Throwable): Unit = Unit
/**
* Logs a message at the info level.
*/
fun i(msg: String): Unit = Unit
/**
* Logs a message at the info level.
*/
fun i(msg: String, throwable: Throwable): Unit = Unit
/**
* Logs a message at the debug level.
*/
fun d(msg: String): Unit = Unit
/**
* Logs a message at the debug level.
*/
fun d(msg: String, throwable: Throwable): Unit = Unit
}

View File

@ -0,0 +1,149 @@
package com.bugsnag.android
import android.content.Context
import android.content.pm.PackageManager
import android.os.Bundle
import androidx.annotation.VisibleForTesting
import java.lang.IllegalArgumentException
internal class ManifestConfigLoader {
companion object {
// mandatory
private const val BUGSNAG_NS = "com.bugsnag.android"
private const val API_KEY = "$BUGSNAG_NS.API_KEY"
internal const val BUILD_UUID = "$BUGSNAG_NS.BUILD_UUID"
// detection
private const val AUTO_TRACK_SESSIONS = "$BUGSNAG_NS.AUTO_TRACK_SESSIONS"
private const val AUTO_DETECT_ERRORS = "$BUGSNAG_NS.AUTO_DETECT_ERRORS"
private const val PERSIST_USER = "$BUGSNAG_NS.PERSIST_USER"
private const val SEND_THREADS = "$BUGSNAG_NS.SEND_THREADS"
// endpoints
private const val ENDPOINT_NOTIFY = "$BUGSNAG_NS.ENDPOINT_NOTIFY"
private const val ENDPOINT_SESSIONS = "$BUGSNAG_NS.ENDPOINT_SESSIONS"
// app/project packages
private const val APP_VERSION = "$BUGSNAG_NS.APP_VERSION"
private const val VERSION_CODE = "$BUGSNAG_NS.VERSION_CODE"
private const val RELEASE_STAGE = "$BUGSNAG_NS.RELEASE_STAGE"
private const val ENABLED_RELEASE_STAGES = "$BUGSNAG_NS.ENABLED_RELEASE_STAGES"
private const val DISCARD_CLASSES = "$BUGSNAG_NS.DISCARD_CLASSES"
private const val PROJECT_PACKAGES = "$BUGSNAG_NS.PROJECT_PACKAGES"
private const val REDACTED_KEYS = "$BUGSNAG_NS.REDACTED_KEYS"
// misc
private const val MAX_BREADCRUMBS = "$BUGSNAG_NS.MAX_BREADCRUMBS"
private const val MAX_PERSISTED_EVENTS = "$BUGSNAG_NS.MAX_PERSISTED_EVENTS"
private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS"
private const val LAUNCH_CRASH_THRESHOLD_MS = "$BUGSNAG_NS.LAUNCH_CRASH_THRESHOLD_MS"
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
private const val APP_TYPE = "$BUGSNAG_NS.APP_TYPE"
}
fun load(ctx: Context, userSuppliedApiKey: String?): Configuration {
try {
val packageManager = ctx.packageManager
val packageName = ctx.packageName
val ai = packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA)
val data = ai.metaData
return load(data, userSuppliedApiKey)
} catch (exc: Exception) {
throw IllegalStateException("Bugsnag is unable to read config from manifest.", exc)
}
}
/**
* Populates the config with meta-data values supplied from the manifest as a Bundle.
*
* @param data the manifest bundle
*/
@VisibleForTesting
internal fun load(data: Bundle?, userSuppliedApiKey: String?): Configuration {
// get the api key from the JVM call, or lookup in the manifest if null
val apiKey = (userSuppliedApiKey ?: data?.getString(API_KEY))
?: throw IllegalArgumentException("No Bugsnag API key set")
val config = Configuration(apiKey)
if (data != null) {
loadDetectionConfig(config, data)
loadEndpointsConfig(config, data)
loadAppConfig(config, data)
// misc config
with(config) {
maxBreadcrumbs = data.getInt(MAX_BREADCRUMBS, maxBreadcrumbs)
maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents)
maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions)
launchDurationMillis = data.getInt(
LAUNCH_CRASH_THRESHOLD_MS,
launchDurationMillis.toInt()
).toLong()
launchDurationMillis = data.getInt(
LAUNCH_DURATION_MILLIS,
launchDurationMillis.toInt()
).toLong()
sendLaunchCrashesSynchronously = data.getBoolean(
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
sendLaunchCrashesSynchronously
)
}
}
return config
}
private fun loadDetectionConfig(config: Configuration, data: Bundle) {
with(config) {
autoTrackSessions = data.getBoolean(AUTO_TRACK_SESSIONS, autoTrackSessions)
autoDetectErrors = data.getBoolean(AUTO_DETECT_ERRORS, autoDetectErrors)
persistUser = data.getBoolean(PERSIST_USER, persistUser)
val str = data.getString(SEND_THREADS)
if (str != null) {
sendThreads = ThreadSendPolicy.fromString(str)
}
}
}
private fun loadEndpointsConfig(config: Configuration, data: Bundle) {
if (data.containsKey(ENDPOINT_NOTIFY)) {
val endpoint = data.getString(ENDPOINT_NOTIFY, config.endpoints.notify)
val sessionEndpoint = data.getString(ENDPOINT_SESSIONS, config.endpoints.sessions)
config.endpoints = EndpointConfiguration(endpoint, sessionEndpoint)
}
}
private fun loadAppConfig(config: Configuration, data: Bundle) {
with(config) {
releaseStage = data.getString(RELEASE_STAGE, config.releaseStage)
appVersion = data.getString(APP_VERSION, config.appVersion)
appType = data.getString(APP_TYPE, config.appType)
if (data.containsKey(VERSION_CODE)) {
versionCode = data.getInt(VERSION_CODE)
}
if (data.containsKey(ENABLED_RELEASE_STAGES)) {
enabledReleaseStages = getStrArray(data, ENABLED_RELEASE_STAGES, enabledReleaseStages)
}
discardClasses = getStrArray(data, DISCARD_CLASSES, discardClasses) ?: emptySet()
projectPackages = getStrArray(data, PROJECT_PACKAGES, emptySet()) ?: emptySet()
redactedKeys = getStrArray(data, REDACTED_KEYS, redactedKeys) ?: emptySet()
}
}
private fun getStrArray(
data: Bundle,
key: String,
default: Set<String>?
): Set<String>? {
val delimitedStr = data.getString(key)
return when (val ary = delimitedStr?.split(",")) {
null -> default
else -> ary.toSet()
}
}
}

View File

@ -0,0 +1,151 @@
@file:Suppress("UNCHECKED_CAST")
package com.bugsnag.android
import java.io.IOException
import java.util.concurrent.ConcurrentHashMap
/**
* A container for additional diagnostic information you'd like to send with
* every error report.
*
* Diagnostic information is presented on your Bugsnag dashboard in tabs.
*/
internal data class Metadata @JvmOverloads constructor(
internal val store: ConcurrentHashMap<String, Any> = ConcurrentHashMap()
) : JsonStream.Streamable, MetadataAware {
val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer()
var redactedKeys: Set<String>
get() = jsonStreamer.redactedKeys
set(value) {
jsonStreamer.redactedKeys = value
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
jsonStreamer.objectToStream(store, writer, true)
}
override fun addMetadata(section: String, value: Map<String, Any?>) {
value.entries.forEach {
addMetadata(section, it.key, it.value)
}
}
override fun addMetadata(section: String, key: String, value: Any?) {
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)
}
}
private fun insertValue(map: MutableMap<String, Any>, key: String, newValue: Any) {
var obj = newValue
// only merge if both the existing and new value are maps
val existingValue = map[key]
if (obj is MutableMap<*, *> && existingValue is MutableMap<*, *>) {
val maps = listOf(existingValue as Map<String, Any>, newValue as Map<String, Any>)
obj = mergeMaps(maps)
}
map[key] = obj
}
override fun clearMetadata(section: String) {
store.remove(section)
}
override fun clearMetadata(section: String, key: String) {
val tab = store[section]
if (tab is MutableMap<*, *>) {
tab.remove(key)
if (tab.isEmpty()) {
store.remove(section)
}
}
}
override fun getMetadata(section: String): Map<String, Any>? {
return store[section] as (Map<String, Any>?)
}
override fun getMetadata(section: String, key: String): Any? {
return when (val tab = store[section]) {
is Map<*, *> -> (tab as Map<String, Any>?)!![key]
else -> tab
}
}
fun toMap(): ConcurrentHashMap<String, Any> {
val hashMap = ConcurrentHashMap(store)
// deep copy each section
store.entries.forEach {
if (it.value is ConcurrentHashMap<*, *>) {
hashMap[it.key] = ConcurrentHashMap(it.value as ConcurrentHashMap<*, *>)
}
}
return hashMap
}
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))
newMeta.redactedKeys = redactKeys.toSet()
return newMeta
}
internal fun mergeMaps(data: List<Map<String, Any>>): ConcurrentHashMap<String, Any> {
val keys = data.flatMap { it.keys }.toSet()
val result = ConcurrentHashMap<String, Any>()
for (map in data) {
for (key in keys) {
getMergeValue(result, key, map)
}
}
return result
}
private fun getMergeValue(
result: ConcurrentHashMap<String, Any>,
key: String,
map: Map<String, Any>
) {
val baseValue = result[key]
val overridesValue = map[key]
if (overridesValue != null) {
if (baseValue is Map<*, *> && overridesValue is Map<*, *>) {
// Both original and overrides are Maps, go deeper
val first = baseValue as Map<String, Any>?
val second = overridesValue as Map<String, Any>?
result[key] = mergeMaps(listOf(first!!, second!!))
} else {
result[key] = overridesValue
}
} else {
if (baseValue != null) { // No collision, just use base value
result[key] = baseValue
}
}
}
}
fun copy(): Metadata {
return this.copy(store = toMap())
.also { it.redactedKeys = redactedKeys.toSet() }
}
}

View File

@ -0,0 +1,12 @@
package com.bugsnag.android
internal interface MetadataAware {
fun addMetadata(section: String, value: Map<String, Any?>)
fun addMetadata(section: String, key: String, value: Any?)
fun clearMetadata(section: String)
fun clearMetadata(section: String, key: String)
fun getMetadata(section: String): Map<String, Any>?
fun getMetadata(section: String, key: String): Any?
}

View File

@ -0,0 +1,67 @@
package com.bugsnag.android
import com.bugsnag.android.StateEvent.AddMetadata
internal data class MetadataState(val metadata: Metadata = Metadata()) :
BaseObservable(),
MetadataAware {
override fun addMetadata(section: String, value: Map<String, Any?>) {
metadata.addMetadata(section, value)
notifyMetadataAdded(section, value)
}
override fun addMetadata(section: String, key: String, value: Any?) {
metadata.addMetadata(section, key, value)
notifyMetadataAdded(section, key, value)
}
override fun clearMetadata(section: String) {
metadata.clearMetadata(section)
notifyClear(section, null)
}
override fun clearMetadata(section: String, key: String) {
metadata.clearMetadata(section, key)
notifyClear(section, key)
}
private fun notifyClear(section: String, key: String?) {
when (key) {
null -> notifyObservers(StateEvent.ClearMetadataSection(section))
else -> notifyObservers(StateEvent.ClearMetadataValue(section, key))
}
}
override fun getMetadata(section: String) = metadata.getMetadata(section)
override fun getMetadata(section: String, key: String) = metadata.getMetadata(section, key)
/**
* Fires the initial observable messages for all the metadata which has been added before an
* Observer was added. This is used initially to populate the NDK with data.
*/
fun emitObservableEvent() {
val sections = metadata.store.keys
for (section in sections) {
val data = metadata.getMetadata(section)
data?.entries?.forEach {
notifyMetadataAdded(section, it.key, it.value)
}
}
}
private fun notifyMetadataAdded(section: String, key: String, value: Any?) {
when (value) {
null -> notifyClear(section, key)
else -> notifyObservers(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)))
}
}
}

View File

@ -0,0 +1,407 @@
package com.bugsnag.android;
import android.annotation.SuppressLint;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.nio.charset.Charset;
import java.util.Collection;
import java.util.Date;
import java.util.HashMap;
import java.util.List;
import java.util.Locale;
import java.util.Map;
/**
* Used as the entry point for native code to allow proguard to obfuscate other areas if needed
*/
public class NativeInterface {
// The default charset on Android is always UTF-8
private static Charset UTF8Charset = Charset.defaultCharset();
/**
* Static reference used if not using Bugsnag.start()
*/
@SuppressLint("StaticFieldLeak")
private static Client client;
@NonNull
private static Client getClient() {
if (client != null) {
return client;
} else {
return Bugsnag.getClient();
}
}
/**
* Caches a client instance for responding to future events
*/
public static void setClient(@NonNull Client client) {
NativeInterface.client = client;
}
@Nullable
public static String getContext() {
return getClient().getContext();
}
/**
* Retrieves the directory used to store native crash reports
*/
@NonNull
public static String getNativeReportPath() {
ImmutableConfig config = getClient().getConfig();
File persistenceDirectory = config.getPersistenceDirectory();
return new File(persistenceDirectory, "bugsnag-native").getAbsolutePath();
}
/**
* Retrieve user data from the static Client instance as a Map
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,String> getUser() {
HashMap<String, String> userData = new HashMap<>();
User user = getClient().getUser();
userData.put("id", user.getId());
userData.put("name", user.getName());
userData.put("email", user.getEmail());
return userData;
}
/**
* Retrieve app data from the static Client instance as a Map
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getApp() {
HashMap<String,Object> data = new HashMap<>();
AppDataCollector source = getClient().getAppDataCollector();
AppWithState app = source.generateAppWithState();
data.put("version", app.getVersion());
data.put("releaseStage", app.getReleaseStage());
data.put("id", app.getId());
data.put("type", app.getType());
data.put("buildUUID", app.getBuildUuid());
data.put("duration", app.getDuration());
data.put("durationInForeground", app.getDurationInForeground());
data.put("versionCode", app.getVersionCode());
data.put("inForeground", app.getInForeground());
data.put("isLaunching", app.isLaunching());
data.put("binaryArch", app.getBinaryArch());
data.putAll(source.getAppDataMetadata());
return data;
}
/**
* Retrieve device data from the static Client instance as a Map
*/
@NonNull
@SuppressWarnings("unused")
public static Map<String,Object> getDevice() {
DeviceDataCollector source = getClient().getDeviceDataCollector();
HashMap<String, Object> deviceData = new HashMap<>(source.getDeviceMetadata());
DeviceWithState src = source.generateDeviceWithState(new Date().getTime());
deviceData.put("freeDisk", src.getFreeDisk());
deviceData.put("freeMemory", src.getFreeMemory());
deviceData.put("orientation", src.getOrientation());
deviceData.put("time", src.getTime());
deviceData.put("cpuAbi", src.getCpuAbi());
deviceData.put("jailbroken", src.getJailbroken());
deviceData.put("id", src.getId());
deviceData.put("locale", src.getLocale());
deviceData.put("manufacturer", src.getManufacturer());
deviceData.put("model", src.getModel());
deviceData.put("osName", src.getOsName());
deviceData.put("osVersion", src.getOsVersion());
deviceData.put("runtimeVersions", src.getRuntimeVersions());
deviceData.put("totalMemory", src.getTotalMemory());
return deviceData;
}
/**
* Retrieve the CPU ABI(s) for the current device
*/
@NonNull
public static String[] getCpuAbi() {
return getClient().getDeviceDataCollector().getCpuAbi();
}
/**
* Retrieves global metadata from the static Client instance as a Map
*/
@NonNull
public static Map<String, Object> getMetadata() {
return getClient().getMetadata();
}
/**
* Retrieves a list of stored breadcrumbs from the static Client instance
*/
@NonNull
public static List<Breadcrumb> getBreadcrumbs() {
return getClient().getBreadcrumbs();
}
/**
* Sets the user
*
* @param id id
* @param email email
* @param name name
*/
@SuppressWarnings("unused")
public static void setUser(@Nullable final String id,
@Nullable final String email,
@Nullable final String name) {
Client client = getClient();
client.setUser(id, email, name);
}
/**
* Sets the user
*
* @param idBytes id
* @param emailBytes email
* @param nameBytes name
*/
@SuppressWarnings("unused")
public static void setUser(@Nullable final byte[] idBytes,
@Nullable final byte[] emailBytes,
@Nullable final byte[] nameBytes) {
String id = idBytes == null ? null : new String(idBytes, UTF8Charset);
String email = emailBytes == null ? null : new String(emailBytes, UTF8Charset);
String name = nameBytes == null ? null : new String(nameBytes, UTF8Charset);
setUser(id, email, name);
}
/**
* Leave a "breadcrumb" log message
*/
public static void leaveBreadcrumb(@NonNull final String name,
@NonNull final BreadcrumbType type) {
if (name == null) {
return;
}
getClient().leaveBreadcrumb(name, new HashMap<String, Object>(), type);
}
/**
* Leave a "breadcrumb" log message
*/
public static void leaveBreadcrumb(@NonNull final byte[] nameBytes,
@NonNull final BreadcrumbType type) {
if (nameBytes == null) {
return;
}
String name = new String(nameBytes, UTF8Charset);
getClient().leaveBreadcrumb(name, new HashMap<String, Object>(), type);
}
/**
* Leaves a breadcrumb on the static client instance
*/
public static void leaveBreadcrumb(@NonNull String message,
@NonNull String type,
@NonNull Map<String, Object> metadata) {
String typeName = type.toUpperCase(Locale.US);
getClient().leaveBreadcrumb(message, metadata, BreadcrumbType.valueOf(typeName));
}
/**
* Remove metadata from subsequent exception reports
*/
public static void clearMetadata(@NonNull String section, @Nullable String key) {
if (key == null) {
getClient().clearMetadata(section);
} else {
getClient().clearMetadata(section, key);
}
}
/**
* Add metadata to subsequent exception reports
*/
public static void addMetadata(@NonNull final String tab,
@Nullable final String key,
@Nullable final Object value) {
getClient().addMetadata(tab, key, value);
}
/**
* Return the client report release stage
*/
@Nullable
public static String getReleaseStage() {
return getClient().getConfig().getReleaseStage();
}
/**
* Return the client session endpoint
*/
@NonNull
public static String getSessionEndpoint() {
return getClient().getConfig().getEndpoints().getSessions();
}
/**
* Return the client report endpoint
*/
@NonNull
public static String getEndpoint() {
return getClient().getConfig().getEndpoints().getNotify();
}
/**
* Set the client report context
*/
public static void setContext(@Nullable final String context) {
getClient().setContext(context);
}
/**
* Set the binary arch used in the application
*/
public static void setBinaryArch(@NonNull final String binaryArch) {
getClient().setBinaryArch(binaryArch);
}
/**
* Return the client report app version
*/
@Nullable
public static String getAppVersion() {
return getClient().getConfig().getAppVersion();
}
/**
* Return which release stages notify
*/
@Nullable
public static Collection<String> getEnabledReleaseStages() {
return getClient().getConfig().getEnabledReleaseStages();
}
/**
* Update the current session with a given start time, ID, and event counts
*/
public static void registerSession(long startedAt, @Nullable String sessionId,
int unhandledCount, int handledCount) {
Client client = getClient();
User user = client.getUser();
Date startDate = startedAt > 0 ? new Date(startedAt) : null;
client.getSessionTracker().registerExistingSession(startDate, sessionId, user,
unhandledCount, handledCount);
}
/**
* Deliver a report, serialized as an event JSON payload.
*
* @param releaseStageBytes The release stage in which the event was
* captured. Used to determine whether the report
* should be discarded, based on configured release
* stages
* @param payloadBytes The raw JSON payload of the event
* @param apiKey The apiKey for the event
* @param isLaunching whether the crash occurred when the app was launching
*/
@SuppressWarnings("unused")
public static void deliverReport(@Nullable byte[] releaseStageBytes,
@NonNull byte[] payloadBytes,
@NonNull String apiKey,
boolean isLaunching) {
if (payloadBytes == null) {
return;
}
String payload = new String(payloadBytes, UTF8Charset);
String releaseStage = releaseStageBytes == null
? null
: new String(releaseStageBytes, UTF8Charset);
Client client = getClient();
ImmutableConfig config = client.getConfig();
if (releaseStage == null
|| releaseStage.length() == 0
|| config.shouldNotifyForReleaseStage()) {
EventStore eventStore = client.getEventStore();
String filename = eventStore.getNdkFilename(payload, apiKey);
if (isLaunching) {
filename = filename.replace(".json", "startupcrash.json");
}
eventStore.enqueueContentForDelivery(payload, filename);
}
}
/**
* Notifies using the Android SDK
*
* @param nameBytes the error name
* @param messageBytes the error message
* @param severity the error severity
* @param stacktrace a stacktrace
*/
public static void notify(@NonNull final byte[] nameBytes,
@NonNull final byte[] messageBytes,
@NonNull final Severity severity,
@NonNull final StackTraceElement[] stacktrace) {
if (nameBytes == null || messageBytes == null || stacktrace == null) {
return;
}
String name = new String(nameBytes, UTF8Charset);
String message = new String(messageBytes, UTF8Charset);
notify(name, message, severity, stacktrace);
}
/**
* Notifies using the Android SDK
*
* @param name the error name
* @param message the error message
* @param severity the error severity
* @param stacktrace a stacktrace
*/
public static void notify(@NonNull final String name,
@NonNull final String message,
@NonNull final Severity severity,
@NonNull final StackTraceElement[] stacktrace) {
Throwable exc = new RuntimeException();
exc.setStackTrace(stacktrace);
getClient().notify(exc, new OnErrorCallback() {
@Override
public boolean onError(@NonNull Event event) {
event.updateSeverityInternal(severity);
List<Error> errors = event.getErrors();
Error error = event.getErrors().get(0);
// update the error's type to C
if (!errors.isEmpty()) {
error.setErrorClass(name);
error.setErrorMessage(message);
for (Error err : errors) {
err.setType(ErrorType.C);
}
}
return true;
}
});
}
@NonNull
public static Event createEvent(@Nullable Throwable exc,
@NonNull Client client,
@NonNull SeverityReason severityReason) {
Metadata metadata = client.getMetadataState().getMetadata();
return new Event(exc, client.getConfig(), severityReason, metadata, client.logger);
}
@NonNull
public static Logger getLogger() {
return getClient().getConfig().getLogger();
}
}

View File

@ -0,0 +1,61 @@
package com.bugsnag.android
import java.io.IOException
/**
* Represents a single native stackframe
*/
class NativeStackframe internal constructor(
/**
* The name of the method that was being executed
*/
var method: String?,
/**
* The location of the source file
*/
var file: String?,
/**
* The line number within the source file this stackframe refers to
*/
var lineNumber: Number?,
/**
* The address of the instruction where the event occurred.
*/
var frameAddress: Long?,
/**
* The address of the function where the event occurred.
*/
var symbolAddress: Long?,
/**
* The address of the library where the event occurred.
*/
var loadAddress: Long?
) : JsonStream.Streamable {
/**
* The type of the error
*/
var type: ErrorType? = ErrorType.C
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("method").value(method)
writer.name("file").value(file)
writer.name("lineNumber").value(lineNumber)
writer.name("frameAddress").value(frameAddress)
writer.name("symbolAddress").value(symbolAddress)
writer.name("loadAddress").value(loadAddress)
type?.let {
writer.name("type").value(it.desc)
}
writer.endObject()
}
}

View File

@ -0,0 +1,3 @@
package com.bugsnag.android
internal object NoopLogger : Logger

View File

@ -0,0 +1,31 @@
package com.bugsnag.android
import java.io.IOException
/**
* Information about this library, including name and version.
*/
class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier",
var version: String = "5.9.2",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {
var dependencies = listOf<Notifier>()
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("name").value(name)
writer.name("version").value(version)
writer.name("url").value(url)
if (dependencies.isNotEmpty()) {
writer.name("dependencies")
writer.beginArray()
dependencies.forEach { writer.value(it) }
writer.endArray()
}
writer.endObject()
}
}

View File

@ -0,0 +1,67 @@
package com.bugsnag.android
import java.io.IOException
import java.lang.reflect.Array
internal class ObjectJsonStreamer {
companion object {
private const val REDACTED_PLACEHOLDER = "[REDACTED]"
private const val OBJECT_PLACEHOLDER = "[OBJECT]"
}
var redactedKeys = setOf("password")
// Write complex/nested values to a JsonStreamer
@Throws(IOException::class)
fun objectToStream(obj: Any?, writer: JsonStream, shouldRedactKeys: Boolean = false) {
when {
obj == null -> writer.nullValue()
obj is String -> writer.value(obj)
obj is Number -> writer.value(obj)
obj is Boolean -> writer.value(obj)
obj is JsonStream.Streamable -> obj.toStream(writer)
obj is Map<*, *> -> mapToStream(writer, obj, shouldRedactKeys)
obj is Collection<*> -> collectionToStream(writer, obj)
obj.javaClass.isArray -> arrayToStream(writer, obj)
else -> writer.value(OBJECT_PLACEHOLDER)
}
}
private fun mapToStream(writer: JsonStream, obj: Map<*, *>, shouldRedactKeys: Boolean) {
writer.beginObject()
obj.entries.forEach {
val keyObj = it.key
if (keyObj is String) {
writer.name(keyObj)
if (shouldRedactKeys && isRedactedKey(keyObj)) {
writer.value(REDACTED_PLACEHOLDER)
} else {
objectToStream(it.value, writer, shouldRedactKeys)
}
}
}
writer.endObject()
}
private fun collectionToStream(writer: JsonStream, obj: Collection<*>) {
writer.beginArray()
obj.forEach { objectToStream(it, writer) }
writer.endArray()
}
private fun arrayToStream(writer: JsonStream, obj: Any) {
// Primitive array objects
writer.beginArray()
val length = Array.getLength(obj)
var i = 0
while (i < length) {
objectToStream(Array.get(obj, i), writer)
i += 1
}
writer.endArray()
}
// Should this key be redacted
private fun isRedactedKey(key: String) = redactedKeys.any { key.contains(it) }
}

View File

@ -0,0 +1,32 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
/**
* Add a "on breadcrumb" callback, to execute code before every
* breadcrumb captured by Bugsnag.
* <p>
* You can use this to modify breadcrumbs before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a breadcrumb.
* <p>
* For example:
* <p>
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
* public boolean onBreadcrumb(Breadcrumb breadcrumb) {
* return false; // ignore the breadcrumb
* }
* })
*/
public interface OnBreadcrumbCallback {
/**
* Runs the "on breadcrumb" callback. If the callback returns
* <code>false</code> any further OnBreadcrumbCallback callbacks will not be called
* and the breadcrumb will not be captured by Bugsnag.
*
* @param breadcrumb the breadcrumb to be captured by Bugsnag
* @see Breadcrumb
*/
boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb);
}

View File

@ -0,0 +1,24 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
/**
* A callback to be run before error reports are sent to Bugsnag.
* <p>
* <p>You can use this to add or modify information attached to an error
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to halt execution.
* <p>"on error" callbacks added via the JVM API do not run when a fatal C/C++ crash occurs.
*/
public interface OnErrorCallback {
/**
* Runs the "on error" callback. If the callback returns
* <code>false</code> any further OnErrorCallback callbacks will not be called
* and the event will not be sent to Bugsnag.
*
* @param event the event to be sent to Bugsnag
* @see Event
*/
boolean onError(@NonNull Event event);
}

View File

@ -0,0 +1,23 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
/**
* A callback to be run before sessions are sent to Bugsnag.
* <p>
* <p>You can use this to add or modify information attached to a session
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to halt execution.
*/
public interface OnSessionCallback {
/**
* Runs the "on session" callback. If the callback returns
* <code>false</code> any further OnSessionCallback callbacks will not be called
* and the session will not be sent to Bugsnag.
*
* @param session the session to be sent to Bugsnag
* @see Session
*/
boolean onSession(@NonNull Session session);
}

View File

@ -0,0 +1,19 @@
package com.bugsnag.android
/**
* A plugin allows for additional functionality to be added to the Bugsnag SDK.
*/
interface Plugin {
/**
* Loads a plugin with the given Client. When this method is invoked the plugin should
* activate its behaviour - for example, by capturing an additional source of errors.
*/
fun load(client: Client)
/**
* Unloads a plugin. When this is invoked the plugin should cease all custom behaviour and
* restore the application to its unloaded state.
*/
fun unload()
}

View File

@ -0,0 +1,47 @@
package com.bugsnag.android
internal class PluginClient(
userPlugins: Set<Plugin>,
immutableConfig: ImmutableConfig,
private val logger: Logger
) {
protected val plugins: Set<Plugin>
init {
val set = mutableSetOf<Plugin>()
set.addAll(userPlugins)
// instantiate ANR + NDK plugins by reflection as bugsnag-android-core has no
// direct dependency on the artefacts
if (immutableConfig.enabledErrorTypes.ndkCrashes) {
instantiatePlugin("com.bugsnag.android.NdkPlugin")?.let { set.add(it) }
}
if (immutableConfig.enabledErrorTypes.anrs) {
instantiatePlugin("com.bugsnag.android.AnrPlugin")?.let { set.add(it) }
}
instantiatePlugin("com.bugsnag.android.BugsnagReactNativePlugin")?.let { set.add(it) }
plugins = set.toSet()
}
private fun instantiatePlugin(clz: String): Plugin? {
return try {
val pluginClz = Class.forName(clz)
pluginClz.newInstance() as Plugin
} catch (exc: ClassNotFoundException) {
logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.")
null
} catch (exc: Throwable) {
logger.e("Failed to load plugin '$clz'", exc)
null
}
}
fun loadPlugins(client: Client) = plugins.forEach {
try {
it.load(client)
} catch (exc: Throwable) {
logger.e("Failed to load plugin $it, continuing with initialisation.", exc)
}
}
}

View File

@ -0,0 +1,141 @@
package com.bugsnag.android
import androidx.annotation.VisibleForTesting
import java.io.BufferedReader
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicBoolean
/**
* Attempts to detect whether the device is rooted. Root detection errs on the side of false
* negatives rather than false positives.
*
* This class will only give a reasonable indication that a device has been rooted - as it's
* possible to manipulate Java return values & native library loading, it will always be possible
* for a determined application to defeat these root checks.
*/
internal class RootDetector @JvmOverloads constructor(
private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo(),
private val rootBinaryLocations: List<String> = ROOT_INDICATORS,
private val buildProps: File = BUILD_PROP_FILE,
private val logger: Logger
) {
companion object {
private val BUILD_PROP_FILE = File("/system/build.prop")
private val ROOT_INDICATORS = listOf(
// Common binaries
"/system/xbin/su",
"/system/bin/su",
// < Android 5.0
"/system/app/Superuser.apk",
"/system/app/SuperSU.apk",
// >= Android 5.0
"/system/app/Superuser",
"/system/app/SuperSU",
// Fallback
"/system/xbin/daemonsu",
// Systemless root
"/su/bin"
)
}
private val libraryLoaded = AtomicBoolean(false)
init {
try {
System.loadLibrary("bugsnag-root-detection")
libraryLoaded.set(true)
} catch (ignored: UnsatisfiedLinkError) {
// library couldn't load. This could be due to root detection countermeasures,
// or down to genuine OS level bugs with library loading - in either case
// Bugsnag will default to skipping the checks.
}
}
/**
* Determines whether the device is rooted or not.
*/
fun isRooted(): Boolean {
return try {
checkBuildTags() || checkSuExists() || checkBuildProps() || checkRootBinaries() || nativeCheckRoot()
} catch (exc: Throwable) {
logger.w("Root detection failed", exc)
false
}
}
/**
* Checks whether the su binary exists by running `which su`. A non-empty result
* indicates that the binary is present, which is a good indicator that the device
* may have been rooted.
*/
private fun checkSuExists(): Boolean = checkSuExists(ProcessBuilder())
/**
* Checks whether the build tags contain 'test-keys', which indicates that the OS was signed
* with non-standard keys.
*/
internal fun checkBuildTags(): Boolean = deviceBuildInfo.tags?.contains("test-keys") == true
/**
* Checks whether common root binaries exist on disk, which are a good indicator of whether
* the device is rooted.
*/
internal fun checkRootBinaries(): Boolean {
runCatching {
for (candidate in rootBinaryLocations) {
if (File(candidate).exists()) {
return true
}
}
}
return false
}
/**
* Checks the contents of /system/build.prop to see whether it contains dangerous properties.
* These properties give a good indication that a phone might be using a custom
* ROM and is therefore rooted.
*/
internal fun checkBuildProps(): Boolean {
runCatching {
return buildProps.bufferedReader().useLines { lines ->
lines
.map { line ->
line.replace("\\s".toRegex(), "")
}.filter { line ->
line.startsWith("ro.debuggable=[1]") || line.startsWith("ro.secure=[0]")
}.count() > 0
}
}
return false
}
@VisibleForTesting
internal fun checkSuExists(processBuilder: ProcessBuilder): Boolean {
processBuilder.command(listOf("which", "su"))
var process: Process? = null
return try {
process = processBuilder.start()
val output = process.inputStream.bufferedReader().use(BufferedReader::readText)
output.isNotBlank()
} catch (ignored: IOException) {
false
} finally {
process?.destroy()
}
}
private external fun performNativeRootChecks(): Boolean
/**
* Performs root checks which require native code.
*/
private fun nativeCheckRoot(): Boolean = when {
libraryLoaded.get() -> performNativeRootChecks()
else -> false
}
}

View File

@ -0,0 +1,239 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Date;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicInteger;
/**
* Represents a contiguous session in an application.
*/
@SuppressWarnings("ConstantConditions")
public final class Session implements JsonStream.Streamable, UserAware {
private final File file;
private final Notifier notifier;
private String id;
private Date startedAt;
private User user;
private final Logger logger;
private App app;
private Device device;
private final AtomicBoolean autoCaptured = new AtomicBoolean(false);
private final AtomicInteger unhandledCount = new AtomicInteger();
private final AtomicInteger handledCount = new AtomicInteger();
private final AtomicBoolean tracked = new AtomicBoolean(false);
final AtomicBoolean isPaused = new AtomicBoolean(false);
static Session copySession(Session session) {
Session copy = new Session(session.id, session.startedAt, session.user,
session.unhandledCount.get(), session.handledCount.get(), session.notifier,
session.logger);
copy.tracked.set(session.tracked.get());
copy.autoCaptured.set(session.isAutoCaptured());
return copy;
}
Session(String id, Date startedAt, User user, boolean autoCaptured,
Notifier notifier, Logger logger) {
this(null, notifier, logger);
this.id = id;
this.startedAt = new Date(startedAt.getTime());
this.user = user;
this.autoCaptured.set(autoCaptured);
}
Session(String id, Date startedAt, User user, int unhandledCount, int handledCount,
Notifier notifier, Logger logger) {
this(id, startedAt, user, false, notifier, logger);
this.unhandledCount.set(unhandledCount);
this.handledCount.set(handledCount);
this.tracked.set(true);
}
Session(File file, Notifier notifier, Logger logger) {
this.file = file;
this.logger = logger;
Notifier copy = new Notifier(notifier.getName(), notifier.getVersion(), notifier.getUrl());
copy.setDependencies(new ArrayList<>(notifier.getDependencies()));
this.notifier = copy;
}
private void logNull(String property) {
logger.e("Invalid null value supplied to session." + property + ", ignoring");
}
/**
* Retrieves the session ID. This must be a unique value across all of your sessions.
*/
@NonNull
public String getId() {
return id;
}
/**
* Sets the session ID. This must be a unique value across all of your sessions.
*/
public void setId(@NonNull String id) {
if (id != null) {
this.id = id;
} else {
logNull("id");
}
}
/**
* Gets the session start time.
*/
@NonNull
public Date getStartedAt() {
return startedAt;
}
/**
* Sets the session start time.
*/
public void setStartedAt(@NonNull Date startedAt) {
if (startedAt != null) {
this.startedAt = startedAt;
} else {
logNull("startedAt");
}
}
/**
* Returns the currently set User information.
*/
@NonNull
@Override
public User getUser() {
return user;
}
/**
* Sets the user associated with the session.
*/
@Override
public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
user = new User(id, email, name);
}
/**
* Information set by the notifier about your app can be found in this field. These values
* can be accessed and amended if necessary.
*/
@NonNull
public App getApp() {
return app;
}
/**
* Information set by the notifier about your device can be found in this field. These values
* can be accessed and amended if necessary.
*/
@NonNull
public Device getDevice() {
return device;
}
void setApp(App app) {
this.app = app;
}
void setDevice(Device device) {
this.device = device;
}
int getUnhandledCount() {
return unhandledCount.intValue();
}
int getHandledCount() {
return handledCount.intValue();
}
Session incrementHandledAndCopy() {
handledCount.incrementAndGet();
return copySession(this);
}
Session incrementUnhandledAndCopy() {
unhandledCount.incrementAndGet();
return copySession(this);
}
AtomicBoolean isTracked() {
return tracked;
}
boolean isAutoCaptured() {
return autoCaptured.get();
}
void setAutoCaptured(boolean autoCaptured) {
this.autoCaptured.set(autoCaptured);
}
/**
* Determines whether a cached session payload is v1 (where only the session is stored)
* or v2 (where the whole payload including app/device is stored).
*
* @return whether the payload is v2
*/
boolean isV2Payload() {
return file != null && file.getName().endsWith("_v2.json");
}
Notifier getNotifier() {
return notifier;
}
@Override
public void toStream(@NonNull JsonStream writer) throws IOException {
if (file != null) {
if (isV2Payload()) {
serializeV2Payload(writer);
} else {
serializeV1Payload(writer);
}
} else {
writer.beginObject();
writer.name("notifier").value(notifier);
writer.name("app").value(app);
writer.name("device").value(device);
writer.name("sessions").beginArray();
serializeSessionInfo(writer);
writer.endArray();
writer.endObject();
}
}
private void serializeV2Payload(@NonNull JsonStream writer) throws IOException {
writer.value(file);
}
private void serializeV1Payload(@NonNull JsonStream writer) throws IOException {
writer.beginObject();
writer.name("notifier").value(notifier);
writer.name("app").value(app);
writer.name("device").value(device);
writer.name("sessions").beginArray();
writer.value(file);
writer.endArray();
writer.endObject();
}
void serializeSessionInfo(@NonNull JsonStream writer) throws IOException {
writer.beginObject();
writer.name("id").value(id);
writer.name("startedAt").value(DateUtils.toIso8601(startedAt));
writer.name("user").value(user);
writer.endObject();
}
}

View File

@ -0,0 +1,22 @@
package com.bugsnag.android
import android.app.Activity
import android.app.Application
import android.os.Bundle
internal class SessionLifecycleCallback(
private val sessionTracker: SessionTracker
) : Application.ActivityLifecycleCallbacks {
override fun onActivityStarted(activity: Activity) =
sessionTracker.onActivityStarted(activity.javaClass.simpleName)
override fun onActivityStopped(activity: Activity) =
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityResumed(activity: Activity) {}
override fun onActivityPaused(activity: Activity) {}
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {}
override fun onActivityDestroyed(activity: Activity) {}
}

View File

@ -0,0 +1,54 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
import java.util.Comparator;
import java.util.Locale;
import java.util.UUID;
/**
* Store and flush Sessions which couldn't be sent immediately due to
* lack of network connectivity.
*/
class SessionStore extends FileStore {
static final Comparator<File> SESSION_COMPARATOR = new Comparator<File>() {
@Override
public int compare(File lhs, File rhs) {
if (lhs == null && rhs == null) {
return 0;
}
if (lhs == null) {
return 1;
}
if (rhs == null) {
return -1;
}
String lhsName = lhs.getName();
String rhsName = rhs.getName();
return lhsName.compareTo(rhsName);
}
};
SessionStore(@NonNull ImmutableConfig config,
@NonNull Logger logger,
@Nullable Delegate delegate) {
super(new File(config.getPersistenceDirectory(), "bugsnag-sessions"),
config.getMaxPersistedSessions(),
SESSION_COMPARATOR,
logger,
delegate);
}
@NonNull
@Override
String getFilename(Object object) {
return String.format(Locale.US,
"%s%d_v2.json",
UUID.randomUUID().toString(),
System.currentTimeMillis());
}
}

View File

@ -0,0 +1,401 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File;
import java.util.Collection;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.UUID;
import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.RejectedExecutionException;
import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.atomic.AtomicReference;
class SessionTracker extends BaseObservable {
private static final int DEFAULT_TIMEOUT_MS = 30000;
private final Collection<String>
foregroundActivities = new ConcurrentLinkedQueue<>();
private final long timeoutMs;
private final ImmutableConfig configuration;
private final CallbackState callbackState;
private final Client client;
final SessionStore sessionStore;
// This most recent time an Activity was stopped.
private final AtomicLong lastExitedForegroundMs = new AtomicLong(0);
// The first Activity in this 'session' was started at this time.
private final AtomicLong lastEnteredForegroundMs = new AtomicLong(0);
private final AtomicReference<Session> currentSession = new AtomicReference<>();
private final ForegroundDetector foregroundDetector;
final BackgroundTaskService backgroundTaskService;
final Logger logger;
SessionTracker(ImmutableConfig configuration,
CallbackState callbackState,
Client client,
SessionStore sessionStore,
Logger logger,
BackgroundTaskService backgroundTaskService) {
this(configuration, callbackState, client, DEFAULT_TIMEOUT_MS,
sessionStore, logger, backgroundTaskService);
}
SessionTracker(ImmutableConfig configuration,
CallbackState callbackState,
Client client,
long timeoutMs,
SessionStore sessionStore,
Logger logger,
BackgroundTaskService backgroundTaskService) {
this.configuration = configuration;
this.callbackState = callbackState;
this.client = client;
this.timeoutMs = timeoutMs;
this.sessionStore = sessionStore;
this.foregroundDetector = new ForegroundDetector(client.getAppContext());
this.backgroundTaskService = backgroundTaskService;
this.logger = logger;
notifyNdkInForeground();
}
/**
* Starts a new session with the given date and user.
* <p>
* A session will only be created if {@link Configuration#getAutoTrackSessions()} returns
* true.
*
* @param date the session start date
* @param user the session user (if any)
*/
@Nullable
@VisibleForTesting
Session startNewSession(@NonNull Date date, @Nullable User user,
boolean autoCaptured) {
String id = UUID.randomUUID().toString();
Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger);
currentSession.set(session);
trackSessionIfNeeded(session);
return session;
}
Session startSession(boolean autoCaptured) {
return startNewSession(new Date(), client.getUser(), autoCaptured);
}
void pauseSession() {
Session session = currentSession.get();
if (session != null) {
session.isPaused.set(true);
notifyObservers(StateEvent.PauseSession.INSTANCE);
}
}
boolean resumeSession() {
Session session = currentSession.get();
boolean resumed;
if (session == null) {
session = startSession(false);
resumed = false;
} else {
resumed = session.isPaused.compareAndSet(true, false);
}
if (session != null) {
notifySessionStartObserver(session);
}
return resumed;
}
private void notifySessionStartObserver(Session session) {
String startedAt = DateUtils.toIso8601(session.getStartedAt());
notifyObservers(new StateEvent.StartSession(session.getId(), startedAt,
session.getHandledCount(), session.getUnhandledCount()));
}
/**
* Cache details of a previously captured session.
* Append session details to all subsequent reports.
*
* @param date the session start date
* @param sessionId the unique session identifier
* @param user the session user (if any)
* @param unhandledCount the number of unhandled events which have occurred during the session
* @param handledCount the number of handled events which have occurred during the session
* @return the session
*/
@Nullable
Session registerExistingSession(@Nullable Date date, @Nullable String sessionId,
@Nullable User user, int unhandledCount,
int handledCount) {
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);
}
currentSession.set(session);
return session;
}
/**
* Determines whether or not a session should be tracked. If this is true, the session will be
* stored and sent to the Bugsnag API, otherwise no action will occur in this method.
*
* @param session the session
*/
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)) {
notifySessionStartObserver(session);
flushAsync();
flushInMemorySession(session);
}
}
@Nullable
Session getCurrentSession() {
Session session = currentSession.get();
if (session != null && !session.isPaused.get()) {
return session;
}
return null;
}
/**
* Increments the unhandled error count on the current session, then returns a deep-copy
* of the current session.
*
* @return a copy of the current session, or null if no session has been started.
*/
Session incrementUnhandledAndCopy() {
Session session = getCurrentSession();
if (session != null) {
return session.incrementUnhandledAndCopy();
}
return null;
}
/**
* Increments the handled error count on the current session, then returns a deep-copy
* of the current session.
*
* @return a copy of the current session, or null if no session has been started.
*/
Session incrementHandledAndCopy() {
Session session = getCurrentSession();
if (session != null) {
return session.incrementHandledAndCopy();
}
return null;
}
/**
* Asynchronously flushes any session payloads stored on disk
*/
void flushAsync() {
try {
backgroundTaskService.submitTask(TaskType.SESSION_REQUEST, new Runnable() {
@Override
public void run() {
flushStoredSessions();
}
});
} catch (RejectedExecutionException ex) {
logger.w("Failed to flush session reports", ex);
}
}
/**
* Attempts to flush session payloads stored on disk
*/
void flushStoredSessions() {
List<File> storedFiles = sessionStore.findStoredFiles();
for (File storedFile : storedFiles) {
flushStoredSession(storedFile);
}
}
void flushStoredSession(File storedFile) {
logger.d("SessionTracker#flushStoredSession() - attempting delivery");
Session payload = new Session(storedFile, client.getNotifier(), logger);
if (!payload.isV2Payload()) { // collect data here
payload.setApp(client.getAppDataCollector().generateApp());
payload.setDevice(client.getDeviceDataCollector().generateDevice());
}
DeliveryStatus deliveryStatus = deliverSessionPayload(payload);
switch (deliveryStatus) {
case DELIVERED:
sessionStore.deleteStoredFiles(Collections.singletonList(storedFile));
logger.d("Sent 1 new session to Bugsnag");
break;
case UNDELIVERED:
sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile));
logger.w("Leaving session payload for future delivery");
break;
case FAILURE:
// drop bad data
logger.w("Deleting invalid session tracking payload");
sessionStore.deleteStoredFiles(Collections.singletonList(storedFile));
break;
default:
break;
}
}
private void flushInMemorySession(final Session session) {
try {
backgroundTaskService.submitTask(TaskType.SESSION_REQUEST, new Runnable() {
@Override
public void run() {
deliverInMemorySession(session);
}
});
} catch (RejectedExecutionException exception) {
// This is on the current thread but there isn't much else we can do
sessionStore.write(session);
}
}
void deliverInMemorySession(Session session) {
try {
logger.d("SessionTracker#trackSessionIfNeeded() - attempting initial delivery");
DeliveryStatus deliveryStatus = deliverSessionPayload(session);
switch (deliveryStatus) {
case UNDELIVERED:
logger.w("Storing session payload for future delivery");
sessionStore.write(session);
break;
case FAILURE:
logger.w("Dropping invalid session tracking payload");
break;
case DELIVERED:
logger.d("Sent 1 new session to Bugsnag");
break;
default:
break;
}
} catch (Exception exception) {
logger.w("Session tracking payload failed", exception);
}
}
DeliveryStatus deliverSessionPayload(Session payload) {
DeliveryParams params = configuration.getSessionApiDeliveryParams();
Delivery delivery = configuration.getDelivery();
return delivery.deliver(payload, params);
}
void onActivityStarted(String activityName) {
updateForegroundTracker(activityName, true, System.currentTimeMillis());
}
void onActivityStopped(String activityName) {
updateForegroundTracker(activityName, false, System.currentTimeMillis());
}
/**
* Tracks whether an activity is in the foreground or not.
* <p>
* If an activity leaves the foreground, a timeout should be recorded (e.g. 30s), during which
* no new sessions should be automatically started.
* <p>
* If an activity comes to the foreground and is the only foreground activity, a new session
* should be started, unless the app is within a timeout period.
*
* @param activityName the activity name
* @param activityStarting whether the activity is being started or not
* @param nowMs The current time in ms
*/
void updateForegroundTracker(String activityName, boolean activityStarting, long nowMs) {
if (activityStarting) {
long noActivityRunningForMs = nowMs - lastExitedForegroundMs.get();
//FUTURE:SM Race condition between isEmpty and put
if (foregroundActivities.isEmpty()) {
lastEnteredForegroundMs.set(nowMs);
if (noActivityRunningForMs >= timeoutMs
&& configuration.getAutoTrackSessions()) {
startNewSession(new Date(nowMs), client.getUser(), true);
}
}
foregroundActivities.add(activityName);
} else {
foregroundActivities.remove(activityName);
if (foregroundActivities.isEmpty()) {
lastExitedForegroundMs.set(nowMs);
}
}
notifyNdkInForeground();
}
private void notifyNdkInForeground() {
Boolean inForeground = isInForeground();
boolean foreground = inForeground != null ? inForeground : false;
notifyObservers(new StateEvent.UpdateInForeground(foreground, getContextActivity()));
}
@Nullable
Boolean isInForeground() {
return foregroundDetector.isInForeground();
}
//FUTURE:SM This shouldnt be here
@Nullable
Long getDurationInForegroundMs(long nowMs) {
long durationMs = 0;
long sessionStartTimeMs = lastEnteredForegroundMs.get();
Boolean inForeground = isInForeground();
if (inForeground == null) {
return null;
}
if (inForeground && sessionStartTimeMs != 0) {
durationMs = nowMs - sessionStartTimeMs;
}
return durationMs > 0 ? durationMs : 0;
}
@Nullable
String getContextActivity() {
if (foregroundActivities.isEmpty()) {
return null;
} else {
// linked hash set retains order of added activity and ensures uniqueness
// therefore obtain the most recently added
int size = foregroundActivities.size();
String[] activities = foregroundActivities.toArray(new String[size]);
return activities[size - 1];
}
}
}

View File

@ -0,0 +1,20 @@
package com.bugsnag.android
import java.io.IOException
/**
* The severity of an Event, one of "error", "warning" or "info".
*
* By default, unhandled exceptions will be Severity.ERROR and handled
* exceptions sent with bugsnag.notify will be Severity.WARNING.
*/
enum class Severity(private val str: String) : JsonStream.Streamable {
ERROR("error"),
WARNING("warning"),
INFO("info");
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.value(str)
}
}

View File

@ -0,0 +1,157 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import androidx.annotation.StringDef;
import java.io.IOException;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
final class SeverityReason implements JsonStream.Streamable {
@StringDef({REASON_UNHANDLED_EXCEPTION, REASON_STRICT_MODE, REASON_HANDLED_EXCEPTION,
REASON_USER_SPECIFIED, REASON_CALLBACK_SPECIFIED, REASON_PROMISE_REJECTION,
REASON_LOG, REASON_SIGNAL, REASON_ANR})
@Retention(RetentionPolicy.SOURCE)
@interface SeverityReasonType {
}
static final String REASON_UNHANDLED_EXCEPTION = "unhandledException";
static final String REASON_STRICT_MODE = "strictMode";
static final String REASON_HANDLED_EXCEPTION = "handledException";
static final String REASON_USER_SPECIFIED = "userSpecifiedSeverity";
static final String REASON_CALLBACK_SPECIFIED = "userCallbackSetSeverity";
static final String REASON_PROMISE_REJECTION = "unhandledPromiseRejection";
static final String REASON_SIGNAL = "signal";
static final String REASON_LOG = "log";
static final String REASON_ANR = "anrError";
@SeverityReasonType
private final String severityReasonType;
@Nullable
private final String attributeValue;
private final Severity defaultSeverity;
private Severity currentSeverity;
private boolean unhandled;
final boolean originalUnhandled;
static SeverityReason newInstance(@SeverityReasonType String severityReasonType) {
return newInstance(severityReasonType, null, null);
}
static SeverityReason newInstance(@SeverityReasonType String severityReasonType,
@Nullable Severity severity,
@Nullable String attrVal) {
if (severityReasonType.equals(REASON_STRICT_MODE) && Intrinsics.isEmpty(attrVal)) {
throw new IllegalArgumentException("No reason supplied for strictmode");
}
if (!(severityReasonType.equals(REASON_STRICT_MODE)
|| severityReasonType.equals(REASON_LOG)) && !Intrinsics.isEmpty(attrVal)) {
throw new IllegalArgumentException("attributeValue should not be supplied");
}
switch (severityReasonType) {
case REASON_UNHANDLED_EXCEPTION:
case REASON_PROMISE_REJECTION:
case REASON_ANR:
return new SeverityReason(severityReasonType, Severity.ERROR, true, null);
case REASON_STRICT_MODE:
return new SeverityReason(severityReasonType, Severity.WARNING, true, attrVal);
case REASON_HANDLED_EXCEPTION:
return new SeverityReason(severityReasonType, Severity.WARNING, false, null);
case REASON_USER_SPECIFIED:
case REASON_CALLBACK_SPECIFIED:
return new SeverityReason(severityReasonType, severity, false, null);
case REASON_LOG:
return new SeverityReason(severityReasonType, severity, false, attrVal);
default:
String msg = String.format("Invalid argument '%s' for severityReason",
severityReasonType);
throw new IllegalArgumentException(msg);
}
}
SeverityReason(String severityReasonType, Severity currentSeverity, boolean unhandled,
@Nullable String attributeValue) {
this(severityReasonType, currentSeverity, unhandled, unhandled, attributeValue);
}
SeverityReason(String severityReasonType, Severity currentSeverity, boolean unhandled,
boolean originalUnhandled, @Nullable String attributeValue) {
this.severityReasonType = severityReasonType;
this.unhandled = unhandled;
this.originalUnhandled = originalUnhandled;
this.defaultSeverity = currentSeverity;
this.currentSeverity = currentSeverity;
this.attributeValue = attributeValue;
}
String calculateSeverityReasonType() {
return defaultSeverity == currentSeverity ? severityReasonType : REASON_CALLBACK_SPECIFIED;
}
Severity getCurrentSeverity() {
return currentSeverity;
}
boolean getUnhandled() {
return unhandled;
}
void setUnhandled(boolean unhandled) {
this.unhandled = unhandled;
}
boolean getUnhandledOverridden() {
return unhandled != originalUnhandled;
}
boolean isOriginalUnhandled() {
return originalUnhandled;
}
@Nullable
String getAttributeValue() {
return attributeValue;
}
void setCurrentSeverity(Severity severity) {
this.currentSeverity = severity;
}
String getSeverityReasonType() {
return severityReasonType;
}
@Override
public void toStream(@NonNull JsonStream writer) throws IOException {
writer.beginObject()
.name("type").value(calculateSeverityReasonType())
.name("unhandledOverridden").value(getUnhandledOverridden());
if (attributeValue != null) {
String attributeKey = null;
switch (severityReasonType) {
case REASON_LOG:
attributeKey = "level";
break;
case REASON_STRICT_MODE:
attributeKey = "violationType";
break;
default:
break;
}
if (attributeKey != null) {
writer.name("attributes").beginObject()
.name(attributeKey).value(attributeValue)
.endObject();
}
}
writer.endObject();
}
}

View File

@ -0,0 +1,37 @@
package com.bugsnag.android
import android.annotation.SuppressLint
import android.content.Context
/**
* Reads legacy information left in SharedPreferences and migrates it to the new location.
*/
internal class SharedPrefMigrator(context: Context) {
private val prefs = context
.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
fun loadDeviceId() = prefs.getString(INSTALL_ID_KEY, null)
fun loadUser(deviceId: String?) = User(
prefs.getString(USER_ID_KEY, deviceId),
prefs.getString(USER_EMAIL_KEY, null),
prefs.getString(USER_NAME_KEY, null)
)
fun hasPrefs() = prefs.contains(INSTALL_ID_KEY)
@SuppressLint("ApplySharedPref")
fun deleteLegacyPrefs() {
if (hasPrefs()) {
prefs.edit().clear().commit()
}
}
companion object {
private const val INSTALL_ID_KEY = "install.iud"
private const val USER_ID_KEY = "user.id"
private const val USER_NAME_KEY = "user.name"
private const val USER_EMAIL_KEY = "user.email"
}
}

View File

@ -0,0 +1,123 @@
package com.bugsnag.android
import java.io.IOException
/**
* Represents a single stackframe from a [Throwable]
*/
class Stackframe : JsonStream.Streamable {
/**
* The name of the method that was being executed
*/
var method: String?
set(value) {
nativeFrame?.method = value
field = value
}
/**
* The location of the source file
*/
var file: String?
set(value) {
nativeFrame?.file = value
field = value
}
/**
* The line number within the source file this stackframe refers to
*/
var lineNumber: Number?
set(value) {
nativeFrame?.lineNumber = value
field = value
}
/**
* Whether the package is considered to be in your project for the purposes of grouping and
* readability on the Bugsnag dashboard. Project package names can be set in
* [Configuration.projectPackages]
*/
var inProject: Boolean?
/**
* Lines of the code surrounding the frame, where the lineNumber is the key (React Native only)
*/
var code: Map<String, String?>?
/**
* The column number of the frame (React Native only)
*/
var columnNumber: Number?
/**
* The type of the error
*/
var type: ErrorType? = null
set(value) {
nativeFrame?.type = value
field = value
}
@JvmOverloads
internal constructor(
method: String?,
file: String?,
lineNumber: Number?,
inProject: Boolean?,
code: Map<String, String?>? = null,
columnNumber: Number? = null
) {
this.method = method
this.file = file
this.lineNumber = lineNumber
this.inProject = inProject
this.code = code
this.columnNumber = columnNumber
}
private var nativeFrame: NativeStackframe? = null
constructor(nativeFrame: NativeStackframe) : this(
nativeFrame.method,
nativeFrame.file,
nativeFrame.lineNumber,
false,
null
) {
this.nativeFrame = nativeFrame
this.type = nativeFrame.type
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
val ndkFrame = nativeFrame
if (ndkFrame != null) {
ndkFrame.toStream(writer)
return
}
writer.beginObject()
writer.name("method").value(method)
writer.name("file").value(file)
writer.name("lineNumber").value(lineNumber)
writer.name("inProject").value(inProject)
writer.name("columnNumber").value(columnNumber)
type?.let {
writer.name("type").value(it.desc)
}
code?.let { map: Map<String, String?> ->
writer.name("code")
map.forEach {
writer.beginObject()
writer.name(it.key)
writer.value(it.value)
writer.endObject()
}
}
writer.endObject()
}
}

View File

@ -0,0 +1,83 @@
package com.bugsnag.android
import java.io.IOException
/**
* Serialize an exception stacktrace and mark frames as "in-project"
* where appropriate.
*/
internal class Stacktrace : JsonStream.Streamable {
companion object {
private const val STACKTRACE_TRIM_LENGTH = 200
/**
* Calculates whether a stackframe is 'in project' or not by checking its class against
* [Configuration.getProjectPackages].
*
* For example if the projectPackages included 'com.example', then
* the `com.example.Foo` class would be considered in project, but `org.example.Bar` would
* 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
}
}
}
val trace: List<Stackframe>
constructor(frames: List<Stackframe>) {
trace = limitTraceLength(frames)
}
private fun <T> limitTraceLength(frames: List<T>): List<T> {
return when {
frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH)
else -> frames
}
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginArray()
trace.forEach { writer.value(it) }
writer.endArray()
}
}

View File

@ -0,0 +1,45 @@
package com.bugsnag.android
sealed class StateEvent {
class Install(
val apiKey: String,
val autoDetectNdkCrashes: Boolean,
val appVersion: String?,
val buildUuid: String?,
val releaseStage: String?,
val lastRunInfoPath: String,
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 AddBreadcrumb(
val message: String,
val type: BreadcrumbType,
val timestamp: String,
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,
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 UpdateUser(val user: User) : StateEvent()
}

View File

@ -0,0 +1,98 @@
package com.bugsnag.android;
import android.annotation.SuppressLint;
import android.text.TextUtils;
import androidx.annotation.Nullable;
import java.util.HashMap;
import java.util.Locale;
import java.util.Map;
class StrictModeHandler {
// Byte 1: Thread-policy (needs to be synced with StrictMode constants)
private static final int DETECT_DISK_WRITE = 0x01;
private static final int DETECT_DISK_READ = 0x02;
private static final int DETECT_NETWORK = 0x04;
private static final int DETECT_CUSTOM = 0x08;
private static final int DETECT_RESOURCE_MISMATCH = 0x10;
// Byte 2: Process-policy (needs to be synced with StrictMode constants)
private static final int DETECT_VM_CURSOR_LEAKS = 0x01 << 8;
private static final int DETECT_VM_CLOSABLE_LEAKS = 0x02 << 8;
private static final int DETECT_VM_ACTIVITY_LEAKS = 0x04 << 8;
private static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8;
private static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8;
private static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8;
private static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8;
private static final String STRICT_MODE_CLZ_NAME = "android.os.strictmode";
@SuppressLint("UseSparseArrays")
private static final Map<Integer, String> POLICY_CODE_MAP = new HashMap<>();
static {
POLICY_CODE_MAP.put(DETECT_DISK_WRITE, "DiskWrite");
POLICY_CODE_MAP.put(DETECT_DISK_READ, "DiskRead");
POLICY_CODE_MAP.put(DETECT_NETWORK, "NetworkOperation");
POLICY_CODE_MAP.put(DETECT_CUSTOM, "CustomSlowCall");
POLICY_CODE_MAP.put(DETECT_RESOURCE_MISMATCH, "ResourceMismatch");
POLICY_CODE_MAP.put(DETECT_VM_CURSOR_LEAKS, "CursorLeak");
POLICY_CODE_MAP.put(DETECT_VM_CLOSABLE_LEAKS, "CloseableLeak");
POLICY_CODE_MAP.put(DETECT_VM_ACTIVITY_LEAKS, "ActivityLeak");
POLICY_CODE_MAP.put(DETECT_VM_INSTANCE_LEAKS, "InstanceLeak");
POLICY_CODE_MAP.put(DETECT_VM_REGISTRATION_LEAKS, "RegistrationLeak");
POLICY_CODE_MAP.put(DETECT_VM_FILE_URI_EXPOSURE, "FileUriLeak");
POLICY_CODE_MAP.put(DETECT_VM_CLEARTEXT_NETWORK, "CleartextNetwork");
}
/**
* Checks whether a throwable was originally thrown from the StrictMode class
*
* @param throwable the throwable
* @return true if the throwable's root cause is a StrictMode policy violation
*/
boolean isStrictModeThrowable(Throwable throwable) {
Throwable cause = getRootCause(throwable);
Class<? extends Throwable> causeClass = cause.getClass();
String simpleName = causeClass.getName();
return simpleName.toLowerCase(Locale.US).startsWith(STRICT_MODE_CLZ_NAME);
}
@Nullable
String getViolationDescription(String exceptionMessage) {
if (TextUtils.isEmpty(exceptionMessage)) {
throw new IllegalArgumentException();
}
int indexOf = exceptionMessage.lastIndexOf("violation=");
if (indexOf != -1) {
String substring = exceptionMessage.substring(indexOf);
substring = substring.replace("violation=", "");
if (TextUtils.isDigitsOnly(substring)) {
Integer code = Integer.valueOf(substring);
return POLICY_CODE_MAP.get(code);
}
}
return null;
}
/**
* Recurse the stack to get the original cause of the throwable
*
* @param throwable the throwable
* @return the root cause of the throwable
*/
private Throwable getRootCause(Throwable throwable) {
Throwable cause = throwable.getCause();
if (cause == null) {
return throwable;
} else {
return getRootCause(cause);
}
}
}

View File

@ -0,0 +1,39 @@
package com.bugsnag.android
import android.util.JsonReader
import java.io.File
import java.io.IOException
import java.util.concurrent.locks.ReentrantReadWriteLock
import kotlin.concurrent.withLock
/**
* Persists and loads a [Streamable] object to the file system. This is intended for use
* primarily as a replacement for primitive value stores such as [SharedPreferences].
*
* This class is made thread safe through the use of a [ReadWriteLock].
*/
internal class SynchronizedStreamableStore<T : JsonStream.Streamable>(
private val file: File
) {
private val lock = ReentrantReadWriteLock()
@Throws(IOException::class)
fun persist(streamable: T) {
lock.writeLock().withLock {
file.writer().buffered().use {
streamable.toStream(JsonStream(it))
true
}
}
}
@Throws(IOException::class)
fun load(loadCallback: (JsonReader) -> T): T {
lock.readLock().withLock {
return file.reader().buffered().use {
loadCallback(JsonReader(it))
}
}
}
}

View File

@ -0,0 +1,191 @@
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;
}
}

View File

@ -0,0 +1,114 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
import java.io.IOException;
import java.util.List;
/**
* A representation of a thread recorded in an {@link Event}
*/
@SuppressWarnings("ConstantConditions")
public class Thread implements JsonStream.Streamable {
private final ThreadInternal impl;
private final Logger logger;
Thread(
long id,
@NonNull String name,
@NonNull ThreadType type,
boolean errorReportingThread,
@NonNull Stacktrace stacktrace,
@NonNull Logger logger) {
this.impl = new ThreadInternal(id, name, type, errorReportingThread, stacktrace);
this.logger = logger;
}
private void logNull(String property) {
logger.e("Invalid null value supplied to thread." + property + ", ignoring");
}
/**
* Sets the unique ID of the thread (from {@link java.lang.Thread})
*/
public void setId(long id) {
impl.setId(id);
}
/**
* Gets the unique ID of the thread (from {@link java.lang.Thread})
*/
public long getId() {
return impl.getId();
}
/**
* Sets the name of the thread (from {@link java.lang.Thread})
*/
public void setName(@NonNull String name) {
if (name != null) {
impl.setName(name);
} else {
logNull("name");
}
}
/**
* Gets the name of the thread (from {@link java.lang.Thread})
*/
@NonNull
public String getName() {
return impl.getName();
}
/**
* Sets the type of thread based on the originating platform (intended for internal use only)
*/
public void setType(@NonNull ThreadType type) {
if (type != null) {
impl.setType(type);
} else {
logNull("type");
}
}
/**
* Gets the type of thread based on the originating platform (intended for internal use only)
*/
@NonNull
public ThreadType getType() {
return impl.getType();
}
/**
* Gets whether the thread was the thread that caused the event
*/
public boolean getErrorReportingThread() {
return impl.isErrorReportingThread();
}
/**
* Sets a representation of the thread's stacktrace
*/
public void setStacktrace(@NonNull List<Stackframe> stacktrace) {
if (!CollectionUtils.containsNullElements(stacktrace)) {
impl.setStacktrace(stacktrace);
} else {
logNull("stacktrace");
}
}
/**
* Gets a representation of the thread's stacktrace
*/
@NonNull
public List<Stackframe> getStacktrace() {
return impl.getStacktrace();
}
@Override
public void toStream(@NonNull JsonStream stream) throws IOException {
impl.toStream(stream);
}
}

View File

@ -0,0 +1,32 @@
package com.bugsnag.android
import java.io.IOException
class ThreadInternal internal constructor(
var id: Long,
var name: String,
var type: ThreadType,
val isErrorReportingThread: Boolean,
stacktrace: Stacktrace
) : JsonStream.Streamable {
var stacktrace: MutableList<Stackframe> = stacktrace.trace.toMutableList()
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name("id").value(id)
writer.name("name").value(name)
writer.name("type").value(type.desc)
writer.name("stacktrace")
writer.beginArray()
stacktrace.forEach { writer.value(it) }
writer.endArray()
if (isErrorReportingThread) {
writer.name("errorReportingThread").value(true)
}
writer.endObject()
}
}

View File

@ -0,0 +1,27 @@
package com.bugsnag.android
/**
* Controls whether we should capture and serialize the state of all threads at the time
* of an error.
*/
enum class ThreadSendPolicy {
/**
* Threads should be captured for all events.
*/
ALWAYS,
/**
* Threads should be captured for unhandled events only.
*/
UNHANDLED_ONLY,
/**
* Threads should never be captured.
*/
NEVER;
internal companion object {
fun fromString(str: String) = values().find { it.name == str } ?: ALWAYS
}
}

View File

@ -0,0 +1,84 @@
package com.bugsnag.android
import java.io.IOException
/**
* Capture and serialize the state of all threads at the time of an exception.
*/
internal class ThreadState @JvmOverloads constructor(
exc: Throwable?,
isUnhandled: Boolean,
sendThreads: ThreadSendPolicy,
projectPackages: Collection<String>,
logger: Logger,
currentThread: java.lang.Thread = java.lang.Thread.currentThread(),
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>> = java.lang.Thread.getAllStackTraces()
) : JsonStream.Streamable {
internal constructor(
exc: Throwable?,
isUnhandled: Boolean,
config: ImmutableConfig
) : this(exc, isUnhandled, config.sendThreads, config.projectPackages, config.logger)
val threads: MutableList<Thread>
init {
val recordThreads = sendThreads == ThreadSendPolicy.ALWAYS ||
(sendThreads == ThreadSendPolicy.UNHANDLED_ONLY && isUnhandled)
threads = when {
recordThreads -> captureThreadTrace(
stackTraces,
currentThread,
exc,
isUnhandled,
projectPackages,
logger
)
else -> mutableListOf()
}
}
private fun captureThreadTrace(
stackTraces: MutableMap<java.lang.Thread, Array<StackTraceElement>>,
currentThread: java.lang.Thread,
exc: Throwable?,
isUnhandled: Boolean,
projectPackages: Collection<String>,
logger: Logger
): MutableList<Thread> {
// API 24/25 don't record the currentThread, add it in manually
// https://issuetracker.google.com/issues/64122757
if (!stackTraces.containsKey(currentThread)) {
stackTraces[currentThread] = currentThread.stackTrace
}
if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces
stackTraces[currentThread] = exc.stackTrace
}
val currentThreadId = currentThread.id
return stackTraces.keys
.sortedBy { it.id }
.mapNotNull { thread ->
val trace = stackTraces[thread]
if (trace != null) {
val stacktrace = Stacktrace.stacktraceFromJavaTrace(trace, projectPackages, logger)
val errorThread = thread.id == currentThreadId
Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, stacktrace, logger)
} else {
null
}
}.toMutableList()
}
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginArray()
for (thread in threads) {
writer.value(thread)
}
writer.endArray()
}
}

View File

@ -0,0 +1,22 @@
package com.bugsnag.android
/**
* Represents the type of thread captured
*/
enum class ThreadType(internal val desc: String) {
/**
* A thread captured from Android's JVM layer
*/
ANDROID("android"),
/**
* A thread captured from Android's NDK layer
*/
C("c"),
/**
* A thread captured from JavaScript
*/
REACTNATIVEJS("reactnativejs")
}

View File

@ -0,0 +1,83 @@
package com.bugsnag.android
import android.util.JsonReader
import java.io.IOException
/**
* Information about the current user of your application.
*/
class User @JvmOverloads internal constructor(
/**
* @return the user ID, by default a UUID generated on installation
*/
val id: String? = null,
/**
* @return the user's email, if available
*/
val email: String? = null,
/**
* @return the user's name, if available
*/
val name: String? = null
) : JsonStream.Streamable {
@Throws(IOException::class)
override fun toStream(writer: JsonStream) {
writer.beginObject()
writer.name(KEY_ID).value(id)
writer.name(KEY_EMAIL).value(email)
writer.name(KEY_NAME).value(name)
writer.endObject()
}
internal companion object : JsonReadable<User> {
private const val KEY_ID = "id"
private const val KEY_NAME = "name"
private const val KEY_EMAIL = "email"
override fun fromReader(reader: JsonReader): User {
var user: User
with(reader) {
beginObject()
var id: String? = null
var email: String? = null
var name: String? = null
while (hasNext()) {
val key = nextName()
val value = nextString()
when (key) {
KEY_ID -> id = value
KEY_EMAIL -> email = value
KEY_NAME -> name = value
}
}
user = User(id, email, name)
endObject()
}
return user
}
}
override fun equals(other: Any?): Boolean {
if (this === other) return true
if (javaClass != other?.javaClass) return false
other as User
if (id != other.id) return false
if (email != other.email) return false
if (name != other.name) return false
return true
}
override fun hashCode(): Int {
var result = id?.hashCode() ?: 0
result = 31 * result + (email?.hashCode() ?: 0)
result = 31 * result + (name?.hashCode() ?: 0)
return result
}
}

View File

@ -0,0 +1,6 @@
package com.bugsnag.android
internal interface UserAware {
fun getUser(): User
fun setUser(id: String?, email: String?, name: String?)
}

View File

@ -0,0 +1,11 @@
package com.bugsnag.android
internal class UserState(user: User) : BaseObservable() {
var user = user
set(value) {
field = value
emitObservableEvent()
}
fun emitObservableEvent() = notifyObservers(StateEvent.UpdateUser(user))
}

View File

@ -0,0 +1,97 @@
package com.bugsnag.android
import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference
/**
* This class is responsible for persisting and retrieving user information.
*/
internal class UserStore @JvmOverloads constructor(
private val config: ImmutableConfig,
private val deviceId: String?,
file: File = File(config.persistenceDirectory, "user-info"),
private val sharedPrefMigrator: SharedPrefMigrator,
private val logger: Logger
) {
private val synchronizedStreamableStore: SynchronizedStreamableStore<User>
private val persist = config.persistUser
private val previousUser = AtomicReference<User?>(null)
init {
try {
if (!file.exists()) {
file.createNewFile()
}
} catch (exc: IOException) {
logger.w("Failed to created device ID file", exc)
}
this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
}
/**
* Loads the user state which should be used by the [Client]. This is supplied either from
* the [Configuration] value, or a file in the [Configuration.getPersistenceDirectory] if
* [Configuration.getPersistUser] is true.
*
* If no user is stored on disk, then a default [User] is used which uses the device ID
* as its ID.
*
* The [UserState] provides a mechanism for observing value changes to its user property,
* so to avoid interfering with this the method should only be called once for each [Client].
*/
fun load(initialUser: User): UserState {
val validConfigUser = validUser(initialUser)
val loadedUser = when {
validConfigUser -> initialUser
persist -> loadPersistedUser()
else -> null
}
val userState = when {
loadedUser != null && validUser(loadedUser) -> UserState(loadedUser)
else -> UserState(User(deviceId, null, null))
}
userState.addObserver { _, arg ->
if (arg is StateEvent.UpdateUser) {
save(arg.user)
}
}
return userState
}
/**
* Persists the user if [Configuration.getPersistUser] is true and the object is different
* from the previously persisted value.
*/
fun save(user: User) {
if (persist && user != previousUser.getAndSet(user)) {
try {
synchronizedStreamableStore.persist(user)
} catch (exc: Exception) {
logger.w("Failed to persist user info", exc)
}
}
}
private fun validUser(user: User) =
user.id != null || user.name != null || user.email != null
private fun loadPersistedUser(): User? {
return if (sharedPrefMigrator.hasPrefs()) {
val legacyUser = sharedPrefMigrator.loadUser(deviceId)
save(legacyUser)
legacyUser
} else {
return try {
synchronizedStreamableStore.load(User.Companion::fromReader)
} catch (exc: Exception) {
logger.w("Failed to load user info", exc)
null
}
}
}
}

View File

@ -10,6 +10,9 @@ buildscript {
classpath 'com.android.tools.build:gradle:4.2.1'
// https://github.com/bugsnag/bugsnag-android-gradle-plugin
classpath 'com.bugsnag:bugsnag-android-gradle-plugin:5.7.6'
// https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-gradle-plugin
classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.5.0"
classpath "org.jetbrains.kotlin:kotlin-android-extensions:1.5.0"
}
}