mirror of https://github.com/M66B/FairEmail.git
parent
17809acd7d
commit
3d0596420a
@ -1,89 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.app.Activity
|
||||
import android.app.Application
|
||||
import android.content.Intent
|
||||
import android.os.Build
|
||||
import android.os.Bundle
|
||||
import java.util.WeakHashMap
|
||||
|
||||
internal class ActivityBreadcrumbCollector(
|
||||
private val cb: (message: String, method: Map<String, Any>) -> Unit
|
||||
) : Application.ActivityLifecycleCallbacks {
|
||||
|
||||
private val prevState = WeakHashMap<Activity, String>()
|
||||
|
||||
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {
|
||||
leaveBreadcrumb(
|
||||
activity,
|
||||
"onCreate()",
|
||||
mutableMapOf<String, Any>().apply {
|
||||
set("hasBundle", savedInstanceState != null)
|
||||
setActivityIntentMetadata(activity.intent)
|
||||
}
|
||||
)
|
||||
}
|
||||
|
||||
override fun onActivityStarted(activity: Activity) =
|
||||
leaveBreadcrumb(activity, "onStart()")
|
||||
|
||||
override fun onActivityResumed(activity: Activity) =
|
||||
leaveBreadcrumb(activity, "onResume()")
|
||||
|
||||
override fun onActivityPaused(activity: Activity) =
|
||||
leaveBreadcrumb(activity, "onPause()")
|
||||
|
||||
override fun onActivityStopped(activity: Activity) =
|
||||
leaveBreadcrumb(activity, "onStop()")
|
||||
|
||||
override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) =
|
||||
leaveBreadcrumb(activity, "onSaveInstanceState()")
|
||||
|
||||
override fun onActivityDestroyed(activity: Activity) {
|
||||
leaveBreadcrumb(activity, "onDestroy()")
|
||||
prevState.remove(activity)
|
||||
}
|
||||
|
||||
private fun getActivityName(activity: Activity) = activity.javaClass.simpleName
|
||||
|
||||
private fun leaveBreadcrumb(
|
||||
activity: Activity,
|
||||
lifecycleCallback: String,
|
||||
metadata: MutableMap<String, Any> = mutableMapOf()
|
||||
) {
|
||||
val previousVal = prevState[activity]
|
||||
|
||||
if (previousVal != null) {
|
||||
metadata["previous"] = previousVal
|
||||
}
|
||||
|
||||
val activityName = getActivityName(activity)
|
||||
cb("$activityName#$lifecycleCallback", metadata)
|
||||
prevState[activity] = lifecycleCallback
|
||||
}
|
||||
|
||||
private fun MutableMap<String, Any>.setActivityIntentMetadata(intent: Intent?) {
|
||||
if (intent == null) return
|
||||
|
||||
intent.action?.let { set("action", it) }
|
||||
intent.categories?.let { set("categories", it.joinToString(", ")) }
|
||||
intent.type?.let { set("type", it) }
|
||||
|
||||
if (intent.flags != 0) {
|
||||
@Suppress("MagicNumber") // hex radix
|
||||
set("flags", "0x${intent.flags.toString(16)}")
|
||||
}
|
||||
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.Q) {
|
||||
intent.identifier?.let { set("id", it) }
|
||||
}
|
||||
|
||||
set("hasData", intent.data != null)
|
||||
|
||||
try {
|
||||
set("hasExtras", intent.extras?.keySet()?.joinToString(", ") ?: false)
|
||||
} catch (re: Exception) {
|
||||
// deliberately ignore
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
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]
|
||||
*/
|
||||
buildUuid: Provider<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 {
|
||||
|
||||
private var buildUuidProvider: Provider<String?>? = buildUuid
|
||||
|
||||
var buildUuid: String? = null
|
||||
get() = field ?: buildUuidProvider?.getOrNull()
|
||||
set(value) {
|
||||
field = value
|
||||
buildUuidProvider = null
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,247 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CACHED
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_CANT_SAVE_STATE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_EMPTY
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_FOREGROUND_SERVICE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_GONE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_PERCEPTIBLE_PRE_26
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_SERVICE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_TOP_SLEEPING_PRE_28
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.IMPORTANCE_VISIBLE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.REASON_PROVIDER_IN_USE
|
||||
import android.app.ActivityManager.RunningAppProcessInfo.REASON_SERVICE_IN_USE
|
||||
import android.app.Application
|
||||
import android.content.Context
|
||||
import android.content.pm.PackageManager
|
||||
import android.os.Build.VERSION
|
||||
import android.os.Build.VERSION_CODES
|
||||
import android.os.Process
|
||||
import android.os.SystemClock
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
|
||||
/**
|
||||
* 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 memoryTrimState: MemoryTrimState
|
||||
) {
|
||||
|
||||
var codeBundleId: String? = null
|
||||
|
||||
private val packageName: String = appContext.packageName
|
||||
private val bgWorkRestricted = isBackgroundWorkRestricted()
|
||||
|
||||
private var binaryArch: String? = null
|
||||
private val appName = getAppName()
|
||||
private val processName = findProcessName()
|
||||
private val releaseStage = config.releaseStage
|
||||
private val versionName = config.appVersion ?: config.packageInfo?.versionName
|
||||
private val installerPackage = getInstallerPackageName()
|
||||
|
||||
fun generateApp(): App =
|
||||
App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId)
|
||||
|
||||
fun generateAppWithState(): AppWithState {
|
||||
val inForeground = sessionTracker.isInForeground
|
||||
val durationInForeground = calculateDurationInForeground(inForeground)
|
||||
|
||||
return AppWithState(
|
||||
config, binaryArch, packageName, releaseStage, versionName, codeBundleId,
|
||||
getDurationMs(), durationInForeground, inForeground,
|
||||
launchCrashTracker.isLaunching()
|
||||
)
|
||||
}
|
||||
|
||||
@SuppressLint("SwitchIntDef")
|
||||
@Suppress("DEPRECATION")
|
||||
private fun getProcessImportance(): String? {
|
||||
try {
|
||||
val appInfo = ActivityManager.RunningAppProcessInfo()
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN) {
|
||||
ActivityManager.getMyMemoryState(appInfo)
|
||||
} else {
|
||||
val expectedPid = Process.myPid()
|
||||
activityManager?.runningAppProcesses
|
||||
?.find { it.pid == expectedPid }
|
||||
?.let {
|
||||
appInfo.importance = it.importance
|
||||
appInfo.pid = expectedPid
|
||||
}
|
||||
}
|
||||
|
||||
if (appInfo.pid == 0) {
|
||||
return null
|
||||
}
|
||||
|
||||
return when (appInfo.importance) {
|
||||
IMPORTANCE_FOREGROUND -> "foreground"
|
||||
IMPORTANCE_FOREGROUND_SERVICE -> "foreground service"
|
||||
IMPORTANCE_TOP_SLEEPING -> "top sleeping"
|
||||
IMPORTANCE_TOP_SLEEPING_PRE_28 -> "top sleeping"
|
||||
IMPORTANCE_VISIBLE -> "visible"
|
||||
IMPORTANCE_PERCEPTIBLE -> "perceptible"
|
||||
IMPORTANCE_PERCEPTIBLE_PRE_26 -> "perceptible"
|
||||
IMPORTANCE_CANT_SAVE_STATE -> "can't save state"
|
||||
IMPORTANCE_CANT_SAVE_STATE_PRE_26 -> "can't save state"
|
||||
IMPORTANCE_SERVICE -> "service"
|
||||
IMPORTANCE_CACHED -> "cached/background"
|
||||
IMPORTANCE_GONE -> "gone"
|
||||
IMPORTANCE_EMPTY -> "empty"
|
||||
REASON_PROVIDER_IN_USE -> "provider in use"
|
||||
REASON_SERVICE_IN_USE -> "service in use"
|
||||
else -> "unknown importance (${appInfo.importance})"
|
||||
}
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
fun getAppDataMetadata(): MutableMap<String, Any?> {
|
||||
val map = HashMap<String, Any?>()
|
||||
map["name"] = appName
|
||||
map["activeScreen"] = sessionTracker.contextActivity
|
||||
map["lowMemory"] = memoryTrimState.isLowMemory
|
||||
map["memoryTrimLevel"] = memoryTrimState.trimLevelDescription
|
||||
map["processImportance"] = getProcessImportance()
|
||||
|
||||
populateRuntimeMemoryMetadata(map)
|
||||
|
||||
bgWorkRestricted?.let {
|
||||
map["backgroundWorkRestricted"] = bgWorkRestricted
|
||||
}
|
||||
processName?.let {
|
||||
map["processName"] = it
|
||||
}
|
||||
return map
|
||||
}
|
||||
|
||||
private fun populateRuntimeMemoryMetadata(map: MutableMap<String, Any?>) {
|
||||
val runtime = Runtime.getRuntime()
|
||||
val totalMemory = runtime.totalMemory()
|
||||
val freeMemory = runtime.freeMemory()
|
||||
map["memoryUsage"] = totalMemory - freeMemory
|
||||
map["totalMemory"] = totalMemory
|
||||
map["freeMemory"] = freeMemory
|
||||
map["memoryLimit"] = runtime.maxMemory()
|
||||
map["installerPackage"] = installerPackage
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 || VERSION.SDK_INT < VERSION_CODES.P) {
|
||||
null
|
||||
} else if (activityManager.isBackgroundRestricted) {
|
||||
true // only return non-null value if true to avoid noise in error reports
|
||||
} else {
|
||||
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(inForeground: Boolean? = sessionTracker.isInForeground): Long? {
|
||||
if (inForeground == null) {
|
||||
return null
|
||||
}
|
||||
|
||||
val nowMs = SystemClock.elapsedRealtime()
|
||||
var durationMs: Long = 0
|
||||
|
||||
val sessionStartTimeMs: Long = sessionTracker.lastEnteredForegroundMs
|
||||
|
||||
if (inForeground && sessionStartTimeMs != 0L) {
|
||||
durationMs = nowMs - sessionStartTimeMs
|
||||
}
|
||||
|
||||
return if (durationMs > 0) durationMs else 0
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of the running Android app, from android:label in
|
||||
* AndroidManifest.xml
|
||||
*/
|
||||
private fun getAppName(): String? {
|
||||
val copy = config.appInfo
|
||||
return when {
|
||||
packageManager != null && copy != null -> {
|
||||
packageManager.getApplicationLabel(copy).toString()
|
||||
}
|
||||
|
||||
else -> null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The name of installer / vendor package of the app
|
||||
*/
|
||||
fun getInstallerPackageName(): String? {
|
||||
try {
|
||||
if (VERSION.SDK_INT >= VERSION_CODES.R)
|
||||
return packageManager?.getInstallSourceInfo(packageName)?.installingPackageName
|
||||
@Suppress("DEPRECATION")
|
||||
return packageManager?.getInstallerPackageName(packageName)
|
||||
} catch (e: Exception) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds the name of the current process, or null if this cannot be found.
|
||||
*/
|
||||
@SuppressLint("PrivateApi")
|
||||
private fun findProcessName(): String? {
|
||||
return runCatching {
|
||||
when {
|
||||
VERSION.SDK_INT >= VERSION_CODES.P -> {
|
||||
Application.getProcessName()
|
||||
}
|
||||
|
||||
else -> {
|
||||
// see https://stackoverflow.com/questions/19631894
|
||||
val clz = Class.forName("android.app.ActivityThread")
|
||||
val methodName = when {
|
||||
VERSION.SDK_INT >= VERSION_CODES.JELLY_BEAN_MR2 -> "currentProcessName"
|
||||
else -> "currentPackageName"
|
||||
}
|
||||
|
||||
val getProcessName = clz.getDeclaredMethod(methodName)
|
||||
getProcessName.invoke(null) as String
|
||||
}
|
||||
}
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
companion object {
|
||||
internal val startTimeMs = SystemClock.elapsedRealtime()
|
||||
|
||||
/**
|
||||
* 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
|
||||
|
||||
private const val IMPORTANCE_CANT_SAVE_STATE_PRE_26 = 170
|
||||
}
|
||||
}
|
||||
@ -1,130 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
import com.bugsnag.android.internal.dag.ValueProvider
|
||||
|
||||
/**
|
||||
* 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 internal constructor(
|
||||
binaryArch: String?,
|
||||
id: String?,
|
||||
releaseStage: String?,
|
||||
version: String?,
|
||||
codeBundleId: String?,
|
||||
buildUuid: Provider<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
|
||||
) {
|
||||
|
||||
constructor(
|
||||
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
|
||||
*/
|
||||
duration: Number?,
|
||||
|
||||
/**
|
||||
* The number of milliseconds the application was running in the foreground before the
|
||||
* event occurred
|
||||
*/
|
||||
durationInForeground: Number?,
|
||||
|
||||
/**
|
||||
* Whether the application was in the foreground when the event occurred
|
||||
*/
|
||||
inForeground: Boolean?,
|
||||
|
||||
/**
|
||||
* Whether the application was launching when the event occurred
|
||||
*/
|
||||
isLaunching: Boolean?
|
||||
) : this(
|
||||
binaryArch,
|
||||
id,
|
||||
releaseStage,
|
||||
version,
|
||||
codeBundleId,
|
||||
buildUuid?.let(::ValueProvider),
|
||||
type,
|
||||
versionCode,
|
||||
duration,
|
||||
durationInForeground,
|
||||
inForeground,
|
||||
isLaunching
|
||||
)
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.StateObserver
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
internal open class BaseObservable {
|
||||
|
||||
internal val observers = CopyOnWriteArrayList<StateObserver>()
|
||||
|
||||
/**
|
||||
* Adds an observer that can react to [StateEvent] messages.
|
||||
*/
|
||||
fun addObserver(observer: StateObserver) {
|
||||
observers.addIfAbsent(observer)
|
||||
}
|
||||
|
||||
/**
|
||||
* Removes a previously added observer that reacts to [StateEvent] messages.
|
||||
*/
|
||||
fun removeObserver(observer: StateObserver) {
|
||||
observers.remove(observer)
|
||||
}
|
||||
|
||||
/**
|
||||
* This method should be invoked when the notifier's state has changed. If an observer
|
||||
* has been set, it will be notified of the [StateEvent] message so that it can react
|
||||
* appropriately. If no observer has been set then this method will no-op.
|
||||
*/
|
||||
internal inline fun updateState(provider: () -> StateEvent) {
|
||||
// optimization to avoid unnecessary iterator and StateEvent construction
|
||||
if (observers.isEmpty()) {
|
||||
return
|
||||
}
|
||||
|
||||
// construct the StateEvent object and notify observers
|
||||
val event = provider()
|
||||
observers.forEach { it.onStateChange(event) }
|
||||
}
|
||||
|
||||
/**
|
||||
* An eager version of [updateState], which is intended primarily for use in Java code.
|
||||
* If the event will occur very frequently, you should consider calling the lazy method
|
||||
* instead.
|
||||
*/
|
||||
fun updateState(event: StateEvent) = updateState { event }
|
||||
}
|
||||
@ -1,114 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils;
|
||||
|
||||
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 {
|
||||
|
||||
// non-private to allow direct field access optimizations
|
||||
final BreadcrumbInternal impl;
|
||||
private final Logger logger;
|
||||
|
||||
Breadcrumb(@NonNull BreadcrumbInternal impl, @NonNull Logger logger) {
|
||||
this.impl = impl;
|
||||
this.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.message = message;
|
||||
} else {
|
||||
logNull("message");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the description of the breadcrumb
|
||||
*/
|
||||
@NonNull
|
||||
public String getMessage() {
|
||||
return impl.message;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets the type of breadcrumb left - one of those enabled in
|
||||
* {@link Configuration#getEnabledBreadcrumbTypes()}
|
||||
*/
|
||||
public void setType(@NonNull BreadcrumbType type) {
|
||||
if (type != null) {
|
||||
impl.type = type;
|
||||
} else {
|
||||
logNull("type");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets the type of breadcrumb left - one of those enabled in
|
||||
* {@link Configuration#getEnabledBreadcrumbTypes()}
|
||||
*/
|
||||
@NonNull
|
||||
public BreadcrumbType getType() {
|
||||
return impl.type;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sets diagnostic data relating to the breadcrumb
|
||||
*/
|
||||
public void setMetadata(@Nullable Map<String, Object> metadata) {
|
||||
impl.metadata = metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets diagnostic data relating to the breadcrumb
|
||||
*/
|
||||
@Nullable
|
||||
public Map<String, Object> getMetadata() {
|
||||
return impl.metadata;
|
||||
}
|
||||
|
||||
/**
|
||||
* The timestamp that the breadcrumb was left
|
||||
*/
|
||||
@NonNull
|
||||
public Date getTimestamp() {
|
||||
return impl.timestamp;
|
||||
}
|
||||
|
||||
@NonNull
|
||||
String getStringTimestamp() {
|
||||
return DateUtils.toIso8601(impl.timestamp);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toStream(@NonNull JsonStream stream) throws IOException {
|
||||
impl.toStream(stream);
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.StringUtils
|
||||
import com.bugsnag.android.internal.TrimMetrics
|
||||
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(
|
||||
@JvmField var message: String,
|
||||
@JvmField var type: BreadcrumbType,
|
||||
@JvmField var metadata: MutableMap<String, Any?>?,
|
||||
@JvmField val timestamp: Date = Date()
|
||||
) : JsonStream.Streamable { // JvmField allows direct field access optimizations
|
||||
|
||||
internal constructor(message: String) : this(
|
||||
message,
|
||||
BreadcrumbType.MANUAL,
|
||||
mutableMapOf(),
|
||||
Date()
|
||||
)
|
||||
|
||||
internal fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics {
|
||||
val metadata = this.metadata ?: return TrimMetrics(0, 0)
|
||||
return StringUtils.trimStringValuesTo(maxStringLength, metadata)
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toStream(writer: JsonStream) {
|
||||
writer.beginObject()
|
||||
writer.name("timestamp").value(timestamp)
|
||||
writer.name("name").value(message)
|
||||
writer.name("type").value(type.toString())
|
||||
writer.name("metaData")
|
||||
writer.value(metadata, true)
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
|
||||
/**
|
||||
* Stores breadcrumbs added to the [Client] in a ring buffer. If the number of breadcrumbs exceeds
|
||||
* the maximum configured limit then the oldest breadcrumb in the ring buffer will be overwritten.
|
||||
*
|
||||
* When the breadcrumbs are required for generation of an event a [List] is constructed and
|
||||
* breadcrumbs added in the order of their addition.
|
||||
*/
|
||||
internal class BreadcrumbState(
|
||||
private val maxBreadcrumbs: Int,
|
||||
private val callbackState: CallbackState,
|
||||
private val logger: Logger
|
||||
) : BaseObservable(), JsonStream.Streamable {
|
||||
|
||||
/*
|
||||
* We use the `index` as both a pointer to the tail of our ring-buffer, and also as "cheat"
|
||||
* semaphore. When the ring-buffer is being copied - the index is set to a negative number,
|
||||
* which is an invalid array-index. By masking the `expected` value in a `compareAndSet` with
|
||||
* `validIndexMask`: the CAS operation will only succeed if it wouldn't interrupt a concurrent
|
||||
* `copy()` call.
|
||||
*/
|
||||
private val validIndexMask: Int = Int.MAX_VALUE
|
||||
|
||||
private val store = arrayOfNulls<Breadcrumb?>(maxBreadcrumbs)
|
||||
private val index = AtomicInteger(0)
|
||||
|
||||
fun add(breadcrumb: Breadcrumb) {
|
||||
if (maxBreadcrumbs == 0 || !callbackState.runOnBreadcrumbTasks(breadcrumb, logger)) {
|
||||
return
|
||||
}
|
||||
|
||||
// store the breadcrumb in the ring buffer
|
||||
val position = getBreadcrumbIndex()
|
||||
store[position] = breadcrumb
|
||||
|
||||
updateState {
|
||||
// use direct field access to avoid overhead of accessor method
|
||||
StateEvent.AddBreadcrumb(
|
||||
breadcrumb.impl.message,
|
||||
breadcrumb.impl.type,
|
||||
DateUtils.toIso8601(breadcrumb.impl.timestamp),
|
||||
breadcrumb.impl.metadata ?: mutableMapOf()
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the index in the ring buffer where the breadcrumb should be stored.
|
||||
*/
|
||||
private fun getBreadcrumbIndex(): Int {
|
||||
while (true) {
|
||||
val currentValue = index.get() and validIndexMask
|
||||
val nextValue = (currentValue + 1) % maxBreadcrumbs
|
||||
if (index.compareAndSet(currentValue, nextValue)) {
|
||||
return currentValue
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a copy of the breadcrumbs in the order of their addition.
|
||||
*/
|
||||
fun copy(): List<Breadcrumb> {
|
||||
if (maxBreadcrumbs == 0) {
|
||||
return emptyList()
|
||||
}
|
||||
|
||||
// Set a negative value that stops any other thread from adding a breadcrumb.
|
||||
// This handles reentrancy by waiting here until the old value has been reset.
|
||||
var tail = -1
|
||||
while (tail == -1) {
|
||||
tail = index.getAndSet(-1)
|
||||
}
|
||||
|
||||
try {
|
||||
val result = arrayOfNulls<Breadcrumb>(maxBreadcrumbs)
|
||||
store.copyInto(result, 0, tail, maxBreadcrumbs)
|
||||
store.copyInto(result, maxBreadcrumbs - tail, 0, tail)
|
||||
return result.filterNotNull()
|
||||
} finally {
|
||||
index.set(tail)
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toStream(writer: JsonStream) {
|
||||
val crumbs = copy()
|
||||
writer.beginArray()
|
||||
crumbs.forEach { it.toStream(writer) }
|
||||
writer.endArray()
|
||||
}
|
||||
}
|
||||
@ -1,45 +0,0 @@
|
||||
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
|
||||
|
||||
internal companion object {
|
||||
internal fun fromDescriptor(type: String) = values().singleOrNull { it.type == type }
|
||||
}
|
||||
}
|
||||
@ -1,488 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns true if one of the <code>start</code> methods have been has been called and
|
||||
* so Bugsnag is initialized; false if <code>start</code> has not been called and the
|
||||
* other methods will throw IllegalStateException.
|
||||
*/
|
||||
public static boolean isStarted() {
|
||||
return client != null;
|
||||
}
|
||||
|
||||
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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* For example:
|
||||
* <p>
|
||||
* 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.
|
||||
* <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 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* For example:
|
||||
* <p>
|
||||
* 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>.
|
||||
*
|
||||
* @return true if a previous session was resumed, false if a new session was started.
|
||||
* @see #startSession()
|
||||
* @see #pauseSession()
|
||||
* @see Configuration#setAutoTrackSessions(boolean)
|
||||
*/
|
||||
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()}.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single feature flag with no variant. If there is an existing feature flag with the
|
||||
* same name, it will be overwritten to have no variant.
|
||||
*
|
||||
* @param name the name of the feature flag to add
|
||||
* @see #addFeatureFlag(String, String)
|
||||
*/
|
||||
public static void addFeatureFlag(@NonNull String name) {
|
||||
getClient().addFeatureFlag(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a single feature flag with an optional variant. If there is an existing feature
|
||||
* flag with the same name, it will be overwritten with the new variant. If the variant is
|
||||
* {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}.
|
||||
*
|
||||
* @param name the name of the feature flag to add
|
||||
* @param variant the variant to set the feature flag to, or {@code null} to specify a feature
|
||||
* flag with no variant
|
||||
*/
|
||||
public static void addFeatureFlag(@NonNull String name, @Nullable String variant) {
|
||||
getClient().addFeatureFlag(name, variant);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a collection of feature flags. This method behaves exactly the same as calling
|
||||
* {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects.
|
||||
*
|
||||
* @param featureFlags the feature flags to add
|
||||
* @see #addFeatureFlag(String, String)
|
||||
*/
|
||||
public static void addFeatureFlags(@NonNull Iterable<FeatureFlag> featureFlags) {
|
||||
getClient().addFeatureFlags(featureFlags);
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove a single feature flag regardless of its current status. This will stop the specified
|
||||
* feature flag from being reported. If the named feature flag does not exist this will
|
||||
* have no effect.
|
||||
*
|
||||
* @param name the name of the feature flag to remove
|
||||
*/
|
||||
public static void clearFeatureFlag(@NonNull String name) {
|
||||
getClient().clearFeatureFlag(name);
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all of the feature flags. This will stop all feature flags from being reported.
|
||||
*/
|
||||
public static void clearFeatureFlags() {
|
||||
getClient().clearFeatureFlags();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current Bugsnag Client instance.
|
||||
*/
|
||||
@NonNull
|
||||
public static Client getClient() {
|
||||
if (client == null) {
|
||||
synchronized (lock) {
|
||||
if (client == null) {
|
||||
throw new IllegalStateException("You must call Bugsnag.start before any"
|
||||
+ " other Bugsnag methods");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return client;
|
||||
}
|
||||
}
|
||||
@ -1,292 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils
|
||||
import com.bugsnag.android.internal.InternalMetricsImpl
|
||||
import com.bugsnag.android.internal.dag.ValueProvider
|
||||
import java.text.DateFormat
|
||||
import java.text.SimpleDateFormat
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.TimeZone
|
||||
import java.util.UUID
|
||||
|
||||
internal class BugsnagEventMapper(
|
||||
private val logger: Logger
|
||||
) {
|
||||
|
||||
internal fun convertToEvent(map: Map<in String, Any?>, apiKey: String): Event {
|
||||
return Event(convertToEventImpl(map, apiKey), logger)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun convertToEventImpl(map: Map<in String, Any?>, apiKey: String): EventInternal {
|
||||
val event = EventInternal(apiKey, logger)
|
||||
|
||||
// populate exceptions. check this early to avoid unnecessary serialization if
|
||||
// no stacktrace was gathered.
|
||||
val exceptions = map["exceptions"] as? List<MutableMap<String, Any?>>
|
||||
exceptions?.mapTo(event.errors) { Error(convertErrorInternal(it), this.logger) }
|
||||
|
||||
// populate user
|
||||
event.userImpl = convertUser(map.readEntry("user"))
|
||||
|
||||
// populate metadata
|
||||
val metadataMap: Map<String, Map<String, Any?>> =
|
||||
(map["metaData"] as? Map<String, Map<String, Any?>>).orEmpty()
|
||||
metadataMap.forEach { (key, value) ->
|
||||
event.addMetadata(key, value)
|
||||
}
|
||||
|
||||
val featureFlagsList: List<Map<String, Any?>> =
|
||||
(map["featureFlags"] as? List<Map<String, Any?>>).orEmpty()
|
||||
featureFlagsList.forEach { featureFlagMap ->
|
||||
event.addFeatureFlag(
|
||||
featureFlagMap.readEntry("featureFlag"),
|
||||
featureFlagMap["variant"] as? String
|
||||
)
|
||||
}
|
||||
|
||||
// populate breadcrumbs
|
||||
val breadcrumbList: List<MutableMap<String, Any?>> =
|
||||
(map["breadcrumbs"] as? List<MutableMap<String, Any?>>).orEmpty()
|
||||
breadcrumbList.mapTo(event.breadcrumbs) {
|
||||
Breadcrumb(
|
||||
convertBreadcrumbInternal(it),
|
||||
logger
|
||||
)
|
||||
}
|
||||
|
||||
// populate context
|
||||
event.context = map["context"] as? String
|
||||
|
||||
// populate groupingHash
|
||||
event.groupingHash = map["groupingHash"] as? String
|
||||
|
||||
// populate app
|
||||
event.app = convertAppWithState(map.readEntry("app"))
|
||||
|
||||
// populate device
|
||||
event.device = convertDeviceWithState(map.readEntry("device"))
|
||||
|
||||
// populate session
|
||||
val sessionMap = map["session"] as? Map<String, Any?>
|
||||
sessionMap?.let {
|
||||
event.session = Session(it, logger, apiKey)
|
||||
}
|
||||
|
||||
// populate threads
|
||||
val threads = map["threads"] as? List<Map<String, Any?>>
|
||||
threads?.mapTo(event.threads) { Thread(convertThread(it), logger) }
|
||||
|
||||
// populate projectPackages
|
||||
val projectPackages = map["projectPackages"] as? List<String>
|
||||
projectPackages?.let {
|
||||
event.projectPackages = projectPackages
|
||||
}
|
||||
|
||||
// populate severity
|
||||
val severityStr: String = map.readEntry("severity")
|
||||
val severity = Severity.fromDescriptor(severityStr)
|
||||
val unhandled: Boolean = map.readEntry("unhandled")
|
||||
val reason = deserializeSeverityReason(map, unhandled, severity)
|
||||
event.updateSeverityReasonInternal(reason)
|
||||
event.normalizeStackframeErrorTypes()
|
||||
|
||||
// populate internalMetrics
|
||||
event.internalMetrics = InternalMetricsImpl(map["usage"] as MutableMap<String, Any>?)
|
||||
|
||||
// populate correlation
|
||||
(map["correlation"] as? Map<String, String>)?.let {
|
||||
val traceId = parseTraceId(it["traceId"])
|
||||
val spanId = it["spanId"]?.parseUnsignedLong()
|
||||
|
||||
if (traceId != null && spanId != null) {
|
||||
event.traceCorrelation = TraceCorrelation(traceId, spanId)
|
||||
}
|
||||
}
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
internal fun convertError(error: Map<in String, Any?>): Error {
|
||||
return Error(convertErrorInternal(error), logger)
|
||||
}
|
||||
|
||||
internal fun convertErrorInternal(error: Map<in String, Any?>): ErrorInternal {
|
||||
return ErrorInternal(
|
||||
error.readEntry("errorClass"),
|
||||
error["message"] as? String,
|
||||
type = error.readEntry<String>("type").let { type ->
|
||||
ErrorType.fromDescriptor(type)
|
||||
?: throw IllegalArgumentException("unknown ErrorType: '$type'")
|
||||
},
|
||||
stacktrace = convertStacktrace(error.readEntry("stacktrace"))
|
||||
)
|
||||
}
|
||||
|
||||
internal fun convertUser(user: Map<String, Any?>): User {
|
||||
return User(
|
||||
user["id"] as? String,
|
||||
user["email"] as? String,
|
||||
user["name"] as? String
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun convertBreadcrumbInternal(breadcrumb: Map<String, Any?>): BreadcrumbInternal {
|
||||
return BreadcrumbInternal(
|
||||
breadcrumb.readEntry("name"),
|
||||
breadcrumb.readEntry<String>("type").let { type ->
|
||||
BreadcrumbType.fromDescriptor(type)
|
||||
?: BreadcrumbType.MANUAL
|
||||
},
|
||||
breadcrumb["metaData"] as? MutableMap<String, Any?>,
|
||||
breadcrumb.readEntry<String>("timestamp").toDate()
|
||||
)
|
||||
}
|
||||
|
||||
internal fun convertAppWithState(app: Map<String, Any?>): AppWithState {
|
||||
return AppWithState(
|
||||
app["binaryArch"] as? String,
|
||||
app["id"] as? String,
|
||||
app["releaseStage"] as? String,
|
||||
app["version"] as? String,
|
||||
app["codeBundleId"] as? String,
|
||||
(app["buildUUID"] as? String)?.let(::ValueProvider),
|
||||
app["type"] as? String,
|
||||
(app["versionCode"] as? Number)?.toInt(),
|
||||
(app["duration"] as? Number)?.toLong(),
|
||||
(app["durationInForeground"] as? Number)?.toLong(),
|
||||
app["inForeground"] as? Boolean,
|
||||
app["isLaunching"] as? Boolean
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun convertDeviceWithState(device: Map<String, Any?>): DeviceWithState {
|
||||
return DeviceWithState(
|
||||
DeviceBuildInfo(
|
||||
device["manufacturer"] as? String,
|
||||
device["model"] as? String,
|
||||
device["osVersion"] as? String,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
null,
|
||||
(device["cpuAbi"] as? List<String>)?.toTypedArray()
|
||||
),
|
||||
device["jailbroken"] as? Boolean,
|
||||
device["id"] as? String,
|
||||
device["locale"] as? String,
|
||||
(device["totalMemory"] as? Number)?.toLong(),
|
||||
(device["runtimeVersions"] as? Map<String, Any>)?.toMutableMap()
|
||||
?: mutableMapOf(),
|
||||
(device["freeDisk"] as? Number)?.toLong(),
|
||||
(device["freeMemory"] as? Number)?.toLong(),
|
||||
device["orientation"] as? String,
|
||||
(device["time"] as? String)?.toDate()
|
||||
)
|
||||
}
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
internal fun convertThread(thread: Map<String, Any?>): ThreadInternal {
|
||||
return ThreadInternal(
|
||||
thread["id"].toString(),
|
||||
thread.readEntry("name"),
|
||||
ErrorType.fromDescriptor(thread.readEntry("type")) ?: ErrorType.ANDROID,
|
||||
thread["errorReportingThread"] == true,
|
||||
thread["state"] as? String ?: "",
|
||||
(thread["stacktrace"] as? List<Map<String, Any?>>)?.let { convertStacktrace(it) }
|
||||
?: Stacktrace(mutableListOf())
|
||||
)
|
||||
}
|
||||
|
||||
internal fun convertStacktrace(trace: List<Map<String, Any?>>): Stacktrace {
|
||||
return Stacktrace(trace.mapTo(ArrayList(trace.size)) { Stackframe(it) })
|
||||
}
|
||||
|
||||
internal fun deserializeSeverityReason(
|
||||
map: Map<in String, Any?>,
|
||||
unhandled: Boolean,
|
||||
severity: Severity?
|
||||
): SeverityReason {
|
||||
val severityReason: Map<String, Any> = map.readEntry("severityReason")
|
||||
val unhandledOverridden: Boolean =
|
||||
severityReason.readEntry("unhandledOverridden")
|
||||
val type: String = severityReason.readEntry("type")
|
||||
val originalUnhandled = when {
|
||||
unhandledOverridden -> !unhandled
|
||||
else -> unhandled
|
||||
}
|
||||
|
||||
val attrMap: Map<String, String>? = severityReason.readEntry("attributes")
|
||||
val entry = attrMap?.entries?.singleOrNull()
|
||||
return SeverityReason(
|
||||
type,
|
||||
severity,
|
||||
unhandled,
|
||||
originalUnhandled,
|
||||
entry?.value,
|
||||
entry?.key
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Convenience method for getting an entry from a Map in the expected type, which
|
||||
* throws useful error messages if the expected type is not there.
|
||||
*/
|
||||
private inline fun <reified T> Map<*, *>.readEntry(key: String): T {
|
||||
when (val value = get(key)) {
|
||||
is T -> return value
|
||||
null -> throw IllegalStateException("cannot find json property '$key'")
|
||||
else -> throw IllegalArgumentException(
|
||||
"json property '$key' not of expected type, found ${value.javaClass.name}"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun String.toDate(): Date {
|
||||
if (isNotEmpty() && this[0] == 't') {
|
||||
// date is in the format 't{epoch millis}'
|
||||
val timestamp = substring(1)
|
||||
timestamp.toLongOrNull()?.let {
|
||||
return Date(it)
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
DateUtils.fromIso8601(this)
|
||||
} catch (pe: IllegalArgumentException) {
|
||||
ndkDateFormatHolder.get()!!.parse(this)
|
||||
?: throw IllegalArgumentException("cannot parse date $this")
|
||||
}
|
||||
}
|
||||
|
||||
private fun parseTraceId(traceId: String?): UUID? {
|
||||
if (traceId?.length != 32) return null
|
||||
val mostSigBits = traceId.substring(0, 16).parseUnsignedLong() ?: return null
|
||||
val leastSigBits = traceId.substring(16).parseUnsignedLong() ?: return null
|
||||
|
||||
return UUID(mostSigBits, leastSigBits)
|
||||
}
|
||||
|
||||
private fun String.parseUnsignedLong(): Long? {
|
||||
if (length != 16) return null
|
||||
return try {
|
||||
(substring(0, 2).toLong(16) shl 56) or
|
||||
substring(2).toLong(16)
|
||||
} catch (nfe: NumberFormatException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
// SimpleDateFormat isn't thread safe, cache one instance per thread as needed.
|
||||
private val ndkDateFormatHolder = object : ThreadLocal<DateFormat>() {
|
||||
override fun initialValue(): DateFormat {
|
||||
return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply {
|
||||
timeZone = TimeZone.getTimeZone("UTC")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.dag.DependencyModule
|
||||
|
||||
/**
|
||||
* A dependency module which constructs the objects that track state in Bugsnag. For example, this
|
||||
* class is responsible for creating classes which track the current breadcrumb/metadata state.
|
||||
*/
|
||||
internal class BugsnagStateModule(
|
||||
cfg: ImmutableConfig,
|
||||
configuration: Configuration
|
||||
) : DependencyModule {
|
||||
|
||||
val clientObservable = ClientObservable()
|
||||
|
||||
val callbackState = configuration.impl.callbackState
|
||||
|
||||
val contextState = ContextState().apply {
|
||||
if (configuration.context != null) {
|
||||
setManualContext(configuration.context)
|
||||
}
|
||||
}
|
||||
|
||||
val breadcrumbState = BreadcrumbState(cfg.maxBreadcrumbs, callbackState, cfg.logger)
|
||||
|
||||
val metadataState = copyMetadataState(configuration)
|
||||
|
||||
val featureFlagState = configuration.impl.featureFlagState.copy()
|
||||
|
||||
private fun copyMetadataState(configuration: Configuration): MetadataState {
|
||||
// performs deep copy of metadata to preserve immutability of Configuration interface
|
||||
val orig = configuration.impl.metadataState.metadata
|
||||
return configuration.impl.metadataState.copy(metadata = orig.copy())
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.StrictMode.OnThreadViolationListener;
|
||||
import android.os.strictmode.Violation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* Sends an error report to Bugsnag for each StrictMode thread policy violation that occurs in
|
||||
* your app.
|
||||
* <p></p>
|
||||
* You should use this class by instantiating Bugsnag in the normal way and then set the
|
||||
* StrictMode policy with
|
||||
* {@link android.os.StrictMode.ThreadPolicy.Builder#penaltyListener
|
||||
* (Executor, OnThreadViolationListener)}.
|
||||
* This functionality is only supported on API 28+.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public class BugsnagThreadViolationListener implements OnThreadViolationListener {
|
||||
|
||||
private final Client client;
|
||||
private final OnThreadViolationListener listener;
|
||||
|
||||
public BugsnagThreadViolationListener() {
|
||||
this(Bugsnag.getClient(), null);
|
||||
}
|
||||
|
||||
public BugsnagThreadViolationListener(@NonNull Client client) {
|
||||
this(client, null);
|
||||
}
|
||||
|
||||
public BugsnagThreadViolationListener(@NonNull Client client,
|
||||
@Nullable OnThreadViolationListener listener) {
|
||||
this.client = client;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onThreadViolation(@NonNull Violation violation) {
|
||||
if (client != null) {
|
||||
client.notify(violation, new StrictModeOnErrorCallback(
|
||||
"StrictMode policy violation detected: ThreadPolicy"
|
||||
));
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onThreadViolation(violation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import android.os.Build;
|
||||
import android.os.StrictMode.OnVmViolationListener;
|
||||
import android.os.strictmode.Violation;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
import androidx.annotation.RequiresApi;
|
||||
|
||||
/**
|
||||
* Sends an error report to Bugsnag for each StrictMode VM policy violation that occurs in
|
||||
* your app.
|
||||
* <p></p>
|
||||
* You should use this class by instantiating Bugsnag in the normal way and then set the
|
||||
* StrictMode policy with
|
||||
* {@link android.os.StrictMode.VmPolicy.Builder#penaltyListener
|
||||
* (Executor, OnVmViolationListener)}.
|
||||
* This functionality is only supported on API 28+.
|
||||
*/
|
||||
@RequiresApi(api = Build.VERSION_CODES.P)
|
||||
public class BugsnagVmViolationListener implements OnVmViolationListener {
|
||||
|
||||
private final Client client;
|
||||
private final OnVmViolationListener listener;
|
||||
|
||||
public BugsnagVmViolationListener() {
|
||||
this(Bugsnag.getClient(), null);
|
||||
}
|
||||
|
||||
public BugsnagVmViolationListener(@NonNull Client client) {
|
||||
this(client, null);
|
||||
}
|
||||
|
||||
public BugsnagVmViolationListener(@NonNull Client client,
|
||||
@Nullable OnVmViolationListener listener) {
|
||||
this.client = client;
|
||||
this.listener = listener;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void onVmViolation(@NonNull Violation violation) {
|
||||
if (client != null) {
|
||||
client.notify(violation, new StrictModeOnErrorCallback(
|
||||
"StrictMode policy violation detected: VmPolicy"
|
||||
));
|
||||
}
|
||||
if (listener != null) {
|
||||
listener.onVmViolation(violation);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
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)
|
||||
}
|
||||
@ -1,169 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.InternalMetrics
|
||||
import com.bugsnag.android.internal.InternalMetricsNoop
|
||||
import java.util.concurrent.CopyOnWriteArrayList
|
||||
|
||||
internal data class CallbackState(
|
||||
val onErrorTasks: MutableCollection<OnErrorCallback> = CopyOnWriteArrayList(),
|
||||
val onBreadcrumbTasks: MutableCollection<OnBreadcrumbCallback> = CopyOnWriteArrayList(),
|
||||
val onSessionTasks: MutableCollection<OnSessionCallback> = CopyOnWriteArrayList(),
|
||||
val onSendTasks: MutableList<OnSendCallback> = CopyOnWriteArrayList()
|
||||
) : CallbackAware {
|
||||
|
||||
private var internalMetrics: InternalMetrics = InternalMetricsNoop()
|
||||
|
||||
companion object {
|
||||
private const val onBreadcrumbName = "onBreadcrumb"
|
||||
private const val onErrorName = "onError"
|
||||
private const val onSendName = "onSendError"
|
||||
private const val onSessionName = "onSession"
|
||||
}
|
||||
|
||||
fun setInternalMetrics(metrics: InternalMetrics) {
|
||||
internalMetrics = metrics
|
||||
internalMetrics.setCallbackCounts(getCallbackCounts())
|
||||
}
|
||||
|
||||
override fun addOnError(onError: OnErrorCallback) {
|
||||
if (onErrorTasks.add(onError)) {
|
||||
internalMetrics.notifyAddCallback(onErrorName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeOnError(onError: OnErrorCallback) {
|
||||
if (onErrorTasks.remove(onError)) {
|
||||
internalMetrics.notifyRemoveCallback(onErrorName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
||||
if (onBreadcrumbTasks.add(onBreadcrumb)) {
|
||||
internalMetrics.notifyAddCallback(onBreadcrumbName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeOnBreadcrumb(onBreadcrumb: OnBreadcrumbCallback) {
|
||||
if (onBreadcrumbTasks.remove(onBreadcrumb)) {
|
||||
internalMetrics.notifyRemoveCallback(onBreadcrumbName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addOnSession(onSession: OnSessionCallback) {
|
||||
if (onSessionTasks.add(onSession)) {
|
||||
internalMetrics.notifyAddCallback(onSessionName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun removeOnSession(onSession: OnSessionCallback) {
|
||||
if (onSessionTasks.remove(onSession)) {
|
||||
internalMetrics.notifyRemoveCallback(onSessionName)
|
||||
}
|
||||
}
|
||||
|
||||
fun addOnSend(onSend: OnSendCallback) {
|
||||
if (onSendTasks.add(onSend)) {
|
||||
internalMetrics.notifyAddCallback(onSendName)
|
||||
}
|
||||
}
|
||||
|
||||
fun addPreOnSend(onSend: OnSendCallback) {
|
||||
onSendTasks.add(0, onSend)
|
||||
internalMetrics.notifyAddCallback(onSendName)
|
||||
}
|
||||
|
||||
fun removeOnSend(onSend: OnSendCallback) {
|
||||
if (onSendTasks.remove(onSend)) {
|
||||
internalMetrics.notifyRemoveCallback(onSendName)
|
||||
}
|
||||
}
|
||||
|
||||
fun runOnErrorTasks(event: Event, logger: Logger): Boolean {
|
||||
// optimization to avoid construction of iterator when no callbacks set
|
||||
if (onErrorTasks.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
onErrorTasks.forEach {
|
||||
try {
|
||||
if (!it.onError(event)) {
|
||||
return false
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.w("OnBreadcrumbCallback threw an Exception", ex)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun runOnBreadcrumbTasks(breadcrumb: Breadcrumb, logger: Logger): Boolean {
|
||||
// optimization to avoid construction of iterator when no callbacks set
|
||||
if (onBreadcrumbTasks.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
onBreadcrumbTasks.forEach {
|
||||
try {
|
||||
if (!it.onBreadcrumb(breadcrumb)) {
|
||||
return false
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.w("OnBreadcrumbCallback threw an Exception", ex)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun runOnSessionTasks(session: Session, logger: Logger): Boolean {
|
||||
// optimization to avoid construction of iterator when no callbacks set
|
||||
if (onSessionTasks.isEmpty()) {
|
||||
return true
|
||||
}
|
||||
onSessionTasks.forEach {
|
||||
try {
|
||||
if (!it.onSession(session)) {
|
||||
return false
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.w("OnSessionCallback threw an Exception", ex)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun runOnSendTasks(event: Event, logger: Logger): Boolean {
|
||||
onSendTasks.forEach {
|
||||
try {
|
||||
if (!it.onSend(event)) {
|
||||
return false
|
||||
}
|
||||
} catch (ex: Throwable) {
|
||||
logger.w("OnSendCallback threw an Exception", ex)
|
||||
}
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun runOnSendTasks(eventSource: () -> Event, logger: Logger): Boolean {
|
||||
if (onSendTasks.isEmpty()) {
|
||||
// avoid constructing event from eventSource if not needed
|
||||
return true
|
||||
}
|
||||
|
||||
return this.runOnSendTasks(eventSource(), logger)
|
||||
}
|
||||
|
||||
fun copy() = this.copy(
|
||||
onErrorTasks = onErrorTasks,
|
||||
onBreadcrumbTasks = onBreadcrumbTasks,
|
||||
onSessionTasks = onSessionTasks,
|
||||
onSendTasks = onSendTasks
|
||||
)
|
||||
|
||||
private fun getCallbackCounts(): Map<String, Int> {
|
||||
return hashMapOf<String, Int>().also { map ->
|
||||
if (onBreadcrumbTasks.count() > 0) map[onBreadcrumbName] = onBreadcrumbTasks.count()
|
||||
if (onErrorTasks.count() > 0) map[onErrorName] = onErrorTasks.count()
|
||||
if (onSendTasks.count() > 0) map[onSendName] = onSendTasks.count()
|
||||
if (onSessionTasks.count() > 0) map[onSessionName] = onSessionTasks.count()
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,28 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
import android.content.res.Configuration
|
||||
|
||||
internal class ClientComponentCallbacks(
|
||||
private val deviceDataCollector: DeviceDataCollector,
|
||||
private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit,
|
||||
val memoryCallback: (Boolean, Int?) -> Unit
|
||||
) : ComponentCallbacks2 {
|
||||
|
||||
override fun onConfigurationChanged(newConfig: Configuration) {
|
||||
val oldOrientation = deviceDataCollector.getOrientationAsString()
|
||||
|
||||
if (deviceDataCollector.updateOrientation(newConfig.orientation)) {
|
||||
val newOrientation = deviceDataCollector.getOrientationAsString()
|
||||
cb(oldOrientation, newOrientation)
|
||||
}
|
||||
}
|
||||
|
||||
override fun onTrimMemory(level: Int) {
|
||||
memoryCallback(level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE, level)
|
||||
}
|
||||
|
||||
override fun onLowMemory() {
|
||||
memoryCallback(true, null)
|
||||
}
|
||||
}
|
||||
@ -1,34 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
|
||||
internal class ClientObservable : BaseObservable() {
|
||||
|
||||
fun postOrientationChange(orientation: String?) {
|
||||
updateState { StateEvent.UpdateOrientation(orientation) }
|
||||
}
|
||||
|
||||
fun postNdkInstall(
|
||||
conf: ImmutableConfig,
|
||||
lastRunInfoPath: String,
|
||||
consecutiveLaunchCrashes: Int
|
||||
) {
|
||||
updateState {
|
||||
StateEvent.Install(
|
||||
conf.apiKey,
|
||||
conf.enabledErrorTypes.ndkCrashes,
|
||||
conf.appVersion,
|
||||
conf.buildUuid?.getOrNull(),
|
||||
conf.releaseStage,
|
||||
lastRunInfoPath,
|
||||
consecutiveLaunchCrashes,
|
||||
conf.sendThreads,
|
||||
conf.maxBreadcrumbs
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
fun postNdkDeliverPending() {
|
||||
updateState { StateEvent.DeliverPending }
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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;
|
||||
}
|
||||
}
|
||||
@ -1,172 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.content.Context
|
||||
import java.io.File
|
||||
import java.util.EnumSet
|
||||
import java.util.regex.Pattern
|
||||
|
||||
internal class ConfigInternal(
|
||||
var apiKey: String?
|
||||
) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware {
|
||||
|
||||
private var user = User()
|
||||
|
||||
@JvmField
|
||||
internal val callbackState: CallbackState = CallbackState()
|
||||
|
||||
@JvmField
|
||||
internal val metadataState: MetadataState = MetadataState()
|
||||
|
||||
@JvmField
|
||||
internal val featureFlagState: FeatureFlagState = FeatureFlagState()
|
||||
|
||||
var appVersion: String? = null
|
||||
var versionCode: Int? = 0
|
||||
var releaseStage: String? = null
|
||||
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
|
||||
var persistUser: Boolean = true
|
||||
var generateAnonymousId: Boolean = true
|
||||
|
||||
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 maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS
|
||||
var threadCollectionTimeLimitMillis: Long = DEFAULT_THREAD_COLLECTION_TIME_LIMIT_MS
|
||||
var maxStringValueLength: Int = DEFAULT_MAX_STRING_VALUE_LENGTH
|
||||
var context: String? = null
|
||||
|
||||
var redactedKeys: Set<Pattern>
|
||||
get() = metadataState.metadata.redactedKeys
|
||||
set(value) {
|
||||
metadataState.metadata.redactedKeys = value
|
||||
}
|
||||
|
||||
var discardClasses: Set<Pattern> = emptySet()
|
||||
var enabledReleaseStages: Set<String>? = null
|
||||
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
|
||||
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE)
|
||||
var projectPackages: Set<String> = emptySet()
|
||||
var persistenceDirectory: File? = null
|
||||
|
||||
var attemptDeliveryOnCrash: Boolean = false
|
||||
|
||||
val notifier: Notifier = Notifier()
|
||||
|
||||
protected val plugins = HashSet<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)
|
||||
fun addOnSend(onSend: OnSendCallback) = callbackState.addOnSend(onSend)
|
||||
fun removeOnSend(onSend: OnSendCallback) = callbackState.removeOnSend(onSend)
|
||||
|
||||
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 addFeatureFlag(name: String) = featureFlagState.addFeatureFlag(name)
|
||||
override fun addFeatureFlag(name: String, variant: String?) =
|
||||
featureFlagState.addFeatureFlag(name, variant)
|
||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) =
|
||||
featureFlagState.addFeatureFlags(featureFlags)
|
||||
override fun clearFeatureFlag(name: String) = featureFlagState.clearFeatureFlag(name)
|
||||
override fun clearFeatureFlags() = featureFlagState.clearFeatureFlags()
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
private fun toCommaSeparated(coll: Collection<Any>?): String {
|
||||
return coll?.map { it.toString() }?.sorted()?.joinToString(",") ?: ""
|
||||
}
|
||||
|
||||
fun getConfigDifferences(): Map<String, Any> {
|
||||
// allocate a local ConfigInternal with all-defaults to compare against
|
||||
val defaultConfig = ConfigInternal("")
|
||||
|
||||
return listOfNotNull(
|
||||
if (plugins.count() > 0) "pluginCount" to plugins.count() else null,
|
||||
if (autoDetectErrors != defaultConfig.autoDetectErrors)
|
||||
"autoDetectErrors" to autoDetectErrors else null,
|
||||
if (autoTrackSessions != defaultConfig.autoTrackSessions)
|
||||
"autoTrackSessions" to autoTrackSessions else null,
|
||||
if (discardClasses.count() > 0)
|
||||
"discardClassesCount" to discardClasses.count() else null,
|
||||
if (enabledBreadcrumbTypes != defaultConfig.enabledBreadcrumbTypes)
|
||||
"enabledBreadcrumbTypes" to toCommaSeparated(enabledBreadcrumbTypes) else null,
|
||||
if (enabledErrorTypes != defaultConfig.enabledErrorTypes)
|
||||
"enabledErrorTypes" to toCommaSeparated(
|
||||
listOfNotNull(
|
||||
if (enabledErrorTypes.anrs) "anrs" else null,
|
||||
if (enabledErrorTypes.ndkCrashes) "ndkCrashes" else null,
|
||||
if (enabledErrorTypes.unhandledExceptions) "unhandledExceptions" else null,
|
||||
if (enabledErrorTypes.unhandledRejections) "unhandledRejections" else null,
|
||||
)
|
||||
) else null,
|
||||
if (launchDurationMillis != 0L) "launchDurationMillis" to launchDurationMillis else null,
|
||||
if (logger != NoopLogger) "logger" to true else null,
|
||||
if (maxBreadcrumbs != defaultConfig.maxBreadcrumbs)
|
||||
"maxBreadcrumbs" to maxBreadcrumbs else null,
|
||||
if (maxPersistedEvents != defaultConfig.maxPersistedEvents)
|
||||
"maxPersistedEvents" to maxPersistedEvents else null,
|
||||
if (maxPersistedSessions != defaultConfig.maxPersistedSessions)
|
||||
"maxPersistedSessions" to maxPersistedSessions else null,
|
||||
if (maxReportedThreads != defaultConfig.maxReportedThreads)
|
||||
"maxReportedThreads" to maxReportedThreads else null,
|
||||
if (threadCollectionTimeLimitMillis != defaultConfig.threadCollectionTimeLimitMillis)
|
||||
"threadCollectionTimeLimitMillis" to threadCollectionTimeLimitMillis else null,
|
||||
if (persistenceDirectory != null)
|
||||
"persistenceDirectorySet" to true else null,
|
||||
if (sendThreads != defaultConfig.sendThreads)
|
||||
"sendThreads" to sendThreads else null,
|
||||
if (attemptDeliveryOnCrash != defaultConfig.attemptDeliveryOnCrash)
|
||||
"attemptDeliveryOnCrash" to attemptDeliveryOnCrash else null
|
||||
).toMap()
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val DEFAULT_MAX_BREADCRUMBS = 100
|
||||
private const val DEFAULT_MAX_PERSISTED_SESSIONS = 128
|
||||
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
|
||||
private const val DEFAULT_MAX_REPORTED_THREADS = 200
|
||||
private const val DEFAULT_THREAD_COLLECTION_TIME_LIMIT_MS: Long = 5000
|
||||
private const val DEFAULT_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
|
||||
private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000
|
||||
|
||||
@JvmStatic
|
||||
fun load(context: Context): Configuration = load(context, null)
|
||||
|
||||
@JvmStatic
|
||||
protected fun load(context: Context, apiKey: String?): Configuration {
|
||||
return ManifestConfigLoader().load(context, apiKey)
|
||||
}
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@ -1,180 +0,0 @@
|
||||
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
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.bugsnag.android.UnknownConnectivity.retrieveNetworkAccessState
|
||||
import java.util.concurrent.atomic.AtomicBoolean
|
||||
|
||||
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.getConnectivityManager()
|
||||
|
||||
private val connectivity: Connectivity =
|
||||
when {
|
||||
cm == null -> UnknownConnectivity
|
||||
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)
|
||||
|
||||
private val activeNetworkInfo: android.net.NetworkInfo?
|
||||
get() = try {
|
||||
cm.activeNetworkInfo
|
||||
} catch (e: NullPointerException) {
|
||||
// in some rare cases we get a remote NullPointerException via Parcel.readException
|
||||
null
|
||||
}
|
||||
|
||||
override fun registerForNetworkChanges() {
|
||||
val intentFilter = IntentFilter(ConnectivityManager.CONNECTIVITY_ACTION)
|
||||
context.registerReceiverSafe(changeReceiver, intentFilter)
|
||||
}
|
||||
|
||||
override fun unregisterForNetworkChanges() = context.unregisterReceiverSafe(changeReceiver)
|
||||
|
||||
override fun hasNetworkConnection(): Boolean {
|
||||
return activeNetworkInfo?.isConnectedOrConnecting ?: false
|
||||
}
|
||||
|
||||
override fun retrieveNetworkAccessState(): String {
|
||||
return when (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() {
|
||||
|
||||
private val receivedFirstCallback = AtomicBoolean(false)
|
||||
|
||||
override fun onReceive(context: Context, intent: Intent) {
|
||||
if (receivedFirstCallback.getAndSet(true)) {
|
||||
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"
|
||||
}
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal class ConnectivityTrackerCallback(
|
||||
private val cb: NetworkChangeCallback?
|
||||
) : ConnectivityManager.NetworkCallback() {
|
||||
|
||||
private val receivedFirstCallback = AtomicBoolean(false)
|
||||
|
||||
override fun onUnavailable() {
|
||||
super.onUnavailable()
|
||||
invokeNetworkCallback(false)
|
||||
}
|
||||
|
||||
override fun onAvailable(network: Network) {
|
||||
super.onAvailable(network)
|
||||
invokeNetworkCallback(true)
|
||||
}
|
||||
|
||||
/**
|
||||
* Invokes the network callback, as long as the ConnectivityManager callback has been
|
||||
* triggered at least once before (when setting a NetworkCallback Android always
|
||||
* invokes the callback with the current network state).
|
||||
*/
|
||||
private fun invokeNetworkCallback(hasConnection: Boolean) {
|
||||
if (receivedFirstCallback.getAndSet(true)) {
|
||||
cb?.invoke(hasConnection, retrieveNetworkAccessState())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Connectivity used in cases where we cannot access the system ConnectivityManager.
|
||||
* We assume that there is some sort of network and do not attempt to report any network changes.
|
||||
*/
|
||||
internal object UnknownConnectivity : Connectivity {
|
||||
override fun registerForNetworkChanges() {}
|
||||
|
||||
override fun unregisterForNetworkChanges() {}
|
||||
|
||||
override fun hasNetworkConnection(): Boolean {
|
||||
return true
|
||||
}
|
||||
|
||||
override fun retrieveNetworkAccessState(): String {
|
||||
return "unknown"
|
||||
}
|
||||
}
|
||||
@ -1,81 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.app.ActivityManager
|
||||
import android.content.BroadcastReceiver
|
||||
import android.content.Context
|
||||
import android.content.Intent
|
||||
import android.content.IntentFilter
|
||||
import android.location.LocationManager
|
||||
import android.net.ConnectivityManager
|
||||
import android.os.Build
|
||||
import android.os.RemoteException
|
||||
import android.os.storage.StorageManager
|
||||
import java.lang.RuntimeException
|
||||
|
||||
/**
|
||||
* 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 if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) {
|
||||
registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED)
|
||||
} else {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
private inline fun <reified T> Context.safeGetSystemService(name: String): T? {
|
||||
return try {
|
||||
getSystemService(name) as? T
|
||||
} catch (exc: RuntimeException) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
@JvmName("getActivityManagerFrom")
|
||||
internal fun Context.getActivityManager(): ActivityManager? =
|
||||
safeGetSystemService(Context.ACTIVITY_SERVICE)
|
||||
|
||||
@JvmName("getConnectivityManagerFrom")
|
||||
internal fun Context.getConnectivityManager(): ConnectivityManager? =
|
||||
safeGetSystemService(Context.CONNECTIVITY_SERVICE)
|
||||
|
||||
@JvmName("getStorageManagerFrom")
|
||||
internal fun Context.getStorageManager(): StorageManager? =
|
||||
safeGetSystemService(Context.STORAGE_SERVICE)
|
||||
|
||||
@JvmName("getLocationManager")
|
||||
internal fun Context.getLocationManager(): LocationManager? =
|
||||
safeGetSystemService(Context.LOCATION_SERVICE)
|
||||
@ -1,36 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* Tracks the current context and allows observers to be notified whenever it changes.
|
||||
*
|
||||
* The default behaviour is to track [SessionTracker.getContextActivity]. However, any value
|
||||
* that the user sets via [Bugsnag.setContext] will override this and be returned instead.
|
||||
*/
|
||||
internal class ContextState : BaseObservable() {
|
||||
|
||||
companion object {
|
||||
private const val MANUAL = "__BUGSNAG_MANUAL_CONTEXT__"
|
||||
}
|
||||
|
||||
private var manualContext: String? = null
|
||||
private var automaticContext: String? = null
|
||||
|
||||
fun setManualContext(context: String?) {
|
||||
manualContext = context
|
||||
automaticContext = MANUAL
|
||||
emitObservableEvent()
|
||||
}
|
||||
|
||||
fun setAutomaticContext(context: String?) {
|
||||
if (automaticContext !== MANUAL) {
|
||||
automaticContext = context
|
||||
emitObservableEvent()
|
||||
}
|
||||
}
|
||||
|
||||
fun getContext(): String? {
|
||||
return automaticContext.takeIf { it !== MANUAL } ?: manualContext
|
||||
}
|
||||
|
||||
fun emitObservableEvent() = updateState { StateEvent.UpdateContext(getContext()) }
|
||||
}
|
||||
@ -1,62 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.os.Environment
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
|
||||
import com.bugsnag.android.internal.dag.ConfigModule
|
||||
import com.bugsnag.android.internal.dag.ContextModule
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
import com.bugsnag.android.internal.dag.SystemServiceModule
|
||||
|
||||
/**
|
||||
* A dependency module which constructs the objects that collect data in Bugsnag. For example, this
|
||||
* class is responsible for creating classes which capture device-specific information.
|
||||
*/
|
||||
internal class DataCollectionModule(
|
||||
contextModule: ContextModule,
|
||||
configModule: ConfigModule,
|
||||
systemServiceModule: SystemServiceModule,
|
||||
trackerModule: TrackerModule,
|
||||
bgTaskService: BackgroundTaskService,
|
||||
connectivity: Connectivity,
|
||||
deviceIdStore: Provider<DeviceIdStore>,
|
||||
memoryTrimState: MemoryTrimState
|
||||
) : BackgroundDependencyModule(bgTaskService) {
|
||||
|
||||
private val ctx = contextModule.ctx
|
||||
private val cfg = configModule.config
|
||||
private val logger = cfg.logger
|
||||
private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo()
|
||||
private val dataDir = Environment.getDataDirectory()
|
||||
|
||||
val appDataCollector = provider {
|
||||
AppDataCollector(
|
||||
ctx,
|
||||
ctx.packageManager,
|
||||
cfg,
|
||||
trackerModule.sessionTracker.get(),
|
||||
systemServiceModule.activityManager,
|
||||
trackerModule.launchCrashTracker,
|
||||
memoryTrimState
|
||||
)
|
||||
}
|
||||
|
||||
private val rootDetection = provider {
|
||||
val rootDetector = RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo)
|
||||
rootDetector.isRooted()
|
||||
}
|
||||
|
||||
val deviceDataCollector = provider {
|
||||
DeviceDataCollector(
|
||||
connectivity,
|
||||
ctx,
|
||||
ctx.resources,
|
||||
deviceIdStore.map { it.load() },
|
||||
deviceBuildInfo,
|
||||
dataDir,
|
||||
rootDetection,
|
||||
bgTaskService,
|
||||
logger
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,40 +0,0 @@
|
||||
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)
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.net.TrafficStats
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
internal class DefaultDelivery(
|
||||
private val connectivity: Connectivity?,
|
||||
private val logger: Logger
|
||||
) : Delivery {
|
||||
|
||||
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val status = deliver(
|
||||
deliveryParams.endpoint,
|
||||
JsonHelper.serialize(payload),
|
||||
payload.integrityToken,
|
||||
deliveryParams.headers
|
||||
)
|
||||
logger.i("Session API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val json = payload.trimToSize().toByteArray()
|
||||
val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers)
|
||||
logger.i("Error API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
fun deliver(
|
||||
urlString: String,
|
||||
json: ByteArray,
|
||||
integrity: String?,
|
||||
headers: Map<String, String?>
|
||||
): DeliveryStatus {
|
||||
|
||||
TrafficStats.setThreadStatsTag(1)
|
||||
if (connectivity != null && !connectivity.hasNetworkConnection()) {
|
||||
return DeliveryStatus.UNDELIVERED
|
||||
}
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
conn = makeRequest(URL(urlString), json, integrity, headers)
|
||||
|
||||
// End the request, get the response code
|
||||
val responseCode = conn.responseCode
|
||||
val status = DeliveryStatus.forHttpResponseCode(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.FAILURE
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Unexpected error delivering payload", exception)
|
||||
return DeliveryStatus.FAILURE
|
||||
} finally {
|
||||
conn?.disconnect()
|
||||
}
|
||||
}
|
||||
|
||||
private fun makeRequest(
|
||||
url: URL,
|
||||
json: ByteArray,
|
||||
integrity: String?,
|
||||
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)
|
||||
|
||||
integrity?.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) {
|
||||
runCatching {
|
||||
logger.i(
|
||||
"Request completed with code $code, " +
|
||||
"message: ${conn.responseMessage}, " +
|
||||
"headers: ${conn.headerFields}"
|
||||
)
|
||||
}
|
||||
runCatching {
|
||||
conn.inputStream.bufferedReader().use {
|
||||
logger.d("Received request response: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
if (status != DeliveryStatus.DELIVERED) {
|
||||
conn.errorStream.bufferedReader().use {
|
||||
logger.w("Request error details: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.net.TrafficStats
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
import java.io.IOException
|
||||
import java.net.HttpURLConnection
|
||||
import java.net.URL
|
||||
|
||||
internal class DefaultDelivery(
|
||||
private val connectivity: Connectivity?,
|
||||
private val logger: Logger
|
||||
) : Delivery {
|
||||
|
||||
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val status = deliver(
|
||||
deliveryParams.endpoint,
|
||||
JsonHelper.serialize(payload),
|
||||
payload.integrityToken,
|
||||
deliveryParams.headers
|
||||
)
|
||||
logger.i("Session API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
|
||||
val json = payload.trimToSize().toByteArray()
|
||||
val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers)
|
||||
logger.i("Error API request finished with status $status")
|
||||
return status
|
||||
}
|
||||
|
||||
fun deliver(
|
||||
urlString: String,
|
||||
json: ByteArray,
|
||||
integrity: String?,
|
||||
headers: Map<String, String?>
|
||||
): DeliveryStatus {
|
||||
|
||||
TrafficStats.setThreadStatsTag(1)
|
||||
if (connectivity != null && !connectivity.hasNetworkConnection()) {
|
||||
return DeliveryStatus.UNDELIVERED
|
||||
}
|
||||
var conn: HttpURLConnection? = null
|
||||
|
||||
try {
|
||||
conn = makeRequest(URL(urlString), json, integrity, headers)
|
||||
|
||||
// End the request, get the response code
|
||||
val responseCode = conn.responseCode
|
||||
val status = DeliveryStatus.forHttpResponseCode(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,
|
||||
integrity: String?,
|
||||
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)
|
||||
|
||||
integrity?.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) {
|
||||
runCatching {
|
||||
logger.i(
|
||||
"Request completed with code $code, " +
|
||||
"message: ${conn.responseMessage}, " +
|
||||
"headers: ${conn.headerFields}"
|
||||
)
|
||||
}
|
||||
runCatching {
|
||||
conn.inputStream.bufferedReader().use {
|
||||
logger.d("Received request response: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
|
||||
runCatching {
|
||||
if (status != DeliveryStatus.DELIVERED) {
|
||||
conn.errorStream.bufferedReader().use {
|
||||
logger.w("Request error details: ${it.readText()}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,39 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.io.IOException
|
||||
import java.security.DigestOutputStream
|
||||
import java.security.MessageDigest
|
||||
|
||||
/**
|
||||
* Denotes objects that are expected to be delivered over a network.
|
||||
*/
|
||||
interface Deliverable {
|
||||
/**
|
||||
* Return the byte representation of this `Deliverable`.
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
fun toByteArray(): ByteArray
|
||||
|
||||
/**
|
||||
* The value of the "Bugsnag-Integrity" HTTP header returned as a String. This value is used
|
||||
* to validate the payload and is expected by the standard BugSnag servers.
|
||||
*/
|
||||
val integrityToken: String?
|
||||
get() {
|
||||
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(toByteArray())
|
||||
}
|
||||
shaDigest.digest().forEach { byte ->
|
||||
builder.append(String.format("%02x", byte))
|
||||
}
|
||||
}
|
||||
return builder.toString()
|
||||
}.getOrElse { return null }
|
||||
}
|
||||
}
|
||||
@ -1,65 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,143 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import static com.bugsnag.android.SeverityReason.REASON_PROMISE_REJECTION;
|
||||
|
||||
import com.bugsnag.android.internal.BackgroundTaskService;
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
import com.bugsnag.android.internal.TaskType;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.VisibleForTesting;
|
||||
|
||||
import java.util.concurrent.Future;
|
||||
import java.util.concurrent.RejectedExecutionException;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
class DeliveryDelegate extends BaseObservable {
|
||||
|
||||
@VisibleForTesting
|
||||
static long DELIVERY_TIMEOUT = 3000L;
|
||||
|
||||
final Logger logger;
|
||||
private final EventStore eventStore;
|
||||
private final ImmutableConfig immutableConfig;
|
||||
private final Notifier notifier;
|
||||
private final CallbackState callbackState;
|
||||
final BackgroundTaskService backgroundTaskService;
|
||||
|
||||
DeliveryDelegate(Logger logger,
|
||||
EventStore eventStore,
|
||||
ImmutableConfig immutableConfig,
|
||||
CallbackState callbackState,
|
||||
Notifier notifier,
|
||||
BackgroundTaskService backgroundTaskService) {
|
||||
this.logger = logger;
|
||||
this.eventStore = eventStore;
|
||||
this.immutableConfig = immutableConfig;
|
||||
this.callbackState = callbackState;
|
||||
this.notifier = notifier;
|
||||
this.backgroundTaskService = backgroundTaskService;
|
||||
}
|
||||
|
||||
void deliver(@NonNull Event event) {
|
||||
logger.d("DeliveryDelegate#deliver() - event being stored/delivered by Client");
|
||||
Session session = event.getSession();
|
||||
|
||||
if (session != null) {
|
||||
if (event.isUnhandled()) {
|
||||
event.setSession(session.incrementUnhandledAndCopy());
|
||||
updateState(StateEvent.NotifyUnhandled.INSTANCE);
|
||||
} else {
|
||||
event.setSession(session.incrementHandledAndCopy());
|
||||
updateState(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);
|
||||
if (anr || promiseRejection) {
|
||||
cacheEvent(event, true);
|
||||
} else if (immutableConfig.getAttemptDeliveryOnCrash()) {
|
||||
cacheAndSendSynchronously(event);
|
||||
} else {
|
||||
cacheEvent(event, false);
|
||||
}
|
||||
} else if (callbackState.runOnSendTasks(event, logger)) {
|
||||
// Build the eventPayload
|
||||
String apiKey = event.getApiKey();
|
||||
EventPayload eventPayload = new EventPayload(apiKey, event, notifier, immutableConfig);
|
||||
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");
|
||||
break;
|
||||
case UNDELIVERED:
|
||||
logger.w("Could not send event(s) to Bugsnag,"
|
||||
+ " saving to disk to send later");
|
||||
cacheEvent(event, false);
|
||||
break;
|
||||
case FAILURE:
|
||||
logger.w("Problem sending event to Bugsnag");
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
return deliveryStatus;
|
||||
}
|
||||
|
||||
private void cacheAndSendSynchronously(@NonNull Event event) {
|
||||
long cutoffTime = System.currentTimeMillis() + DELIVERY_TIMEOUT;
|
||||
Future<String> task = eventStore.writeAndDeliver(event);
|
||||
|
||||
long timeout = cutoffTime - System.currentTimeMillis();
|
||||
if (task != null && timeout > 0) {
|
||||
try {
|
||||
task.get(timeout, TimeUnit.MILLISECONDS);
|
||||
} catch (Exception ex) {
|
||||
logger.w("failed to immediately deliver event", ex);
|
||||
}
|
||||
|
||||
if (!task.isDone()) {
|
||||
task.cancel(true);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private void cacheEvent(@NonNull Event event, boolean attemptSend) {
|
||||
eventStore.write(event);
|
||||
if (attemptSend) {
|
||||
eventStore.flushAsync();
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,63 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils
|
||||
import java.io.OutputStream
|
||||
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 class NullOutputStream : OutputStream() {
|
||||
override fun write(b: Int) = Unit
|
||||
}
|
||||
@ -1,17 +0,0 @@
|
||||
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?>
|
||||
)
|
||||
@ -1,41 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
|
||||
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
|
||||
import java.net.HttpURLConnection.HTTP_OK
|
||||
|
||||
/**
|
||||
* 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;
|
||||
|
||||
companion object {
|
||||
@JvmStatic
|
||||
fun forHttpResponseCode(responseCode: Int): DeliveryStatus {
|
||||
return when {
|
||||
responseCode in HTTP_OK..299 -> DELIVERED
|
||||
responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable
|
||||
responseCode != HTTP_CLIENT_TIMEOUT && // except for 408
|
||||
responseCode != 429 -> FAILURE
|
||||
|
||||
else -> UNDELIVERED
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
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
|
||||
*/
|
||||
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
|
||||
|
||||
var runtimeVersions: MutableMap<String, Any>? = sanitizeRuntimeVersions(runtimeVersions)
|
||||
set(value) {
|
||||
field = sanitizeRuntimeVersions(value)
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
|
||||
private fun sanitizeRuntimeVersions(value: MutableMap<String, Any>?): MutableMap<String, Any>? =
|
||||
value?.mapValuesTo(mutableMapOf()) { (_, value) -> value.toString() }
|
||||
}
|
||||
@ -1,36 +0,0 @@
|
||||
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
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,313 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.annotation.SuppressLint
|
||||
import android.app.ActivityManager
|
||||
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.os.Build
|
||||
import android.provider.Settings
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.TaskType
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
import java.io.File
|
||||
import java.util.Date
|
||||
import java.util.Locale
|
||||
import java.util.concurrent.Callable
|
||||
import java.util.concurrent.Future
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.atomic.AtomicInteger
|
||||
import kotlin.math.max
|
||||
import kotlin.math.min
|
||||
import android.os.Process as AndroidProcess
|
||||
|
||||
internal class DeviceDataCollector(
|
||||
private val connectivity: Connectivity,
|
||||
private val appContext: Context,
|
||||
resources: Resources,
|
||||
private val deviceIdStore: Provider<DeviceIdStore.DeviceIds?>,
|
||||
private val buildInfo: DeviceBuildInfo,
|
||||
private val dataDirectory: File,
|
||||
private val rootedFuture: Provider<Boolean>?,
|
||||
private val 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 var runtimeVersions: MutableMap<String, Any>
|
||||
private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory()
|
||||
private var orientation = AtomicInteger(resources.configuration.orientation)
|
||||
|
||||
init {
|
||||
val map = mutableMapOf<String, Any>()
|
||||
buildInfo.apiLevel?.let { map["androidApiLevel"] = it }
|
||||
buildInfo.osBuild?.let { map["osBuild"] = it }
|
||||
runtimeVersions = map
|
||||
}
|
||||
|
||||
fun generateDevice() = Device(
|
||||
buildInfo,
|
||||
cpuAbi,
|
||||
checkIsRooted(),
|
||||
deviceIdStore.get()?.deviceId,
|
||||
locale,
|
||||
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
|
||||
runtimeVersions.toMutableMap()
|
||||
)
|
||||
|
||||
fun generateDeviceWithState(now: Long) = DeviceWithState(
|
||||
buildInfo,
|
||||
checkIsRooted(),
|
||||
deviceIdStore.get()?.deviceId,
|
||||
locale,
|
||||
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
|
||||
runtimeVersions.toMutableMap(),
|
||||
calculateFreeDisk(),
|
||||
calculateFreeMemory(),
|
||||
getOrientationAsString(),
|
||||
Date(now)
|
||||
)
|
||||
|
||||
fun generateInternalDeviceWithState(now: Long) = DeviceWithState(
|
||||
buildInfo,
|
||||
checkIsRooted(),
|
||||
deviceIdStore.get()?.internalDeviceId,
|
||||
locale,
|
||||
totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
|
||||
runtimeVersions.toMutableMap(),
|
||||
calculateFreeDisk(),
|
||||
calculateFreeMemory(),
|
||||
getOrientationAsString(),
|
||||
Date(now)
|
||||
)
|
||||
|
||||
fun getDeviceMetadata(): Map<String, Any?> {
|
||||
val map = HashMap<String, Any?>()
|
||||
populateBatteryInfo(into = map)
|
||||
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
|
||||
|
||||
/**
|
||||
* Populate the current Battery Info into the specified MutableMap
|
||||
*/
|
||||
private fun populateBatteryInfo(into: MutableMap<String, Any?>) {
|
||||
try {
|
||||
val ifilter = IntentFilter(Intent.ACTION_BATTERY_CHANGED)
|
||||
val batteryStatus = appContext.registerReceiverSafe(null, ifilter, logger)
|
||||
|
||||
if (batteryStatus != null) {
|
||||
val level = batteryStatus.getIntExtra("level", -1)
|
||||
val scale = batteryStatus.getIntExtra("scale", -1)
|
||||
|
||||
if (level != -1 || scale != -1) {
|
||||
val batteryLevel: Float = level.toFloat() / scale.toFloat()
|
||||
into["batteryLevel"] = batteryLevel
|
||||
}
|
||||
|
||||
val status = batteryStatus.getIntExtra("status", -1)
|
||||
val charging =
|
||||
status == BatteryManager.BATTERY_STATUS_CHARGING || status == BatteryManager.BATTERY_STATUS_FULL
|
||||
|
||||
into["charging"] = charging
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Could not get battery status")
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current status of location services
|
||||
*/
|
||||
private fun getLocationStatus(): String? {
|
||||
try {
|
||||
return if (isLocationEnabled()) "allowed" else "disallowed"
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Could not get locationStatus")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
private fun isLocationEnabled() = when {
|
||||
Build.VERSION.SDK_INT >= Build.VERSION_CODES.S ->
|
||||
appContext.getLocationManager()?.isLocationEnabled == true
|
||||
else -> {
|
||||
val cr = appContext.contentResolver
|
||||
@Suppress("DEPRECATION") val providersAllowed =
|
||||
Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
|
||||
providersAllowed != null && providersAllowed.isNotEmpty()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)
|
||||
"${max}x$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 runCatching {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.IO,
|
||||
Callable { dataDirectory.usableSpace }
|
||||
).get()
|
||||
}.getOrDefault(0L)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the amount of memory remaining on the device
|
||||
*/
|
||||
fun calculateFreeMemory(): Long? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
try {
|
||||
val freeMemory = appContext.getActivityManager()
|
||||
?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
|
||||
?.availMem
|
||||
if (freeMemory != null) {
|
||||
return freeMemory
|
||||
}
|
||||
} catch (e: Throwable) {
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
return try {
|
||||
@Suppress("PrivateApi")
|
||||
AndroidProcess::class.java.getDeclaredMethod("getFreeMemory").invoke(null) as Long?
|
||||
} catch (e: Throwable) {
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to retrieve the total amount of memory available on the device
|
||||
*/
|
||||
private fun retrieveTotalDeviceMemory(): Future<Long?>? {
|
||||
return try {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.DEFAULT,
|
||||
Callable {
|
||||
calculateTotalMemory()
|
||||
}
|
||||
)
|
||||
} catch (exc: RejectedExecutionException) {
|
||||
logger.w("Failed to lookup available device memory", exc)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun calculateTotalMemory(): Long? {
|
||||
if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) {
|
||||
val totalMemory = appContext.getActivityManager()
|
||||
?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } }
|
||||
?.totalMem
|
||||
|
||||
if (totalMemory != null) {
|
||||
return totalMemory
|
||||
}
|
||||
}
|
||||
|
||||
// we try falling back to a reflective API
|
||||
return runCatching {
|
||||
@Suppress("PrivateApi")
|
||||
AndroidProcess::class.java.getDeclaredMethod("getTotalMemory").invoke(null) as Long?
|
||||
}.getOrNull()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the current device orientation, eg. "landscape"
|
||||
*/
|
||||
internal fun getOrientationAsString(): String? = when (orientation.get()) {
|
||||
ORIENTATION_LANDSCAPE -> "landscape"
|
||||
ORIENTATION_PORTRAIT -> "portrait"
|
||||
else -> null
|
||||
}
|
||||
|
||||
/**
|
||||
* Called whenever the orientation is updated so that the device information is accurate.
|
||||
* Currently this is only invoked by [ClientComponentCallbacks]. Returns true if the
|
||||
* orientation has changed, otherwise false.
|
||||
*/
|
||||
internal fun updateOrientation(newOrientation: Int): Boolean {
|
||||
return orientation.getAndSet(newOrientation) != newOrientation
|
||||
}
|
||||
|
||||
fun addRuntimeVersionInfo(key: String, value: String) {
|
||||
// Use copy-on-write to avoid a ConcurrentModificationException in generateDeviceWithState
|
||||
val newRuntimeVersions = runtimeVersions.toMutableMap()
|
||||
newRuntimeVersions[key] = value
|
||||
runtimeVersions = newRuntimeVersions
|
||||
}
|
||||
}
|
||||
@ -1,163 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
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 a device ID to a file.
|
||||
*
|
||||
* This class is made multi-process safe through the use of a [FileLock], and thread safe
|
||||
* through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
|
||||
*/
|
||||
class DeviceIdFilePersistence(
|
||||
private val file: File,
|
||||
private val deviceIdGenerator: () -> UUID,
|
||||
private val logger: Logger
|
||||
) : DeviceIdPersistence {
|
||||
private val synchronizedStreamableStore: SynchronizedStreamableStore<DeviceId>
|
||||
|
||||
init {
|
||||
try {
|
||||
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.
|
||||
* If no value is present then a UUID will be generated and persisted.
|
||||
*/
|
||||
override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): 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 if (requestCreateIfDoesNotExist) persistNewDeviceUuid(deviceIdGenerator()) else null
|
||||
}
|
||||
} 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(uuid: 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, uuid)
|
||||
}
|
||||
} catch (exc: IOException) {
|
||||
logger.w("Failed to persist device ID", exc)
|
||||
null
|
||||
}
|
||||
}
|
||||
|
||||
private fun persistNewDeviceIdWithLock(
|
||||
channel: FileChannel,
|
||||
uuid: 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(uuid.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)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
interface DeviceIdPersistence {
|
||||
/**
|
||||
* Loads the device ID from storage.
|
||||
*
|
||||
* Device IDs are UUIDs which are persisted on a per-install basis.
|
||||
*
|
||||
* This method must be thread-safe and multi-process safe.
|
||||
*
|
||||
* Note: requestCreateIfDoesNotExist is only a request; an implementation may still refuse to create a new ID.
|
||||
*/
|
||||
fun loadDeviceId(requestCreateIfDoesNotExist: Boolean): String?
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.content.Context
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.dag.Provider
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* This class is responsible for persisting and retrieving the device ID and internal device ID,
|
||||
* which uniquely identify this device in various contexts.
|
||||
*/
|
||||
internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor(
|
||||
context: Context,
|
||||
private val deviceIdFile: File = File(context.filesDir, "device-id"),
|
||||
private val deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||
private val internalDeviceIdFile: File = File(context.filesDir, "internal-device-id"),
|
||||
private val internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
|
||||
private val sharedPrefMigrator: Provider<SharedPrefMigrator>,
|
||||
config: ImmutableConfig,
|
||||
private val logger: Logger
|
||||
) {
|
||||
|
||||
private lateinit var persistence: DeviceIdPersistence
|
||||
private lateinit var internalPersistence: DeviceIdPersistence
|
||||
private val generateId = config.generateAnonymousId
|
||||
private var deviceIds: DeviceIds? = null
|
||||
|
||||
/**
|
||||
* Loads the device ID from
|
||||
* 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.
|
||||
*/
|
||||
private fun loadDeviceId(): String? {
|
||||
// If generateAnonymousId = false, return null
|
||||
// so that a previously persisted device ID is not returned,
|
||||
// or a new one is not generated and persisted
|
||||
if (!generateId) {
|
||||
return null
|
||||
}
|
||||
var result = persistence.loadDeviceId(false)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
result = sharedPrefMigrator.get().loadDeviceId(false)
|
||||
if (result != null) {
|
||||
return result
|
||||
}
|
||||
return persistence.loadDeviceId(true)
|
||||
}
|
||||
|
||||
private fun loadInternalDeviceId(): String? {
|
||||
// If generateAnonymousId = false, return null
|
||||
// so that a previously persisted device ID is not returned,
|
||||
// or a new one is not generated and persisted
|
||||
if (!generateId) {
|
||||
return null
|
||||
}
|
||||
return internalPersistence.loadDeviceId(true)
|
||||
}
|
||||
|
||||
fun load(): DeviceIds? {
|
||||
if (deviceIds != null) {
|
||||
return deviceIds
|
||||
}
|
||||
|
||||
persistence = DeviceIdFilePersistence(deviceIdFile, deviceIdGenerator, logger)
|
||||
internalPersistence =
|
||||
DeviceIdFilePersistence(internalDeviceIdFile, internalDeviceIdGenerator, logger)
|
||||
|
||||
val deviceId = loadDeviceId()
|
||||
val internalDeviceId = loadInternalDeviceId()
|
||||
|
||||
if (deviceId != null || internalDeviceId != null) {
|
||||
deviceIds = DeviceIds(deviceId, internalDeviceId)
|
||||
}
|
||||
|
||||
return deviceIds
|
||||
}
|
||||
|
||||
data class DeviceIds(
|
||||
val deviceId: String?,
|
||||
val internalDeviceId: String?
|
||||
)
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
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(time)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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"
|
||||
)
|
||||
@ -1,111 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new stackframe to the end of this Error returning the new Stackframe data object.
|
||||
*/
|
||||
@NonNull
|
||||
public Stackframe addStackframe(@Nullable String method,
|
||||
@Nullable String file,
|
||||
long lineNumber) {
|
||||
return impl.addStackframe(method, file, lineNumber);
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
}
|
||||
@ -1,48 +0,0 @@
|
||||
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: MutableList<Stackframe> = stacktrace.trace
|
||||
|
||||
fun addStackframe(method: String?, file: String?, lineNumber: Long): Stackframe {
|
||||
val frame = Stackframe(method, file, lineNumber, null)
|
||||
stacktrace.add(frame)
|
||||
return frame
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
fun createError(
|
||||
exc: Throwable,
|
||||
projectPackages: Collection<String>,
|
||||
logger: Logger
|
||||
): MutableList<Error> {
|
||||
return exc.safeUnrollCauses()
|
||||
.mapTo(mutableListOf()) { currentEx ->
|
||||
// Somehow it's possible for stackTrace to be null in rare cases
|
||||
val stacktrace = currentEx.stackTrace ?: arrayOf<StackTraceElement>()
|
||||
val trace = Stacktrace(stacktrace, projectPackages, logger)
|
||||
val errorInternal = ErrorInternal(
|
||||
currentEx.javaClass.name,
|
||||
currentEx.localizedMessage,
|
||||
trace
|
||||
)
|
||||
|
||||
return@mapTo Error(errorInternal, logger)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,38 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* Represents the type of error captured
|
||||
*/
|
||||
enum class ErrorType(internal val desc: String) {
|
||||
|
||||
/**
|
||||
* An error with an unknown type or source
|
||||
*/
|
||||
UNKNOWN(""),
|
||||
|
||||
/**
|
||||
* 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"),
|
||||
|
||||
/**
|
||||
* An error captured from a Dart / Flutter application
|
||||
*/
|
||||
DART("dart");
|
||||
|
||||
internal companion object {
|
||||
@JvmStatic
|
||||
@JvmName("fromDescriptor")
|
||||
internal fun fromDescriptor(desc: String) = values().find { it.desc == desc }
|
||||
}
|
||||
}
|
||||
@ -1,52 +0,0 @@
|
||||
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)
|
||||
|
||||
override fun equals(other: Any?): Boolean {
|
||||
return other is ErrorTypes &&
|
||||
anrs == other.anrs &&
|
||||
ndkCrashes == other.ndkCrashes &&
|
||||
unhandledExceptions == other.unhandledExceptions &&
|
||||
unhandledRejections == other.unhandledRejections
|
||||
}
|
||||
|
||||
override fun hashCode(): Int {
|
||||
var result = anrs.hashCode()
|
||||
result = 31 * result + ndkCrashes.hashCode()
|
||||
result = 31 * result + unhandledExceptions.hashCode()
|
||||
result = 31 * result + unhandledRejections.hashCode()
|
||||
return result
|
||||
}
|
||||
}
|
||||
@ -1,534 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
import com.bugsnag.android.internal.InternalMetrics;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.util.Collection;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
/**
|
||||
* 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, FeatureFlagAware {
|
||||
|
||||
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(), new FeatureFlags(), logger);
|
||||
}
|
||||
|
||||
Event(@Nullable Throwable originalError,
|
||||
@NonNull ImmutableConfig config,
|
||||
@NonNull SeverityReason severityReason,
|
||||
@NonNull Metadata metadata,
|
||||
@NonNull FeatureFlags featureFlags,
|
||||
@NonNull Logger logger) {
|
||||
this(new EventInternal(originalError, config, severityReason, metadata, featureFlags),
|
||||
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 {@link Throwable} object that caused the event in your application.
|
||||
* <p>
|
||||
* 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()}.
|
||||
* <p>
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new error to this event and return its Error data. The new Error will appear at the
|
||||
* end of the {@link #getErrors() errors list}.
|
||||
*/
|
||||
@NonNull
|
||||
public Error addError(@NonNull Throwable error) {
|
||||
return impl.addError(error);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new empty {@link ErrorType#ANDROID android} error to this event and return its Error
|
||||
* data. The new Error will appear at the end of the {@link #getErrors() errors list}.
|
||||
*/
|
||||
@NonNull
|
||||
public Error addError(@NonNull String errorClass, @Nullable String errorMessage) {
|
||||
return impl.addError(errorClass, errorMessage, ErrorType.ANDROID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new empty error to this event and return its Error data. The new Error will appear
|
||||
* at the end of the {@link #getErrors() errors list}.
|
||||
*/
|
||||
@NonNull
|
||||
public Error addError(@NonNull String errorClass,
|
||||
@Nullable String errorMessage,
|
||||
@NonNull ErrorType errorType) {
|
||||
return impl.addError(errorClass, errorMessage, errorType);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Create, add and return a new empty {@link Thread} object to this event with a given id
|
||||
* and name. This can be used to augment the event with thread data that would not be picked
|
||||
* up as part of a normal event being generated (for example: native threads managed
|
||||
* by cross-platform toolkits).
|
||||
*
|
||||
* @return a new Thread object of type {@link ErrorType#ANDROID} with no stacktrace
|
||||
*/
|
||||
@NonNull
|
||||
public Thread addThread(@NonNull String id,
|
||||
@NonNull String name) {
|
||||
return impl.addThread(
|
||||
id,
|
||||
name,
|
||||
ErrorType.ANDROID,
|
||||
false,
|
||||
Thread.State.RUNNABLE.getDescriptor()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create, add and return a new empty {@link Thread} object to this event with a given id
|
||||
* and name. This can be used to augment the event with thread data that would not be picked
|
||||
* up as part of a normal event being generated (for example: native threads managed
|
||||
* by cross-platform toolkits).
|
||||
*
|
||||
* @return a new Thread object of type {@link ErrorType#ANDROID} with no stacktrace
|
||||
*/
|
||||
@NonNull
|
||||
public Thread addThread(long id,
|
||||
@NonNull String name) {
|
||||
return impl.addThread(
|
||||
Long.toString(id),
|
||||
name,
|
||||
ErrorType.ANDROID,
|
||||
false,
|
||||
Thread.State.RUNNABLE.getDescriptor()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new breadcrumb to this event and return its Breadcrumb object. The new breadcrumb
|
||||
* will be added to the end of the {@link #getBreadcrumbs() breadcrumbs list} by this method.
|
||||
*/
|
||||
@NonNull
|
||||
public Breadcrumb leaveBreadcrumb(@NonNull String message,
|
||||
@NonNull BreadcrumbType type,
|
||||
@Nullable Map<String, Object> metadata) {
|
||||
return impl.leaveBreadcrumb(message, type, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add a new breadcrumb to this event and return its Breadcrumb object. The new breadcrumb
|
||||
* will be added to the end of the {@link #getBreadcrumbs() breadcrumbs list} by this# method.
|
||||
*/
|
||||
@NonNull
|
||||
public Breadcrumb leaveBreadcrumb(@NonNull String message) {
|
||||
return impl.leaveBreadcrumb(message, BreadcrumbType.MANUAL, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* A list of feature flags active at the time of the event.
|
||||
* See {@link FeatureFlag} for details of the data available.
|
||||
*/
|
||||
@NonNull
|
||||
public List<FeatureFlag> getFeatureFlags() {
|
||||
return impl.getFeatureFlags().toList();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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.
|
||||
* <p>
|
||||
* 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.
|
||||
* <p>
|
||||
* 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;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void addFeatureFlag(@NonNull String name) {
|
||||
if (name != null) {
|
||||
impl.addFeatureFlag(name);
|
||||
} else {
|
||||
logNull("addFeatureFlag");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void addFeatureFlag(@NonNull String name, @Nullable String variant) {
|
||||
if (name != null) {
|
||||
impl.addFeatureFlag(name, variant);
|
||||
} else {
|
||||
logNull("addFeatureFlag");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void addFeatureFlags(@NonNull Iterable<FeatureFlag> featureFlags) {
|
||||
if (featureFlags != null) {
|
||||
impl.addFeatureFlags(featureFlags);
|
||||
} else {
|
||||
logNull("addFeatureFlags");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void clearFeatureFlag(@NonNull String name) {
|
||||
if (name != null) {
|
||||
impl.clearFeatureFlag(name);
|
||||
} else {
|
||||
logNull("clearFeatureFlag");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* {@inheritDoc}
|
||||
*/
|
||||
@Override
|
||||
public void clearFeatureFlags() {
|
||||
impl.clearFeatureFlags();
|
||||
}
|
||||
|
||||
@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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Associate this event with a specific trace. This is usually done automatically when
|
||||
* using bugsnag-android-performance, but can also be set manually if required.
|
||||
*
|
||||
* @param traceId the ID of the trace the event occurred within
|
||||
* @param spanId the ID of the span that the event occurred within
|
||||
*/
|
||||
public void setTraceCorrelation(@NonNull UUID traceId, long spanId) {
|
||||
if (traceId != null) {
|
||||
impl.setTraceCorrelation(new TraceCorrelation(traceId, spanId));
|
||||
} else {
|
||||
logNull("traceId");
|
||||
}
|
||||
}
|
||||
|
||||
protected boolean shouldDiscardClass() {
|
||||
return impl.shouldDiscardClass();
|
||||
}
|
||||
|
||||
protected void updateSeverityInternal(@NonNull Severity severity) {
|
||||
impl.updateSeverityInternal(severity);
|
||||
}
|
||||
|
||||
protected void updateSeverityReason(@NonNull @SeverityReason.SeverityReasonType String reason) {
|
||||
impl.updateSeverityReason(reason);
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
void setRedactedKeys(Collection<Pattern> redactedKeys) {
|
||||
impl.setRedactedKeys(redactedKeys);
|
||||
}
|
||||
|
||||
void setInternalMetrics(InternalMetrics metrics) {
|
||||
impl.setInternalMetrics(metrics);
|
||||
}
|
||||
}
|
||||
@ -1,165 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import java.io.File
|
||||
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>
|
||||
) {
|
||||
|
||||
fun encode(): String {
|
||||
return toFilename(apiKey, uuid, timestamp, suffix, errorTypes)
|
||||
}
|
||||
|
||||
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
|
||||
|
||||
internal companion object {
|
||||
private const val STARTUP_CRASH = "startupcrash"
|
||||
private const val NON_JVM_CRASH = "not-jvm"
|
||||
|
||||
/**
|
||||
* Generates a filename for the Event in the format
|
||||
* "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
|
||||
*/
|
||||
fun toFilename(
|
||||
apiKey: String,
|
||||
uuid: String,
|
||||
timestamp: Long,
|
||||
suffix: String,
|
||||
errorTypes: Set<ErrorType>
|
||||
): String {
|
||||
return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
|
||||
}
|
||||
|
||||
@JvmOverloads @JvmStatic
|
||||
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.
|
||||
*/
|
||||
@JvmStatic
|
||||
fun fromFile(file: File, config: ImmutableConfig): EventFilenameInfo {
|
||||
return EventFilenameInfo(
|
||||
findApiKeyInFilename(file, config),
|
||||
"", // ignore UUID field when reading from file as unused
|
||||
findTimestampInFilename(file),
|
||||
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
|
||||
*/
|
||||
internal 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
|
||||
*/
|
||||
internal 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
|
||||
*/
|
||||
internal 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 encoded in the filename, or an empty string if this
|
||||
* information is not encoded for the given event
|
||||
*/
|
||||
@JvmStatic
|
||||
fun findTimestampInFilename(eventFile: File): Long {
|
||||
val name = eventFile.nameWithoutExtension
|
||||
return name.substringBefore("_", missingDelimiterValue = "-1").toLongOrNull() ?: -1
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the error types for the given event
|
||||
*/
|
||||
internal 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
|
||||
*/
|
||||
internal fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
|
||||
return when {
|
||||
obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH
|
||||
launching == true -> STARTUP_CRASH
|
||||
else -> ""
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,396 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.InternalMetrics
|
||||
import com.bugsnag.android.internal.InternalMetricsNoop
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
import com.bugsnag.android.internal.TrimMetrics
|
||||
import java.io.IOException
|
||||
import java.util.Date
|
||||
import java.util.regex.Pattern
|
||||
|
||||
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
|
||||
|
||||
@JvmOverloads
|
||||
internal constructor(
|
||||
originalError: Throwable? = null,
|
||||
config: ImmutableConfig,
|
||||
severityReason: SeverityReason,
|
||||
data: Metadata = Metadata(),
|
||||
featureFlags: FeatureFlags = FeatureFlags()
|
||||
) : this(
|
||||
config.apiKey,
|
||||
config.logger,
|
||||
mutableListOf(),
|
||||
config.discardClasses.toSet(),
|
||||
when (originalError) {
|
||||
null -> mutableListOf()
|
||||
else -> Error.createError(originalError, config.projectPackages, config.logger)
|
||||
},
|
||||
data.copy(),
|
||||
featureFlags.copy(),
|
||||
originalError,
|
||||
config.projectPackages,
|
||||
severityReason,
|
||||
ThreadState(originalError, severityReason.unhandled, config).threads,
|
||||
User(),
|
||||
config.redactedKeys.toSet()
|
||||
)
|
||||
|
||||
internal constructor(
|
||||
apiKey: String,
|
||||
logger: Logger,
|
||||
breadcrumbs: MutableList<Breadcrumb> = mutableListOf(),
|
||||
discardClasses: Set<Pattern> = setOf(),
|
||||
errors: MutableList<Error> = mutableListOf(),
|
||||
metadata: Metadata = Metadata(),
|
||||
featureFlags: FeatureFlags = FeatureFlags(),
|
||||
originalError: Throwable? = null,
|
||||
projectPackages: Collection<String> = setOf(),
|
||||
severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION),
|
||||
threads: MutableList<Thread> = mutableListOf(),
|
||||
user: User = User(),
|
||||
redactionKeys: Set<Pattern>? = null
|
||||
) {
|
||||
this.logger = logger
|
||||
this.apiKey = apiKey
|
||||
this.breadcrumbs = breadcrumbs
|
||||
this.discardClasses = discardClasses
|
||||
this.errors = errors
|
||||
this.metadata = metadata
|
||||
this.featureFlags = featureFlags
|
||||
this.originalError = originalError
|
||||
this.projectPackages = projectPackages
|
||||
this.severityReason = severityReason
|
||||
this.threads = threads
|
||||
this.userImpl = user
|
||||
|
||||
redactionKeys?.let {
|
||||
this.redactedKeys = it
|
||||
}
|
||||
}
|
||||
|
||||
val originalError: Throwable?
|
||||
internal var severityReason: SeverityReason
|
||||
|
||||
val logger: Logger
|
||||
val metadata: Metadata
|
||||
val featureFlags: FeatureFlags
|
||||
private val discardClasses: Set<Pattern>
|
||||
internal var projectPackages: Collection<String>
|
||||
|
||||
private val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer().apply {
|
||||
redactedKeys = redactedKeys.toSet()
|
||||
}
|
||||
|
||||
@JvmField
|
||||
internal var session: Session? = null
|
||||
|
||||
var severity: Severity
|
||||
get() = severityReason.currentSeverity
|
||||
set(value) {
|
||||
severityReason.currentSeverity = value
|
||||
}
|
||||
|
||||
var apiKey: String
|
||||
lateinit var app: AppWithState
|
||||
lateinit var device: DeviceWithState
|
||||
var unhandled: Boolean
|
||||
get() = severityReason.unhandled
|
||||
set(value) {
|
||||
severityReason.unhandled = value
|
||||
}
|
||||
|
||||
var breadcrumbs: MutableList<Breadcrumb>
|
||||
var errors: MutableList<Error>
|
||||
var threads: MutableList<Thread>
|
||||
var groupingHash: String? = null
|
||||
var context: String? = null
|
||||
|
||||
var redactedKeys: Collection<Pattern>
|
||||
get() = jsonStreamer.redactedKeys
|
||||
set(value) {
|
||||
jsonStreamer.redactedKeys = value.toSet()
|
||||
metadata.redactedKeys = value.toSet()
|
||||
}
|
||||
var internalMetrics: InternalMetrics = InternalMetricsNoop()
|
||||
|
||||
/**
|
||||
* @return user information associated with this Event
|
||||
*/
|
||||
internal var userImpl: User
|
||||
|
||||
var traceCorrelation: TraceCorrelation? = null
|
||||
|
||||
fun getUnhandledOverridden(): Boolean = severityReason.unhandledOverridden
|
||||
|
||||
fun getOriginalUnhandled(): Boolean = severityReason.originalUnhandled
|
||||
|
||||
protected fun shouldDiscardClass(): Boolean {
|
||||
return when {
|
||||
errors.isEmpty() -> true
|
||||
else -> errors.any { error ->
|
||||
discardClasses.any { pattern ->
|
||||
pattern.matcher(error.errorClass).matches()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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(parentWriter: JsonStream) {
|
||||
val writer = JsonStream(parentWriter, jsonStreamer)
|
||||
// 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(userImpl)
|
||||
|
||||
// Write diagnostics
|
||||
writer.name("app").value(app)
|
||||
writer.name("device").value(device)
|
||||
writer.name("breadcrumbs").value(breadcrumbs)
|
||||
writer.name("groupingHash").value(groupingHash)
|
||||
val usage = internalMetrics.toJsonableMap()
|
||||
if (usage.isNotEmpty()) {
|
||||
writer.name("usage")
|
||||
writer.beginObject()
|
||||
usage.forEach { entry ->
|
||||
writer.name(entry.key).value(entry.value)
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
|
||||
writer.name("threads")
|
||||
writer.beginArray()
|
||||
threads.forEach { writer.value(it) }
|
||||
writer.endArray()
|
||||
|
||||
writer.name("featureFlags").value(featureFlags)
|
||||
|
||||
traceCorrelation?.let { correlation ->
|
||||
writer.name("correlation").value(correlation)
|
||||
}
|
||||
|
||||
if (session != null) {
|
||||
val copy = Session.copySession(session)
|
||||
writer.name("session").beginObject()
|
||||
writer.name("id").value(copy.id)
|
||||
writer.name("startedAt").value(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)
|
||||
}
|
||||
|
||||
internal fun normalizeStackframeErrorTypes() {
|
||||
if (getErrorTypesFromStackframes().size == 1) {
|
||||
errors.flatMap { it.stacktrace }.forEach {
|
||||
it.type = null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
internal fun updateSeverityReasonInternal(severityReason: SeverityReason) {
|
||||
this.severityReason = severityReason
|
||||
}
|
||||
|
||||
protected fun updateSeverityInternal(severity: Severity) {
|
||||
severityReason = SeverityReason(
|
||||
severityReason.severityReasonType,
|
||||
severity,
|
||||
severityReason.unhandled,
|
||||
severityReason.unhandledOverridden,
|
||||
severityReason.attributeValue,
|
||||
severityReason.attributeKey
|
||||
)
|
||||
}
|
||||
|
||||
protected fun updateSeverityReason(@SeverityReason.SeverityReasonType reason: String) {
|
||||
severityReason = SeverityReason(
|
||||
reason,
|
||||
severityReason.currentSeverity,
|
||||
severityReason.unhandled,
|
||||
severityReason.unhandledOverridden,
|
||||
severityReason.attributeValue,
|
||||
severityReason.attributeKey
|
||||
)
|
||||
}
|
||||
|
||||
fun getSeverityReasonType(): String = severityReason.severityReasonType
|
||||
|
||||
fun trimMetadataStringsTo(maxLength: Int): TrimMetrics {
|
||||
var stringCount = 0
|
||||
var charCount = 0
|
||||
|
||||
var stringAndCharCounts = metadata.trimMetadataStringsTo(maxLength)
|
||||
stringCount += stringAndCharCounts.itemsTrimmed
|
||||
charCount += stringAndCharCounts.dataTrimmed
|
||||
for (breadcrumb in breadcrumbs) {
|
||||
stringAndCharCounts = breadcrumb.impl.trimMetadataStringsTo(maxLength)
|
||||
stringCount += stringAndCharCounts.itemsTrimmed
|
||||
charCount += stringAndCharCounts.dataTrimmed
|
||||
}
|
||||
return TrimMetrics(stringCount, charCount)
|
||||
}
|
||||
|
||||
fun trimBreadcrumbsBy(byteCount: Int): TrimMetrics {
|
||||
var removedBreadcrumbCount = 0
|
||||
var removedByteCount = 0
|
||||
while (removedByteCount < byteCount && breadcrumbs.isNotEmpty()) {
|
||||
val breadcrumb = breadcrumbs.removeAt(0)
|
||||
removedByteCount += JsonHelper.serialize(breadcrumb).size
|
||||
removedBreadcrumbCount++
|
||||
}
|
||||
when (removedBreadcrumbCount) {
|
||||
1 -> breadcrumbs.add(Breadcrumb("Removed to reduce payload size", logger))
|
||||
else -> breadcrumbs.add(
|
||||
Breadcrumb(
|
||||
"Removed, along with ${removedBreadcrumbCount - 1} older breadcrumbs, to reduce payload size",
|
||||
logger
|
||||
)
|
||||
)
|
||||
}
|
||||
return TrimMetrics(removedBreadcrumbCount, removedByteCount)
|
||||
}
|
||||
|
||||
override fun setUser(id: String?, email: String?, name: String?) {
|
||||
userImpl = User(id, email, name)
|
||||
}
|
||||
|
||||
override fun getUser() = userImpl
|
||||
|
||||
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)
|
||||
|
||||
override fun addFeatureFlag(name: String) = featureFlags.addFeatureFlag(name)
|
||||
|
||||
override fun addFeatureFlag(name: String, variant: String?) =
|
||||
featureFlags.addFeatureFlag(name, variant)
|
||||
|
||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) =
|
||||
this.featureFlags.addFeatureFlags(featureFlags)
|
||||
|
||||
override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name)
|
||||
|
||||
override fun clearFeatureFlags() = featureFlags.clearFeatureFlags()
|
||||
|
||||
fun addError(thrownError: Throwable?): Error {
|
||||
if (thrownError == null) {
|
||||
val newError = Error(
|
||||
ErrorInternal("null", null, Stacktrace(ArrayList())),
|
||||
logger
|
||||
)
|
||||
errors.add(newError)
|
||||
return newError
|
||||
} else {
|
||||
val newErrors = Error.createError(thrownError, projectPackages, logger)
|
||||
errors.addAll(newErrors)
|
||||
return newErrors.first()
|
||||
}
|
||||
}
|
||||
|
||||
fun addError(errorClass: String?, errorMessage: String?, errorType: ErrorType?): Error {
|
||||
val error = Error(
|
||||
ErrorInternal(
|
||||
errorClass.toString(),
|
||||
errorMessage,
|
||||
Stacktrace(ArrayList()),
|
||||
errorType ?: ErrorType.ANDROID
|
||||
),
|
||||
logger
|
||||
)
|
||||
errors.add(error)
|
||||
return error
|
||||
}
|
||||
|
||||
fun addThread(
|
||||
id: String?,
|
||||
name: String?,
|
||||
errorType: ErrorType,
|
||||
isErrorReportingThread: Boolean,
|
||||
state: String
|
||||
): Thread {
|
||||
val thread = Thread(
|
||||
ThreadInternal(
|
||||
id.toString(),
|
||||
name.toString(),
|
||||
errorType,
|
||||
isErrorReportingThread,
|
||||
state,
|
||||
Stacktrace(ArrayList())
|
||||
),
|
||||
logger
|
||||
)
|
||||
threads.add(thread)
|
||||
return thread
|
||||
}
|
||||
|
||||
fun leaveBreadcrumb(
|
||||
message: String?,
|
||||
type: BreadcrumbType?,
|
||||
metadata: MutableMap<String, Any?>?
|
||||
): Breadcrumb {
|
||||
val breadcrumb = Breadcrumb(
|
||||
message.toString(),
|
||||
type ?: BreadcrumbType.MANUAL,
|
||||
metadata,
|
||||
Date(),
|
||||
logger
|
||||
)
|
||||
|
||||
breadcrumbs.add(breadcrumb)
|
||||
return breadcrumb
|
||||
}
|
||||
}
|
||||
@ -1,145 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
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?,
|
||||
event: Event? = null,
|
||||
eventFile: File? = null,
|
||||
notifier: Notifier,
|
||||
private val config: ImmutableConfig
|
||||
) : JsonStream.Streamable, Deliverable {
|
||||
|
||||
var event: Event? = event
|
||||
internal set
|
||||
|
||||
internal var eventFile: File? = eventFile
|
||||
private set
|
||||
|
||||
private var cachedBytes: ByteArray? = null
|
||||
|
||||
private val logger: Logger get() = config.logger
|
||||
|
||||
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
|
||||
dependencies = notifier.dependencies.toMutableList()
|
||||
}
|
||||
|
||||
internal fun getErrorTypes(): Set<ErrorType> {
|
||||
val event = this.event
|
||||
|
||||
return event?.impl?.getErrorTypesFromStackframes() ?: (
|
||||
eventFile?.let { EventFilenameInfo.fromFile(it, config).errorTypes }
|
||||
?: emptySet()
|
||||
)
|
||||
}
|
||||
|
||||
private fun decodedEvent(): Event {
|
||||
val localEvent = event
|
||||
if (localEvent != null) {
|
||||
return localEvent
|
||||
}
|
||||
|
||||
val eventSource = MarshalledEventSource(eventFile!!, apiKey ?: config.apiKey, logger)
|
||||
val decodedEvent = eventSource()
|
||||
|
||||
// cache the decoded Event object
|
||||
event = decodedEvent
|
||||
|
||||
return decodedEvent
|
||||
}
|
||||
|
||||
/**
|
||||
* If required trim this `EventPayload` so that its [encoded data](toByteArray) will usually be
|
||||
* less-than or equal to [maxSizeBytes]. This function may make no changes to the payload, and
|
||||
* may also not achieve the requested [maxSizeBytes]. The default use of the function is
|
||||
* configured to [DEFAULT_MAX_PAYLOAD_SIZE].
|
||||
*
|
||||
* @return `this` for call chaining
|
||||
*/
|
||||
@JvmOverloads
|
||||
fun trimToSize(maxSizeBytes: Int = DEFAULT_MAX_PAYLOAD_SIZE): EventPayload {
|
||||
var json = toByteArray()
|
||||
if (json.size <= maxSizeBytes) {
|
||||
return this
|
||||
}
|
||||
|
||||
val event = decodedEvent()
|
||||
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(config.maxStringValueLength)
|
||||
event.impl.internalMetrics.setMetadataTrimMetrics(
|
||||
itemsTrimmed,
|
||||
dataTrimmed
|
||||
)
|
||||
|
||||
json = rebuildPayloadCache()
|
||||
if (json.size <= maxSizeBytes) {
|
||||
return this
|
||||
}
|
||||
|
||||
val breadcrumbAndBytesRemovedCounts =
|
||||
event.impl.trimBreadcrumbsBy(json.size - maxSizeBytes)
|
||||
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
|
||||
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
|
||||
breadcrumbAndBytesRemovedCounts.dataTrimmed
|
||||
)
|
||||
|
||||
rebuildPayloadCache()
|
||||
return this
|
||||
}
|
||||
|
||||
@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()
|
||||
}
|
||||
|
||||
/**
|
||||
* Transform this `EventPayload` to a byte array suitable for delivery to a BugSnag event
|
||||
* endpoint (typically configured using [EndpointConfiguration.notify]).
|
||||
*/
|
||||
@Throws(IOException::class)
|
||||
override fun toByteArray(): ByteArray {
|
||||
var payload = cachedBytes
|
||||
if (payload == null) {
|
||||
payload = JsonHelper.serialize(this)
|
||||
cachedBytes = payload
|
||||
}
|
||||
return payload
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun rebuildPayloadCache(): ByteArray {
|
||||
cachedBytes = null
|
||||
return toByteArray()
|
||||
}
|
||||
|
||||
companion object {
|
||||
/**
|
||||
* The default maximum payload size for [trimToSize], payloads larger than this will
|
||||
* typically be rejected by BugSnag.
|
||||
*/
|
||||
// 1MB with some fiddle room in case of encoding overhead
|
||||
const val DEFAULT_MAX_PAYLOAD_SIZE = 999700
|
||||
}
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
|
||||
import com.bugsnag.android.internal.dag.ConfigModule
|
||||
import com.bugsnag.android.internal.dag.ContextModule
|
||||
import com.bugsnag.android.internal.dag.SystemServiceModule
|
||||
|
||||
/**
|
||||
* A dependency module which constructs the objects that persist events to disk in Bugsnag.
|
||||
*/
|
||||
internal class EventStorageModule(
|
||||
contextModule: ContextModule,
|
||||
configModule: ConfigModule,
|
||||
dataCollectionModule: DataCollectionModule,
|
||||
bgTaskService: BackgroundTaskService,
|
||||
trackerModule: TrackerModule,
|
||||
systemServiceModule: SystemServiceModule,
|
||||
notifier: Notifier,
|
||||
callbackState: CallbackState
|
||||
) : BackgroundDependencyModule(bgTaskService) {
|
||||
|
||||
private val cfg = configModule.config
|
||||
|
||||
private val delegate = provider {
|
||||
if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS))
|
||||
InternalReportDelegate(
|
||||
contextModule.ctx,
|
||||
cfg.logger,
|
||||
cfg,
|
||||
systemServiceModule.storageManager,
|
||||
dataCollectionModule.appDataCollector.get(),
|
||||
dataCollectionModule.deviceDataCollector,
|
||||
trackerModule.sessionTracker.get(),
|
||||
notifier,
|
||||
bgTaskService
|
||||
) else null
|
||||
}
|
||||
|
||||
val eventStore = provider {
|
||||
EventStore(
|
||||
cfg,
|
||||
cfg.logger,
|
||||
notifier,
|
||||
bgTaskService,
|
||||
delegate.getOrNull(),
|
||||
callbackState
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,273 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.os.SystemClock
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.findTimestampInFilename
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.fromEvent
|
||||
import com.bugsnag.android.EventFilenameInfo.Companion.fromFile
|
||||
import com.bugsnag.android.JsonStream.Streamable
|
||||
import com.bugsnag.android.internal.BackgroundTaskService
|
||||
import com.bugsnag.android.internal.ForegroundDetector
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import com.bugsnag.android.internal.TaskType
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Date
|
||||
import java.util.concurrent.Callable
|
||||
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.
|
||||
*/
|
||||
internal class EventStore(
|
||||
private val config: ImmutableConfig,
|
||||
logger: Logger,
|
||||
notifier: Notifier,
|
||||
bgTaskService: BackgroundTaskService,
|
||||
delegate: Delegate?,
|
||||
callbackState: CallbackState
|
||||
) : FileStore(
|
||||
File(config.persistenceDirectory.value, "bugsnag/errors"),
|
||||
config.maxPersistedEvents,
|
||||
logger,
|
||||
delegate
|
||||
) {
|
||||
private val notifier: Notifier
|
||||
private val bgTaskService: BackgroundTaskService
|
||||
private val callbackState: CallbackState
|
||||
override val logger: Logger
|
||||
|
||||
/**
|
||||
* Flush startup crashes synchronously on the main thread. Startup crashes block the main thread
|
||||
* when being sent (subject to [Configuration.setSendLaunchCrashesSynchronously])
|
||||
*/
|
||||
fun flushOnLaunch() {
|
||||
if (!config.sendLaunchCrashesSynchronously) {
|
||||
return
|
||||
}
|
||||
val future = try {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Runnable { flushLaunchCrashReport() }
|
||||
)
|
||||
} catch (exc: RejectedExecutionException) {
|
||||
logger.d("Failed to flush launch crash reports, continuing.", exc)
|
||||
return
|
||||
}
|
||||
try {
|
||||
// Calculate the maximum amount of time we are prepared to block while sending
|
||||
// startup crashes, based on how long we think startup has taken so-far.
|
||||
// This attempts to mitigate possible startup ANRs that can occur when other SDKs
|
||||
// have blocked the main thread before this code is reached.
|
||||
val currentStartupDuration =
|
||||
SystemClock.elapsedRealtime() - ForegroundDetector.startupTime
|
||||
var timeout = LAUNCH_CRASH_TIMEOUT_MS - currentStartupDuration
|
||||
|
||||
if (timeout <= 0) {
|
||||
// if Bugsnag.start is called too long after Application.onCreate is expected to
|
||||
// have returned, we use a full LAUNCH_CRASH_TIMEOUT_MS instead of a calculated one
|
||||
// assuming that the app is already fully started
|
||||
timeout = LAUNCH_CRASH_TIMEOUT_MS
|
||||
}
|
||||
|
||||
future.get(timeout, TimeUnit.MILLISECONDS)
|
||||
} catch (exc: InterruptedException) {
|
||||
logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
|
||||
} catch (exc: ExecutionException) {
|
||||
logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
|
||||
} catch (exc: TimeoutException) {
|
||||
logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushLaunchCrashReport() {
|
||||
val storedFiles = findStoredFiles()
|
||||
val launchCrashReport = findLaunchCrashReport(storedFiles)
|
||||
|
||||
// cancel non-launch crash reports
|
||||
launchCrashReport?.let { storedFiles.remove(it) }
|
||||
cancelQueuedFiles(storedFiles)
|
||||
if (launchCrashReport != null) {
|
||||
logger.i("Attempting to send the most recent launch crash report")
|
||||
flushReports(listOf(launchCrashReport))
|
||||
logger.i("Continuing with Bugsnag initialisation")
|
||||
} else {
|
||||
logger.d("No startupcrash events to flush to Bugsnag.")
|
||||
}
|
||||
}
|
||||
|
||||
fun findLaunchCrashReport(storedFiles: Collection<File>): File? {
|
||||
return storedFiles
|
||||
.asSequence()
|
||||
.filter { fromFile(it, config).isLaunchCrashReport() }
|
||||
.maxWithOrNull(EVENT_COMPARATOR)
|
||||
}
|
||||
|
||||
fun writeAndDeliver(streamable: Streamable): Future<String>? {
|
||||
val filename = write(streamable) ?: return null
|
||||
try {
|
||||
return bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Callable {
|
||||
flushEventFile(File(filename))
|
||||
filename
|
||||
}
|
||||
)
|
||||
} catch (exception: RejectedExecutionException) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
/**
|
||||
* Flush any on-disk errors to Bugsnag
|
||||
*/
|
||||
fun flushAsync() {
|
||||
try {
|
||||
bgTaskService.submitTask(
|
||||
TaskType.ERROR_REQUEST,
|
||||
Runnable {
|
||||
val storedFiles = findStoredFiles()
|
||||
if (storedFiles.isEmpty()) {
|
||||
logger.d("No regular events to flush to Bugsnag.")
|
||||
}
|
||||
flushReports(storedFiles)
|
||||
}
|
||||
)
|
||||
} catch (exception: RejectedExecutionException) {
|
||||
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushReports(storedReports: Collection<File>) {
|
||||
if (!storedReports.isEmpty()) {
|
||||
val size = storedReports.size
|
||||
logger.i("Sending $size saved error(s) to Bugsnag")
|
||||
for (eventFile in storedReports) {
|
||||
flushEventFile(eventFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun flushEventFile(eventFile: File) {
|
||||
try {
|
||||
val (apiKey) = fromFile(eventFile, config)
|
||||
val payload = createEventPayload(eventFile, apiKey)
|
||||
if (payload == null) {
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else {
|
||||
deliverEventPayload(eventFile, payload)
|
||||
}
|
||||
} catch (exception: Exception) {
|
||||
handleEventFlushFailure(exception, eventFile)
|
||||
}
|
||||
}
|
||||
|
||||
private fun deliverEventPayload(eventFile: File, payload: EventPayload) {
|
||||
val deliveryParams = config.getErrorApiDeliveryParams(payload)
|
||||
val delivery = config.delivery
|
||||
when (delivery.deliver(payload, deliveryParams)) {
|
||||
DeliveryStatus.DELIVERED -> {
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
logger.i("Deleting sent error file $eventFile.name")
|
||||
}
|
||||
|
||||
DeliveryStatus.UNDELIVERED -> undeliveredEventPayload(eventFile)
|
||||
DeliveryStatus.FAILURE -> {
|
||||
val exc: Exception = RuntimeException("Failed to deliver event payload")
|
||||
handleEventFlushFailure(exc, eventFile)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private fun undeliveredEventPayload(eventFile: File) {
|
||||
if (isTooBig(eventFile)) {
|
||||
logger.w(
|
||||
"Discarding over-sized event (${eventFile.length()}) after failed delivery"
|
||||
)
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else if (isTooOld(eventFile)) {
|
||||
logger.w(
|
||||
"Discarding historical event (from ${getCreationDate(eventFile)}) after failed delivery"
|
||||
)
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
} else {
|
||||
cancelQueuedFiles(setOf(eventFile))
|
||||
logger.w(
|
||||
"Could not send previously saved error(s) to Bugsnag, will try again later"
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun createEventPayload(eventFile: File, apiKey: String): EventPayload? {
|
||||
@Suppress("NAME_SHADOWING")
|
||||
var apiKey: String? = apiKey
|
||||
val eventSource = MarshalledEventSource(eventFile, apiKey!!, logger)
|
||||
try {
|
||||
if (!callbackState.runOnSendTasks(eventSource, logger)) {
|
||||
// do not send the payload at all, we must block sending
|
||||
return null
|
||||
}
|
||||
} catch (ioe: Exception) {
|
||||
logger.w("could not parse event payload", ioe)
|
||||
eventSource.clear()
|
||||
}
|
||||
val processedEvent = eventSource.event
|
||||
return if (processedEvent != null) {
|
||||
apiKey = processedEvent.apiKey
|
||||
EventPayload(apiKey, processedEvent, null, notifier, config)
|
||||
} else {
|
||||
EventPayload(apiKey, null, eventFile, notifier, config)
|
||||
}
|
||||
}
|
||||
|
||||
private fun handleEventFlushFailure(exc: Exception, eventFile: File) {
|
||||
logger.e(exc.message ?: "Failed to send event", exc)
|
||||
deleteStoredFiles(setOf(eventFile))
|
||||
}
|
||||
|
||||
override fun getFilename(obj: Any?): String {
|
||||
return obj?.let { fromEvent(obj = it, apiKey = null, config = config) }?.encode() ?: ""
|
||||
}
|
||||
|
||||
fun getNdkFilename(obj: Any?, apiKey: String?): String {
|
||||
return obj?.let { fromEvent(obj = it, apiKey = apiKey, config = config) }?.encode() ?: ""
|
||||
}
|
||||
|
||||
init {
|
||||
this.logger = logger
|
||||
this.notifier = notifier
|
||||
this.bgTaskService = bgTaskService
|
||||
this.callbackState = callbackState
|
||||
}
|
||||
|
||||
private fun isTooBig(file: File): Boolean {
|
||||
return file.length() > oneMegabyte
|
||||
}
|
||||
|
||||
private fun isTooOld(file: File): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.DATE, -60)
|
||||
return findTimestampInFilename(file) < cal.timeInMillis
|
||||
}
|
||||
|
||||
private fun getCreationDate(file: File): Date {
|
||||
return Date(findTimestampInFilename(file))
|
||||
}
|
||||
|
||||
companion object {
|
||||
private const val LAUNCH_CRASH_TIMEOUT_MS: Long = 2000
|
||||
val EVENT_COMPARATOR: Comparator<in File?> = Comparator { lhs, rhs ->
|
||||
when {
|
||||
lhs == null && rhs == null -> 0
|
||||
lhs == null -> 1
|
||||
rhs == null -> -1
|
||||
else -> lhs.compareTo(rhs)
|
||||
}
|
||||
}
|
||||
private const val oneMegabyte = 1024L * 1024L
|
||||
}
|
||||
}
|
||||
@ -1,88 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
void install() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(this);
|
||||
}
|
||||
|
||||
void uninstall() {
|
||||
Thread.setDefaultUncaughtExceptionHandler(originalHandler);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void uncaughtException(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||
try {
|
||||
if (client.getConfig().shouldDiscardError(throwable)) {
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
} catch (Throwable ignored) {
|
||||
// the runtime would ignore any exceptions here, we make that absolutely clear
|
||||
// to avoid any possible unhandled-exception loops
|
||||
} finally {
|
||||
forwardToOriginalHandler(thread, throwable);
|
||||
}
|
||||
}
|
||||
|
||||
private void forwardToOriginalHandler(@NonNull Thread thread, @NonNull Throwable throwable) {
|
||||
// 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,134 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* Represents a single feature-flag / experiment marker within Bugsnag. Each {@code FeatureFlag}
|
||||
* object has a {@link #getName() name} and an optional {@link #getVariant() variant} which can be
|
||||
* used to identify runtime experiments and groups when reporting errors.
|
||||
*
|
||||
* @see Bugsnag#addFeatureFlag(String, String)
|
||||
* @see Event#addFeatureFlag(String, String)
|
||||
*/
|
||||
public final class FeatureFlag implements Map.Entry<String, String> {
|
||||
private final String name;
|
||||
|
||||
private final String variant;
|
||||
|
||||
/**
|
||||
* Create a named {@code FeatureFlag} with no variant
|
||||
*
|
||||
* @param name the identifying name of the new {@code FeatureFlag} (not {@code null})
|
||||
* @see Bugsnag#addFeatureFlag(String)
|
||||
* @see Event#addFeatureFlag(String)
|
||||
*/
|
||||
public FeatureFlag(@NonNull String name) {
|
||||
this(name, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code FeatureFlag} with a name and (optionally) a variant.
|
||||
*
|
||||
* @param name the identifying name of the new {@code FeatureFlag} (not {@code null})
|
||||
* @param variant the feature variant
|
||||
*/
|
||||
public FeatureFlag(@NonNull String name, @Nullable String variant) {
|
||||
if (name == null) {
|
||||
throw new NullPointerException("FeatureFlags cannot have null name");
|
||||
}
|
||||
|
||||
this.name = name;
|
||||
this.variant = variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new {@code FeatureFlag} based on an existing {@code Map.Entry}. This is the same
|
||||
* as {@code new FeatureFlag(mapEntry.getKey(), mapEntry.getValue())}.
|
||||
*
|
||||
* @param mapEntry an existing {@code Map.Entry} to copy the feature flag from
|
||||
*/
|
||||
public FeatureFlag(@NonNull Map.Entry<String, String> mapEntry) {
|
||||
this(mapEntry.getKey(), mapEntry.getValue());
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public String getName() {
|
||||
return name;
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public String getVariant() {
|
||||
return variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getName()}.
|
||||
*
|
||||
* @return the name of this {@code FeatureFlag}
|
||||
* @see #getName()
|
||||
*/
|
||||
@NonNull
|
||||
@Override
|
||||
public String getKey() {
|
||||
return name;
|
||||
}
|
||||
|
||||
/**
|
||||
* Same as {@link #getVariant()}.
|
||||
*
|
||||
* @return the variant of this {@code FeatureFlag} (may be {@code null})
|
||||
* @see #getVariant()
|
||||
*/
|
||||
@Nullable
|
||||
@Override
|
||||
public String getValue() {
|
||||
return variant;
|
||||
}
|
||||
|
||||
/**
|
||||
* Throws {@code UnsupportedOperationException} as {@code FeatureFlag} is considered immutable.
|
||||
*
|
||||
* @param value ignored
|
||||
* @return nothing
|
||||
*/
|
||||
@Override
|
||||
@Nullable
|
||||
public String setValue(@Nullable String value) {
|
||||
throw new UnsupportedOperationException("FeatureFlag is immutable");
|
||||
}
|
||||
|
||||
@Override
|
||||
public int hashCode() {
|
||||
// Follows the Map.Entry contract
|
||||
return getKey().hashCode() ^ (getValue() == null ? 0 : getValue().hashCode());
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean equals(Object other) {
|
||||
if (this == other) {
|
||||
return true;
|
||||
}
|
||||
|
||||
// This follows the contract defined in Map.Entry exactly
|
||||
if (!(other instanceof Map.Entry)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
Map.Entry<? extends Object, ? extends Object> e2 =
|
||||
(Map.Entry<? extends Object, ? extends Object>) other;
|
||||
|
||||
return getKey().equals(e2.getKey())
|
||||
&& (getValue() == null ? e2.getValue() == null : getValue().equals(e2.getValue()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public String toString() {
|
||||
return "FeatureFlag{"
|
||||
+ "name='" + name + '\''
|
||||
+ ", variant='" + variant + '\''
|
||||
+ '}';
|
||||
}
|
||||
}
|
||||
@ -1,46 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
internal interface FeatureFlagAware {
|
||||
/**
|
||||
* Add a single feature flag with no variant. If there is an existing feature flag with the
|
||||
* same name, it will be overwritten to have no variant.
|
||||
*
|
||||
* @param name the name of the feature flag to add
|
||||
* @see #addFeatureFlag(String, String)
|
||||
*/
|
||||
fun addFeatureFlag(name: String)
|
||||
|
||||
/**
|
||||
* Add a single feature flag with an optional variant. If there is an existing feature
|
||||
* flag with the same name, it will be overwritten with the new variant. If the variant is
|
||||
* {@code null} this method has the same behaviour as {@link #addFeatureFlag(String)}.
|
||||
*
|
||||
* @param name the name of the feature flag to add
|
||||
* @param variant the variant to set the feature flag to, or {@code null} to specify a feature
|
||||
* flag with no variant
|
||||
*/
|
||||
fun addFeatureFlag(name: String, variant: String?)
|
||||
|
||||
/**
|
||||
* Add a collection of feature flags. This method behaves exactly the same as calling
|
||||
* {@link #addFeatureFlag(String, String)} for each of the {@code FeatureFlag} objects.
|
||||
*
|
||||
* @param featureFlags the feature flags to add
|
||||
* @see #addFeatureFlag(String, String)
|
||||
*/
|
||||
fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>)
|
||||
|
||||
/**
|
||||
* Remove a single feature flag regardless of its current status. This will stop the specified
|
||||
* feature flag from being reported. If the named feature flag does not exist this will
|
||||
* have no effect.
|
||||
*
|
||||
* @param name the name of the feature flag to remove
|
||||
*/
|
||||
fun clearFeatureFlag(name: String)
|
||||
|
||||
/**
|
||||
* Clear all of the feature flags. This will stop all feature flags from being reported.
|
||||
*/
|
||||
fun clearFeatureFlags()
|
||||
}
|
||||
@ -1,50 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
internal data class FeatureFlagState(
|
||||
val featureFlags: FeatureFlags = FeatureFlags()
|
||||
) : BaseObservable(), FeatureFlagAware {
|
||||
override fun addFeatureFlag(name: String) {
|
||||
this.featureFlags.addFeatureFlag(name)
|
||||
updateState {
|
||||
StateEvent.AddFeatureFlag(name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addFeatureFlag(name: String, variant: String?) {
|
||||
this.featureFlags.addFeatureFlag(name, variant)
|
||||
updateState {
|
||||
StateEvent.AddFeatureFlag(name, variant)
|
||||
}
|
||||
}
|
||||
|
||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
||||
featureFlags.forEach { (name, variant) ->
|
||||
addFeatureFlag(name, variant)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearFeatureFlag(name: String) {
|
||||
this.featureFlags.clearFeatureFlag(name)
|
||||
updateState {
|
||||
StateEvent.ClearFeatureFlag(name)
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearFeatureFlags() {
|
||||
this.featureFlags.clearFeatureFlags()
|
||||
updateState {
|
||||
StateEvent.ClearFeatureFlags
|
||||
}
|
||||
}
|
||||
|
||||
fun emitObservableEvent() {
|
||||
val flags = toList()
|
||||
|
||||
flags.forEach { (name, variant) ->
|
||||
updateState { StateEvent.AddFeatureFlag(name, variant) }
|
||||
}
|
||||
}
|
||||
|
||||
fun toList(): List<FeatureFlag> = featureFlags.toList()
|
||||
fun copy() = FeatureFlagState(featureFlags.copy())
|
||||
}
|
||||
@ -1,121 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.io.IOException
|
||||
import kotlin.math.max
|
||||
|
||||
internal class FeatureFlags private constructor(
|
||||
@Volatile
|
||||
private var flags: Array<FeatureFlag>
|
||||
) : JsonStream.Streamable, FeatureFlagAware {
|
||||
|
||||
/*
|
||||
* Implemented as *effectively* a CopyOnWriteArrayList - but since FeatureFlags are
|
||||
* key/value pairs, CopyOnWriteArrayList would require external locking (in addition to it's
|
||||
* internal locking) for us to be sure we are not adding duplicates.
|
||||
*
|
||||
* This class aims to have similar performance while also ensuring that the FeatureFlag object
|
||||
* themselves don't leak, as they are mutable and we want 'copy' to be an O(1) snapshot
|
||||
* operation for when an Event is created.
|
||||
*
|
||||
* It's assumed that *most* FeatureFlags will be added up-front, or during the normal app
|
||||
* lifecycle (not during an Event).
|
||||
*
|
||||
* As such a copy-on-write structure allows an Event to simply capture a reference to the
|
||||
* "snapshot" of FeatureFlags that were active when the Event was created.
|
||||
*/
|
||||
|
||||
constructor() : this(emptyArray<FeatureFlag>())
|
||||
|
||||
override fun addFeatureFlag(name: String) {
|
||||
addFeatureFlag(name, null)
|
||||
}
|
||||
|
||||
override fun addFeatureFlag(name: String, variant: String?) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
val index = flagArray.indexOfFirst { it.name == name }
|
||||
|
||||
flags = when {
|
||||
// this is a new FeatureFlag
|
||||
index == -1 -> flagArray + FeatureFlag(name, variant)
|
||||
|
||||
// this is a change to an existing FeatureFlag
|
||||
flagArray[index].variant != variant -> flagArray.copyOf().also {
|
||||
// replace the existing FeatureFlag in-place
|
||||
it[index] = FeatureFlag(name, variant)
|
||||
}
|
||||
|
||||
// no actual change, so we return
|
||||
else -> return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
|
||||
val newFlags = ArrayList<FeatureFlag>(
|
||||
// try to guess a reasonable upper-bound for the output array
|
||||
if (featureFlags is Collection<*>) flagArray.size + featureFlags.size
|
||||
else max(flagArray.size * 2, flagArray.size)
|
||||
)
|
||||
|
||||
newFlags.addAll(flagArray)
|
||||
|
||||
featureFlags.forEach { (name, variant) ->
|
||||
val existingIndex = newFlags.indexOfFirst { it.name == name }
|
||||
when (existingIndex) {
|
||||
// add a new flag to the end of the list
|
||||
-1 -> newFlags.add(FeatureFlag(name, variant))
|
||||
// replace the existing flag
|
||||
else -> newFlags[existingIndex] = FeatureFlag(name, variant)
|
||||
}
|
||||
}
|
||||
|
||||
flags = newFlags.toTypedArray()
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearFeatureFlag(name: String) {
|
||||
synchronized(this) {
|
||||
val flagArray = flags
|
||||
val index = flagArray.indexOfFirst { it.name == name }
|
||||
if (index == -1) {
|
||||
return
|
||||
}
|
||||
|
||||
val out = arrayOfNulls<FeatureFlag>(flagArray.size - 1)
|
||||
flagArray.copyInto(out, 0, 0, index)
|
||||
flagArray.copyInto(out, index, index + 1)
|
||||
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
flags = out as Array<FeatureFlag>
|
||||
}
|
||||
}
|
||||
|
||||
override fun clearFeatureFlags() {
|
||||
synchronized(this) {
|
||||
flags = emptyArray()
|
||||
}
|
||||
}
|
||||
|
||||
@Throws(IOException::class)
|
||||
override fun toStream(stream: JsonStream) {
|
||||
val storeCopy = flags
|
||||
stream.beginArray()
|
||||
storeCopy.forEach { (name, variant) ->
|
||||
stream.beginObject()
|
||||
stream.name("featureFlag").value(name)
|
||||
if (variant != null) {
|
||||
stream.name("variant").value(variant)
|
||||
}
|
||||
stream.endObject()
|
||||
}
|
||||
stream.endArray()
|
||||
}
|
||||
|
||||
fun toList(): List<FeatureFlag> = flags.map { (name, variant) -> FeatureFlag(name, variant) }
|
||||
|
||||
fun copy() = FeatureFlags(flags)
|
||||
}
|
||||
@ -1,185 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.JsonStream.Streamable
|
||||
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.concurrent.ConcurrentSkipListSet
|
||||
import java.util.concurrent.locks.Lock
|
||||
import java.util.concurrent.locks.ReentrantLock
|
||||
|
||||
internal abstract class FileStore(
|
||||
val storageDir: File,
|
||||
private val maxStoreCount: Int,
|
||||
protected open val logger: Logger,
|
||||
protected val delegate: Delegate?
|
||||
) {
|
||||
internal fun 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
|
||||
*/
|
||||
fun onErrorIOFailure(exception: Exception?, errorFile: File?, context: String?)
|
||||
}
|
||||
|
||||
private val lock: Lock = ReentrantLock()
|
||||
private val queuedFiles: MutableCollection<File> = ConcurrentSkipListSet()
|
||||
|
||||
/**
|
||||
* 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 fun isStorageDirValid(storageDir: File): Boolean {
|
||||
try {
|
||||
storageDir.mkdirs()
|
||||
} catch (exception: Exception) {
|
||||
logger.e("Could not prepare file storage directory", exception)
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
fun enqueueContentForDelivery(content: String?, filename: String) {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return
|
||||
}
|
||||
discardOldestFileIfNeeded()
|
||||
lock.lock()
|
||||
var out: Writer? = null
|
||||
val filePath = File(storageDir, filename).absolutePath
|
||||
try {
|
||||
val fos = FileOutputStream(filePath)
|
||||
out = BufferedWriter(OutputStreamWriter(fos, "UTF-8"))
|
||||
out.write(content)
|
||||
} catch (exc: Exception) {
|
||||
val eventFile = File(filePath)
|
||||
delegate?.onErrorIOFailure(exc, eventFile, "NDK Crash report copy")
|
||||
IOUtils.deleteFile(eventFile, logger)
|
||||
} finally {
|
||||
try {
|
||||
out?.close()
|
||||
} catch (exception: Exception) {
|
||||
logger.w("Failed to close unsent payload writer: $filename", exception)
|
||||
}
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun write(streamable: Streamable): String? {
|
||||
if (!isStorageDirValid(storageDir)) {
|
||||
return null
|
||||
}
|
||||
if (maxStoreCount == 0) {
|
||||
return null
|
||||
}
|
||||
discardOldestFileIfNeeded()
|
||||
val filename = File(storageDir, getFilename(streamable)).absolutePath
|
||||
var stream: JsonStream? = null
|
||||
lock.lock()
|
||||
try {
|
||||
val fos = FileOutputStream(filename)
|
||||
val out: Writer = BufferedWriter(OutputStreamWriter(fos, "UTF-8"))
|
||||
stream = JsonStream(out)
|
||||
stream.value(streamable)
|
||||
logger.i("Saved unsent payload to disk: '$filename'")
|
||||
return filename
|
||||
} catch (exc: FileNotFoundException) {
|
||||
logger.w("Ignoring FileNotFoundException - unable to create file", exc)
|
||||
} catch (exc: Exception) {
|
||||
val eventFile = File(filename)
|
||||
delegate?.onErrorIOFailure(exc, eventFile, "Crash report serialization")
|
||||
IOUtils.deleteFile(eventFile, logger)
|
||||
} finally {
|
||||
IOUtils.closeQuietly(stream)
|
||||
lock.unlock()
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun discardOldestFileIfNeeded() {
|
||||
// Limit number of saved payloads to prevent disk space issues
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
val listFiles = storageDir.listFiles() ?: return
|
||||
if (listFiles.size < maxStoreCount) return
|
||||
val sortedListFiles = listFiles.sortedBy { it.lastModified() }
|
||||
// Number of files to discard takes into account that a new file may need to be written
|
||||
val numberToDiscard = listFiles.size - maxStoreCount + 1
|
||||
var discardedCount = 0
|
||||
for (file in sortedListFiles) {
|
||||
if (discardedCount == numberToDiscard) {
|
||||
return
|
||||
} else if (!queuedFiles.contains(file)) {
|
||||
logger.w(
|
||||
"Discarding oldest error as stored error limit reached: '" +
|
||||
file.path + '\''
|
||||
)
|
||||
deleteStoredFiles(setOf(file))
|
||||
discardedCount++
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
abstract fun getFilename(obj: Any?): String
|
||||
|
||||
fun findStoredFiles(): MutableList<File> {
|
||||
lock.lock()
|
||||
return try {
|
||||
val files: MutableList<File> = ArrayList()
|
||||
if (isStorageDirValid(storageDir)) {
|
||||
val values = storageDir.listFiles()
|
||||
if (values != null) {
|
||||
for (value in values) {
|
||||
// delete any tombstoned/empty files, as they contain no useful info
|
||||
if (value.length() == 0L) {
|
||||
if (!value.delete()) {
|
||||
value.deleteOnExit()
|
||||
}
|
||||
} else if (value.isFile && !queuedFiles.contains(value)) {
|
||||
files.add(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
queuedFiles.addAll(files)
|
||||
files
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun cancelQueuedFiles(files: Collection<File>?) {
|
||||
lock.lock()
|
||||
try {
|
||||
if (files != null) {
|
||||
queuedFiles.removeAll(files)
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
|
||||
fun deleteStoredFiles(storedFiles: Collection<File>?) {
|
||||
lock.lock()
|
||||
try {
|
||||
if (storedFiles != null) {
|
||||
queuedFiles.removeAll(storedFiles)
|
||||
for (storedFile in storedFiles) {
|
||||
if (!storedFile.delete()) {
|
||||
storedFile.deleteOnExit()
|
||||
}
|
||||
}
|
||||
}
|
||||
} finally {
|
||||
lock.unlock()
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,55 +0,0 @@
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,144 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import static com.bugsnag.android.DeliveryHeadersKt.HEADER_INTERNAL_ERROR;
|
||||
import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
|
||||
|
||||
import com.bugsnag.android.internal.BackgroundTaskService;
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
import com.bugsnag.android.internal.TaskType;
|
||||
import com.bugsnag.android.internal.dag.Provider;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
import android.content.Context;
|
||||
import android.os.Build;
|
||||
import android.os.storage.StorageManager;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
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;
|
||||
|
||||
@Nullable
|
||||
final StorageManager storageManager;
|
||||
|
||||
final AppDataCollector appDataCollector;
|
||||
final Provider<DeviceDataCollector> deviceDataCollector;
|
||||
final Context appContext;
|
||||
final SessionTracker sessionTracker;
|
||||
final Notifier notifier;
|
||||
final BackgroundTaskService backgroundTaskService;
|
||||
|
||||
InternalReportDelegate(Context context,
|
||||
Logger logger,
|
||||
ImmutableConfig immutableConfig,
|
||||
@Nullable StorageManager storageManager,
|
||||
AppDataCollector appDataCollector,
|
||||
Provider<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 (storageManager != null && 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.get().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, "bugsnag-android");
|
||||
headers.remove(DeliveryHeadersKt.HEADER_API_KEY);
|
||||
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
|
||||
defaultDelivery.deliver(
|
||||
params.getEndpoint(),
|
||||
payload.toByteArray(),
|
||||
payload.getIntegrityToken(),
|
||||
headers
|
||||
);
|
||||
}
|
||||
|
||||
} catch (Exception exception) {
|
||||
logger.w("Failed to report internal event to Bugsnag", exception);
|
||||
}
|
||||
}
|
||||
});
|
||||
} catch (RejectedExecutionException ignored) {
|
||||
// drop internal report
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,8 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
class Intrinsics {
|
||||
|
||||
static boolean isEmpty(CharSequence str) {
|
||||
return str == null || str.length() == 0;
|
||||
}
|
||||
}
|
||||
@ -1,14 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,75 +0,0 @@
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
@ -1,97 +0,0 @@
|
||||
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();
|
||||
}
|
||||
|
||||
JsonStream(@NonNull JsonStream stream, @NonNull ObjectJsonStreamer streamer) {
|
||||
super(stream.out);
|
||||
setSerializeNulls(stream.getSerializeNulls());
|
||||
this.out = stream.out;
|
||||
this.objectJsonStreamer = streamer;
|
||||
}
|
||||
|
||||
// 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 {
|
||||
if (object instanceof File) {
|
||||
value((File) object);
|
||||
} else {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@ -1,670 +0,0 @@
|
||||
/*
|
||||
* 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.write(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.
|
||||
* @return this writer.
|
||||
*/
|
||||
public JsonWriter value(double value) throws IOException {
|
||||
if (!lenient && (Double.isNaN(value) || Double.isInfinite(value))) {
|
||||
// omit these values instead of attempting to write them
|
||||
deferredName = null;
|
||||
} else {
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.write(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();
|
||||
}
|
||||
|
||||
String string = value.toString();
|
||||
if (!lenient
|
||||
&& (string.equals("-Infinity") || string.equals("Infinity") || string.equals("NaN"))) {
|
||||
// omit this value
|
||||
deferredName = null;
|
||||
} else {
|
||||
writeDeferredName();
|
||||
beforeValue();
|
||||
out.write(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.write(',');
|
||||
newline();
|
||||
break;
|
||||
|
||||
case DANGLING_NAME: // value for name
|
||||
out.write(separator);
|
||||
replaceTop(NONEMPTY_OBJECT);
|
||||
break;
|
||||
|
||||
default:
|
||||
throw new IllegalStateException("Nesting problem.");
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,26 +0,0 @@
|
||||
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)"
|
||||
}
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
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.value, "bugsnag/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.parentFile?.mkdirs()
|
||||
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.append("$key$KEY_VALUE_DELIMITER$value")
|
||||
sb.append("\n")
|
||||
}
|
||||
|
||||
override fun toString() = sb.toString()
|
||||
}
|
||||
@ -1,43 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
import java.util.concurrent.RejectedExecutionException
|
||||
import java.util.concurrent.ScheduledThreadPoolExecutor
|
||||
import java.util.concurrent.TimeUnit
|
||||
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)
|
||||
updateState { StateEvent.UpdateIsLaunching(false) }
|
||||
logger.d("App launch period marked as complete")
|
||||
}
|
||||
|
||||
fun isLaunching() = launching.get()
|
||||
}
|
||||
@ -1,51 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.TaskType;
|
||||
|
||||
import java.util.concurrent.atomic.AtomicBoolean;
|
||||
|
||||
class LibraryLoader {
|
||||
|
||||
private final AtomicBoolean attemptedLoad = new AtomicBoolean();
|
||||
private boolean loaded = false;
|
||||
|
||||
/**
|
||||
* 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(final String name, final Client client, final OnErrorCallback callback) {
|
||||
try {
|
||||
client.bgTaskService.submitTask(TaskType.IO, new Runnable() {
|
||||
@Override
|
||||
public void run() {
|
||||
loadLibInternal(name, client, callback);
|
||||
}
|
||||
}).get();
|
||||
return loaded;
|
||||
} catch (Throwable exc) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
void loadLibInternal(String name, Client client, OnErrorCallback callback) {
|
||||
if (!attemptedLoad.getAndSet(true)) {
|
||||
try {
|
||||
System.loadLibrary(name);
|
||||
loaded = true;
|
||||
} catch (UnsatisfiedLinkError error) {
|
||||
client.notify(error, callback);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
boolean isLoaded() {
|
||||
return loaded;
|
||||
}
|
||||
}
|
||||
@ -1,47 +0,0 @@
|
||||
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
|
||||
}
|
||||
@ -1,171 +0,0 @@
|
||||
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
|
||||
import java.util.regex.Pattern
|
||||
|
||||
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"
|
||||
private const val GENERATE_ANONYMOUS_ID = "$BUGSNAG_NS.GENERATE_ANONYMOUS_ID"
|
||||
|
||||
// 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 MAX_REPORTED_THREADS = "$BUGSNAG_NS.MAX_REPORTED_THREADS"
|
||||
private const val THREAD_COLLECTION_TIME_LIMIT_MS = "$BUGSNAG_NS.THREAD_COLLECTION_TIME_LIMIT_MS"
|
||||
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"
|
||||
private const val ATTEMPT_DELIVERY_ON_CRASH = "$BUGSNAG_NS.ATTEMPT_DELIVERY_ON_CRASH"
|
||||
}
|
||||
|
||||
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)
|
||||
maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads)
|
||||
threadCollectionTimeLimitMillis = data.getLong(
|
||||
THREAD_COLLECTION_TIME_LIMIT_MS,
|
||||
threadCollectionTimeLimitMillis
|
||||
)
|
||||
launchDurationMillis = data.getInt(
|
||||
LAUNCH_DURATION_MILLIS,
|
||||
launchDurationMillis.toInt()
|
||||
).toLong()
|
||||
sendLaunchCrashesSynchronously = data.getBoolean(
|
||||
SEND_LAUNCH_CRASHES_SYNCHRONOUSLY,
|
||||
sendLaunchCrashesSynchronously
|
||||
)
|
||||
isAttemptDeliveryOnCrash = data.getBoolean(
|
||||
ATTEMPT_DELIVERY_ON_CRASH,
|
||||
isAttemptDeliveryOnCrash
|
||||
)
|
||||
}
|
||||
}
|
||||
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)
|
||||
generateAnonymousId = data.getBoolean(GENERATE_ANONYMOUS_ID, generateAnonymousId)
|
||||
|
||||
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 = getPatternSet(data, DISCARD_CLASSES, discardClasses) ?: emptySet()
|
||||
projectPackages = getStrArray(data, PROJECT_PACKAGES, emptySet()) ?: emptySet()
|
||||
redactedKeys = getPatternSet(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()
|
||||
}
|
||||
}
|
||||
|
||||
private fun getPatternSet(
|
||||
data: Bundle,
|
||||
key: String,
|
||||
default: Set<Pattern>?
|
||||
): Set<Pattern>? {
|
||||
val delimitedStr = data.getString(key) ?: return default
|
||||
return delimitedStr.splitToSequence(',')
|
||||
.map { Pattern.compile(it) }
|
||||
.toSet()
|
||||
}
|
||||
}
|
||||
@ -1,42 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
import java.io.File
|
||||
|
||||
internal class MarshalledEventSource(
|
||||
private val eventFile: File,
|
||||
private val apiKey: String,
|
||||
private val logger: Logger
|
||||
) : () -> Event {
|
||||
|
||||
/**
|
||||
* The parsed and possibly processed event. This field remains `null` if the `EventSource`
|
||||
* is not used, and may not reflect the same data as is stored in `eventFile` (as the `Event`
|
||||
* is mutable, and may have been modified after loading).
|
||||
*/
|
||||
var event: Event? = null
|
||||
private set
|
||||
|
||||
override fun invoke(): Event {
|
||||
var unmarshalledEvent = event
|
||||
if (unmarshalledEvent == null) {
|
||||
unmarshalledEvent = unmarshall()
|
||||
event = unmarshalledEvent
|
||||
}
|
||||
|
||||
return unmarshalledEvent
|
||||
}
|
||||
|
||||
fun clear() {
|
||||
event = null
|
||||
}
|
||||
|
||||
private fun unmarshall(): Event {
|
||||
val eventMapper = BugsnagEventMapper(logger)
|
||||
val jsonMap = JsonHelper.deserialize(eventFile)
|
||||
return Event(
|
||||
eventMapper.convertToEventImpl(jsonMap, apiKey),
|
||||
logger
|
||||
)
|
||||
}
|
||||
}
|
||||
@ -1,41 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import android.content.ComponentCallbacks2
|
||||
|
||||
internal class MemoryTrimState : BaseObservable() {
|
||||
var isLowMemory: Boolean = false
|
||||
var memoryTrimLevel: Int? = null
|
||||
|
||||
val trimLevelDescription: String get() = descriptionFor(memoryTrimLevel)
|
||||
|
||||
fun updateMemoryTrimLevel(newTrimLevel: Int?): Boolean {
|
||||
if (memoryTrimLevel == newTrimLevel) {
|
||||
return false
|
||||
}
|
||||
|
||||
memoryTrimLevel = newTrimLevel
|
||||
return true
|
||||
}
|
||||
|
||||
fun emitObservableEvent() {
|
||||
updateState {
|
||||
StateEvent.UpdateMemoryTrimEvent(
|
||||
isLowMemory,
|
||||
memoryTrimLevel,
|
||||
trimLevelDescription
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
private fun descriptionFor(memoryTrimLevel: Int?) = when (memoryTrimLevel) {
|
||||
null -> "None"
|
||||
ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> "Complete"
|
||||
ComponentCallbacks2.TRIM_MEMORY_MODERATE -> "Moderate"
|
||||
ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> "Background"
|
||||
ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> "UI hidden"
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> "Running critical"
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> "Running low"
|
||||
ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> "Running moderate"
|
||||
else -> "Unknown ($memoryTrimLevel)"
|
||||
}
|
||||
}
|
||||
@ -1,158 +0,0 @@
|
||||
@file:Suppress("UNCHECKED_CAST")
|
||||
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.StringUtils
|
||||
import com.bugsnag.android.internal.TrimMetrics
|
||||
import java.io.IOException
|
||||
import java.util.concurrent.ConcurrentHashMap
|
||||
import java.util.regex.Pattern
|
||||
|
||||
/**
|
||||
* 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: MutableMap<String, MutableMap<String, Any>> = ConcurrentHashMap()
|
||||
) : JsonStream.Streamable, MetadataAware {
|
||||
|
||||
val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer()
|
||||
|
||||
var redactedKeys: Set<Pattern>
|
||||
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 {
|
||||
val tab = store[section] ?: ConcurrentHashMap()
|
||||
store[section] = tab
|
||||
insertValue(tab, 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 (existingValue != null && obj is Map<*, *>) {
|
||||
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]
|
||||
tab?.remove(key)
|
||||
|
||||
if (tab.isNullOrEmpty()) {
|
||||
store.remove(section)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getMetadata(section: String): Map<String, Any>? {
|
||||
return store[section]
|
||||
}
|
||||
|
||||
override fun getMetadata(section: String, key: String): Any? {
|
||||
return getMetadata(section)?.get(key)
|
||||
}
|
||||
|
||||
fun toMap(): MutableMap<String, MutableMap<String, Any>> {
|
||||
val copy = ConcurrentHashMap(store)
|
||||
|
||||
// deep copy each section
|
||||
store.entries.forEach {
|
||||
copy[it.key] = ConcurrentHashMap(it.value)
|
||||
}
|
||||
return copy
|
||||
}
|
||||
|
||||
companion object {
|
||||
fun merge(vararg data: Metadata): Metadata {
|
||||
val stores = data.map { it.toMap() }
|
||||
val redactKeys = data.flatMap { it.jsonStreamer.redactedKeys }
|
||||
val newMeta = Metadata(mergeMaps(stores) as MutableMap<String, MutableMap<String, Any>>)
|
||||
newMeta.redactedKeys = redactKeys.toSet()
|
||||
return newMeta
|
||||
}
|
||||
|
||||
internal fun mergeMaps(data: List<Map<String, Any>>): MutableMap<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: MutableMap<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() }
|
||||
}
|
||||
|
||||
fun trimMetadataStringsTo(maxStringLength: Int): TrimMetrics {
|
||||
var stringCount = 0
|
||||
var charCount = 0
|
||||
store.forEach { entry ->
|
||||
val stringAndCharCounts = StringUtils.trimStringValuesTo(
|
||||
maxStringLength,
|
||||
entry.value as MutableMap<String, Any?>
|
||||
)
|
||||
|
||||
stringCount += stringAndCharCounts.itemsTrimmed
|
||||
charCount += stringAndCharCounts.dataTrimmed
|
||||
}
|
||||
return TrimMetrics(stringCount, charCount)
|
||||
}
|
||||
}
|
||||
@ -1,12 +0,0 @@
|
||||
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?
|
||||
}
|
||||
@ -1,67 +0,0 @@
|
||||
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 -> updateState { StateEvent.ClearMetadataSection(section) }
|
||||
else -> updateState { 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 -> updateState { AddMetadata(section, key, metadata.getMetadata(section, key)) }
|
||||
}
|
||||
}
|
||||
|
||||
private fun notifyMetadataAdded(section: String, value: Map<String, Any?>) {
|
||||
value.entries.forEach {
|
||||
updateState { AddMetadata(section, it.key, metadata.getMetadata(section, it.key)) }
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,640 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig;
|
||||
import com.bugsnag.android.internal.JsonHelper;
|
||||
|
||||
import android.annotation.SuppressLint;
|
||||
|
||||
import androidx.annotation.NonNull;
|
||||
import androidx.annotation.Nullable;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.File;
|
||||
import java.nio.charset.Charset;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Collection;
|
||||
import java.util.Date;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Locale;
|
||||
import java.util.Map;
|
||||
import java.util.regex.Pattern;
|
||||
|
||||
|
||||
/**
|
||||
* 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();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an empty Event for a "handled exception" report. The returned Event will have
|
||||
* no Error objects, metadata, breadcrumbs, or feature flags. It's indented that the caller
|
||||
* will populate the Error and then pass the Event object to
|
||||
* {@link Client#populateAndNotifyAndroidEvent(Event, OnErrorCallback)}.
|
||||
*/
|
||||
private static Event createEmptyEvent() {
|
||||
Client client = getClient();
|
||||
|
||||
return new Event(
|
||||
new EventInternal(
|
||||
(Throwable) null,
|
||||
client.getConfig(),
|
||||
SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION),
|
||||
client.getMetadataState().getMetadata().copy()
|
||||
),
|
||||
client.getLogger()
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 File getNativeReportPath() {
|
||||
return getNativeReportPath(getPersistenceDirectory());
|
||||
}
|
||||
|
||||
private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) {
|
||||
return new File(persistenceDirectory, "bugsnag/native");
|
||||
}
|
||||
|
||||
private static @NonNull File getPersistenceDirectory() {
|
||||
return getClient().getConfig().getPersistenceDirectory().getValue();
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add metadata to subsequent exception reports with a Hashmap
|
||||
*/
|
||||
public static void addMetadata(@NonNull final String tab,
|
||||
@NonNull final Map<String, ?> metadata) {
|
||||
getClient().addMetadata(tab, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Ask if an error class is on the configurable discard list.
|
||||
* This is used by the native layer to decide whether to pass an event to
|
||||
* deliverReport() or not.
|
||||
*
|
||||
* @param name The error class to ask about.
|
||||
*/
|
||||
@SuppressWarnings("unused")
|
||||
public static boolean isDiscardErrorClass(@NonNull String name) {
|
||||
Collection<Pattern> discardClasses = getClient().getConfig().getDiscardClasses();
|
||||
if (discardClasses.isEmpty()) {
|
||||
return false;
|
||||
}
|
||||
for (Pattern pattern : discardClasses) {
|
||||
if (pattern.matcher(name).matches()) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
private static void deepMerge(Map<String, Object> src, Map<String, Object> dst) {
|
||||
for (Map.Entry<String, Object> entry : src.entrySet()) {
|
||||
String key = entry.getKey();
|
||||
Object srcValue = entry.getValue();
|
||||
Object dstValue = dst.get(key);
|
||||
if (srcValue instanceof Map && (dstValue instanceof Map)) {
|
||||
deepMerge((Map<String, Object>) srcValue, (Map<String, Object>) dstValue);
|
||||
} else if (srcValue instanceof Collection && dstValue instanceof Collection) {
|
||||
// Just append everything because we don't know enough about the context or
|
||||
// provenance of the data to make an intelligent decision about this.
|
||||
((Collection<Object>) dstValue).addAll((Collection<Object>) srcValue);
|
||||
} else {
|
||||
dst.put(key, srcValue);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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,
|
||||
@Nullable byte[] staticDataBytes,
|
||||
@NonNull String apiKey,
|
||||
boolean isLaunching) {
|
||||
// If there's saved static data, merge it directly into the payload map.
|
||||
if (staticDataBytes != null) {
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> payloadMap = (Map<String, Object>) JsonHelper.INSTANCE.deserialize(
|
||||
new ByteArrayInputStream(payloadBytes));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> staticDataMap =
|
||||
(Map<String, Object>) JsonHelper.INSTANCE.deserialize(
|
||||
new ByteArrayInputStream(staticDataBytes));
|
||||
deepMerge(staticDataMap, payloadMap);
|
||||
ByteArrayOutputStream os = new ByteArrayOutputStream();
|
||||
JsonHelper.INSTANCE.serialize(payloadMap, os);
|
||||
payloadBytes = os.toByteArray();
|
||||
}
|
||||
|
||||
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.shouldDiscardByReleaseStage()) {
|
||||
EventStore eventStore = client.getEventStore();
|
||||
|
||||
String filename = eventStore.getNdkFilename(payload, apiKey);
|
||||
if (isLaunching) {
|
||||
filename = filename.replace(".json", "startupcrash.json");
|
||||
}
|
||||
eventStore.enqueueContentForDelivery(payload, filename);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Attempt to deliver an existing event file that is not current enqueued for delivery. The
|
||||
* filename is expected to be in the standard {@link EventFilenameInfo} format, and the file
|
||||
* should contain a correctly formatted {@link Event} object. This method will attempt to
|
||||
* move the file into place, and flush the queue asynchronously. If the file cannot be moved
|
||||
* into the queue directory, the file is deleted before returning.
|
||||
*
|
||||
* @param reportFile the file to enqueue for delivery
|
||||
*/
|
||||
public static void deliverReport(@NonNull File reportFile) {
|
||||
EventStore eventStore = getClient().eventStore;
|
||||
File eventFile = new File(eventStore.getStorageDir(), reportFile.getName());
|
||||
if (reportFile.renameTo(eventFile)) {
|
||||
eventStore.flushAsync();
|
||||
} else {
|
||||
reportFile.delete();
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 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) {
|
||||
if (getClient().getConfig().shouldDiscardError(name)) {
|
||||
return;
|
||||
}
|
||||
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;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* 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 NativeStackframe[] 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 NativeStackframe[] stacktrace) {
|
||||
Client client = getClient();
|
||||
|
||||
if (client.getConfig().shouldDiscardError(name)) {
|
||||
return;
|
||||
}
|
||||
|
||||
Event event = createEmptyEvent();
|
||||
event.updateSeverityInternal(severity);
|
||||
|
||||
List<Stackframe> stackframes = new ArrayList<>(stacktrace.length);
|
||||
for (NativeStackframe nativeStackframe : stacktrace) {
|
||||
stackframes.add(new Stackframe(nativeStackframe));
|
||||
}
|
||||
event.getErrors().add(new Error(
|
||||
new ErrorInternal(name, message, new Stacktrace(stackframes), ErrorType.C),
|
||||
client.getLogger()
|
||||
));
|
||||
|
||||
getClient().populateAndNotifyAndroidEvent(event, null);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an {@code Event} object
|
||||
*
|
||||
* @param exc the Throwable object that caused the event
|
||||
* @param client the Client object that the event is associated with
|
||||
* @param severityReason the severity of the Event
|
||||
* @return a new {@code Event} object
|
||||
*/
|
||||
@NonNull
|
||||
public static Event createEvent(@Nullable Throwable exc,
|
||||
@NonNull Client client,
|
||||
@NonNull SeverityReason severityReason) {
|
||||
Metadata metadata = client.getMetadataState().getMetadata();
|
||||
FeatureFlags featureFlags = client.getFeatureFlagState().getFeatureFlags();
|
||||
return new Event(exc, client.getConfig(), severityReason, metadata, featureFlags,
|
||||
client.logger);
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public static Logger getLogger() {
|
||||
return getClient().getConfig().getLogger();
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches automatic error detection on/off after Bugsnag has initialized.
|
||||
* This is required to support legacy functionality in Unity.
|
||||
*
|
||||
* @param autoNotify whether errors should be automatically detected.
|
||||
*/
|
||||
public static void setAutoNotify(boolean autoNotify) {
|
||||
getClient().setAutoNotify(autoNotify);
|
||||
}
|
||||
|
||||
/**
|
||||
* Switches automatic ANR detection on/off after Bugsnag has initialized.
|
||||
* This is required to support legacy functionality in Unity.
|
||||
*
|
||||
* @param autoDetectAnrs whether ANRs should be automatically detected.
|
||||
*/
|
||||
public static void setAutoDetectAnrs(boolean autoDetectAnrs) {
|
||||
getClient().setAutoDetectAnrs(autoDetectAnrs);
|
||||
}
|
||||
|
||||
public static void startSession() {
|
||||
getClient().startSession();
|
||||
}
|
||||
|
||||
public static void pauseSession() {
|
||||
getClient().pauseSession();
|
||||
}
|
||||
|
||||
public static boolean resumeSession() {
|
||||
return getClient().resumeSession();
|
||||
}
|
||||
|
||||
@Nullable
|
||||
public static Session getCurrentSession() {
|
||||
return getClient().sessionTracker.getCurrentSession();
|
||||
}
|
||||
|
||||
/**
|
||||
* Marks the launch period as complete
|
||||
*/
|
||||
public static void markLaunchCompleted() {
|
||||
getClient().markLaunchCompleted();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the last run info object
|
||||
*/
|
||||
@Nullable
|
||||
public static LastRunInfo getLastRunInfo() {
|
||||
return getClient().getLastRunInfo();
|
||||
}
|
||||
}
|
||||
@ -1,74 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.JsonHelper
|
||||
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?,
|
||||
|
||||
/**
|
||||
* Whether this frame identifies the program counter
|
||||
*/
|
||||
var isPC: Boolean?,
|
||||
|
||||
/**
|
||||
* The type of the error
|
||||
*/
|
||||
var type: ErrorType? = null,
|
||||
|
||||
/**
|
||||
* Identifies the exact build this frame originates from.
|
||||
*/
|
||||
var codeIdentifier: String? = null,
|
||||
) : JsonStream.Streamable {
|
||||
|
||||
@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)
|
||||
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) }
|
||||
symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) }
|
||||
loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
|
||||
writer.name("codeIdentifier").value(codeIdentifier)
|
||||
writer.name("isPC").value(isPC)
|
||||
|
||||
type?.let {
|
||||
writer.name("type").value(it.desc)
|
||||
}
|
||||
writer.endObject()
|
||||
}
|
||||
}
|
||||
@ -1,101 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.lang.reflect.Method
|
||||
|
||||
/**
|
||||
* Calls the NDK plugin if it is loaded, otherwise does nothing / returns the default.
|
||||
*/
|
||||
internal object NdkPluginCaller {
|
||||
private var ndkPlugin: Plugin? = null
|
||||
private var setInternalMetricsEnabled: Method? = null
|
||||
private var setStaticData: Method? = null
|
||||
private var getSignalUnwindStackFunction: Method? = null
|
||||
private var getCurrentCallbackSetCounts: Method? = null
|
||||
private var getCurrentNativeApiCallUsage: Method? = null
|
||||
private var initCallbackCounts: Method? = null
|
||||
private var notifyAddCallback: Method? = null
|
||||
private var notifyRemoveCallback: Method? = null
|
||||
|
||||
private fun getMethod(name: String, vararg parameterTypes: Class<*>): Method? {
|
||||
val plugin = ndkPlugin
|
||||
if (plugin == null) {
|
||||
return null
|
||||
}
|
||||
return plugin.javaClass.getMethod(name, *parameterTypes)
|
||||
}
|
||||
|
||||
fun setNdkPlugin(plugin: Plugin?) {
|
||||
if (plugin != null) {
|
||||
ndkPlugin = plugin
|
||||
setInternalMetricsEnabled = getMethod("setInternalMetricsEnabled", Boolean::class.java)
|
||||
setStaticData = getMethod("setStaticData", Map::class.java)
|
||||
getSignalUnwindStackFunction = getMethod("getSignalUnwindStackFunction")
|
||||
getCurrentCallbackSetCounts = getMethod("getCurrentCallbackSetCounts")
|
||||
getCurrentNativeApiCallUsage = getMethod("getCurrentNativeApiCallUsage")
|
||||
initCallbackCounts = getMethod("initCallbackCounts", Map::class.java)
|
||||
notifyAddCallback = getMethod("notifyAddCallback", String::class.java)
|
||||
notifyRemoveCallback = getMethod("notifyRemoveCallback", String::class.java)
|
||||
}
|
||||
}
|
||||
|
||||
fun getSignalUnwindStackFunction(): Long {
|
||||
val method = getSignalUnwindStackFunction
|
||||
if (method != null) {
|
||||
return method.invoke(ndkPlugin) as Long
|
||||
}
|
||||
return 0
|
||||
}
|
||||
|
||||
fun setInternalMetricsEnabled(enabled: Boolean) {
|
||||
val method = setInternalMetricsEnabled
|
||||
if (method != null) {
|
||||
method.invoke(ndkPlugin, enabled)
|
||||
}
|
||||
}
|
||||
|
||||
fun getCurrentCallbackSetCounts(): Map<String, Int>? {
|
||||
val method = getCurrentCallbackSetCounts
|
||||
if (method != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return method.invoke(ndkPlugin) as Map<String, Int>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun getCurrentNativeApiCallUsage(): Map<String, Boolean>? {
|
||||
val method = getCurrentNativeApiCallUsage
|
||||
if (method != null) {
|
||||
@Suppress("UNCHECKED_CAST")
|
||||
return method.invoke(ndkPlugin) as Map<String, Boolean>
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
fun initCallbackCounts(counts: Map<String, Int>) {
|
||||
val method = initCallbackCounts
|
||||
if (method != null) {
|
||||
method.invoke(ndkPlugin, counts)
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyAddCallback(callback: String) {
|
||||
val method = notifyAddCallback
|
||||
if (method != null) {
|
||||
method.invoke(ndkPlugin, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun notifyRemoveCallback(callback: String) {
|
||||
val method = notifyRemoveCallback
|
||||
if (method != null) {
|
||||
method.invoke(ndkPlugin, callback)
|
||||
}
|
||||
}
|
||||
|
||||
fun setStaticData(data: Map<String, Any>) {
|
||||
val method = setStaticData
|
||||
if (method != null) {
|
||||
method.invoke(ndkPlugin, data)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,3 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
internal object NoopLogger : Logger
|
||||
@ -1,31 +0,0 @@
|
||||
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 = "6.10.0",
|
||||
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()
|
||||
}
|
||||
}
|
||||
@ -1,73 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils
|
||||
import java.io.IOException
|
||||
import java.lang.reflect.Array
|
||||
import java.util.Date
|
||||
import java.util.regex.Pattern
|
||||
|
||||
internal class ObjectJsonStreamer {
|
||||
|
||||
companion object {
|
||||
internal const val REDACTED_PLACEHOLDER = "[REDACTED]"
|
||||
internal const val OBJECT_PLACEHOLDER = "[OBJECT]"
|
||||
|
||||
internal val DEFAULT_REDACTED_KEYS = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE))
|
||||
}
|
||||
|
||||
var redactedKeys = DEFAULT_REDACTED_KEYS
|
||||
|
||||
// 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 Date -> writer.value(DateUtils.toIso8601(obj))
|
||||
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 { it.matcher(key).matches() }
|
||||
}
|
||||
@ -1,31 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* Add a "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 `false` from any callback to ignore a breadcrumb.
|
||||
*
|
||||
*
|
||||
* For example:
|
||||
*
|
||||
*
|
||||
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
|
||||
* public boolean onBreadcrumb(Breadcrumb breadcrumb) {
|
||||
* return false; // ignore the breadcrumb
|
||||
* }
|
||||
* })
|
||||
*/
|
||||
fun interface OnBreadcrumbCallback {
|
||||
/**
|
||||
* Runs the "on breadcrumb" callback. If the callback returns
|
||||
* `false` 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
|
||||
*/
|
||||
fun onBreadcrumb(breadcrumb: Breadcrumb): Boolean
|
||||
}
|
||||
@ -1,22 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be run before error reports are sent to Bugsnag.
|
||||
*
|
||||
* You can use this to add or modify information attached to an error
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* `false` from any callback to halt execution.
|
||||
*
|
||||
* "on error" callbacks added via the JVM API do not run when a fatal C/C++ crash occurs.
|
||||
*/
|
||||
fun interface OnErrorCallback {
|
||||
/**
|
||||
* Runs the "on error" callback. If the callback returns
|
||||
* `false` 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
|
||||
*/
|
||||
fun onError(event: Event): Boolean
|
||||
}
|
||||
@ -1,10 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be invoked before an [Event] is uploaded to a server. Similar to
|
||||
* [OnErrorCallback], an `OnSendCallback` may modify the `Event`
|
||||
* contents or even reject the entire payload by returning `false`.
|
||||
*/
|
||||
fun interface OnSendCallback {
|
||||
fun onSend(event: Event): Boolean
|
||||
}
|
||||
@ -1,20 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
/**
|
||||
* A callback to be run before sessions are sent to Bugsnag.
|
||||
*
|
||||
* You can use this to add or modify information attached to a session
|
||||
* before it is sent to your dashboard. You can also return
|
||||
* `false` from any callback to halt execution.
|
||||
*/
|
||||
fun interface OnSessionCallback {
|
||||
/**
|
||||
* Runs the "on session" callback. If the callback returns
|
||||
* `false` 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
|
||||
*/
|
||||
fun onSession(session: Session): Boolean
|
||||
}
|
||||
@ -1,19 +0,0 @@
|
||||
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()
|
||||
}
|
||||
@ -1,98 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.internal.ImmutableConfig
|
||||
|
||||
internal class PluginClient(
|
||||
userPlugins: Set<Plugin>,
|
||||
private val immutableConfig: ImmutableConfig,
|
||||
private val logger: Logger
|
||||
) {
|
||||
|
||||
companion object {
|
||||
private const val NDK_PLUGIN = "com.bugsnag.android.NdkPlugin"
|
||||
private const val ANR_PLUGIN = "com.bugsnag.android.AnrPlugin"
|
||||
private const val RN_PLUGIN = "com.bugsnag.android.BugsnagReactNativePlugin"
|
||||
}
|
||||
|
||||
private val plugins: Set<Plugin>
|
||||
private val ndkPlugin = instantiatePlugin(NDK_PLUGIN, immutableConfig.enabledErrorTypes.ndkCrashes)
|
||||
private val anrPlugin = instantiatePlugin(ANR_PLUGIN, immutableConfig.enabledErrorTypes.anrs)
|
||||
private val rnPlugin = instantiatePlugin(RN_PLUGIN, immutableConfig.enabledErrorTypes.unhandledRejections)
|
||||
|
||||
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
|
||||
ndkPlugin?.let(set::add)
|
||||
anrPlugin?.let(set::add)
|
||||
rnPlugin?.let(set::add)
|
||||
plugins = set.toSet()
|
||||
}
|
||||
|
||||
private fun instantiatePlugin(clz: String, isWarningEnabled: Boolean): Plugin? {
|
||||
return try {
|
||||
val pluginClz = Class.forName(clz)
|
||||
pluginClz.getDeclaredConstructor().newInstance() as Plugin
|
||||
} catch (exc: ClassNotFoundException) {
|
||||
if (isWarningEnabled) {
|
||||
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 getNdkPlugin(): Plugin? = ndkPlugin
|
||||
|
||||
fun loadPlugins(client: Client) {
|
||||
plugins.forEach { plugin ->
|
||||
try {
|
||||
loadPluginInternal(plugin, client)
|
||||
} catch (exc: Throwable) {
|
||||
logger.e("Failed to load plugin $plugin, continuing with initialisation.", exc)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoNotify(client: Client, autoNotify: Boolean) {
|
||||
setAutoDetectAnrs(client, autoNotify)
|
||||
|
||||
if (autoNotify) {
|
||||
ndkPlugin?.load(client)
|
||||
} else {
|
||||
ndkPlugin?.unload()
|
||||
}
|
||||
}
|
||||
|
||||
fun setAutoDetectAnrs(client: Client, autoDetectAnrs: Boolean) {
|
||||
if (autoDetectAnrs) {
|
||||
anrPlugin?.load(client)
|
||||
} else {
|
||||
anrPlugin?.unload()
|
||||
}
|
||||
}
|
||||
|
||||
fun findPlugin(clz: Class<*>): Plugin? = plugins.find { it.javaClass == clz }
|
||||
|
||||
private fun loadPluginInternal(plugin: Plugin, client: Client) {
|
||||
val name = plugin.javaClass.name
|
||||
val errorTypes = immutableConfig.enabledErrorTypes
|
||||
|
||||
// only initialize NDK/ANR plugins if automatic detection enabled
|
||||
if (name == NDK_PLUGIN) {
|
||||
if (errorTypes.ndkCrashes) {
|
||||
plugin.load(client)
|
||||
}
|
||||
} else if (name == ANR_PLUGIN) {
|
||||
if (errorTypes.anrs) {
|
||||
plugin.load(client)
|
||||
}
|
||||
} else {
|
||||
plugin.load(client)
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,150 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import androidx.annotation.VisibleForTesting
|
||||
import java.io.File
|
||||
import java.io.IOException
|
||||
import java.io.Reader
|
||||
|
||||
/**
|
||||
* 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"
|
||||
)
|
||||
}
|
||||
|
||||
@Volatile
|
||||
private var libraryLoaded = false
|
||||
|
||||
init {
|
||||
try {
|
||||
System.loadLibrary("bugsnag-root-detection")
|
||||
libraryLoaded = 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]")
|
||||
}.any()
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
@VisibleForTesting
|
||||
internal fun checkSuExists(processBuilder: ProcessBuilder): Boolean {
|
||||
processBuilder.command(listOf("which", "su"))
|
||||
|
||||
var process: Process? = null
|
||||
return try {
|
||||
process = processBuilder.start()
|
||||
process.inputStream.bufferedReader().use { it.isNotBlank() }
|
||||
} catch (ignored: IOException) {
|
||||
false
|
||||
} finally {
|
||||
process?.destroy()
|
||||
}
|
||||
}
|
||||
|
||||
private external fun performNativeRootChecks(): Boolean
|
||||
|
||||
private fun Reader.isNotBlank(): Boolean {
|
||||
while (true) {
|
||||
val ch = read()
|
||||
when {
|
||||
ch == -1 -> return false
|
||||
!ch.toChar().isWhitespace() -> return true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Performs root checks which require native code.
|
||||
*/
|
||||
private fun nativeCheckRoot(): Boolean = when {
|
||||
libraryLoaded -> performNativeRootChecks()
|
||||
else -> false
|
||||
}
|
||||
}
|
||||
@ -1,316 +0,0 @@
|
||||
package com.bugsnag.android;
|
||||
|
||||
import com.bugsnag.android.internal.DateUtils;
|
||||
import com.bugsnag.android.internal.JsonHelper;
|
||||
|
||||
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.Map;
|
||||
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, Deliverable, 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 volatile boolean autoCaptured = false;
|
||||
private final AtomicInteger unhandledCount = new AtomicInteger();
|
||||
private final AtomicInteger handledCount = new AtomicInteger();
|
||||
private final AtomicBoolean tracked = new AtomicBoolean(false);
|
||||
private final AtomicBoolean isPaused = new AtomicBoolean(false);
|
||||
|
||||
private String apiKey;
|
||||
|
||||
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, session.getApiKey());
|
||||
copy.tracked.set(session.tracked.get());
|
||||
copy.autoCaptured = session.isAutoCaptured();
|
||||
return copy;
|
||||
}
|
||||
|
||||
Session(Map<String, Object> map, Logger logger, String apiKey) {
|
||||
this(null, null, logger, apiKey);
|
||||
setId((String) map.get("id"));
|
||||
|
||||
String timestamp = (String) map.get("startedAt");
|
||||
setStartedAt(DateUtils.fromIso8601(timestamp));
|
||||
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> events = (Map<String, Object>) map.get("events");
|
||||
|
||||
Number handled = (Number) events.get("handled");
|
||||
handledCount.set(handled.intValue());
|
||||
|
||||
Number unhandled = (Number) events.get("unhandled");
|
||||
unhandledCount.set(unhandled.intValue());
|
||||
}
|
||||
|
||||
Session(String id, Date startedAt, User user, boolean autoCaptured,
|
||||
Notifier notifier, Logger logger, String apiKey) {
|
||||
this(null, notifier, logger, apiKey);
|
||||
this.id = id;
|
||||
this.startedAt = new Date(startedAt.getTime());
|
||||
this.user = user;
|
||||
this.autoCaptured = autoCaptured;
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
Session(String id, Date startedAt, User user, int unhandledCount, int handledCount,
|
||||
Notifier notifier, Logger logger, String apiKey) {
|
||||
this(id, startedAt, user, false, notifier, logger, apiKey);
|
||||
this.unhandledCount.set(unhandledCount);
|
||||
this.handledCount.set(handledCount);
|
||||
this.tracked.set(true);
|
||||
this.apiKey = apiKey;
|
||||
}
|
||||
|
||||
Session(File file, Notifier notifier, Logger logger, String apiKey) {
|
||||
this.file = file;
|
||||
this.logger = logger;
|
||||
this.apiKey = SessionFilenameInfo.findApiKeyInFilename(file, apiKey);
|
||||
if (notifier != null) {
|
||||
Notifier copy = new Notifier(notifier.getName(),
|
||||
notifier.getVersion(), notifier.getUrl());
|
||||
copy.setDependencies(new ArrayList<>(notifier.getDependencies()));
|
||||
this.notifier = copy;
|
||||
} else {
|
||||
this.notifier = null;
|
||||
}
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
|
||||
boolean markTracked() {
|
||||
return tracked.compareAndSet(false, true);
|
||||
}
|
||||
|
||||
boolean markResumed() {
|
||||
return isPaused.compareAndSet(true, false);
|
||||
}
|
||||
|
||||
void markPaused() {
|
||||
isPaused.set(true);
|
||||
}
|
||||
|
||||
boolean isPaused() {
|
||||
return isPaused.get();
|
||||
}
|
||||
|
||||
boolean isAutoCaptured() {
|
||||
return autoCaptured;
|
||||
}
|
||||
|
||||
void setAutoCaptured(boolean autoCaptured) {
|
||||
this.autoCaptured = 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 isLegacyPayload() {
|
||||
return !(file != null
|
||||
&& (file.getName().endsWith("_v2.json") || file.getName().endsWith("_v3.json")));
|
||||
}
|
||||
|
||||
Notifier getNotifier() {
|
||||
return notifier;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void toStream(@NonNull JsonStream writer) throws IOException {
|
||||
if (file != null) {
|
||||
if (!isLegacyPayload()) {
|
||||
serializePayload(writer);
|
||||
} else {
|
||||
serializeLegacyPayload(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();
|
||||
}
|
||||
}
|
||||
|
||||
@NonNull
|
||||
public byte[] toByteArray() throws IOException {
|
||||
return JsonHelper.INSTANCE.serialize(this);
|
||||
}
|
||||
|
||||
@Nullable
|
||||
@Override
|
||||
public String getIntegrityToken() {
|
||||
return Deliverable.DefaultImpls.getIntegrityToken(this);
|
||||
}
|
||||
|
||||
private void serializePayload(@NonNull JsonStream writer) throws IOException {
|
||||
writer.value(file);
|
||||
}
|
||||
|
||||
private void serializeLegacyPayload(@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(startedAt);
|
||||
writer.name("user").value(user);
|
||||
writer.endObject();
|
||||
}
|
||||
|
||||
/**
|
||||
* The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag
|
||||
* is initialized, you may choose to send certain sessions to a different Bugsnag project.
|
||||
*/
|
||||
public void setApiKey(@NonNull String apiKey) {
|
||||
if (apiKey != null) {
|
||||
this.apiKey = apiKey;
|
||||
} else {
|
||||
logNull("apiKey");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag
|
||||
* is initialized, you may choose to send certain sessions to a different Bugsnag project.
|
||||
*/
|
||||
@NonNull
|
||||
public String getApiKey() {
|
||||
return apiKey;
|
||||
}
|
||||
}
|
||||
@ -1,87 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import java.io.File
|
||||
import java.util.UUID
|
||||
|
||||
/**
|
||||
* Represents important information about a session filename.
|
||||
* Currently the following information is encoded:
|
||||
*
|
||||
* uuid - to disambiguate stored error reports
|
||||
* timestamp - to sort error reports by time of capture
|
||||
*/
|
||||
internal data class SessionFilenameInfo(
|
||||
var apiKey: String,
|
||||
val timestamp: Long,
|
||||
val uuid: String
|
||||
) {
|
||||
|
||||
fun encode(): String {
|
||||
return toFilename(apiKey, timestamp, uuid)
|
||||
}
|
||||
|
||||
internal companion object {
|
||||
|
||||
const val uuidLength = 36
|
||||
|
||||
/**
|
||||
* Generates a filename for the session in the format
|
||||
* "[UUID][timestamp]_v2.json"
|
||||
*/
|
||||
fun toFilename(apiKey: String, timestamp: Long, uuid: String): String {
|
||||
return "${apiKey}_${uuid}${timestamp}_v3.json"
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun defaultFilename(obj: Any?, apiKey: String): SessionFilenameInfo {
|
||||
val sanitizedApiKey = when (obj) {
|
||||
is Session -> obj.apiKey
|
||||
else -> apiKey
|
||||
}
|
||||
|
||||
return SessionFilenameInfo(
|
||||
sanitizedApiKey,
|
||||
System.currentTimeMillis(),
|
||||
UUID.randomUUID().toString()
|
||||
)
|
||||
}
|
||||
|
||||
fun fromFile(file: File, defaultApiKey: String): SessionFilenameInfo {
|
||||
return SessionFilenameInfo(
|
||||
findApiKeyInFilename(file, defaultApiKey),
|
||||
findTimestampInFilename(file),
|
||||
findUuidInFilename(file)
|
||||
)
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun findUuidInFilename(file: File): String {
|
||||
var fileName = file.name
|
||||
if (isFileV3(file)) {
|
||||
fileName = file.name.substringAfter('_')
|
||||
}
|
||||
return fileName.takeIf { it.length >= uuidLength }?.take(uuidLength) ?: ""
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun findTimestampInFilename(file: File): Long {
|
||||
var fileName = file.name
|
||||
if (isFileV3(file)) {
|
||||
fileName = file.name.substringAfter('_')
|
||||
}
|
||||
return fileName.drop(findUuidInFilename(file).length)
|
||||
.substringBefore('_')
|
||||
.toLongOrNull() ?: -1
|
||||
}
|
||||
|
||||
@JvmStatic
|
||||
fun findApiKeyInFilename(file: File?, defaultApiKey: String): String {
|
||||
if (file == null || !isFileV3(file)) {
|
||||
return defaultApiKey
|
||||
}
|
||||
return file.name.substringBefore('_').takeUnless { it.isEmpty() } ?: defaultApiKey
|
||||
}
|
||||
|
||||
internal fun isFileV3(file: File): Boolean = file.name.endsWith("_v3.json")
|
||||
}
|
||||
}
|
||||
@ -1,57 +0,0 @@
|
||||
package com.bugsnag.android
|
||||
|
||||
import com.bugsnag.android.SessionFilenameInfo.Companion.defaultFilename
|
||||
import com.bugsnag.android.SessionFilenameInfo.Companion.findTimestampInFilename
|
||||
import java.io.File
|
||||
import java.util.Calendar
|
||||
import java.util.Comparator
|
||||
import java.util.Date
|
||||
|
||||
/**
|
||||
* Store and flush Sessions which couldn't be sent immediately due to
|
||||
* lack of network connectivity.
|
||||
*/
|
||||
internal class SessionStore(
|
||||
bugsnagDir: File,
|
||||
maxPersistedSessions: Int,
|
||||
private val apiKey: String,
|
||||
logger: Logger,
|
||||
delegate: Delegate?
|
||||
) : FileStore(
|
||||
File(bugsnagDir, "sessions"),
|
||||
maxPersistedSessions,
|
||||
logger,
|
||||
delegate
|
||||
) {
|
||||
fun isTooOld(file: File?): Boolean {
|
||||
val cal = Calendar.getInstance()
|
||||
cal.add(Calendar.DATE, -60)
|
||||
return findTimestampInFilename(file!!) < cal.timeInMillis
|
||||
}
|
||||
|
||||
fun getCreationDate(file: File?): Date {
|
||||
return Date(findTimestampInFilename(file!!))
|
||||
}
|
||||
|
||||
companion object {
|
||||
val SESSION_COMPARATOR: Comparator<in File?> = Comparator { lhs, rhs ->
|
||||
if (lhs == null && rhs == null) {
|
||||
return@Comparator 0
|
||||
}
|
||||
if (lhs == null) {
|
||||
return@Comparator 1
|
||||
}
|
||||
if (rhs == null) {
|
||||
return@Comparator -1
|
||||
}
|
||||
val lhsName = lhs.name
|
||||
val rhsName = rhs.name
|
||||
lhsName.compareTo(rhsName)
|
||||
}
|
||||
}
|
||||
|
||||
override fun getFilename(obj: Any?): String {
|
||||
val sessionInfo = defaultFilename(obj, apiKey)
|
||||
return sessionInfo.encode()
|
||||
}
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Loading…
Reference in new issue