Removed Bugsnag

pull/217/head
M66B 10 months ago
parent 17809acd7d
commit 3d0596420a

@ -57,7 +57,7 @@ jobs:
- name: Check with Lint
run: ./gradlew lintGithubRelease
- name: Build with Gradle
run: ./gradlew assembleGithubRelease assembleLargeRelease assemblePlayRelease uploadBugsnagGithub-releaseMapping uploadBugsnagLarge-releaseMapping uploadBugsnagPlay-releaseMapping
run: ./gradlew assembleGithubRelease assembleLargeRelease assemblePlayRelease
#https://docs.github.com/en/actions/security-guides/using-artifact-attestations-to-establish-provenance-for-builds
- name: Generate artifact attestation
uses: actions/attest-build-provenance@v1

@ -1,5 +1,4 @@
apply plugin: 'com.android.application'
apply plugin: 'com.bugsnag.android.gradle'
apply plugin: 'kotlin-android'
apply plugin: 'de.undercouch.download'
@ -57,7 +56,7 @@ android {
// https://developer.android.com/guide/practices/page-sizes
ndkVersion "27.2.12479018" // r27c
ndk {
// Bugsnag, sqlite
// sqlite
// https://developer.android.com/ndk/guides/abis
abiFilters "armeabi-v7a", "x86", "arm64-v8a", "x86_64"
}
@ -183,7 +182,6 @@ android {
buildConfigField "String", "TX_URI", "\"\""
buildConfigField "String", "GPA_URI", "\"\""
buildConfigField "String", "INFO_URI", "\"\""
buildConfigField "String", "BUGSNAG_URI", "\"\""
buildConfigField "String", "DEV_DOMAIN", "\"\""
}
debug {
@ -194,7 +192,6 @@ android {
buildConfigField "String", "TX_URI", localProperties.getProperty("paypal.uri", "\"\"")
buildConfigField "String", "GPA_URI", localProperties.getProperty("gpa.uri", "\"\"")
buildConfigField "String", "INFO_URI", localProperties.getProperty("info.uri", "\"\"")
buildConfigField "String", "BUGSNAG_URI", localProperties.getProperty("bugsnag.uri", "\"\"")
buildConfigField "String", "DEV_DOMAIN", localProperties.getProperty("dev.domain", "\"\"")
}
}
@ -387,15 +384,6 @@ android {
// }
// }
}
bugsnag {
// https://docs.bugsnag.com/build-integrations/gradle/
uploadJvmMappings = false // disables upload of ProGuard/DexGuard/R8 mapping files
uploadNdkMappings = false // disables upload of NDK mapping files
reportBuilds = false // disables upload of build metadata
overwrite = true
builderName = "M66B"
}
}
tasks.register('copyMarkdown', Copy) {
@ -590,7 +578,6 @@ dependencies {
def minidns_version = "1.0.5"
def openpgp_version = "12.0"
def badge_version = "1.1.22"
def bugsnag_version = "6.10.0"
def biweekly_version = "0.6.8"
def vcard_version = "0.12.1"
def relinker_version = "1.4.5"
@ -783,14 +770,6 @@ dependencies {
// https://mvnrepository.com/artifact/me.leolin/ShortcutBadger
implementation "me.leolin:ShortcutBadger:$badge_version"
// https://github.com/bugsnag/bugsnag-android
// https://mvnrepository.com/artifact/com.bugsnag/bugsnag-android
implementation("com.bugsnag:bugsnag-android:$bugsnag_version") {
exclude group: "com.bugsnag", module: "bugsnag-plugin-android-anr"
exclude group: "com.bugsnag", module: "bugsnag-plugin-android-ndk"
exclude group: "com.bugsnag", module: "bugsnag-android-core"
}
// https://github.com/mangstadt/biweekly
// https://mvnrepository.com/artifact/net.sf.biweekly/biweekly
implementation("net.sf.biweekly:biweekly:$biweekly_version") {

@ -155,9 +155,6 @@
android:resource="@xml/car"
tools:node="remove" />
<!-- https://play.google.com/about/auto/developer-distribution-agreement-addendum/ -->
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="9d2d57476a0614974449a3ec33f2604a" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="unused" />

@ -164,9 +164,6 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/car" />
<!-- https://play.google.com/about/auto/developer-distribution-agreement-addendum/ -->
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="9d2d57476a0614974449a3ec33f2604a" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="unused" />

@ -162,9 +162,6 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/car" />
<!-- https://play.google.com/about/auto/developer-distribution-agreement-addendum/ -->
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="9d2d57476a0614974449a3ec33f2604a" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="unused" />

@ -162,9 +162,6 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/car" />
<!-- https://play.google.com/about/auto/developer-distribution-agreement-addendum/ -->
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="9d2d57476a0614974449a3ec33f2604a" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="unused" />

@ -154,12 +154,6 @@
android:name="com.google.android.gms.car.application"
android:resource="@xml/car" />
<!-- https://play.google.com/about/auto/developer-distribution-agreement-addendum/ -->
<meta-data
android:name="com.bugsnag.android.API_KEY"
android:value="9d2d57476a0614974449a3ec33f2604a" />
<meta-data
android:name="com.bugsnag.android.BUILD_UUID"
android:value="${build_uuid}" />
<meta-data
android:name="com.google.android.backup.api_key"
android:value="unused" />

@ -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…
Cancel
Save