mirror of https://github.com/M66B/FairEmail.git
Build Bugsnag inline
This commit is contained in:
parent
779de80459
commit
c49509cfcb
|
@ -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
|
||||
|
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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())
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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));
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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?>
|
||||
)
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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!!))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
)
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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 -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
||||
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
|
@ -0,0 +1,8 @@
|
|||
package com.bugsnag.android;
|
||||
|
||||
class Intrinsics {
|
||||
|
||||
static boolean isEmpty(CharSequence str) {
|
||||
return str == null || str.length() == 0;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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;
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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.");
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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() }
|
||||
}
|
||||
}
|
|
@ -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?
|
||||
}
|
|
@ -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)))
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
package com.bugsnag.android
|
||||
|
||||
internal object NoopLogger : Logger
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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) }
|
||||
}
|
|
@ -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);
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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);
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
}
|
|
@ -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) {}
|
||||
}
|
|
@ -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());
|
||||
}
|
||||
|
||||
}
|
|
@ -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];
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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)
|
||||
}
|
||||
}
|
|
@ -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();
|
||||
}
|
||||
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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;
|
||||
}
|
||||
|
||||
}
|
|
@ -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);
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -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()
|
||||
}
|
||||
}
|
|
@ -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")
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
|
@ -0,0 +1,6 @@
|
|||
package com.bugsnag.android
|
||||
|
||||
internal interface UserAware {
|
||||
fun getUser(): User
|
||||
fun setUser(id: String?, email: String?, name: String?)
|
||||
}
|
|
@ -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))
|
||||
}
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
||||
|
|
Loading…
Reference in New Issue