mirror of https://github.com/M66B/FairEmail.git
parent
779de80459
commit
c49509cfcb
@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Loading…
Reference in new issue