diff --git a/.github/workflows/android.yml b/.github/workflows/android.yml index a428c9fb3b..4db4d6b686 100644 --- a/.github/workflows/android.yml +++ b/.github/workflows/android.yml @@ -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 diff --git a/app/build.gradle b/app/build.gradle index 14ac2c547a..473a0202ce 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -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") { diff --git a/app/src/amazon/AndroidManifest.xml b/app/src/amazon/AndroidManifest.xml index e633fcc237..070c14acd4 100644 --- a/app/src/amazon/AndroidManifest.xml +++ b/app/src/amazon/AndroidManifest.xml @@ -155,9 +155,6 @@ android:resource="@xml/car" tools:node="remove" /> - diff --git a/app/src/debug/AndroidManifest.xml b/app/src/debug/AndroidManifest.xml index e572b1d8d3..649a0731b4 100644 --- a/app/src/debug/AndroidManifest.xml +++ b/app/src/debug/AndroidManifest.xml @@ -164,9 +164,6 @@ android:name="com.google.android.gms.car.application" android:resource="@xml/car" /> - diff --git a/app/src/fdroid/AndroidManifest.xml b/app/src/fdroid/AndroidManifest.xml index ce33ddb1b8..9ae59140b3 100644 --- a/app/src/fdroid/AndroidManifest.xml +++ b/app/src/fdroid/AndroidManifest.xml @@ -162,9 +162,6 @@ android:name="com.google.android.gms.car.application" android:resource="@xml/car" /> - diff --git a/app/src/github/AndroidManifest.xml b/app/src/github/AndroidManifest.xml index dbce272e0f..3d3d329711 100644 --- a/app/src/github/AndroidManifest.xml +++ b/app/src/github/AndroidManifest.xml @@ -162,9 +162,6 @@ android:name="com.google.android.gms.car.application" android:resource="@xml/car" /> - diff --git a/app/src/main/AndroidManifest.xml b/app/src/main/AndroidManifest.xml index f8c3eb74cc..542bded093 100644 --- a/app/src/main/AndroidManifest.xml +++ b/app/src/main/AndroidManifest.xml @@ -154,12 +154,6 @@ android:name="com.google.android.gms.car.application" android:resource="@xml/car" /> - - diff --git a/app/src/main/java/com/bugsnag/android/ActivityBreadcrumbCollector.kt b/app/src/main/java/com/bugsnag/android/ActivityBreadcrumbCollector.kt deleted file mode 100644 index 0b031a729c..0000000000 --- a/app/src/main/java/com/bugsnag/android/ActivityBreadcrumbCollector.kt +++ /dev/null @@ -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) -> Unit -) : Application.ActivityLifecycleCallbacks { - - private val prevState = WeakHashMap() - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - leaveBreadcrumb( - activity, - "onCreate()", - mutableMapOf().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 = 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.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 - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/App.kt b/app/src/main/java/com/bugsnag/android/App.kt deleted file mode 100644 index 78c9a3d5ab..0000000000 --- a/app/src/main/java/com/bugsnag/android/App.kt +++ /dev/null @@ -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?, - - /** - * 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? = 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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt deleted file mode 100644 index a74579eea9..0000000000 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ /dev/null @@ -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 { - val map = HashMap() - 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) { - 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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/AppWithState.kt b/app/src/main/java/com/bugsnag/android/AppWithState.kt deleted file mode 100644 index b087a2d8bd..0000000000 --- a/app/src/main/java/com/bugsnag/android/AppWithState.kt +++ /dev/null @@ -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?, - 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) - } -} diff --git a/app/src/main/java/com/bugsnag/android/BaseObservable.kt b/app/src/main/java/com/bugsnag/android/BaseObservable.kt deleted file mode 100644 index d9e8e36185..0000000000 --- a/app/src/main/java/com/bugsnag/android/BaseObservable.kt +++ /dev/null @@ -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() - - /** - * 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 } -} diff --git a/app/src/main/java/com/bugsnag/android/Breadcrumb.java b/app/src/main/java/com/bugsnag/android/Breadcrumb.java deleted file mode 100644 index 7e6114e464..0000000000 --- a/app/src/main/java/com/bugsnag/android/Breadcrumb.java +++ /dev/null @@ -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 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 metadata) { - impl.metadata = metadata; - } - - /** - * Gets diagnostic data relating to the breadcrumb - */ - @Nullable - public Map 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); - } -} diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt deleted file mode 100644 index 14d6730f4b..0000000000 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbInternal.kt +++ /dev/null @@ -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?, - @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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt deleted file mode 100644 index 20192ebea9..0000000000 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbState.kt +++ /dev/null @@ -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(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 { - 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(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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt b/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt deleted file mode 100644 index 10ef85de12..0000000000 --- a/app/src/main/java/com/bugsnag/android/BreadcrumbType.kt +++ /dev/null @@ -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 } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Bugsnag.java b/app/src/main/java/com/bugsnag/android/Bugsnag.java deleted file mode 100644 index 11538b580d..0000000000 --- a/app/src/main/java/com/bugsnag/android/Bugsnag.java +++ /dev/null @@ -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: - *

- * 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 this - */ - @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 this - * @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 this - * @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 start methods have been has been called and - * so Bugsnag is initialized; false if start 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. - *

- * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - @Nullable - public static String getContext() { - return getClient().getContext(); - } - - /** - * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts - * represent what was happening in your application at the time an error occurs. - *

- * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - public static void setContext(@Nullable final String context) { - getClient().setContext(context); - } - - /** - * Sets the user associated with the event. - */ - public static void setUser(@Nullable final String id, - @Nullable final String email, - @Nullable final String name) { - getClient().setUser(id, email, name); - } - - /** - * Returns the currently set User information. - */ - @NonNull - public static User getUser() { - return getClient().getUser(); - } - - /** - * Add a "on error" callback, to execute code at the point where an error report is - * captured in Bugsnag. - *

- * You can use this to add or modify information attached to an Event - * before it is sent to your dashboard. You can also return - * false from any callback to prevent delivery. "on error" - * callbacks do not run before reports generated in the event - * of immediate app termination from crashes in C/C++ code. - *

- * For example: - *

- * Bugsnag.addOnError(new OnErrorCallback() { - * public boolean run(Event event) { - * event.setSeverity(Severity.INFO); - * return true; - * } - * }) - * - * @param onError a callback to run before sending errors to Bugsnag - * @see OnErrorCallback - */ - public static void addOnError(@NonNull OnErrorCallback onError) { - getClient().addOnError(onError); - } - - /** - * Removes a previously added "on error" callback - * - * @param onError the callback to remove - */ - public static void removeOnError(@NonNull OnErrorCallback onError) { - getClient().removeOnError(onError); - } - - /** - * Add an "on breadcrumb" callback, to execute code before every - * breadcrumb captured by Bugsnag. - *

- * You can use this to modify breadcrumbs before they are stored by Bugsnag. - * You can also return false from any callback to ignore a breadcrumb. - *

- * For example: - *

- * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { - * public boolean run(Breadcrumb breadcrumb) { - * return false; // ignore the breadcrumb - * } - * }) - * - * @param onBreadcrumb a callback to run before a breadcrumb is captured - * @see OnBreadcrumbCallback - */ - public static void addOnBreadcrumb(@NonNull final OnBreadcrumbCallback onBreadcrumb) { - getClient().addOnBreadcrumb(onBreadcrumb); - } - - /** - * Removes a previously added "on breadcrumb" callback - * - * @param onBreadcrumb the callback to remove - */ - public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { - getClient().removeOnBreadcrumb(onBreadcrumb); - } - - /** - * Add an "on session" callback, to execute code before every - * session captured by Bugsnag. - *

- * You can use this to modify sessions before they are stored by Bugsnag. - * You can also return false from any callback to ignore a session. - *

- * For example: - *

- * Bugsnag.onSession(new OnSessionCallback() { - * public boolean run(Session session) { - * return false; // ignore the session - * } - * }) - * - * @param onSession a callback to run before a session is captured - * @see OnSessionCallback - */ - public static void addOnSession(@NonNull OnSessionCallback onSession) { - getClient().addOnSession(onSession); - } - - /** - * Removes a previously added "on session" callback - * - * @param onSession the callback to remove - */ - public static void removeOnSession(@NonNull OnSessionCallback onSession) { - getClient().removeOnSession(onSession); - } - - /** - * Notify Bugsnag of a handled exception - * - * @param exception the exception to send to Bugsnag - */ - public static void notify(@NonNull final Throwable exception) { - getClient().notify(exception); - } - - /** - * Notify Bugsnag of a handled exception - * - * @param exception the exception to send to Bugsnag - * @param onError callback invoked on the generated error report for - * additional modification - */ - public static void notify(@NonNull final Throwable exception, - @Nullable final OnErrorCallback onError) { - getClient().notify(exception, onError); - } - - /** - * Adds a map of multiple metadata key-value pairs to the specified section. - */ - public static void addMetadata(@NonNull String section, @NonNull Map 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 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 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. - *

- * 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 - * - * stability score. 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. - *

- * 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. - *

- * 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 - * - * stability score. - * - * @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. - *

- * 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 - * - * stability score. This can be advantageous if, for example, you do not wish the - * stability score to include crashes in a background service. - * - * @see #startSession() - * @see #resumeSession() - * @see Configuration#setAutoTrackSessions(boolean) - */ - public static void pauseSession() { - getClient().pauseSession(); - } - - /** - * Returns the current buffer of breadcrumbs that will be sent with captured events. This - * ordered list represents the most recent breadcrumbs to be captured up to the limit - * set in {@link Configuration#getMaxBreadcrumbs()}. - *

- * The returned collection is readonly and mutating the list will cause no effect on the - * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should - * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and - * {@link Configuration#addOnBreadcrumb(OnBreadcrumbCallback)} instead. - * - * @return a list of collected breadcrumbs - */ - @NonNull - public static List getBreadcrumbs() { - return getClient().getBreadcrumbs(); - } - - /** - * Retrieves information about the last launch of the application, if it has been run before. - *

- * For example, this allows checking whether the app crashed on its last launch, which could - * be used to perform conditional behaviour to recover from crashes, such as clearing the - * app data cache. - */ - @Nullable - public static LastRunInfo getLastRunInfo() { - return getClient().getLastRunInfo(); - } - - /** - * Informs Bugsnag that the application has finished launching. Once this has been called - * {@link AppWithState#isLaunching()} will always be false in any new error reports, - * and synchronous delivery will not be attempted on the next launch for any fatal crashes. - *

- * By default this method will be called after Bugsnag is initialized when - * {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually - * has precedence over the value supplied via the launchDurationMillis configuration option. - */ - public static void markLaunchCompleted() { - getClient().markLaunchCompleted(); - } - - /** - * 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 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; - } -} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt deleted file mode 100644 index 4fae3a1fe4..0000000000 --- a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ /dev/null @@ -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, apiKey: String): Event { - return Event(convertToEventImpl(map, apiKey), logger) - } - - @Suppress("UNCHECKED_CAST") - internal fun convertToEventImpl(map: Map, 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> - exceptions?.mapTo(event.errors) { Error(convertErrorInternal(it), this.logger) } - - // populate user - event.userImpl = convertUser(map.readEntry("user")) - - // populate metadata - val metadataMap: Map> = - (map["metaData"] as? Map>).orEmpty() - metadataMap.forEach { (key, value) -> - event.addMetadata(key, value) - } - - val featureFlagsList: List> = - (map["featureFlags"] as? List>).orEmpty() - featureFlagsList.forEach { featureFlagMap -> - event.addFeatureFlag( - featureFlagMap.readEntry("featureFlag"), - featureFlagMap["variant"] as? String - ) - } - - // populate breadcrumbs - val breadcrumbList: List> = - (map["breadcrumbs"] as? List>).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 - sessionMap?.let { - event.session = Session(it, logger, apiKey) - } - - // populate threads - val threads = map["threads"] as? List> - threads?.mapTo(event.threads) { Thread(convertThread(it), logger) } - - // populate projectPackages - val projectPackages = map["projectPackages"] as? List - 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?) - - // populate correlation - (map["correlation"] as? Map)?.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): Error { - return Error(convertErrorInternal(error), logger) - } - - internal fun convertErrorInternal(error: Map): ErrorInternal { - return ErrorInternal( - error.readEntry("errorClass"), - error["message"] as? String, - type = error.readEntry("type").let { type -> - ErrorType.fromDescriptor(type) - ?: throw IllegalArgumentException("unknown ErrorType: '$type'") - }, - stacktrace = convertStacktrace(error.readEntry("stacktrace")) - ) - } - - internal fun convertUser(user: Map): User { - return User( - user["id"] as? String, - user["email"] as? String, - user["name"] as? String - ) - } - - @Suppress("UNCHECKED_CAST") - internal fun convertBreadcrumbInternal(breadcrumb: Map): BreadcrumbInternal { - return BreadcrumbInternal( - breadcrumb.readEntry("name"), - breadcrumb.readEntry("type").let { type -> - BreadcrumbType.fromDescriptor(type) - ?: BreadcrumbType.MANUAL - }, - breadcrumb["metaData"] as? MutableMap, - breadcrumb.readEntry("timestamp").toDate() - ) - } - - internal fun convertAppWithState(app: Map): 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): 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)?.toTypedArray() - ), - device["jailbroken"] as? Boolean, - device["id"] as? String, - device["locale"] as? String, - (device["totalMemory"] as? Number)?.toLong(), - (device["runtimeVersions"] as? Map)?.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): 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>)?.let { convertStacktrace(it) } - ?: Stacktrace(mutableListOf()) - ) - } - - internal fun convertStacktrace(trace: List>): Stacktrace { - return Stacktrace(trace.mapTo(ArrayList(trace.size)) { Stackframe(it) }) - } - - internal fun deserializeSeverityReason( - map: Map, - unhandled: Boolean, - severity: Severity? - ): SeverityReason { - val severityReason: Map = 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? = 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 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() { - override fun initialValue(): DateFormat { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt b/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt deleted file mode 100644 index 51d565a83d..0000000000 --- a/app/src/main/java/com/bugsnag/android/BugsnagStateModule.kt +++ /dev/null @@ -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()) - } -} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java b/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java deleted file mode 100644 index 22969f4b59..0000000000 --- a/app/src/main/java/com/bugsnag/android/BugsnagThreadViolationListener.java +++ /dev/null @@ -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. - *

- * 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); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java b/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java deleted file mode 100644 index 74faed3e18..0000000000 --- a/app/src/main/java/com/bugsnag/android/BugsnagVmViolationListener.java +++ /dev/null @@ -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. - *

- * 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); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/CallbackAware.kt b/app/src/main/java/com/bugsnag/android/CallbackAware.kt deleted file mode 100644 index 8dae0a395e..0000000000 --- a/app/src/main/java/com/bugsnag/android/CallbackAware.kt +++ /dev/null @@ -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) -} diff --git a/app/src/main/java/com/bugsnag/android/CallbackState.kt b/app/src/main/java/com/bugsnag/android/CallbackState.kt deleted file mode 100644 index 2625130448..0000000000 --- a/app/src/main/java/com/bugsnag/android/CallbackState.kt +++ /dev/null @@ -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 = CopyOnWriteArrayList(), - val onBreadcrumbTasks: MutableCollection = CopyOnWriteArrayList(), - val onSessionTasks: MutableCollection = CopyOnWriteArrayList(), - val onSendTasks: MutableList = 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 { - return hashMapOf().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() - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java deleted file mode 100644 index cea27385ac..0000000000 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ /dev/null @@ -1,1198 +0,0 @@ -package com.bugsnag.android; - -import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; - -import com.bugsnag.android.internal.BackgroundTaskService; -import com.bugsnag.android.internal.ForegroundDetector; -import com.bugsnag.android.internal.ImmutableConfig; -import com.bugsnag.android.internal.InternalMetrics; -import com.bugsnag.android.internal.InternalMetricsImpl; -import com.bugsnag.android.internal.InternalMetricsNoop; -import com.bugsnag.android.internal.StateObserver; -import com.bugsnag.android.internal.TaskType; -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; - -import android.app.Application; -import android.content.Context; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import kotlin.Unit; -import kotlin.jvm.functions.Function2; - -import java.io.File; -import java.util.Collection; -import java.util.Collections; -import java.util.Date; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.concurrent.Callable; -import java.util.concurrent.RejectedExecutionException; -import java.util.regex.Pattern; - -/** - * A Bugsnag Client instance allows you to use Bugsnag in your Android app. - * Typically you'd instead use the static access provided in the Bugsnag class. - *

- * Example usage: - *

- * Client client = new Client(this, "your-api-key"); - * client.notify(new RuntimeException("something broke!")); - * - * @see Bugsnag - */ -@SuppressWarnings({"checkstyle:JavadocTagContinuationIndentation", "ConstantConditions"}) -public class Client implements MetadataAware, CallbackAware, UserAware, FeatureFlagAware { - - final ImmutableConfig immutableConfig; - - final MetadataState metadataState; - final FeatureFlagState featureFlagState; - - private final InternalMetrics internalMetrics; - private final ContextState contextState; - private final CallbackState callbackState; - private final Provider userState; - private final Map configDifferences; - - final Context appContext; - - @NonNull - final DeviceDataCollector deviceDataCollector; - - @NonNull - final AppDataCollector appDataCollector; - - @NonNull - final BreadcrumbState breadcrumbState; - - @NonNull - final MemoryTrimState memoryTrimState = new MemoryTrimState(); - - @NonNull - protected final EventStore eventStore; - - final SessionTracker sessionTracker; - - final SystemBroadcastReceiver systemBroadcastReceiver; - - final Logger logger; - final Connectivity connectivity; - final DeliveryDelegate deliveryDelegate; - - final ClientObservable clientObservable; - PluginClient pluginClient; - - final Notifier notifier; - - @Nullable - final LastRunInfo lastRunInfo; - final LastRunInfoStore lastRunInfoStore; - final LaunchCrashTracker launchCrashTracker; - final BackgroundTaskService bgTaskService = new BackgroundTaskService(); - private final ExceptionHandler exceptionHandler; - - /** - * Initialize a Bugsnag client - * - * @param androidContext an Android context, usually this - */ - public Client(@NonNull Context androidContext) { - this(androidContext, Configuration.load(androidContext)); - } - - /** - * Initialize a Bugsnag client - * - * @param androidContext an Android context, usually this - * @param apiKey your Bugsnag API key from your Bugsnag dashboard - */ - public Client(@NonNull Context androidContext, @NonNull String apiKey) { - this(androidContext, Configuration.load(androidContext, apiKey)); - } - - /** - * Initialize a Bugsnag client - * - * @param androidContext an Android context, usually this - * @param configuration a configuration for the Client - */ - public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) { - ContextModule contextModule = new ContextModule(androidContext, bgTaskService); - appContext = contextModule.getCtx(); - - notifier = configuration.getNotifier(); - - connectivity = new ConnectivityCompat(appContext, new Function2() { - @Override - public Unit invoke(Boolean hasConnection, String networkState) { - Map data = new HashMap<>(); - data.put("hasConnection", hasConnection); - data.put("networkState", networkState); - leaveAutoBreadcrumb("Connectivity changed", BreadcrumbType.STATE, data); - if (hasConnection) { - eventStore.flushAsync(); - sessionTracker.flushAsync(); - } - return null; - } - }); - - // set sensible defaults for delivery/project packages etc if not set - ConfigModule configModule = new ConfigModule( - contextModule, - configuration, - connectivity, - bgTaskService - ); - - immutableConfig = configModule.getConfig(); - logger = immutableConfig.getLogger(); - - if (!(androidContext instanceof Application)) { - logger.w("You should initialize Bugsnag from the onCreate() callback of your " - + "Application subclass, as this guarantees errors are captured as early " - + "as possible. " - + "If a custom Application subclass is not possible in your app then you " - + "should suppress this warning by passing the Application context instead: " - + "Bugsnag.start(context.getApplicationContext()). " - + "For further info see: " - + "https://docs.bugsnag.com/platforms/android/#basic-configuration"); - } - - // setup storage as soon as possible - final StorageModule storageModule = new StorageModule(appContext, - immutableConfig, bgTaskService); - - // setup state trackers for bugsnag - BugsnagStateModule bugsnagStateModule = - new BugsnagStateModule(immutableConfig, configuration); - clientObservable = bugsnagStateModule.getClientObservable(); - callbackState = bugsnagStateModule.getCallbackState(); - breadcrumbState = bugsnagStateModule.getBreadcrumbState(); - contextState = bugsnagStateModule.getContextState(); - metadataState = bugsnagStateModule.getMetadataState(); - featureFlagState = bugsnagStateModule.getFeatureFlagState(); - - // lookup system services - final SystemServiceModule systemServiceModule = - new SystemServiceModule(contextModule, bgTaskService); - - // setup further state trackers and data collection - TrackerModule trackerModule = new TrackerModule(configModule, - storageModule, this, bgTaskService, callbackState); - - DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, - configModule, systemServiceModule, trackerModule, - bgTaskService, connectivity, storageModule.getDeviceIdStore(), - memoryTrimState); - - // load the device + user information - userState = storageModule.loadUser(configuration.getUser()); - - EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, - dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier, - callbackState); - - eventStore = eventStorageModule.getEventStore().get(); - - deliveryDelegate = new DeliveryDelegate(logger, eventStore, - immutableConfig, callbackState, notifier, bgTaskService); - - exceptionHandler = new ExceptionHandler(this, logger); - - // load last run info - lastRunInfoStore = storageModule.getLastRunInfoStore().getOrNull(); - lastRunInfo = storageModule.getLastRunInfo().getOrNull(); - - launchCrashTracker = trackerModule.getLaunchCrashTracker(); - sessionTracker = trackerModule.getSessionTracker().get(); - appDataCollector = dataCollectionModule.getAppDataCollector().get(); - deviceDataCollector = dataCollectionModule.getDeviceDataCollector().get(); - - Set userPlugins = configuration.getPlugins(); - pluginClient = new PluginClient(userPlugins, immutableConfig, logger); - - if (configuration.getTelemetry().contains(Telemetry.USAGE)) { - internalMetrics = new InternalMetricsImpl(); - } else { - internalMetrics = new InternalMetricsNoop(); - } - - configDifferences = configuration.impl.getConfigDifferences(); - systemBroadcastReceiver = new SystemBroadcastReceiver(this, logger); - - start(); - } - - @VisibleForTesting - Client( - ImmutableConfig immutableConfig, - MetadataState metadataState, - ContextState contextState, - CallbackState callbackState, - Provider userState, - FeatureFlagState featureFlagState, - ClientObservable clientObservable, - Context appContext, - @NonNull DeviceDataCollector deviceDataCollector, - @NonNull AppDataCollector appDataCollector, - @NonNull BreadcrumbState breadcrumbState, - @NonNull EventStore eventStore, - SystemBroadcastReceiver systemBroadcastReceiver, - SessionTracker sessionTracker, - Connectivity connectivity, - Logger logger, - DeliveryDelegate deliveryDelegate, - LastRunInfoStore lastRunInfoStore, - LaunchCrashTracker launchCrashTracker, - ExceptionHandler exceptionHandler, - Notifier notifier - ) { - this.immutableConfig = immutableConfig; - this.metadataState = metadataState; - this.contextState = contextState; - this.callbackState = callbackState; - this.userState = userState; - this.featureFlagState = featureFlagState; - this.clientObservable = clientObservable; - this.appContext = appContext; - this.deviceDataCollector = deviceDataCollector; - this.appDataCollector = appDataCollector; - this.breadcrumbState = breadcrumbState; - this.eventStore = eventStore; - this.systemBroadcastReceiver = systemBroadcastReceiver; - this.sessionTracker = sessionTracker; - this.connectivity = connectivity; - this.logger = logger; - this.deliveryDelegate = deliveryDelegate; - this.lastRunInfoStore = lastRunInfoStore; - this.launchCrashTracker = launchCrashTracker; - this.lastRunInfo = null; - this.exceptionHandler = exceptionHandler; - this.notifier = notifier; - internalMetrics = new InternalMetricsNoop(); - configDifferences = new HashMap<>(); - } - - private void start() { - if (immutableConfig.getEnabledErrorTypes().getUnhandledExceptions()) { - exceptionHandler.install(); - } - - // Initialise plugins before attempting anything else - NativeInterface.setClient(Client.this); - pluginClient.loadPlugins(Client.this); - NdkPluginCaller.INSTANCE.setNdkPlugin(pluginClient.getNdkPlugin()); - if (immutableConfig.getTelemetry().contains(Telemetry.USAGE)) { - NdkPluginCaller.INSTANCE.setInternalMetricsEnabled(true); - } - - // Flush any on-disk errors and sessions - eventStore.flushOnLaunch(); - eventStore.flushAsync(); - sessionTracker.flushAsync(); - - // These call into NdkPluginCaller to sync with the native side, so they must happen later - internalMetrics.setConfigDifferences(configDifferences); - callbackState.setInternalMetrics(internalMetrics); - - // Register listeners for system events in the background - registerLifecycleCallbacks(); - registerComponentCallbacks(); - registerListenersInBackground(); - - // Leave auto breadcrumb - Map data = new HashMap<>(); - leaveAutoBreadcrumb("Bugsnag loaded", BreadcrumbType.STATE, data); - - logger.d("Bugsnag loaded"); - } - - void registerLifecycleCallbacks() { - if (appContext instanceof Application) { - Application application = (Application) appContext; - ForegroundDetector.registerOn(application); - ForegroundDetector.registerActivityCallbacks(sessionTracker); - - if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { - ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector( - new Function2, Unit>() { - @SuppressWarnings("unchecked") - @Override - public Unit invoke(String activity, Map metadata) { - leaveBreadcrumb(activity, (Map) metadata, - BreadcrumbType.STATE); - return null; - } - } - ); - application.registerActivityLifecycleCallbacks(activityCb); - } - } - } - - /** - * Registers listeners for system events in the background. This offloads work from the main - * thread that collects useful information from callbacks, but that don't need to be done - * immediately on client construction. - */ - void registerListenersInBackground() { - try { - bgTaskService.submitTask(TaskType.DEFAULT, new Runnable() { - @Override - public void run() { - connectivity.registerForNetworkChanges(); - SystemBroadcastReceiver.register(appContext, systemBroadcastReceiver, logger); - } - }); - } catch (RejectedExecutionException ex) { - logger.w("Failed to register for system events", ex); - } - } - - - /** - * Load information about the last run, and reset the persisted information to the defaults. - */ - private void persistRunInfo(final LastRunInfo runInfo) { - try { - bgTaskService.submitTask(TaskType.IO, new Runnable() { - @Override - public void run() { - lastRunInfoStore.persist(runInfo); - } - }); - } catch (RejectedExecutionException exc) { - logger.w("Failed to persist last run info", exc); - } - } - - private void logNull(String property) { - logger.e("Invalid null value supplied to client." + property + ", ignoring"); - } - - private void registerComponentCallbacks() { - appContext.registerComponentCallbacks(new ClientComponentCallbacks( - deviceDataCollector, - new Function2() { - @Override - public Unit invoke(String oldOrientation, String newOrientation) { - Map data = new HashMap<>(); - data.put("from", oldOrientation); - data.put("to", newOrientation); - leaveAutoBreadcrumb("Orientation changed", BreadcrumbType.STATE, data); - clientObservable.postOrientationChange(newOrientation); - return null; - } - }, new Function2() { - @Override - public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { - memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); - if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { - leaveAutoBreadcrumb( - "Trim Memory", - BreadcrumbType.STATE, - Collections.singletonMap( - "trimLevel", memoryTrimState.getTrimLevelDescription() - ) - ); - } - - memoryTrimState.emitObservableEvent(); - return null; - } - } - )); - } - - void setupNdkPlugin() { - if (!setupNdkDirectory()) { - logger.w("Failed to setup NDK directory."); - return; - } - - String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath(); - int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0; - clientObservable.postNdkInstall(immutableConfig, lastRunInfoPath, crashes); - syncInitialState(); - clientObservable.postNdkDeliverPending(); - } - - private boolean setupNdkDirectory() { - try { - return bgTaskService.submitTask(TaskType.IO, new Callable() { - @Override - public Boolean call() { - File outFile = NativeInterface.getNativeReportPath(); - return outFile.exists() || outFile.mkdirs(); - } - }).get(); - } catch (Throwable exc) { - return false; - } - } - - void addObserver(StateObserver observer) { - metadataState.addObserver(observer); - breadcrumbState.addObserver(observer); - sessionTracker.addObserver(observer); - clientObservable.addObserver(observer); - userState.get().addObserver(observer); - contextState.addObserver(observer); - deliveryDelegate.addObserver(observer); - launchCrashTracker.addObserver(observer); - memoryTrimState.addObserver(observer); - featureFlagState.addObserver(observer); - } - - void removeObserver(StateObserver observer) { - metadataState.removeObserver(observer); - breadcrumbState.removeObserver(observer); - sessionTracker.removeObserver(observer); - clientObservable.removeObserver(observer); - userState.get().removeObserver(observer); - contextState.removeObserver(observer); - deliveryDelegate.removeObserver(observer); - launchCrashTracker.removeObserver(observer); - memoryTrimState.removeObserver(observer); - featureFlagState.removeObserver(observer); - } - - /** - * Sends initial state values for Metadata/User/Context to any registered observers. - */ - void syncInitialState() { - metadataState.emitObservableEvent(); - contextState.emitObservableEvent(); - userState.get().emitObservableEvent(); - memoryTrimState.emitObservableEvent(); - featureFlagState.emitObservableEvent(); - } - - /** - * Starts tracking a new session. You should disable automatic session tracking via - * {@link Configuration#setAutoTrackSessions(boolean)} if you call this method. - *

- * 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 - * - * stability score. 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 void startSession() { - sessionTracker.startSession(false); - } - - /** - * Pauses tracking of a session. You should disable automatic session tracking via - * {@link Configuration#setAutoTrackSessions(boolean)} if you call this method. - *

- * 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 - * - * stability score. 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 void pauseSession() { - sessionTracker.pauseSession(); - } - - /** - * 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. - *

- * 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. - *

- * 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 - * - * stability score. - * - * @return true if a previous session was resumed, false if a new session was started. - * @see #startSession() - * @see #pauseSession() - * @see Configuration#setAutoTrackSessions(boolean) - */ - public boolean resumeSession() { - return sessionTracker.resumeSession(); - } - - /** - * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts - * represent what was happening in your application at the time an error occurs. - * - * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - @Nullable - public String getContext() { - return contextState.getContext(); - } - - /** - * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts - * represent what was happening in your application at the time an error occurs. - * - * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - public void setContext(@Nullable String context) { - contextState.setManualContext(context); - } - - /** - * Sets the user associated with the event. - */ - @Override - public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) { - userState.get().setUser(new User(id, email, name)); - } - - /** - * Returns the currently set User information. - */ - @NonNull - @Override - public User getUser() { - return userState.get().getUser(); - } - - /** - * Add a "on error" callback, to execute code at the point where an error report is - * captured in Bugsnag. - * - * You can use this to add or modify information attached to an Event - * before it is sent to your dashboard. You can also return - * false from any callback to prevent delivery. "on error" - * callbacks do not run before reports generated in the event - * of immediate app termination from crashes in C/C++ code. - * - * For example: - * - * Bugsnag.addOnError(new OnErrorCallback() { - * public boolean run(Event event) { - * event.setSeverity(Severity.INFO); - * return true; - * } - * }) - * - * @param onError a callback to run before sending errors to Bugsnag - * @see OnErrorCallback - */ - @Override - public void addOnError(@NonNull OnErrorCallback onError) { - if (onError != null) { - callbackState.addOnError(onError); - } else { - logNull("addOnError"); - } - } - - /** - * Removes a previously added "on error" callback - * - * @param onError the callback to remove - */ - @Override - public void removeOnError(@NonNull OnErrorCallback onError) { - if (onError != null) { - callbackState.removeOnError(onError); - } else { - logNull("removeOnError"); - } - } - - /** - * Add an "on breadcrumb" callback, to execute code before every - * breadcrumb captured by Bugsnag. - * - * You can use this to modify breadcrumbs before they are stored by Bugsnag. - * You can also return false from any callback to ignore a breadcrumb. - * - * For example: - * - * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { - * public boolean run(Breadcrumb breadcrumb) { - * return false; // ignore the breadcrumb - * } - * }) - * - * @param onBreadcrumb a callback to run before a breadcrumb is captured - * @see OnBreadcrumbCallback - */ - @Override - public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { - if (onBreadcrumb != null) { - callbackState.addOnBreadcrumb(onBreadcrumb); - } else { - logNull("addOnBreadcrumb"); - } - } - - /** - * Removes a previously added "on breadcrumb" callback - * - * @param onBreadcrumb the callback to remove - */ - @Override - public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { - if (onBreadcrumb != null) { - callbackState.removeOnBreadcrumb(onBreadcrumb); - } else { - logNull("removeOnBreadcrumb"); - } - } - - /** - * Add an "on session" callback, to execute code before every - * session captured by Bugsnag. - * - * You can use this to modify sessions before they are stored by Bugsnag. - * You can also return false from any callback to ignore a session. - * - * For example: - * - * Bugsnag.onSession(new OnSessionCallback() { - * public boolean run(Session session) { - * return false; // ignore the session - * } - * }) - * - * @param onSession a callback to run before a session is captured - * @see OnSessionCallback - */ - @Override - public void addOnSession(@NonNull OnSessionCallback onSession) { - if (onSession != null) { - callbackState.addOnSession(onSession); - } else { - logNull("addOnSession"); - } - } - - /** - * Removes a previously added "on session" callback - * - * @param onSession the callback to remove - */ - @Override - public void removeOnSession(@NonNull OnSessionCallback onSession) { - if (onSession != null) { - callbackState.removeOnSession(onSession); - } else { - logNull("removeOnSession"); - } - } - - /** - * Notify Bugsnag of a handled exception - * - * @param exception the exception to send to Bugsnag - */ - public void notify(@NonNull Throwable exception) { - notify(exception, null); - } - - /** - * Notify Bugsnag of a handled exception - * - * @param exc the exception to send to Bugsnag - * @param onError callback invoked on the generated error report for - * additional modification - */ - public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { - if (exc != null) { - if (immutableConfig.shouldDiscardError(exc)) { - return; - } - SeverityReason severityReason = SeverityReason.newInstance(REASON_HANDLED_EXCEPTION); - Metadata metadata = metadataState.getMetadata(); - FeatureFlags featureFlags = featureFlagState.getFeatureFlags(); - Event event = new Event(exc, immutableConfig, severityReason, metadata, featureFlags, - logger); - populateAndNotifyAndroidEvent(event, onError); - } else { - logNull("notify"); - } - } - - /** - * Caches an error then attempts to notify. - * - * Should only ever be called from the {@link ExceptionHandler}. - */ - void notifyUnhandledException(@NonNull Throwable exc, Metadata metadata, - @SeverityReason.SeverityReasonType String severityReason, - @Nullable String attributeValue) { - SeverityReason handledState - = SeverityReason.newInstance(severityReason, Severity.ERROR, attributeValue); - Metadata data = Metadata.Companion.merge(metadataState.getMetadata(), metadata); - Event event = new Event(exc, immutableConfig, handledState, - data, featureFlagState.getFeatureFlags(), logger); - populateAndNotifyAndroidEvent(event, null); - - // persist LastRunInfo so that on relaunch users can check the app crashed - int consecutiveLaunchCrashes = lastRunInfo == null ? 0 - : lastRunInfo.getConsecutiveLaunchCrashes(); - boolean launching = launchCrashTracker.isLaunching(); - if (launching) { - consecutiveLaunchCrashes += 1; - } - LastRunInfo runInfo = new LastRunInfo(consecutiveLaunchCrashes, true, launching); - persistRunInfo(runInfo); - - // suspend execution of any further background tasks, waiting for previously - // submitted ones to complete. - bgTaskService.shutdown(); - } - - void populateAndNotifyAndroidEvent(@NonNull Event event, - @Nullable OnErrorCallback onError) { - // Capture the state of the app and device and attach diagnostics to the event - event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime())); - event.addMetadata("device", deviceDataCollector.getDeviceMetadata()); - - // add additional info that belongs in metadata - // generate new object each time, as this can be mutated by end-users - event.setApp(appDataCollector.generateAppWithState()); - event.addMetadata("app", appDataCollector.getAppDataMetadata()); - - // Attach breadcrumbState to the event - event.setBreadcrumbs(breadcrumbState.copy()); - - // Attach user info to the event - User user = userState.get().getUser(); - event.setUser(user.getId(), user.getEmail(), user.getName()); - - // Attach context to the event - event.setContext(contextState.getContext()); - - event.setInternalMetrics(internalMetrics); - - notifyInternal(event, onError); - } - - void notifyInternal(@NonNull Event event, - @Nullable OnErrorCallback onError) { - // set the redacted keys on the event as this - // will not have been set for RN/Unity events - Collection redactedKeys = metadataState.getMetadata().getRedactedKeys(); - event.setRedactedKeys(redactedKeys); - - // get session for event - Session currentSession = sessionTracker.getCurrentSession(); - - if (currentSession != null - && (immutableConfig.getAutoTrackSessions() || !currentSession.isAutoCaptured())) { - event.setSession(currentSession); - } - - // Run on error tasks, don't notify if any return false - if (!callbackState.runOnErrorTasks(event, logger) - || (onError != null - && !onError.onError(event))) { - logger.d("Skipping notification - onError task returned false"); - return; - } - - // leave an error breadcrumb of this event - for the next event - leaveErrorBreadcrumb(event); - - deliveryDelegate.deliver(event); - } - - /** - * Returns the current buffer of breadcrumbs that will be sent with captured events. This - * ordered list represents the most recent breadcrumbs to be captured up to the limit - * set in {@link Configuration#getMaxBreadcrumbs()}. - * - * The returned collection is readonly and mutating the list will cause no effect on the - * Client's state. If you wish to alter the breadcrumbs collected by the Client then you should - * use {@link Configuration#setEnabledBreadcrumbTypes(Set)} and - * {@link Configuration#addOnBreadcrumb(OnBreadcrumbCallback)} instead. - * - * @return a list of collected breadcrumbs - */ - @NonNull - public List getBreadcrumbs() { - return breadcrumbState.copy(); - } - - @NonNull - AppDataCollector getAppDataCollector() { - return appDataCollector; - } - - @NonNull - DeviceDataCollector getDeviceDataCollector() { - return deviceDataCollector; - } - - /** - * Adds a map of multiple metadata key-value pairs to the specified section. - */ - @Override - public void addMetadata(@NonNull String section, @NonNull Map value) { - if (section != null && value != null) { - metadataState.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) { - metadataState.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) { - metadataState.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) { - metadataState.clearMetadata(section, key); - } else { - logNull("clearMetadata"); - } - } - - /** - * Returns a map of data in the specified section. - */ - @Nullable - @Override - public Map getMetadata(@NonNull String section) { - if (section != null) { - return metadataState.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 metadataState.getMetadata(section, key); - } else { - logNull("getMetadata"); - return null; - } - } - - // cast map to retain original signature until next major version bump, as this - // method signature is used by Unity/React native - @NonNull - @SuppressWarnings({"unchecked", "rawtypes"}) - Map getMetadata() { - return (Map) metadataState.getMetadata().toMap(); - } - - /** - * 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 void leaveBreadcrumb(@NonNull String message) { - if (message != null) { - breadcrumbState.add(new Breadcrumb(message, logger)); - } else { - logNull("leaveBreadcrumb"); - } - } - - /** - * 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 void leaveBreadcrumb(@NonNull String message, - @NonNull Map metadata, - @NonNull BreadcrumbType type) { - if (message != null && type != null && metadata != null) { - breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); - } else { - logNull("leaveBreadcrumb"); - } - } - - /** - * Intended for internal use only - leaves a breadcrumb if the type is enabled for automatic - * breadcrumbs. - * - * @param message A short label - * @param type A category for the breadcrumb - * @param metadata Additional diagnostic information about the app environment - */ - void leaveAutoBreadcrumb(@NonNull String message, - @NonNull BreadcrumbType type, - @NonNull Map metadata) { - if (!immutableConfig.shouldDiscardBreadcrumb(type)) { - breadcrumbState.add(new Breadcrumb(message, type, metadata, new Date(), logger)); - } - } - - private void leaveErrorBreadcrumb(@NonNull Event event) { - // Add a breadcrumb for this event occurring - List errors = event.getErrors(); - - if (errors.size() > 0) { - String errorClass = errors.get(0).getErrorClass(); - String message = errors.get(0).getErrorMessage(); - - Map data = new HashMap<>(); - data.put("errorClass", errorClass); - data.put("message", message); - data.put("unhandled", String.valueOf(event.isUnhandled())); - data.put("severity", event.getSeverity().toString()); - breadcrumbState.add(new Breadcrumb(errorClass, - BreadcrumbType.ERROR, data, new Date(), logger)); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void addFeatureFlag(@NonNull String name) { - if (name != null) { - featureFlagState.addFeatureFlag(name); - } else { - logNull("addFeatureFlag"); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void addFeatureFlag(@NonNull String name, @Nullable String variant) { - if (name != null) { - featureFlagState.addFeatureFlag(name, variant); - } else { - logNull("addFeatureFlag"); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void addFeatureFlags(@NonNull Iterable featureFlags) { - if (featureFlags != null) { - featureFlagState.addFeatureFlags(featureFlags); - } else { - logNull("addFeatureFlags"); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void clearFeatureFlag(@NonNull String name) { - if (name != null) { - featureFlagState.clearFeatureFlag(name); - } else { - logNull("clearFeatureFlag"); - } - } - - /** - * {@inheritDoc} - */ - @Override - public void clearFeatureFlags() { - featureFlagState.clearFeatureFlags(); - } - - /** - * Retrieves information about the last launch of the application, if it has been run before. - * - * For example, this allows checking whether the app crashed on its last launch, which could - * be used to perform conditional behaviour to recover from crashes, such as clearing the - * app data cache. - */ - @Nullable - public LastRunInfo getLastRunInfo() { - return lastRunInfo; - } - - /** - * Informs Bugsnag that the application has finished launching. Once this has been called - * {@link AppWithState#isLaunching()} will always be false in any new error reports, - * and synchronous delivery will not be attempted on the next launch for any fatal crashes. - * - * By default this method will be called after Bugsnag is initialized when - * {@link Configuration#getLaunchDurationMillis()} has elapsed. Invoking this method manually - * has precedence over the value supplied via the launchDurationMillis configuration option. - */ - public void markLaunchCompleted() { - launchCrashTracker.markLaunchCompleted(); - } - - SessionTracker getSessionTracker() { - return sessionTracker; - } - - @NonNull - EventStore getEventStore() { - return eventStore; - } - - /** - * Finalize by removing the receiver - * - * @throws Throwable if something goes wrong - */ - @SuppressWarnings("checkstyle:NoFinalizer") - protected void finalize() throws Throwable { - if (systemBroadcastReceiver != null) { - try { - ContextExtensionsKt.unregisterReceiverSafe(appContext, - systemBroadcastReceiver, logger); - } catch (IllegalArgumentException exception) { - logger.w("Receiver not registered"); - } - } - super.finalize(); - } - - ImmutableConfig getConfig() { - return immutableConfig; - } - - void setBinaryArch(String binaryArch) { - getAppDataCollector().setBinaryArch(binaryArch); - } - - Context getAppContext() { - return appContext; - } - - /** - * Intended for internal use only - sets the code bundle id for React Native - */ - @Nullable - String getCodeBundleId() { - return appDataCollector.getCodeBundleId(); - } - - /** - * Intended for internal use only - sets the code bundle id for React Native - */ - void setCodeBundleId(@Nullable String codeBundleId) { - appDataCollector.setCodeBundleId(codeBundleId); - } - - void addRuntimeVersionInfo(@NonNull String key, @NonNull String value) { - deviceDataCollector.addRuntimeVersionInfo(key, value); - } - - @VisibleForTesting - void close() { - connectivity.unregisterForNetworkChanges(); - bgTaskService.shutdown(); - } - - Logger getLogger() { - return logger; - } - - /** - * Retrieves an instantiated plugin of the given type, or null if none has been created - */ - @SuppressWarnings("rawtypes") - @Nullable - Plugin getPlugin(@NonNull Class clz) { - return pluginClient.findPlugin(clz); - } - - Notifier getNotifier() { - return notifier; - } - - MetadataState getMetadataState() { - return metadataState; - } - - FeatureFlagState getFeatureFlagState() { - return featureFlagState; - } - - ContextState getContextState() { - return contextState; - } - - void setAutoNotify(boolean autoNotify) { - pluginClient.setAutoNotify(this, autoNotify); - - if (autoNotify) { - exceptionHandler.install(); - } else { - exceptionHandler.uninstall(); - } - } - - void setAutoDetectAnrs(boolean autoDetectAnrs) { - pluginClient.setAutoDetectAnrs(this, autoDetectAnrs); - } - - void addOnSend(OnSendCallback callback) { - callbackState.addPreOnSend(callback); - } -} diff --git a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt deleted file mode 100644 index d4d97d1df3..0000000000 --- a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/com/bugsnag/android/ClientObservable.kt b/app/src/main/java/com/bugsnag/android/ClientObservable.kt deleted file mode 100644 index 989ea30fba..0000000000 --- a/app/src/main/java/com/bugsnag/android/ClientObservable.kt +++ /dev/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 } - } -} diff --git a/app/src/main/java/com/bugsnag/android/CollectionUtils.java b/app/src/main/java/com/bugsnag/android/CollectionUtils.java deleted file mode 100644 index b0ca8f363f..0000000000 --- a/app/src/main/java/com/bugsnag/android/CollectionUtils.java +++ /dev/null @@ -1,19 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.Nullable; - -import java.util.Collection; - -class CollectionUtils { - static boolean containsNullElements(@Nullable Collection data) { - if (data == null) { - return true; - } - for (T datum : data) { - if (datum == null) { - return true; - } - } - return false; - } -} diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt deleted file mode 100644 index f6565b7b3f..0000000000 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ /dev/null @@ -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 - get() = metadataState.metadata.redactedKeys - set(value) { - metadataState.metadata.redactedKeys = value - } - - var discardClasses: Set = emptySet() - var enabledReleaseStages: Set? = null - var enabledBreadcrumbTypes: Set? = null - var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE) - var projectPackages: Set = emptySet() - var persistenceDirectory: File? = null - - var attemptDeliveryOnCrash: Boolean = false - - val notifier: Notifier = Notifier() - - protected val plugins = HashSet() - - 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) = - 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) = - 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?): String { - return coll?.map { it.toString() }?.sorted()?.joinToString(",") ?: "" - } - - fun getConfigDifferences(): Map { - // 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) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java deleted file mode 100644 index c0b4e68f4c..0000000000 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ /dev/null @@ -1,1175 +0,0 @@ -package com.bugsnag.android; - -import android.content.Context; - -import androidx.annotation.IntRange; -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.File; -import java.util.Map; -import java.util.Set; -import java.util.regex.Pattern; - -/** - * User-specified configuration storage object, contains information - * specified at the client level, api-key and endpoint configuration. - */ -@SuppressWarnings("ConstantConditions") // suppress warning about making redundant null checks -public class Configuration implements CallbackAware, MetadataAware, UserAware, FeatureFlagAware { - - private static final int MIN_BREADCRUMBS = 0; - private static final int MAX_BREADCRUMBS = 500; - private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0; - - final ConfigInternal impl; - - /** - * Constructs a new Configuration object with default values. - */ - public Configuration(@NonNull String apiKey) { - impl = new ConfigInternal(apiKey); - } - - /** - * Loads a Configuration object from values supplied as meta-data elements in your - * AndroidManifest. - */ - @NonNull - public static Configuration load(@NonNull Context context) { - return ConfigInternal.load(context); - } - - @NonNull - static Configuration load(@NonNull Context context, @NonNull String apiKey) { - return ConfigInternal.load(context, apiKey); - } - - private void logNull(String property) { - getLogger().e("Invalid null value supplied to config." + property + ", ignoring"); - } - - /** - * Retrieves the API key used for events sent to Bugsnag. - */ - @NonNull - public String getApiKey() { - return impl.getApiKey(); - } - - /** - * Changes the API key used for events sent to Bugsnag. - */ - public void setApiKey(@NonNull String apiKey) { - impl.setApiKey(apiKey); - } - - /** - * Set the application version sent to Bugsnag. We'll automatically pull your app version - * from the versionName field in your AndroidManifest.xml file. - */ - @Nullable - public String getAppVersion() { - return impl.getAppVersion(); - } - - /** - * Set the application version sent to Bugsnag. We'll automatically pull your app version - * from the versionName field in your AndroidManifest.xml file. - */ - public void setAppVersion(@Nullable String appVersion) { - impl.setAppVersion(appVersion); - } - - /** - * We'll automatically pull your versionCode from the versionCode field - * in your AndroidManifest.xml file. If you'd like to override this you - * can set this property. - */ - @Nullable - public Integer getVersionCode() { - return impl.getVersionCode(); - } - - /** - * We'll automatically pull your versionCode from the versionCode field - * in your AndroidManifest.xml file. If you'd like to override this you - * can set this property. - */ - public void setVersionCode(@Nullable Integer versionCode) { - impl.setVersionCode(versionCode); - } - - /** - * If you would like to distinguish between errors that happen in different stages of the - * application release process (development, production, etc) you can set the releaseStage - * that is reported to Bugsnag. - * - * If you are running a debug build, we'll automatically set this to "development", - * otherwise it is set to "production". You can control whether events are sent for - * specific release stages using the enabledReleaseStages option. - */ - @Nullable - public String getReleaseStage() { - return impl.getReleaseStage(); - } - - /** - * If you would like to distinguish between errors that happen in different stages of the - * application release process (development, production, etc) you can set the releaseStage - * that is reported to Bugsnag. - * - * If you are running a debug build, we'll automatically set this to "development", - * otherwise it is set to "production". You can control whether events are sent for - * specific release stages using the enabledReleaseStages option. - */ - public void setReleaseStage(@Nullable String releaseStage) { - impl.setReleaseStage(releaseStage); - } - - /** - * Controls whether we should capture and serialize the state of all threads at the time - * of an error. - * - * By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to - * Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY - * to only do so for unhandled errors. - */ - @NonNull - public ThreadSendPolicy getSendThreads() { - return impl.getSendThreads(); - } - - /** - * Controls whether we should capture and serialize the state of all threads at the time - * of an error. - * - * By default sendThreads is set to Thread.ThreadSendPolicy.ALWAYS. This can be set to - * Thread.ThreadSendPolicy.NEVER to disable or Thread.ThreadSendPolicy.UNHANDLED_ONLY - * to only do so for unhandled errors. - */ - public void setSendThreads(@NonNull ThreadSendPolicy sendThreads) { - if (sendThreads != null) { - impl.setSendThreads(sendThreads); - } else { - logNull("sendThreads"); - } - } - - /** - * Set whether or not Bugsnag should persist user information between application sessions. - * - * If enabled then any user information set will be re-used until the user information is - * removed manually by calling {@link Bugsnag#setUser(String, String, String)} - * with null arguments. - */ - public boolean getPersistUser() { - return impl.getPersistUser(); - } - - /** - * Set whether or not Bugsnag should persist user information between application sessions. - * - * If enabled then any user information set will be re-used until the user information is - * removed manually by calling {@link Bugsnag#setUser(String, String, String)} - * with null arguments. - */ - public void setPersistUser(boolean persistUser) { - impl.setPersistUser(persistUser); - } - - /** - * Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage - * - * If disabled, any device ID that has been persisted will not be retrieved, and no new - * device ID will be generated or stored - */ - public boolean getGenerateAnonymousId() { - return impl.getGenerateAnonymousId(); - } - - /** - * Set whether or not Bugsnag should generate an anonymous ID and persist it in local storage - * - * If disabled, any device ID that has been persisted will not be retrieved, and no new - * device ID will be generated or stored - */ - public void setGenerateAnonymousId(boolean generateAnonymousId) { - impl.setGenerateAnonymousId(generateAnonymousId); - } - - /** - * Sets the directory where event and session JSON payloads should be persisted if a network - * request is not successful. If you use Bugsnag in multiple processes, then a unique - * persistenceDirectory must be configured for each process to prevent duplicate - * requests being made by each instantiation of Bugsnag. - *

- * The persistenceDirectory also stores user information if {@link #getPersistUser()} has been - * set to true. - *

- * By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}. - *

- * If the persistenceDirectory is changed between application launches, no attempt will be made - * to deliver events or sessions cached in the previous location. - */ - @Nullable - public File getPersistenceDirectory() { - return impl.getPersistenceDirectory(); - } - - /** - * Sets the directory where event and session JSON payloads should be persisted if a network - * request is not successful. If you use Bugsnag in multiple processes, then a unique - * persistenceDirectory must be configured for each process to prevent duplicate - * requests being made by each instantiation of Bugsnag. - *

- * The persistenceDirectory also stores user information if {@link #getPersistUser()} has been - * set to true. - *

- * By default, bugsnag sets the persistenceDirectory to {@link Context#getCacheDir()}. - *

- * If the persistenceDirectory is changed between application launches, no attempt will be made - * to deliver events or sessions cached in the previous location. - */ - public void setPersistenceDirectory(@Nullable File directory) { - impl.setPersistenceDirectory(directory); - } - - /** - * Sets whether or not Bugsnag should send crashes synchronously that occurred during - * the application's launch period. By default this behavior is enabled. - * - * See {@link #setLaunchDurationMillis(long)} - */ - public boolean getSendLaunchCrashesSynchronously() { - return impl.getSendLaunchCrashesSynchronously(); - } - - /** - * Sets whether or not Bugsnag should send crashes synchronously that occurred during - * the application's launch period. By default this behavior is enabled. - * - * See {@link #setLaunchDurationMillis(long)} - */ - public void setSendLaunchCrashesSynchronously(boolean sendLaunchCrashesSynchronously) { - impl.setSendLaunchCrashesSynchronously(sendLaunchCrashesSynchronously); - } - - /** - * Sets the threshold in milliseconds for an uncaught error to be considered as a crash on - * launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent - * event synchronously. - * - * By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes - * as launch crashes until markLaunchCompleted() is called. - */ - public long getLaunchDurationMillis() { - return impl.getLaunchDurationMillis(); - } - - /** - * Sets the threshold in milliseconds for an uncaught error to be considered as a crash on - * launch. If a crash is detected on launch, Bugsnag will attempt to send the most recent - * event synchronously. - * - * By default, this value is set at 5,000ms. Setting the value to 0 will count all crashes - * as launch crashes until markLaunchCompleted() is called. - */ - public void setLaunchDurationMillis(@IntRange(from = 0) long launchDurationMillis) { - if (launchDurationMillis >= MIN_LAUNCH_CRASH_THRESHOLD_MS) { - impl.setLaunchDurationMillis(launchDurationMillis); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option launchDurationMillis should be a positive long value." - + "Supplied value is " + launchDurationMillis); - } - } - - /** - * Sets whether or not Bugsnag should automatically capture and report User sessions whenever - * the app enters the foreground. - * - * By default this behavior is enabled. - */ - public boolean getAutoTrackSessions() { - return impl.getAutoTrackSessions(); - } - - /** - * Sets whether or not Bugsnag should automatically capture and report User sessions whenever - * the app enters the foreground. - * - * By default this behavior is enabled. - */ - public void setAutoTrackSessions(boolean autoTrackSessions) { - impl.setAutoTrackSessions(autoTrackSessions); - } - - /** - * Bugsnag will automatically detect different types of error in your application. - * If you wish to control exactly which types are enabled, set this property. - */ - @NonNull - public ErrorTypes getEnabledErrorTypes() { - return impl.getEnabledErrorTypes(); - } - - /** - * Bugsnag will automatically detect different types of error in your application. - * If you wish to control exactly which types are enabled, set this property. - */ - public void setEnabledErrorTypes(@NonNull ErrorTypes enabledErrorTypes) { - if (enabledErrorTypes != null) { - impl.setEnabledErrorTypes(enabledErrorTypes); - } else { - logNull("enabledErrorTypes"); - } - } - - /** - * If you want to disable automatic detection of all errors, you can set this property to false. - * By default this property is true. - * - * Setting autoDetectErrors to false will disable all automatic errors, regardless of the - * error types enabled by enabledErrorTypes - */ - public boolean getAutoDetectErrors() { - return impl.getAutoDetectErrors(); - } - - /** - * If you want to disable automatic detection of all errors, you can set this property to false. - * By default this property is true. - * - * Setting autoDetectErrors to false will disable all automatic errors, regardless of the - * error types enabled by enabledErrorTypes - */ - public void setAutoDetectErrors(boolean autoDetectErrors) { - impl.setAutoDetectErrors(autoDetectErrors); - } - - /** - * If your app's codebase contains different entry-points/processes, but reports to a single - * Bugsnag project, you might want to add information denoting the type of process the error - * came from. - * - * This information can be used in the dashboard to filter errors and to determine whether - * an error is limited to a subset of appTypes. - * - * By default, this value is set to 'android'. - */ - @Nullable - public String getAppType() { - return impl.getAppType(); - } - - /** - * If your app's codebase contains different entry-points/processes, but reports to a single - * Bugsnag project, you might want to add information denoting the type of process the error - * came from. - * - * This information can be used in the dashboard to filter errors and to determine whether - * an error is limited to a subset of appTypes. - * - * By default, this value is set to 'android'. - */ - public void setAppType(@Nullable String appType) { - impl.setAppType(appType); - } - - /** - * By default, the notifier's log messages will be logged using android.util.Log - * with a "Bugsnag" tag unless the releaseStage is "production". - * - * To override this behavior, an alternative instance can be provided that implements the - * Logger interface. - */ - @Nullable - public Logger getLogger() { - return impl.getLogger(); - } - - /** - * By default, the notifier's log messages will be logged using android.util.Log - * with a "Bugsnag" tag unless the releaseStage is "production". - * - * To override this behavior, an alternative instance can be provided that implements the - * Logger interface. - */ - public void setLogger(@Nullable Logger logger) { - impl.setLogger(logger); - } - - /** - * The Delivery implementation used to make network calls to the Bugsnag - * Error Reporting and - * Sessions API. - * - * This may be useful if you have requirements such as certificate pinning and rotation, - * which are not supported by the default implementation. - * - * To provide custom delivery functionality, create a class which implements the Delivery - * interface. Please note that request bodies must match the structure specified in the - * Error Reporting and - * Sessions API documentation. - * - * You can use the return type from the deliver functions to control the strategy for - * retrying the transmission at a later date. - * - * If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache - * the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED - * or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload - * and no further delivery will be attempted. - */ - @NonNull - public Delivery getDelivery() { - return impl.getDelivery(); - } - - /** - * The Delivery implementation used to make network calls to the Bugsnag - * Error Reporting and - * Sessions API. - * - * This may be useful if you have requirements such as certificate pinning and rotation, - * which are not supported by the default implementation. - * - * To provide custom delivery functionality, create a class which implements the Delivery - * interface. Please note that request bodies must match the structure specified in the - * Error Reporting and - * Sessions API documentation. - * - * You can use the return type from the deliver functions to control the strategy for - * retrying the transmission at a later date. - * - * If DeliveryStatus.UNDELIVERED is returned, the notifier will automatically cache - * the payload and trigger delivery later on. Otherwise, if either DeliveryStatus.DELIVERED - * or DeliveryStatus.FAILURE is returned the notifier will removed any cached payload - * and no further delivery will be attempted. - */ - public void setDelivery(@NonNull Delivery delivery) { - if (delivery != null) { - impl.setDelivery(delivery); - } else { - logNull("delivery"); - } - } - - /** - * Set the endpoints to send data to. By default we'll send error reports to - * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can - * override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints. - */ - @NonNull - public EndpointConfiguration getEndpoints() { - return impl.getEndpoints(); - } - - /** - * Set the endpoints to send data to. By default we'll send error reports to - * https://notify.bugsnag.com, and sessions to https://sessions.bugsnag.com, but you can - * override this if you are using Bugsnag Enterprise to point to your own Bugsnag endpoints. - */ - public void setEndpoints(@NonNull EndpointConfiguration endpoints) { - if (endpoints != null) { - impl.setEndpoints(endpoints); - } else { - logNull("endpoints"); - } - } - - /** - * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, - * the oldest breadcrumbs will be deleted. - * - * By default, 100 breadcrumbs are stored: this can be amended up to a maximum of 500. - */ - public int getMaxBreadcrumbs() { - return impl.getMaxBreadcrumbs(); - } - - /** - * Sets the maximum number of breadcrumbs which will be stored. Once the threshold is reached, - * the oldest breadcrumbs will be deleted. - * - * By default, 100 breadcrumbs are stored: this can be amended up to a maximum of 500. - */ - public void setMaxBreadcrumbs(@IntRange(from = 0, to = 500) int maxBreadcrumbs) { - if (maxBreadcrumbs >= MIN_BREADCRUMBS && maxBreadcrumbs <= MAX_BREADCRUMBS) { - impl.setMaxBreadcrumbs(maxBreadcrumbs); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option maxBreadcrumbs should be an integer between 0-500. " - + "Supplied value is " + maxBreadcrumbs); - } - } - - /** - * Sets the maximum number of persisted events which will be stored. Once the threshold is - * reached, the oldest event will be deleted. - * - * By default, 32 events are persisted. - */ - public int getMaxPersistedEvents() { - return impl.getMaxPersistedEvents(); - } - - /** - * Sets the maximum number of persisted events which will be stored. Once the threshold is - * reached, the oldest event will be deleted. - * - * By default, 32 events are persisted. - */ - public void setMaxPersistedEvents(@IntRange(from = 0) int maxPersistedEvents) { - if (maxPersistedEvents >= 0) { - impl.setMaxPersistedEvents(maxPersistedEvents); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option maxPersistedEvents should be a positive integer." - + "Supplied value is " + maxPersistedEvents); - } - } - - /** - * Gets the maximum number of threads that will be reported with an event. Once the threshold is - * reached, all remaining threads will be omitted. - * - * By default, up to 200 threads are reported. - */ - public int getMaxReportedThreads() { - return impl.getMaxReportedThreads(); - } - - /** - * Sets the maximum number of threads that will be reported with an event. Once the threshold is - * reached, all remaining threads will be omitted. - * - * By default, up to 200 threads are reported. - */ - public void setMaxReportedThreads(@IntRange(from = 0) int maxReportedThreads) { - if (maxReportedThreads >= 0) { - impl.setMaxReportedThreads(maxReportedThreads); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option maxReportedThreads should be a positive integer." - + "Supplied value is " + maxReportedThreads); - } - } - - - /** - * Gets the maximum time for collecting threads and traces. - * By default, up to 200 threads are reported. - */ - public long getThreadCollectionTimeLimitMillis() { - return impl.getThreadCollectionTimeLimitMillis(); - } - - /** - * Sets the maximum time for collecting threads and traces. - * By default, up to 500 milliseconds are reported. - */ - public void setThreadCollectionTimeLimitMillis( - @IntRange(from = 0) long threadCollectionTimeLimitMillis - ) { - if (threadCollectionTimeLimitMillis >= 0) { - impl.setThreadCollectionTimeLimitMillis(threadCollectionTimeLimitMillis); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option threadCollectionTimeLimitMillis should be a positive integer." - + "Supplied value is " + threadCollectionTimeLimitMillis); - } - } - - /** - * Sets the maximum number of persisted sessions which will be stored. Once the threshold is - * reached, the oldest session will be deleted. - * - * By default, 128 sessions are persisted. - */ - public int getMaxPersistedSessions() { - return impl.getMaxPersistedSessions(); - } - - /** - * Sets the maximum number of persisted sessions which will be stored. Once the threshold is - * reached, the oldest session will be deleted. - * - * By default, 128 sessions are persisted. - */ - public void setMaxPersistedSessions(@IntRange(from = 0) int maxPersistedSessions) { - if (maxPersistedSessions >= 0) { - impl.setMaxPersistedSessions(maxPersistedSessions); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option maxPersistedSessions should be a positive integer." - + "Supplied value is " + maxPersistedSessions); - } - } - - /** - * Gets the maximum string length in any metadata field. Once the threshold is - * reached in a particular string, all excess characters will be deleted. - * - * By default, the limit is 10,000. - */ - public int getMaxStringValueLength() { - return impl.getMaxStringValueLength(); - } - - /** - * Sets the maximum string length in any metadata field. Once the threshold is - * reached in a particular string, all excess characters will be deleted. - * - * By default, the limit is 10,000. - */ - public void setMaxStringValueLength(@IntRange(from = 0) int maxStringValueLength) { - if (maxStringValueLength >= 0) { - impl.setMaxStringValueLength(maxStringValueLength); - } else { - getLogger().e("Invalid configuration value detected. " - + "Option maxStringValueLength should be a positive integer." - + "Supplied value is " + maxStringValueLength); - } - } - - /** - * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts - * represent what was happening in your application at the time an error occurs. - * - * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - @Nullable - public String getContext() { - return impl.getContext(); - } - - /** - * Bugsnag uses the concept of "contexts" to help display and group your errors. Contexts - * represent what was happening in your application at the time an error occurs. - * - * In an android app the "context" is automatically set as the foreground Activity. - * If you would like to set this value manually, you should alter this property. - */ - public void setContext(@Nullable String context) { - impl.setContext(context); - } - - /** - * Sets which values should be removed from any Metadata objects before - * sending them to Bugsnag. Use this if you want to ensure you don't send - * sensitive data such as passwords, and credit card numbers to our - * servers. Any keys which contain these strings will be filtered. - * - * By default, redactedKeys is set to "password" - */ - @NonNull - public Set getRedactedKeys() { - return impl.getRedactedKeys(); - } - - /** - * Sets which values should be removed from any Metadata objects before - * sending them to Bugsnag. Use this if you want to ensure you don't send - * sensitive data such as passwords, and credit card numbers to our - * servers. Any keys which contain these strings will be filtered. - * - * By default, redactedKeys is set to "password" - */ - public void setRedactedKeys(@NonNull Set redactedKeys) { - if (CollectionUtils.containsNullElements(redactedKeys)) { - logNull("redactedKeys"); - } else { - impl.setRedactedKeys(redactedKeys); - } - } - - /** - * Allows you to specify the fully-qualified name of error classes that will be discarded - * before being sent to Bugsnag if they are detected. The notifier performs an exact - * match against the canonical class name. - */ - @NonNull - public Set getDiscardClasses() { - return impl.getDiscardClasses(); - } - - /** - * Allows you to specify the fully-qualified name of error classes that will be discarded - * before being sent to Bugsnag if they are detected. The notifier performs an exact - * match against the canonical class name. - */ - public void setDiscardClasses(@NonNull Set discardClasses) { - if (CollectionUtils.containsNullElements(discardClasses)) { - logNull("discardClasses"); - } else { - impl.setDiscardClasses(discardClasses); - } - } - - /** - * By default, Bugsnag will be notified of events that happen in any releaseStage. - * If you would like to change which release stages notify Bugsnag you can set this property. - */ - @Nullable - public Set getEnabledReleaseStages() { - return impl.getEnabledReleaseStages(); - } - - /** - * By default, Bugsnag will be notified of events that happen in any releaseStage. - * If you would like to change which release stages notify Bugsnag you can set this property. - */ - public void setEnabledReleaseStages(@Nullable Set enabledReleaseStages) { - impl.setEnabledReleaseStages(enabledReleaseStages); - } - - /** - * By default we will automatically add breadcrumbs for common application events such as - * activity lifecycle events and system intents. To amend this behavior, - * override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an - * empty set. - * - * The following breadcrumb types can be enabled: - * - * - Captured errors: left when an error event is sent to the Bugsnag API. - * - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function. - * - Navigation changes: left for Activity Lifecycle events to track the user's journey in - * the app. - * - State changes: state breadcrumbs are left for system broadcast events. For example: - * battery warnings, airplane mode, etc. - * - User interaction: left when the user performs certain system operations. - */ - @Nullable - public Set getEnabledBreadcrumbTypes() { - return impl.getEnabledBreadcrumbTypes(); - } - - /** - * By default we will automatically add breadcrumbs for common application events such as - * activity lifecycle events and system intents. To amend this behavior, - * override the enabled breadcrumb types. All breadcrumbs can be disabled by providing an - * empty set. - * - * The following breadcrumb types can be enabled: - * - * - Captured errors: left when an error event is sent to the Bugsnag API. - * - Manual breadcrumbs: left via the Bugsnag.leaveBreadcrumb function. - * - Navigation changes: left for Activity Lifecycle events to track the user's journey in - * the app. - * - State changes: state breadcrumbs are left for system broadcast events. For example: - * battery warnings, airplane mode, etc. - * - User interaction: left when the user performs certain system operations. - */ - public void setEnabledBreadcrumbTypes(@Nullable Set enabledBreadcrumbTypes) { - impl.setEnabledBreadcrumbTypes(enabledBreadcrumbTypes); - } - - @NonNull - public Set getTelemetry() { - return impl.getTelemetry(); - } - - /** - * Set which telemetry will be sent to Bugsnag. By default, all telemetry is enabled. - * - * The following telemetry can be enabled: - * - * - internal errors: Errors in the Bugsnag SDK itself. - */ - public void setTelemetry(@NonNull Set telemetry) { - if (telemetry != null) { - impl.setTelemetry(telemetry); - } else { - logNull("telemetry"); - } - } - - /** - * Sets which package names Bugsnag should consider as a part of the - * running application. We mark stacktrace lines as in-project if they - * originate from any of these packages and this allows us to improve - * the visual display of the stacktrace on the dashboard. - * - * By default, projectPackages is set to be the package you called Bugsnag.start from. - */ - @NonNull - public Set getProjectPackages() { - return impl.getProjectPackages(); - } - - /** - * Sets which package names Bugsnag should consider as a part of the - * running application. We mark stacktrace lines as in-project if they - * originate from any of these packages and this allows us to improve - * the visual display of the stacktrace on the dashboard. - * - * By default, projectPackages is set to be the package you called Bugsnag.start from. - */ - public void setProjectPackages(@NonNull Set projectPackages) { - if (CollectionUtils.containsNullElements(projectPackages)) { - logNull("projectPackages"); - } else { - impl.setProjectPackages(projectPackages); - } - } - - /** - * Add a "on error" callback, to execute code at the point where an error report is - * captured in Bugsnag. - * - * You can use this to add or modify information attached to an Event - * before it is sent to your dashboard. You can also return - * false from any callback to prevent delivery. "on error" - * callbacks do not run before reports generated in the event - * of immediate app termination from crashes in C/C++ code. - * - * For example: - * - * Bugsnag.addOnError(new OnErrorCallback() { - * public boolean run(Event event) { - * event.setSeverity(Severity.INFO); - * return true; - * } - * }) - * - * @param onError a callback to run before sending errors to Bugsnag - * @see OnErrorCallback - */ - @Override - public void addOnError(@NonNull OnErrorCallback onError) { - if (onError != null) { - impl.addOnError(onError); - } else { - logNull("addOnError"); - } - } - - /** - * Removes a previously added "on error" callback - * @param onError the callback to remove - */ - @Override - public void removeOnError(@NonNull OnErrorCallback onError) { - if (onError != null) { - impl.removeOnError(onError); - } else { - logNull("removeOnError"); - } - } - - /** - * Add an "on breadcrumb" callback, to execute code before every - * breadcrumb captured by Bugsnag. - * - * You can use this to modify breadcrumbs before they are stored by Bugsnag. - * You can also return false from any callback to ignore a breadcrumb. - * - * For example: - * - * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { - * public boolean run(Breadcrumb breadcrumb) { - * return false; // ignore the breadcrumb - * } - * }) - * - * @param onBreadcrumb a callback to run before a breadcrumb is captured - * @see OnBreadcrumbCallback - */ - @Override - public void addOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { - if (onBreadcrumb != null) { - impl.addOnBreadcrumb(onBreadcrumb); - } else { - logNull("addOnBreadcrumb"); - } - } - - /** - * Removes a previously added "on breadcrumb" callback - * @param onBreadcrumb the callback to remove - */ - @Override - public void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) { - if (onBreadcrumb != null) { - impl.removeOnBreadcrumb(onBreadcrumb); - } else { - logNull("removeOnBreadcrumb"); - } - } - - /** - * Add an "on session" callback, to execute code before every - * session captured by Bugsnag. - * - * You can use this to modify sessions before they are stored by Bugsnag. - * You can also return false from any callback to ignore a session. - * - * For example: - * - * Bugsnag.onSession(new OnSessionCallback() { - * public boolean run(Session session) { - * return false; // ignore the session - * } - * }) - * - * @param onSession a callback to run before a session is captured - * @see OnSessionCallback - */ - @Override - public void addOnSession(@NonNull OnSessionCallback onSession) { - if (onSession != null) { - impl.addOnSession(onSession); - } else { - logNull("addOnSession"); - } - } - - /** - * Removes a previously added "on session" callback - * @param onSession the callback to remove - */ - @Override - public void removeOnSession(@NonNull OnSessionCallback onSession) { - if (onSession != null) { - impl.removeOnSession(onSession); - } else { - logNull("removeOnSession"); - } - } - - /** - * Add a callback which will be invoked prior to an event being delivered - * to Bugsnag. The callback can be used to modify events or cancel - * delivering the event altogether by returning false. Note - * that the callback may be invoked in the current or a subsequent app - * launch depending on whether the app terminated prior to delivering the - * event. - * - * @param onSend the callback to add - * @see OnSendCallback - */ - public void addOnSend(@NonNull OnSendCallback onSend) { - if (onSend != null) { - impl.addOnSend(onSend); - } else { - logNull("addOnSend"); - } - } - - /** - * Remove a callback previously added with {@link Configuration#addOnSend(OnSendCallback)} - * - * @param onSend the callback to remove - */ - public void removeOnSend(@NonNull OnSendCallback onSend) { - if (onSend != null) { - impl.removeOnSend(onSend); - } else { - logNull("removeOnSend"); - } - } - - /** - * Adds a map of multiple metadata key-value pairs to the specified section. - */ - @Override - public void addMetadata(@NonNull String section, @NonNull Map value) { - if (section != null && value != null) { - impl.addMetadata(section, value); - } else { - logNull("addMetadata"); - } - } - - /** - * Adds the specified key and value in the specified section. The value can be of - * any primitive type or a collection such as a map, set or array. - */ - @Override - public void addMetadata(@NonNull String section, @NonNull String key, @Nullable Object value) { - if (section != null && key != null) { - impl.addMetadata(section, key, value); - } else { - logNull("addMetadata"); - } - } - - /** - * Removes all the data from the specified section. - */ - @Override - public void clearMetadata(@NonNull String section) { - if (section != null) { - impl.clearMetadata(section); - } else { - logNull("clearMetadata"); - } - } - - /** - * Removes data with the specified key from the specified section. - */ - @Override - public void clearMetadata(@NonNull String section, @NonNull String key) { - if (section != null && key != null) { - impl.clearMetadata(section, key); - } else { - logNull("clearMetadata"); - } - } - - /** - * Returns a map of data in the specified section. - */ - @Nullable - @Override - public Map getMetadata(@NonNull String section) { - if (section != null) { - return impl.getMetadata(section); - } else { - logNull("getMetadata"); - return null; - } - } - - /** - * Returns the value of the specified key in the specified section. - */ - @Nullable - @Override - public Object getMetadata(@NonNull String section, @NonNull String key) { - if (section != null && key != null) { - return impl.getMetadata(section, key); - } else { - logNull("getMetadata"); - return null; - } - } - - /** - * {@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 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(); - } - - /** - * Returns the currently set User information. - */ - @NonNull - @Override - public User getUser() { - return impl.getUser(); - } - - /** - * Sets the user associated with the event. - */ - @Override - public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) { - impl.setUser(id, email, name); - } - - /** - * Adds a plugin which will be loaded when the bugsnag notifier is instantiated. - */ - public void addPlugin(@NonNull Plugin plugin) { - if (plugin != null) { - impl.addPlugin(plugin); - } else { - logNull("addPlugin"); - } - } - - /** - * Whether Bugsnag should try to send crashing errors prior to app termination. - * - * Delivery will only be attempted for uncaught Java / Kotlin exceptions or errors, and - * while in progress will block the crashing thread for up to 3 seconds. - * - * Delivery on crash should be considered unreliable due to the necessary short timeout and - * potential for generating "errors on errors". - * - * Use of this feature is discouraged because it: - * - may cause Application Not Responding (ANR) errors on-top of existing crashes - * - will result in duplicate errors in your Dashboard when errors are not detected as sent - * before termination - * - may prevent other error handlers from detecting or reporting a crash - * - * By default this value is {@code false}. - * - * @param attemptDeliveryOnCrash {@code true} if Bugsnag should try to send crashing errors - * prior to app termination - */ - public void setAttemptDeliveryOnCrash(boolean attemptDeliveryOnCrash) { - impl.setAttemptDeliveryOnCrash(attemptDeliveryOnCrash); - } - - /** - * Whether Bugsnag should try to send crashing errors prior to app termination. - * - * @see #setAttemptDeliveryOnCrash(boolean) - */ - public boolean isAttemptDeliveryOnCrash() { - return impl.getAttemptDeliveryOnCrash(); - } - - Set getPlugins() { - return impl.getPlugins(); - } - - Notifier getNotifier() { - return impl.getNotifier(); - } -} diff --git a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt b/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt deleted file mode 100644 index 97e87553c1..0000000000 --- a/app/src/main/java/com/bugsnag/android/ConnectivityCompat.kt +++ /dev/null @@ -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" - } -} diff --git a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt deleted file mode 100644 index 99e8d87b18..0000000000 --- a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt +++ /dev/null @@ -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 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) diff --git a/app/src/main/java/com/bugsnag/android/ContextState.kt b/app/src/main/java/com/bugsnag/android/ContextState.kt deleted file mode 100644 index 8c377b739c..0000000000 --- a/app/src/main/java/com/bugsnag/android/ContextState.kt +++ /dev/null @@ -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()) } -} diff --git a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt deleted file mode 100644 index e8967de359..0000000000 --- a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt +++ /dev/null @@ -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, - 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 - ) - } -} diff --git a/app/src/main/java/com/bugsnag/android/DebugLogger.kt b/app/src/main/java/com/bugsnag/android/DebugLogger.kt deleted file mode 100644 index e50feca334..0000000000 --- a/app/src/main/java/com/bugsnag/android/DebugLogger.kt +++ /dev/null @@ -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) - } -} diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt deleted file mode 100644 index 2d8861ef24..0000000000 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ /dev/null @@ -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 - ): 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 - ): 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()}") - } - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt.orig b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt.orig deleted file mode 100644 index a186936579..0000000000 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt.orig +++ /dev/null @@ -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 - ): 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 - ): 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()}") - } - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Deliverable.kt b/app/src/main/java/com/bugsnag/android/Deliverable.kt deleted file mode 100644 index c41da5de5e..0000000000 --- a/app/src/main/java/com/bugsnag/android/Deliverable.kt +++ /dev/null @@ -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 } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Delivery.kt b/app/src/main/java/com/bugsnag/android/Delivery.kt deleted file mode 100644 index 0fc2d2c748..0000000000 --- a/app/src/main/java/com/bugsnag/android/Delivery.kt +++ /dev/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 -} diff --git a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java b/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java deleted file mode 100644 index 5c4ef01616..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeliveryDelegate.java +++ /dev/null @@ -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 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(); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt b/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt deleted file mode 100644 index c5d2d7fee7..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeliveryHeaders.kt +++ /dev/null @@ -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 { - 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): 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 = 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 -} diff --git a/app/src/main/java/com/bugsnag/android/DeliveryParams.kt b/app/src/main/java/com/bugsnag/android/DeliveryParams.kt deleted file mode 100644 index 7eac1c2785..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeliveryParams.kt +++ /dev/null @@ -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 -) diff --git a/app/src/main/java/com/bugsnag/android/DeliveryStatus.kt b/app/src/main/java/com/bugsnag/android/DeliveryStatus.kt deleted file mode 100644 index 37c398805f..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeliveryStatus.kt +++ /dev/null @@ -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 - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Device.kt b/app/src/main/java/com/bugsnag/android/Device.kt deleted file mode 100644 index 34d740de8d..0000000000 --- a/app/src/main/java/com/bugsnag/android/Device.kt +++ /dev/null @@ -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?, - - /** - * 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? -) : 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? = 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?): MutableMap? = - value?.mapValuesTo(mutableMapOf()) { (_, value) -> value.toString() } -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceBuildInfo.kt b/app/src/main/java/com/bugsnag/android/DeviceBuildInfo.kt deleted file mode 100644 index 39ed0fa205..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceBuildInfo.kt +++ /dev/null @@ -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? -) { - 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 - ) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt deleted file mode 100644 index b818523837..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ /dev/null @@ -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, - private val buildInfo: DeviceBuildInfo, - private val dataDirectory: File, - private val rootedFuture: Provider?, - 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 - private val totalMemoryFuture: Future? = retrieveTotalDeviceMemory() - private var orientation = AtomicInteger(resources.configuration.orientation) - - init { - val map = mutableMapOf() - 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 { - val map = HashMap() - 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) { - 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 = 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? { - 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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt b/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt deleted file mode 100644 index 60fa1f9364..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt +++ /dev/null @@ -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 - - 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 { - 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) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt b/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt deleted file mode 100644 index 0c9bec8ac5..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt +++ /dev/null @@ -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? -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt b/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt deleted file mode 100644 index a0a2cab06f..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt +++ /dev/null @@ -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, - 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? - ) -} diff --git a/app/src/main/java/com/bugsnag/android/DeviceWithState.kt b/app/src/main/java/com/bugsnag/android/DeviceWithState.kt deleted file mode 100644 index eccb745a27..0000000000 --- a/app/src/main/java/com/bugsnag/android/DeviceWithState.kt +++ /dev/null @@ -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, - - /** - * 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) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/EndpointConfiguration.kt b/app/src/main/java/com/bugsnag/android/EndpointConfiguration.kt deleted file mode 100644 index 5d19fded17..0000000000 --- a/app/src/main/java/com/bugsnag/android/EndpointConfiguration.kt +++ /dev/null @@ -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" -) diff --git a/app/src/main/java/com/bugsnag/android/Error.java b/app/src/main/java/com/bugsnag/android/Error.java deleted file mode 100644 index d711e4d609..0000000000 --- a/app/src/main/java/com/bugsnag/android/Error.java +++ /dev/null @@ -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 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 createError(@NonNull Throwable exc, - @NonNull Collection projectPackages, - @NonNull Logger logger) { - return ErrorInternal.Companion.createError(exc, projectPackages, logger); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/ErrorInternal.kt b/app/src/main/java/com/bugsnag/android/ErrorInternal.kt deleted file mode 100644 index 9e716bbd62..0000000000 --- a/app/src/main/java/com/bugsnag/android/ErrorInternal.kt +++ /dev/null @@ -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 = 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, - logger: Logger - ): MutableList { - return exc.safeUnrollCauses() - .mapTo(mutableListOf()) { currentEx -> - // Somehow it's possible for stackTrace to be null in rare cases - val stacktrace = currentEx.stackTrace ?: arrayOf() - 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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/ErrorType.kt b/app/src/main/java/com/bugsnag/android/ErrorType.kt deleted file mode 100644 index 891b3b308a..0000000000 --- a/app/src/main/java/com/bugsnag/android/ErrorType.kt +++ /dev/null @@ -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 } - } -} diff --git a/app/src/main/java/com/bugsnag/android/ErrorTypes.kt b/app/src/main/java/com/bugsnag/android/ErrorTypes.kt deleted file mode 100644 index 63975d5d9c..0000000000 --- a/app/src/main/java/com/bugsnag/android/ErrorTypes.kt +++ /dev/null @@ -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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/Event.java b/app/src/main/java/com/bugsnag/android/Event.java deleted file mode 100644 index 0c560107e5..0000000000 --- a/app/src/main/java/com/bugsnag/android/Event.java +++ /dev/null @@ -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. - *

- * Manipulating this field does not affect the error information reported to the - * Bugsnag dashboard. Use {@link Event#getErrors()} to access and amend the representation of - * the error that will be sent. - */ - @Nullable - public Throwable getOriginalError() { - return impl.getOriginalError(); - } - - /** - * Information extracted from the {@link Throwable} that caused the event can be found in this - * field. The list contains at least one {@link Error} that represents the thrown object - * with subsequent elements in the list populated from {@link Throwable#getCause()}. - *

- * A reference to the actual {@link Throwable} object that caused the event is available - * through {@link Event#getOriginalError()} ()}. - */ - @NonNull - public List 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 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 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 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 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. - *

- * As the name implies, this option accepts a hash of sorts. - */ - public void setGroupingHash(@Nullable String groupingHash) { - impl.setGroupingHash(groupingHash); - } - - /** - * Set the grouping hash of the event to override the default grouping on the dashboard. - * All events with the same grouping hash will be grouped together into one error. This is an - * advanced usage of the library and mis-using it will cause your events not to group properly - * in your dashboard. - *

- * As the name implies, this option accepts a hash of sorts. - */ - @Nullable - public String getGroupingHash() { - return impl.getGroupingHash(); - } - - /** - * Sets the context of the error. The context is a summary what what was occurring in the - * application at the time of the crash, if available, such as the visible activity. - */ - public void setContext(@Nullable String context) { - impl.setContext(context); - } - - /** - * Returns the context of the error. The context is a summary what what was occurring in the - * application at the time of the crash, if available, such as the visible activity. - */ - @Nullable - public String getContext() { - return impl.getContext(); - } - - /** - * Sets the user associated with the event. - */ - @Override - public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) { - impl.setUser(id, email, name); - } - - /** - * Returns the currently set User information. - */ - @Override - @NonNull - public User getUser() { - return impl.getUser(); - } - - /** - * Adds a map of multiple metadata key-value pairs to the specified section. - */ - @Override - public void addMetadata(@NonNull String section, @NonNull Map 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 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 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 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 redactedKeys) { - impl.setRedactedKeys(redactedKeys); - } - - void setInternalMetrics(InternalMetrics metrics) { - impl.setInternalMetrics(metrics); - } -} diff --git a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt deleted file mode 100644 index f7cfd5f9af..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt +++ /dev/null @@ -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 -) { - - 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 - ): 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 { - val name = eventFile.name - val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1) - val start = name.lastIndexOf("_", end - 1) + 1 - - if (start < end) { - val encodedValues: List = 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 { - 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 -> "" - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/EventInternal.kt b/app/src/main/java/com/bugsnag/android/EventInternal.kt deleted file mode 100644 index 8345929248..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventInternal.kt +++ /dev/null @@ -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 = mutableListOf(), - discardClasses: Set = setOf(), - errors: MutableList = mutableListOf(), - metadata: Metadata = Metadata(), - featureFlags: FeatureFlags = FeatureFlags(), - originalError: Throwable? = null, - projectPackages: Collection = setOf(), - severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), - threads: MutableList = mutableListOf(), - user: User = User(), - redactionKeys: Set? = 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 - internal var projectPackages: Collection - - 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 - var errors: MutableList - var threads: MutableList - var groupingHash: String? = null - var context: String? = null - - var redactedKeys: Collection - 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 { - 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) = - 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) = - 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? - ): Breadcrumb { - val breadcrumb = Breadcrumb( - message.toString(), - type ?: BreadcrumbType.MANUAL, - metadata, - Date(), - logger - ) - - breadcrumbs.add(breadcrumb) - return breadcrumb - } -} diff --git a/app/src/main/java/com/bugsnag/android/EventPayload.kt b/app/src/main/java/com/bugsnag/android/EventPayload.kt deleted file mode 100644 index 01431aea8c..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventPayload.kt +++ /dev/null @@ -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 { - 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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt deleted file mode 100644 index 2d519eb5b1..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt +++ /dev/null @@ -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 - ) - } -} diff --git a/app/src/main/java/com/bugsnag/android/EventStore.kt b/app/src/main/java/com/bugsnag/android/EventStore.kt deleted file mode 100644 index 75300b3907..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventStore.kt +++ /dev/null @@ -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? { - return storedFiles - .asSequence() - .filter { fromFile(it, config).isLaunchCrashReport() } - .maxWithOrNull(EVENT_COMPARATOR) - } - - fun writeAndDeliver(streamable: Streamable): Future? { - 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) { - 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 = 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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java b/app/src/main/java/com/bugsnag/android/ExceptionHandler.java deleted file mode 100644 index 37648a3ca8..0000000000 --- a/app/src/main/java/com/bugsnag/android/ExceptionHandler.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlag.java b/app/src/main/java/com/bugsnag/android/FeatureFlag.java deleted file mode 100644 index 39bef789b8..0000000000 --- a/app/src/main/java/com/bugsnag/android/FeatureFlag.java +++ /dev/null @@ -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 { - 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 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 e2 = - (Map.Entry) 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 + '\'' - + '}'; - } -} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt b/app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt deleted file mode 100644 index 3703eb0596..0000000000 --- a/app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt +++ /dev/null @@ -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) - - /** - * 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() -} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt b/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt deleted file mode 100644 index ba2b78bd9b..0000000000 --- a/app/src/main/java/com/bugsnag/android/FeatureFlagState.kt +++ /dev/null @@ -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) { - 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 = featureFlags.toList() - fun copy() = FeatureFlagState(featureFlags.copy()) -} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlags.kt b/app/src/main/java/com/bugsnag/android/FeatureFlags.kt deleted file mode 100644 index 24de0276b2..0000000000 --- a/app/src/main/java/com/bugsnag/android/FeatureFlags.kt +++ /dev/null @@ -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 -) : 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()) - - 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) { - synchronized(this) { - val flagArray = flags - - val newFlags = ArrayList( - // 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(flagArray.size - 1) - flagArray.copyInto(out, 0, 0, index) - flagArray.copyInto(out, index, index + 1) - - @Suppress("UNCHECKED_CAST") - flags = out as Array - } - } - - 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 = flags.map { (name, variant) -> FeatureFlag(name, variant) } - - fun copy() = FeatureFlags(flags) -} diff --git a/app/src/main/java/com/bugsnag/android/FileStore.kt b/app/src/main/java/com/bugsnag/android/FileStore.kt deleted file mode 100644 index 69f7c77632..0000000000 --- a/app/src/main/java/com/bugsnag/android/FileStore.kt +++ /dev/null @@ -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 = 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 { - lock.lock() - return try { - val files: MutableList = 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?) { - lock.lock() - try { - if (files != null) { - queuedFiles.removeAll(files) - } - } finally { - lock.unlock() - } - } - - fun deleteStoredFiles(storedFiles: Collection?) { - lock.lock() - try { - if (storedFiles != null) { - queuedFiles.removeAll(storedFiles) - for (storedFile in storedFiles) { - if (!storedFile.delete()) { - storedFile.deleteOnExit() - } - } - } - } finally { - lock.unlock() - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/IOUtils.java b/app/src/main/java/com/bugsnag/android/IOUtils.java deleted file mode 100644 index b3b5a13838..0000000000 --- a/app/src/main/java/com/bugsnag/android/IOUtils.java +++ /dev/null @@ -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); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java b/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java deleted file mode 100644 index ad1d791bdf..0000000000 --- a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java +++ /dev/null @@ -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; - 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, - 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 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 - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/Intrinsics.java b/app/src/main/java/com/bugsnag/android/Intrinsics.java deleted file mode 100644 index 70e18f2dbe..0000000000 --- a/app/src/main/java/com/bugsnag/android/Intrinsics.java +++ /dev/null @@ -1,8 +0,0 @@ -package com.bugsnag.android; - -class Intrinsics { - - static boolean isEmpty(CharSequence str) { - return str == null || str.length() == 0; - } -} diff --git a/app/src/main/java/com/bugsnag/android/JsonReadable.kt b/app/src/main/java/com/bugsnag/android/JsonReadable.kt deleted file mode 100644 index dc68f3e82f..0000000000 --- a/app/src/main/java/com/bugsnag/android/JsonReadable.kt +++ /dev/null @@ -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 { - - /** - * Constructs an object from a JSON input. - */ - fun fromReader(reader: JsonReader): T -} diff --git a/app/src/main/java/com/bugsnag/android/JsonScope.java b/app/src/main/java/com/bugsnag/android/JsonScope.java deleted file mode 100644 index 2dc37d8a37..0000000000 --- a/app/src/main/java/com/bugsnag/android/JsonScope.java +++ /dev/null @@ -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; -} diff --git a/app/src/main/java/com/bugsnag/android/JsonStream.java b/app/src/main/java/com/bugsnag/android/JsonStream.java deleted file mode 100644 index a32413457c..0000000000 --- a/app/src/main/java/com/bugsnag/android/JsonStream.java +++ /dev/null @@ -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(); - } -} diff --git a/app/src/main/java/com/bugsnag/android/JsonWriter.java b/app/src/main/java/com/bugsnag/android/JsonWriter.java deleted file mode 100644 index cb4fdddb43..0000000000 --- a/app/src/main/java/com/bugsnag/android/JsonWriter.java +++ /dev/null @@ -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 (RFC 7159) - * 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. - * - *

Encoding JSON

- * 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: - *
    - *
  • To write arrays, 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()}. - *
  • To write objects, 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()}. - *
- * - *

Example

- * Suppose we'd like to encode a stream of messages such as the following:
 {@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
- *     }
- *   }
- * ]}
- * This code encodes the above structure:
   {@code
- *   public void writeJsonStream(OutputStream out, List 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 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 doubles) throws IOException {
- *     writer.beginArray();
- *     for (Double value : doubles) {
- *       writer.value(value);
- *     }
- *     writer.endArray();
- *   }}
- * - *

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 RFC 7159. Setting the writer - * to lenient permits the following: - *

    - *
  • Top-level values of any type. With strict writing, the top-level - * value must be an object or an array. - *
  • Numbers may be {@link Double#isNaN() NaNs} or {@link - * Double#isInfinite() infinities}. - *
- */ - 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."); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/LastRunInfo.kt b/app/src/main/java/com/bugsnag/android/LastRunInfo.kt deleted file mode 100644 index 9797076660..0000000000 --- a/app/src/main/java/com/bugsnag/android/LastRunInfo.kt +++ /dev/null @@ -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)" - } -} diff --git a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt deleted file mode 100644 index 89f7c4516f..0000000000 --- a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt b/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt deleted file mode 100644 index 018b1788fe..0000000000 --- a/app/src/main/java/com/bugsnag/android/LaunchCrashTracker.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/bugsnag/android/LibraryLoader.java b/app/src/main/java/com/bugsnag/android/LibraryLoader.java deleted file mode 100644 index 1a5e8caf77..0000000000 --- a/app/src/main/java/com/bugsnag/android/LibraryLoader.java +++ /dev/null @@ -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. - *

- * 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; - } -} diff --git a/app/src/main/java/com/bugsnag/android/Logger.kt b/app/src/main/java/com/bugsnag/android/Logger.kt deleted file mode 100644 index 300e769a92..0000000000 --- a/app/src/main/java/com/bugsnag/android/Logger.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt deleted file mode 100644 index b357cd9a14..0000000000 --- a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt +++ /dev/null @@ -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? - ): Set? { - 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? - ): Set? { - val delimitedStr = data.getString(key) ?: return default - return delimitedStr.splitToSequence(',') - .map { Pattern.compile(it) } - .toSet() - } -} diff --git a/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt b/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt deleted file mode 100644 index cd5fbf69e0..0000000000 --- a/app/src/main/java/com/bugsnag/android/MarshalledEventSource.kt +++ /dev/null @@ -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 - ) - } -} diff --git a/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt b/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt deleted file mode 100644 index 8168e4881e..0000000000 --- a/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt +++ /dev/null @@ -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)" - } -} diff --git a/app/src/main/java/com/bugsnag/android/Metadata.kt b/app/src/main/java/com/bugsnag/android/Metadata.kt deleted file mode 100644 index a69cdf47e8..0000000000 --- a/app/src/main/java/com/bugsnag/android/Metadata.kt +++ /dev/null @@ -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> = ConcurrentHashMap() -) : JsonStream.Streamable, MetadataAware { - - val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer() - - var redactedKeys: Set - 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) { - 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, 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, newValue as Map) - 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? { - return store[section] - } - - override fun getMetadata(section: String, key: String): Any? { - return getMetadata(section)?.get(key) - } - - fun toMap(): MutableMap> { - 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>) - newMeta.redactedKeys = redactKeys.toSet() - return newMeta - } - - internal fun mergeMaps(data: List>): MutableMap { - val keys = data.flatMap { it.keys }.toSet() - val result = ConcurrentHashMap() - - for (map in data) { - for (key in keys) { - getMergeValue(result, key, map) - } - } - return result - } - - private fun getMergeValue( - result: MutableMap, - key: String, - map: Map - ) { - 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? - val second = overridesValue as Map? - 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 - ) - - stringCount += stringAndCharCounts.itemsTrimmed - charCount += stringAndCharCounts.dataTrimmed - } - return TrimMetrics(stringCount, charCount) - } -} diff --git a/app/src/main/java/com/bugsnag/android/MetadataAware.kt b/app/src/main/java/com/bugsnag/android/MetadataAware.kt deleted file mode 100644 index eb3c6f4c02..0000000000 --- a/app/src/main/java/com/bugsnag/android/MetadataAware.kt +++ /dev/null @@ -1,12 +0,0 @@ -package com.bugsnag.android - -internal interface MetadataAware { - fun addMetadata(section: String, value: Map) - fun addMetadata(section: String, key: String, value: Any?) - - fun clearMetadata(section: String) - fun clearMetadata(section: String, key: String) - - fun getMetadata(section: String): Map? - fun getMetadata(section: String, key: String): Any? -} diff --git a/app/src/main/java/com/bugsnag/android/MetadataState.kt b/app/src/main/java/com/bugsnag/android/MetadataState.kt deleted file mode 100644 index d305b6b6e4..0000000000 --- a/app/src/main/java/com/bugsnag/android/MetadataState.kt +++ /dev/null @@ -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) { - 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) { - value.entries.forEach { - updateState { AddMetadata(section, it.key, metadata.getMetadata(section, it.key)) } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java deleted file mode 100644 index de0da4a21c..0000000000 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ /dev/null @@ -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 getUser() { - HashMap 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 getApp() { - HashMap 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 getDevice() { - DeviceDataCollector source = getClient().getDeviceDataCollector(); - HashMap 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 getMetadata() { - return getClient().getMetadata(); - } - - /** - * Retrieves a list of stored breadcrumbs from the static Client instance - */ - @NonNull - public static List 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(), 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(), type); - } - - /** - * Leaves a breadcrumb on the static client instance - */ - public static void leaveBreadcrumb(@NonNull String message, - @NonNull String type, - @NonNull Map 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 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 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 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 src, Map dst) { - for (Map.Entry 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) srcValue, (Map) 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) dstValue).addAll((Collection) 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 payloadMap = (Map) JsonHelper.INSTANCE.deserialize( - new ByteArrayInputStream(payloadBytes)); - @SuppressWarnings("unchecked") - Map staticDataMap = - (Map) 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 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 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(); - } -} diff --git a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt deleted file mode 100644 index 30b40be440..0000000000 --- a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt +++ /dev/null @@ -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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/NdkPluginCaller.kt b/app/src/main/java/com/bugsnag/android/NdkPluginCaller.kt deleted file mode 100644 index 11deefa3cf..0000000000 --- a/app/src/main/java/com/bugsnag/android/NdkPluginCaller.kt +++ /dev/null @@ -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? { - val method = getCurrentCallbackSetCounts - if (method != null) { - @Suppress("UNCHECKED_CAST") - return method.invoke(ndkPlugin) as Map - } - return null - } - - fun getCurrentNativeApiCallUsage(): Map? { - val method = getCurrentNativeApiCallUsage - if (method != null) { - @Suppress("UNCHECKED_CAST") - return method.invoke(ndkPlugin) as Map - } - return null - } - - fun initCallbackCounts(counts: Map) { - 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) { - val method = setStaticData - if (method != null) { - method.invoke(ndkPlugin, data) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/NoopLogger.kt b/app/src/main/java/com/bugsnag/android/NoopLogger.kt deleted file mode 100644 index c5f5228ab0..0000000000 --- a/app/src/main/java/com/bugsnag/android/NoopLogger.kt +++ /dev/null @@ -1,3 +0,0 @@ -package com.bugsnag.android - -internal object NoopLogger : Logger diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt deleted file mode 100644 index e8c0693cdd..0000000000 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ /dev/null @@ -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() - - @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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt deleted file mode 100644 index abcb927c77..0000000000 --- a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt +++ /dev/null @@ -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() } -} diff --git a/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt b/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt deleted file mode 100644 index fb9dedeb75..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt b/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt deleted file mode 100644 index 299066495a..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/bugsnag/android/OnSendCallback.kt b/app/src/main/java/com/bugsnag/android/OnSendCallback.kt deleted file mode 100644 index ca40dc6fdf..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnSendCallback.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt b/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt deleted file mode 100644 index 87475ef30a..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt +++ /dev/null @@ -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 -} diff --git a/app/src/main/java/com/bugsnag/android/Plugin.kt b/app/src/main/java/com/bugsnag/android/Plugin.kt deleted file mode 100644 index f477beba9a..0000000000 --- a/app/src/main/java/com/bugsnag/android/Plugin.kt +++ /dev/null @@ -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() -} diff --git a/app/src/main/java/com/bugsnag/android/PluginClient.kt b/app/src/main/java/com/bugsnag/android/PluginClient.kt deleted file mode 100644 index a9a46886ad..0000000000 --- a/app/src/main/java/com/bugsnag/android/PluginClient.kt +++ /dev/null @@ -1,98 +0,0 @@ -package com.bugsnag.android - -import com.bugsnag.android.internal.ImmutableConfig - -internal class PluginClient( - userPlugins: Set, - 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 - 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() - 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) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/RootDetector.kt b/app/src/main/java/com/bugsnag/android/RootDetector.kt deleted file mode 100644 index c187121d5e..0000000000 --- a/app/src/main/java/com/bugsnag/android/RootDetector.kt +++ /dev/null @@ -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 = 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 - } -} diff --git a/app/src/main/java/com/bugsnag/android/Session.java b/app/src/main/java/com/bugsnag/android/Session.java deleted file mode 100644 index f838aa37e2..0000000000 --- a/app/src/main/java/com/bugsnag/android/Session.java +++ /dev/null @@ -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 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 events = (Map) 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; - } -} diff --git a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt deleted file mode 100644 index 6ce21ac750..0000000000 --- a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt +++ /dev/null @@ -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") - } -} diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.kt b/app/src/main/java/com/bugsnag/android/SessionStore.kt deleted file mode 100644 index e6e0ce6aa8..0000000000 --- a/app/src/main/java/com/bugsnag/android/SessionStore.kt +++ /dev/null @@ -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 = 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() - } -} diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java deleted file mode 100644 index 25fb7058c4..0000000000 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ /dev/null @@ -1,423 +0,0 @@ -package com.bugsnag.android; - -import com.bugsnag.android.internal.BackgroundTaskService; -import com.bugsnag.android.internal.DateUtils; -import com.bugsnag.android.internal.ForegroundDetector; -import com.bugsnag.android.internal.ImmutableConfig; -import com.bugsnag.android.internal.TaskType; - -import android.app.Activity; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; - -import java.io.File; -import java.util.ArrayDeque; -import java.util.Collections; -import java.util.Date; -import java.util.Deque; -import java.util.List; -import java.util.UUID; -import java.util.concurrent.RejectedExecutionException; - -class SessionTracker extends BaseObservable implements ForegroundDetector.OnActivityCallback { - - private static final int DEFAULT_TIMEOUT_MS = 30000; - - private final Deque - foregroundActivities = new ArrayDeque<>(); - private final long timeoutMs; - - private final ImmutableConfig configuration; - private final CallbackState callbackState; - private final Client client; - final SessionStore sessionStore; - private volatile Session currentSession = null; - final BackgroundTaskService backgroundTaskService; - final Logger logger; - private boolean shouldSuppressFirstAutoSession = true; - - SessionTracker(ImmutableConfig configuration, - CallbackState callbackState, - Client client, - SessionStore sessionStore, - Logger logger, - BackgroundTaskService backgroundTaskService) { - this(configuration, callbackState, client, DEFAULT_TIMEOUT_MS, - sessionStore, logger, backgroundTaskService); - } - - SessionTracker(ImmutableConfig configuration, - CallbackState callbackState, - Client client, - long timeoutMs, - SessionStore sessionStore, - Logger logger, - BackgroundTaskService backgroundTaskService) { - this.configuration = configuration; - this.callbackState = callbackState; - this.client = client; - this.timeoutMs = timeoutMs; - this.sessionStore = sessionStore; - this.backgroundTaskService = backgroundTaskService; - this.logger = logger; - } - - /** - * Starts a new session with the given date and user. - *

- * A session will only be created if {@link Configuration#getAutoTrackSessions()} returns - * true. - * - * @param date the session start date - * @param user the session user (if any) - */ - @Nullable - @VisibleForTesting - Session startNewSession(@NonNull Date date, @Nullable User user, - boolean autoCaptured) { - if (shouldDiscardSession(autoCaptured)) { - return null; - } - String id = UUID.randomUUID().toString(); - Session session = new Session( - id, date, user, autoCaptured, - client.getNotifier(), logger, configuration.getApiKey() - ); - if (trackSessionIfNeeded(session)) { - return session; - } else { - return null; - } - } - - Session startSession(boolean autoCaptured) { - if (shouldDiscardSession(autoCaptured)) { - return null; - } - return startNewSession(new Date(), client.getUser(), autoCaptured); - } - - private boolean shouldDiscardSession(boolean autoCaptured) { - if (client.getConfig().shouldDiscardSession(autoCaptured)) { - return true; - } else { - Session existingSession = currentSession; - if (autoCaptured - && existingSession != null - && !existingSession.isAutoCaptured() - && shouldSuppressFirstAutoSession) { - shouldSuppressFirstAutoSession = false; - return true; - } - - if (autoCaptured) { - shouldSuppressFirstAutoSession = false; - } - } - return false; - } - - - void pauseSession() { - Session session = currentSession; - - if (session != null) { - session.markPaused(); - updateState(StateEvent.PauseSession.INSTANCE); - } - } - - boolean resumeSession() { - Session session = currentSession; - boolean resumed; - - if (session == null) { - session = startSession(false); - resumed = false; - } else { - resumed = session.markResumed(); - } - - if (session != null) { - notifySessionStartObserver(session); - } - return resumed; - } - - private void notifySessionStartObserver(final Session session) { - final String startedAt = DateUtils.toIso8601(session.getStartedAt()); - updateState(new StateEvent.StartSession(session.getId(), startedAt, - session.getHandledCount(), session.getUnhandledCount())); - } - - /** - * Cache details of a previously captured session. - * Append session details to all subsequent reports. - * - * @param date the session start date - * @param sessionId the unique session identifier - * @param user the session user (if any) - * @param unhandledCount the number of unhandled events which have occurred during the session - * @param handledCount the number of handled events which have occurred during the session - * @return the session - */ - @Nullable - Session registerExistingSession(@Nullable Date date, @Nullable String sessionId, - @Nullable User user, int unhandledCount, - int handledCount) { - if (client.getConfig().shouldDiscardSession(false)) { - return null; - } - Session session = null; - if (date != null && sessionId != null) { - session = new Session(sessionId, date, user, unhandledCount, handledCount, - client.getNotifier(), logger, configuration.getApiKey()); - notifySessionStartObserver(session); - } else { - updateState(StateEvent.PauseSession.INSTANCE); - } - currentSession = session; - return session; - } - - /** - * Determines whether or not a session should be tracked. If this is true, the session will be - * stored and sent to the Bugsnag API, otherwise no action will occur in this method. - * - * @param session the session - * @return true if the Session should be tracked - */ - private boolean trackSessionIfNeeded(final Session session) { - logger.d("SessionTracker#trackSessionIfNeeded() - session captured by Client"); - session.setApp(client.getAppDataCollector().generateApp()); - session.setDevice(client.getDeviceDataCollector().generateDevice()); - boolean deliverSession = callbackState.runOnSessionTasks(session, logger); - - if (deliverSession && session.markTracked()) { - currentSession = session; - notifySessionStartObserver(session); - flushInMemorySession(session); - flushAsync(); - return true; - } - return false; - } - - @Nullable - Session getCurrentSession() { - Session session = currentSession; - - if (session != null && !session.isPaused()) { - return session; - } - return null; - } - - /** - * Increments the unhandled error count on the current session, then returns a deep-copy - * of the current session. - * - * @return a copy of the current session, or null if no session has been started. - */ - Session incrementUnhandledAndCopy() { - Session session = getCurrentSession(); - if (session != null) { - return session.incrementUnhandledAndCopy(); - } - return null; - } - - /** - * Increments the handled error count on the current session, then returns a deep-copy - * of the current session. - * - * @return a copy of the current session, or null if no session has been started. - */ - Session incrementHandledAndCopy() { - Session session = getCurrentSession(); - if (session != null) { - return session.incrementHandledAndCopy(); - } - return null; - } - - /** - * Asynchronously flushes any session payloads stored on disk - */ - void flushAsync() { - try { - backgroundTaskService.submitTask(TaskType.SESSION_REQUEST, new Runnable() { - @Override - public void run() { - flushStoredSessions(); - } - }); - } catch (RejectedExecutionException ex) { - logger.w("Failed to flush session reports", ex); - } - } - - /** - * Attempts to flush session payloads stored on disk - */ - void flushStoredSessions() { - List storedFiles = sessionStore.findStoredFiles(); - - for (File storedFile : storedFiles) { - flushStoredSession(storedFile); - } - } - - void flushStoredSession(File storedFile) { - logger.d("SessionTracker#flushStoredSession() - attempting delivery"); - Session payload = new Session( - storedFile, client.getNotifier(), logger, configuration.getApiKey() - ); - - if (payload.isLegacyPayload()) { // collect data here - payload.setApp(client.getAppDataCollector().generateApp()); - payload.setDevice(client.getDeviceDataCollector().generateDevice()); - } - - DeliveryStatus deliveryStatus = deliverSessionPayload(payload); - - switch (deliveryStatus) { - case DELIVERED: - sessionStore.deleteStoredFiles(Collections.singletonList(storedFile)); - logger.d("Sent 1 new session to Bugsnag"); - break; - case UNDELIVERED: - if (sessionStore.isTooOld(storedFile)) { - logger.w("Discarding historical session (from {" - + sessionStore.getCreationDate(storedFile) - + "}) after failed delivery"); - sessionStore.deleteStoredFiles(Collections.singletonList(storedFile)); - } else { - sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile)); - logger.w("Leaving session payload for future delivery"); - } - break; - case FAILURE: - // drop bad data - logger.w("Deleting invalid session tracking payload"); - sessionStore.deleteStoredFiles(Collections.singletonList(storedFile)); - break; - default: - break; - } - } - - private void flushInMemorySession(final Session session) { - try { - backgroundTaskService.submitTask(TaskType.SESSION_REQUEST, new Runnable() { - @Override - public void run() { - deliverInMemorySession(session); - } - }); - } catch (RejectedExecutionException exception) { - // This is on the current thread but there isn't much else we can do - sessionStore.write(session); - } - } - - void deliverInMemorySession(Session session) { - try { - logger.d("SessionTracker#trackSessionIfNeeded() - attempting initial delivery"); - DeliveryStatus deliveryStatus = deliverSessionPayload(session); - - switch (deliveryStatus) { - case UNDELIVERED: - logger.w("Storing session payload for future delivery"); - sessionStore.write(session); - break; - case FAILURE: - logger.w("Dropping invalid session tracking payload"); - break; - case DELIVERED: - logger.d("Sent 1 new session to Bugsnag"); - break; - default: - break; - } - } catch (Exception exception) { - logger.w("Session tracking payload failed", exception); - } - } - - DeliveryStatus deliverSessionPayload(Session payload) { - DeliveryParams params = configuration.getSessionApiDeliveryParams(payload); - Delivery delivery = configuration.getDelivery(); - return delivery.deliver(payload, params); - } - - public void onActivityStarted(Activity activity) { - updateContext( - activity.getClass().getSimpleName(), - true - ); - } - - public void onActivityStopped(Activity activity) { - updateContext( - activity.getClass().getSimpleName(), - false - ); - } - - /** - * Tracks whether an activity is in the foreground or not. - *

- * If an activity leaves the foreground, a timeout should be recorded (e.g. 30s), during which - * no new sessions should be automatically started. - *

- * If an activity comes to the foreground and is the only foreground activity, a new session - * should be started, unless the app is within a timeout period. - * - * @param activityName the activity name - * @param activityStarting whether the activity is being started or not - */ - void updateContext(String activityName, boolean activityStarting) { - if (activityStarting) { - synchronized (foregroundActivities) { - foregroundActivities.add(activityName); - } - } else { - synchronized (foregroundActivities) { - foregroundActivities.removeLastOccurrence(activityName); - } - } - client.getContextState().setAutomaticContext(getContextActivity()); - } - - boolean isInForeground() { - return ForegroundDetector.isInForeground(); - } - - long getLastEnteredForegroundMs() { - return ForegroundDetector.getLastEnteredForegroundMs(); - } - - @Nullable - String getContextActivity() { - synchronized (foregroundActivities) { - return foregroundActivities.peekLast(); - } - } - - @Override - public void onForegroundStatus(boolean foreground, long timestamp) { - if (foreground) { - long noActivityRunningForMs = - timestamp - ForegroundDetector.getLastExitedForegroundMs(); - if (noActivityRunningForMs >= timeoutMs && configuration.getAutoTrackSessions()) { - startNewSession(new Date(), client.getUser(), true); - } - } - - // update any downstream notifiers (NDK, ReactNative, Flutter, etc.) - updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity())); - } -} diff --git a/app/src/main/java/com/bugsnag/android/Severity.kt b/app/src/main/java/com/bugsnag/android/Severity.kt deleted file mode 100644 index 24b31c34e8..0000000000 --- a/app/src/main/java/com/bugsnag/android/Severity.kt +++ /dev/null @@ -1,24 +0,0 @@ -package com.bugsnag.android - -import java.io.IOException - -/** - * The severity of an Event, one of "error", "warning" or "info". - * - * By default, unhandled exceptions will be Severity.ERROR and handled - * exceptions sent with bugsnag.notify will be Severity.WARNING. - */ -enum class Severity(private val str: String) : JsonStream.Streamable { - ERROR("error"), - WARNING("warning"), - INFO("info"); - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - writer.value(str) - } - - internal companion object { - internal fun fromDescriptor(desc: String) = values().find { it.str == desc } - } -} diff --git a/app/src/main/java/com/bugsnag/android/SeverityReason.java b/app/src/main/java/com/bugsnag/android/SeverityReason.java deleted file mode 100644 index e759bd6d59..0000000000 --- a/app/src/main/java/com/bugsnag/android/SeverityReason.java +++ /dev/null @@ -1,155 +0,0 @@ -package com.bugsnag.android; - -import static com.bugsnag.android.Severity.ERROR; -import static com.bugsnag.android.Severity.WARNING; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; -import androidx.annotation.StringDef; - -import java.io.IOException; -import java.lang.annotation.Retention; -import java.lang.annotation.RetentionPolicy; - -final class SeverityReason implements JsonStream.Streamable { - - @StringDef({REASON_UNHANDLED_EXCEPTION, REASON_STRICT_MODE, REASON_HANDLED_EXCEPTION, - REASON_HANDLED_ERROR, REASON_USER_SPECIFIED, REASON_CALLBACK_SPECIFIED, - REASON_PROMISE_REJECTION, REASON_LOG, REASON_SIGNAL, REASON_ANR }) - @Retention(RetentionPolicy.SOURCE) - @interface SeverityReasonType { - } - - static final String REASON_UNHANDLED_EXCEPTION = "unhandledException"; - static final String REASON_STRICT_MODE = "strictMode"; - static final String REASON_HANDLED_EXCEPTION = "handledException"; - static final String REASON_HANDLED_ERROR = "handledError"; - static final String REASON_USER_SPECIFIED = "userSpecifiedSeverity"; - static final String REASON_CALLBACK_SPECIFIED = "userCallbackSetSeverity"; - static final String REASON_PROMISE_REJECTION = "unhandledPromiseRejection"; - static final String REASON_SIGNAL = "signal"; - static final String REASON_LOG = "log"; - static final String REASON_ANR = "anrError"; - - @SeverityReasonType - private final String severityReasonType; - - @Nullable - private final String attributeKey; - - @Nullable - private final String attributeValue; - - private final Severity defaultSeverity; - private Severity currentSeverity; - private boolean unhandled; - final boolean originalUnhandled; - - static SeverityReason newInstance(@SeverityReasonType String severityReasonType) { - return newInstance(severityReasonType, null, null); - } - - static SeverityReason newInstance(@SeverityReasonType String reason, - @Nullable Severity severity, - @Nullable String attrVal) { - - if (reason.equals(REASON_STRICT_MODE) && Intrinsics.isEmpty(attrVal)) { - throw new IllegalArgumentException("No reason supplied for strictmode"); - } - if (!(reason.equals(REASON_STRICT_MODE) - || reason.equals(REASON_LOG)) && !Intrinsics.isEmpty(attrVal)) { - throw new IllegalArgumentException("attributeValue should not be supplied"); - } - - switch (reason) { - case REASON_UNHANDLED_EXCEPTION: - case REASON_PROMISE_REJECTION: - case REASON_SIGNAL: - case REASON_ANR: - return new SeverityReason(reason, ERROR, true, true, null, null); - case REASON_STRICT_MODE: - return new SeverityReason(reason, WARNING, true, true, attrVal, "violationType"); - case REASON_HANDLED_ERROR: - case REASON_HANDLED_EXCEPTION: - return new SeverityReason(reason, WARNING, false, false, null, null); - case REASON_USER_SPECIFIED: - case REASON_CALLBACK_SPECIFIED: - return new SeverityReason(reason, severity, false, false, null, null); - case REASON_LOG: - return new SeverityReason(reason, severity, false, false, attrVal, "level"); - default: - String msg = "Invalid argument for severityReason: '" + reason + '\''; - throw new IllegalArgumentException(msg); - } - } - - SeverityReason(String severityReasonType, - Severity currentSeverity, - boolean unhandled, - boolean originalUnhandled, - @Nullable String attributeValue, - @Nullable String attributeKey) { - this.severityReasonType = severityReasonType; - this.unhandled = unhandled; - this.originalUnhandled = originalUnhandled; - this.defaultSeverity = currentSeverity; - this.currentSeverity = currentSeverity; - this.attributeValue = attributeValue; - this.attributeKey = attributeKey; - } - - String calculateSeverityReasonType() { - return defaultSeverity == currentSeverity ? severityReasonType : REASON_CALLBACK_SPECIFIED; - } - - Severity getCurrentSeverity() { - return currentSeverity; - } - - boolean getUnhandled() { - return unhandled; - } - - void setUnhandled(boolean unhandled) { - this.unhandled = unhandled; - } - - boolean getUnhandledOverridden() { - return unhandled != originalUnhandled; - } - - boolean isOriginalUnhandled() { - return originalUnhandled; - } - - @Nullable - String getAttributeValue() { - return attributeValue; - } - - String getAttributeKey() { - return attributeKey; - } - - void setCurrentSeverity(Severity severity) { - this.currentSeverity = severity; - } - - String getSeverityReasonType() { - return severityReasonType; - } - - @Override - public void toStream(@NonNull JsonStream writer) throws IOException { - writer.beginObject() - .name("type").value(calculateSeverityReasonType()) - .name("unhandledOverridden").value(getUnhandledOverridden()); - - if (attributeKey != null && attributeValue != null) { - writer.name("attributes").beginObject() - .name(attributeKey).value(attributeValue) - .endObject(); - } - writer.endObject(); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt deleted file mode 100644 index 2f7ccc238f..0000000000 --- a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt +++ /dev/null @@ -1,46 +0,0 @@ -package com.bugsnag.android - -import android.annotation.SuppressLint -import android.content.Context -import android.content.SharedPreferences - -/** - * Reads legacy information left in SharedPreferences and migrates it to the new location. - */ -internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence { - - private val prefs: SharedPreferences? = - try { - context.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE) - } catch (e: RuntimeException) { - null - } - - /** - * This implementation will never create an ID; it will only fetch one if present. - */ - override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = - prefs?.getString(INSTALL_ID_KEY, null) - - fun loadUser(deviceId: String?) = User( - prefs?.getString(USER_ID_KEY, deviceId), - prefs?.getString(USER_EMAIL_KEY, null), - prefs?.getString(USER_NAME_KEY, null) - ) - - fun hasPrefs() = prefs?.contains(INSTALL_ID_KEY) == true - - @SuppressLint("ApplySharedPref") - fun deleteLegacyPrefs() { - if (hasPrefs()) { - prefs?.edit()?.clear()?.commit() - } - } - - companion object { - private const val INSTALL_ID_KEY = "install.iud" - private const val USER_ID_KEY = "user.id" - private const val USER_NAME_KEY = "user.name" - private const val USER_EMAIL_KEY = "user.email" - } -} diff --git a/app/src/main/java/com/bugsnag/android/Stackframe.kt b/app/src/main/java/com/bugsnag/android/Stackframe.kt deleted file mode 100644 index 54ca673bd3..0000000000 --- a/app/src/main/java/com/bugsnag/android/Stackframe.kt +++ /dev/null @@ -1,158 +0,0 @@ -package com.bugsnag.android - -import androidx.annotation.RestrictTo -import com.bugsnag.android.internal.JsonHelper -import java.io.IOException - -/** - * Represents a single stackframe from a [Throwable] - */ -class Stackframe : JsonStream.Streamable { - /** - * The name of the method that was being executed - */ - var method: String? - - /** - * The location of the source file - */ - var file: String? - - /** - * The line number within the source file this stackframe refers to - */ - var lineNumber: Number? - - /** - * Whether the package is considered to be in your project for the purposes of grouping and - * readability on the Bugsnag dashboard. Project package names can be set in - * [Configuration.projectPackages] - */ - var inProject: Boolean? - - /** - * Lines of the code surrounding the frame, where the lineNumber is the key (React Native only) - */ - var code: Map? - - /** - * The column number of the frame (React Native only) - */ - var columnNumber: Number? - - /** - * The address of the instruction where the event occurred. - */ - var frameAddress: Long? = null - - /** - * The address of the function where the event occurred. - */ - var symbolAddress: Long? = null - - /** - * The address of the library where the event occurred. - */ - var loadAddress: Long? = null - - /** - * Identifies the exact build this frame originates from. - */ - var codeIdentifier: String? = null - - /** - * Whether this frame identifies the program counter - */ - var isPC: Boolean? = null - - /** - * The type of the error - */ - var type: ErrorType? = null - - @JvmOverloads - @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) - constructor( - method: String?, - file: String?, - lineNumber: Number?, - inProject: Boolean?, - code: Map? = null, - columnNumber: Number? = null - ) { - this.method = method - this.file = file - this.lineNumber = lineNumber - this.inProject = inProject - this.code = code - this.columnNumber = columnNumber - } - - constructor(nativeFrame: NativeStackframe) : this( - nativeFrame.method, - nativeFrame.file, - nativeFrame.lineNumber, - null, - null - ) { - this.frameAddress = nativeFrame.frameAddress - this.symbolAddress = nativeFrame.symbolAddress - this.loadAddress = nativeFrame.loadAddress - this.codeIdentifier = nativeFrame.codeIdentifier - this.isPC = nativeFrame.isPC - this.type = nativeFrame.type - } - - internal constructor(json: Map) { - method = json["method"] as? String - file = json["file"] as? String - lineNumber = JsonHelper.jsonToLong(json["lineNumber"]) - inProject = json["inProject"] as? Boolean - columnNumber = json["columnNumber"] as? Number - frameAddress = JsonHelper.jsonToLong(json["frameAddress"]) - symbolAddress = JsonHelper.jsonToLong(json["symbolAddress"]) - loadAddress = JsonHelper.jsonToLong(json["loadAddress"]) - codeIdentifier = json["codeIdentifier"] as? String - isPC = json["isPC"] as? Boolean - - @Suppress("UNCHECKED_CAST") - code = json["code"] as? Map - type = (json["type"] as? String)?.let { ErrorType.fromDescriptor(it) } - } - - @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) - - inProject?.let { writer.name("inProject").value(it) } - - writer.name("columnNumber").value(columnNumber) - - 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)) } - codeIdentifier?.let { writer.name("codeIdentifier").value(it) } - isPC?.let { writer.name("isPC").value(it) } - - type?.let { - writer.name("type").value(it.desc) - } - - code?.let { map: Map -> - writer.name("code") - - map.forEach { - writer.beginObject() - writer.name(it.key) - writer.value(it.value) - writer.endObject() - } - } - writer.endObject() - } -} diff --git a/app/src/main/java/com/bugsnag/android/Stacktrace.kt b/app/src/main/java/com/bugsnag/android/Stacktrace.kt deleted file mode 100644 index e846f7c6c9..0000000000 --- a/app/src/main/java/com/bugsnag/android/Stacktrace.kt +++ /dev/null @@ -1,92 +0,0 @@ -package com.bugsnag.android - -import java.io.IOException -import kotlin.math.min - -/** - * Serialize an exception stacktrace and mark frames as "in-project" - * where appropriate. - */ -internal class Stacktrace : JsonStream.Streamable { - - companion object { - private const val STACKTRACE_TRIM_LENGTH = 200 - - /** - * Calculates whether a stackframe is 'in project' or not by checking its class against - * [Configuration.getProjectPackages]. - * - * For example if the projectPackages included 'com.example', then - * the `com.example.Foo` class would be considered in project, but `org.example.Bar` would - * not. - */ - fun inProject(className: String, projectPackages: Collection): Boolean? { - return when { - projectPackages.any { className.startsWith(it) } -> true - else -> null - } - } - - fun serializeStackframe( - el: StackTraceElement, - projectPackages: Collection, - logger: Logger - ): Stackframe? { - try { - val className = el.className - val methodName = when { - className.isNotEmpty() -> className + "." + el.methodName - else -> el.methodName - } - - return Stackframe( - methodName, - el.fileName ?: "Unknown", - el.lineNumber, - inProject(className, projectPackages) - ) - } catch (lineEx: Exception) { - logger.w("Failed to serialize stacktrace", lineEx) - return null - } - } - } - - val trace: MutableList - - constructor(frames: MutableList) { - trace = limitTraceLength(frames) - } - - constructor( - stacktrace: Array, - projectPackages: Collection, - logger: Logger - ) { - // avoid allocating new subLists or Arrays by only copying the required number of frames - // mapping them to our internal Stackframes as we go, roughly equivalent to - // stacktrace.take(STACKTRACE_TRIM_LENGTH).mapNotNullTo(ArrayList()) { ... } - val frameCount = min(STACKTRACE_TRIM_LENGTH, stacktrace.size) - trace = ArrayList(frameCount) - for (i in 0 until frameCount) { - val frame = serializeStackframe(stacktrace[i], projectPackages, logger) - if (frame != null) { - trace.add(frame) - } - } - } - - private fun limitTraceLength(frames: MutableList): MutableList { - return when { - frames.size >= STACKTRACE_TRIM_LENGTH -> frames.subList(0, STACKTRACE_TRIM_LENGTH) - else -> frames - } - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - writer.beginArray() - trace.forEach { writer.value(it) } - writer.endArray() - } -} diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt deleted file mode 100644 index e16671559f..0000000000 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.bugsnag.android - -sealed class StateEvent { // JvmField allows direct field access optimizations - - class Install( - @JvmField val apiKey: String, - @JvmField val autoDetectNdkCrashes: Boolean, - @JvmField val appVersion: String?, - @JvmField val buildUuid: String?, - @JvmField val releaseStage: String?, - @JvmField val lastRunInfoPath: String, - @JvmField val consecutiveLaunchCrashes: Int, - @JvmField val sendThreads: ThreadSendPolicy, - @JvmField val maxBreadcrumbs: Int - ) : StateEvent() - - object DeliverPending : StateEvent() - - class AddMetadata( - @JvmField val section: String, - @JvmField val key: String?, - @JvmField val value: Any? - ) : StateEvent() - - class ClearMetadataSection(@JvmField val section: String) : StateEvent() - - class ClearMetadataValue( - @JvmField val section: String, - @JvmField val key: String? - ) : StateEvent() - - class AddBreadcrumb( - @JvmField val message: String, - @JvmField val type: BreadcrumbType, - @JvmField val timestamp: String, - @JvmField val metadata: MutableMap - ) : StateEvent() - - object NotifyHandled : StateEvent() - - object NotifyUnhandled : StateEvent() - - object PauseSession : StateEvent() - - class StartSession( - @JvmField val id: String, - @JvmField val startedAt: String, - @JvmField val handledCount: Int, - val unhandledCount: Int - ) : StateEvent() - - class UpdateContext(@JvmField val context: String?) : StateEvent() - - class UpdateInForeground( - @JvmField val inForeground: Boolean, - val contextActivity: String? - ) : StateEvent() - - class UpdateLastRunInfo(@JvmField val consecutiveLaunchCrashes: Int) : StateEvent() - - class UpdateIsLaunching(@JvmField val isLaunching: Boolean) : StateEvent() - - class UpdateOrientation(@JvmField val orientation: String?) : StateEvent() - - class UpdateUser(@JvmField val user: User) : StateEvent() - - class UpdateMemoryTrimEvent( - @JvmField val isLowMemory: Boolean, - @JvmField val memoryTrimLevel: Int? = null, - @JvmField val memoryTrimLevelDescription: String = "None" - ) : StateEvent() - - class AddFeatureFlag( - @JvmField val name: String, - @JvmField val variant: String? = null - ) : StateEvent() - - class ClearFeatureFlag( - @JvmField val name: String - ) : StateEvent() - - object ClearFeatureFlags : StateEvent() -} diff --git a/app/src/main/java/com/bugsnag/android/StorageModule.kt b/app/src/main/java/com/bugsnag/android/StorageModule.kt deleted file mode 100644 index 46a07da023..0000000000 --- a/app/src/main/java/com/bugsnag/android/StorageModule.kt +++ /dev/null @@ -1,73 +0,0 @@ -package com.bugsnag.android - -import android.content.Context -import com.bugsnag.android.internal.BackgroundTaskService -import com.bugsnag.android.internal.BugsnagStoreMigrator.migrateLegacyFiles -import com.bugsnag.android.internal.ImmutableConfig -import com.bugsnag.android.internal.TaskType -import com.bugsnag.android.internal.dag.BackgroundDependencyModule -import com.bugsnag.android.internal.dag.Provider - -/** - * A dependency module which constructs the objects that store information to disk in Bugsnag. - */ -internal class StorageModule( - appContext: Context, - private val immutableConfig: ImmutableConfig, - bgTaskService: BackgroundTaskService -) : BackgroundDependencyModule(bgTaskService, TaskType.IO) { - - val bugsnagDir = provider { - migrateLegacyFiles(immutableConfig.persistenceDirectory) - } - - val sharedPrefMigrator = provider { - SharedPrefMigrator(appContext) - } - - val deviceIdStore = provider { - DeviceIdStore( - appContext, - sharedPrefMigrator = sharedPrefMigrator, - logger = immutableConfig.logger, - config = immutableConfig - ) - } - - val userStore = provider { - UserStore( - immutableConfig.persistUser, - bugsnagDir, - deviceIdStore.map { it.load() }, - sharedPrefMigrator = sharedPrefMigrator, - logger = immutableConfig.logger - ) - } - - val lastRunInfoStore = provider { - LastRunInfoStore(immutableConfig) - } - - val sessionStore = provider { - SessionStore( - bugsnagDir.get(), - immutableConfig.maxPersistedSessions, - immutableConfig.apiKey, - immutableConfig.logger, - null - ) - } - - val lastRunInfo = lastRunInfoStore.map { lastRunInfoStore -> - val info = lastRunInfoStore.load() - val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false) - lastRunInfoStore.persist(currentRunInfo) - return@map info - } - - fun loadUser(initialUser: User): Provider = provider { - val userState = userStore.get().load(initialUser) - sharedPrefMigrator.getOrNull()?.deleteLegacyPrefs() - return@provider userState - } -} diff --git a/app/src/main/java/com/bugsnag/android/StrictModeHandler.java b/app/src/main/java/com/bugsnag/android/StrictModeHandler.java deleted file mode 100644 index 002466b85b..0000000000 --- a/app/src/main/java/com/bugsnag/android/StrictModeHandler.java +++ /dev/null @@ -1,95 +0,0 @@ -package com.bugsnag.android; - -import android.annotation.SuppressLint; -import android.text.TextUtils; - -import androidx.annotation.Nullable; - -import java.util.HashMap; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -class StrictModeHandler { - - // Byte 1: Thread-policy (needs to be synced with StrictMode constants) - private static final int DETECT_DISK_WRITE = 0x01; - private static final int DETECT_DISK_READ = 0x02; - private static final int DETECT_NETWORK = 0x04; - private static final int DETECT_CUSTOM = 0x08; - private static final int DETECT_RESOURCE_MISMATCH = 0x10; - - // Byte 2: Process-policy (needs to be synced with StrictMode constants) - private static final int DETECT_VM_CURSOR_LEAKS = 0x01 << 8; - private static final int DETECT_VM_CLOSABLE_LEAKS = 0x02 << 8; - private static final int DETECT_VM_ACTIVITY_LEAKS = 0x04 << 8; - private static final int DETECT_VM_INSTANCE_LEAKS = 0x08 << 8; - private static final int DETECT_VM_REGISTRATION_LEAKS = 0x10 << 8; - private static final int DETECT_VM_FILE_URI_EXPOSURE = 0x20 << 8; - private static final int DETECT_VM_CLEARTEXT_NETWORK = 0x40 << 8; - - - private static final String STRICT_MODE_CLZ_NAME = "android.os.strictmode"; - - @SuppressLint("UseSparseArrays") - private static final Map POLICY_CODE_MAP = new HashMap<>(); - - static { - POLICY_CODE_MAP.put(DETECT_DISK_WRITE, "DiskWrite"); - POLICY_CODE_MAP.put(DETECT_DISK_READ, "DiskRead"); - POLICY_CODE_MAP.put(DETECT_NETWORK, "NetworkOperation"); - POLICY_CODE_MAP.put(DETECT_CUSTOM, "CustomSlowCall"); - POLICY_CODE_MAP.put(DETECT_RESOURCE_MISMATCH, "ResourceMismatch"); - - POLICY_CODE_MAP.put(DETECT_VM_CURSOR_LEAKS, "CursorLeak"); - POLICY_CODE_MAP.put(DETECT_VM_CLOSABLE_LEAKS, "CloseableLeak"); - POLICY_CODE_MAP.put(DETECT_VM_ACTIVITY_LEAKS, "ActivityLeak"); - POLICY_CODE_MAP.put(DETECT_VM_INSTANCE_LEAKS, "InstanceLeak"); - POLICY_CODE_MAP.put(DETECT_VM_REGISTRATION_LEAKS, "RegistrationLeak"); - POLICY_CODE_MAP.put(DETECT_VM_FILE_URI_EXPOSURE, "FileUriLeak"); - POLICY_CODE_MAP.put(DETECT_VM_CLEARTEXT_NETWORK, "CleartextNetwork"); - } - - /** - * Checks whether a throwable was originally thrown from the StrictMode class - * - * @param throwable the throwable - * @return true if the throwable's root cause is a StrictMode policy violation - */ - boolean isStrictModeThrowable(Throwable throwable) { - Throwable cause = getRootCause(throwable); - Class causeClass = cause.getClass(); - String simpleName = causeClass.getName(); - return simpleName.toLowerCase(Locale.US).startsWith(STRICT_MODE_CLZ_NAME); - } - - @Nullable - String getViolationDescription(String exceptionMessage) { - if (TextUtils.isEmpty(exceptionMessage)) { - throw new IllegalArgumentException(); - } - int indexOf = exceptionMessage.lastIndexOf("violation="); - - if (indexOf != -1) { - String substring = exceptionMessage.substring(indexOf); - substring = substring.replace("violation=", ""); - - if (TextUtils.isDigitsOnly(substring)) { - Integer code = Integer.valueOf(substring); - return POLICY_CODE_MAP.get(code); - } - } - return null; - } - - /** - * Recurse the stack to get the original cause of the throwable - * - * @param throwable the throwable - * @return the root cause of the throwable - */ - private Throwable getRootCause(Throwable throwable) { - List causes = ThrowableUtils.safeUnrollCauses(throwable); - return causes.get(causes.size() - 1); - } -} diff --git a/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt b/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt deleted file mode 100644 index 4e2413889a..0000000000 --- a/app/src/main/java/com/bugsnag/android/StrictModeOnErrorCallback.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bugsnag.android - -internal class StrictModeOnErrorCallback(private val errMsg: String) : OnErrorCallback { - override fun onError(event: Event): Boolean { - event.updateSeverityInternal(Severity.INFO) - event.updateSeverityReason(SeverityReason.REASON_STRICT_MODE) - val error = event.errors.firstOrNull() - error?.errorMessage = errMsg - return true - } -} diff --git a/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt b/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt deleted file mode 100644 index f7d53367c9..0000000000 --- a/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.bugsnag.android - -import android.util.JsonReader -import java.io.File -import java.io.IOException -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.withLock - -/** - * Persists and loads a [Streamable] object to the file system. This is intended for use - * primarily as a replacement for primitive value stores such as [SharedPreferences]. - * - * This class is made thread safe through the use of a [ReadWriteLock]. - */ -internal class SynchronizedStreamableStore( - internal val file: File -) { - - private val lock = ReentrantReadWriteLock() - - @Throws(IOException::class) - fun persist(streamable: T) { - lock.writeLock().withLock { - file.writer().buffered().use { - streamable.toStream(JsonStream(it)) - true - } - } - } - - @Throws(IOException::class) - fun load(loadCallback: (JsonReader) -> T): T { - lock.readLock().withLock { - return file.reader().buffered().use { - loadCallback(JsonReader(it)) - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt b/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt deleted file mode 100644 index d6c89583c0..0000000000 --- a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt +++ /dev/null @@ -1,131 +0,0 @@ -package com.bugsnag.android - -import android.content.BroadcastReceiver -import android.content.Context -import android.content.Intent -import android.content.IntentFilter -import java.util.HashMap - -/** - * Used to automatically create breadcrumbs for system events - * Broadcast actions and categories can be found in text files in the android folder - * e.g. ~/Library/Android/sdk/platforms/android-9/data/broadcast_actions.txt - * See http://stackoverflow.com/a/27601497 - */ -internal class SystemBroadcastReceiver( - private val client: Client, - private val logger: Logger -) : BroadcastReceiver() { - - companion object { - private const val INTENT_ACTION_KEY = "Intent Action" - - @JvmStatic - fun register(ctx: Context, receiver: SystemBroadcastReceiver, logger: Logger) { - if (receiver.actions.isNotEmpty()) { - val filter = IntentFilter() - receiver.actions.keys.forEach(filter::addAction) - ctx.registerReceiverSafe(receiver, filter, logger) - } - } - - fun isAndroidKey(actionName: String): Boolean { - return actionName.startsWith("android.") - } - - fun shortenActionNameIfNeeded(action: String): String { - return if (isAndroidKey(action)) { - action.substringAfterLast('.') - } else { - action - } - } - } - - val actions: Map = buildActions() - - override fun onReceive(context: Context, intent: Intent) { - try { - val meta: MutableMap = HashMap() - val fullAction = intent.action ?: return - val shortAction = shortenActionNameIfNeeded(fullAction) - meta[INTENT_ACTION_KEY] = fullAction // always add the Intent Action - addExtrasToMetadata(intent, meta, shortAction) - - val type = actions[fullAction] ?: BreadcrumbType.STATE - client.leaveBreadcrumb(shortAction, meta, type) - } catch (ex: Exception) { - logger.w("Failed to leave breadcrumb in SystemBroadcastReceiver: ${ex.message}") - } - } - - private fun addExtrasToMetadata( - intent: Intent, - meta: MutableMap, - shortAction: String - ) { - val extras = intent.extras - extras?.keySet()?.forEach { key -> - @Suppress("DEPRECATION") - val valObj = extras[key] ?: return@forEach - val strVal = valObj.toString() - if (isAndroidKey(key)) { // shorten the Intent action - meta["Extra"] = "$shortAction: $strVal" - } else { - meta[key] = strVal - } - } - } - - /** - * Builds a map of intent actions and their breadcrumb type (if enabled). - * - * Noisy breadcrumbs are omitted, along with anything that involves a state change. - * @return the action map - */ - private fun buildActions(): Map { - val actions: MutableMap = HashMap() - val config = client.config - - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.USER)) { - actions["android.appwidget.action.APPWIDGET_DELETED"] = BreadcrumbType.USER - actions["android.appwidget.action.APPWIDGET_DISABLED"] = BreadcrumbType.USER - actions["android.appwidget.action.APPWIDGET_ENABLED"] = BreadcrumbType.USER - actions["android.intent.action.CAMERA_BUTTON"] = BreadcrumbType.USER - actions["android.intent.action.CLOSE_SYSTEM_DIALOGS"] = BreadcrumbType.USER - actions["android.intent.action.DOCK_EVENT"] = BreadcrumbType.USER - } - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { - actions["android.appwidget.action.APPWIDGET_HOST_RESTORED"] = BreadcrumbType.STATE - actions["android.appwidget.action.APPWIDGET_RESTORED"] = BreadcrumbType.STATE - actions["android.appwidget.action.APPWIDGET_UPDATE"] = BreadcrumbType.STATE - actions["android.appwidget.action.APPWIDGET_UPDATE_OPTIONS"] = BreadcrumbType.STATE - actions["android.intent.action.ACTION_POWER_CONNECTED"] = BreadcrumbType.STATE - actions["android.intent.action.ACTION_POWER_DISCONNECTED"] = BreadcrumbType.STATE - actions["android.intent.action.ACTION_SHUTDOWN"] = BreadcrumbType.STATE - actions["android.intent.action.AIRPLANE_MODE"] = BreadcrumbType.STATE - actions["android.intent.action.BATTERY_LOW"] = BreadcrumbType.STATE - actions["android.intent.action.BATTERY_OKAY"] = BreadcrumbType.STATE - actions["android.intent.action.BOOT_COMPLETED"] = BreadcrumbType.STATE - actions["android.intent.action.CONFIGURATION_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.CONTENT_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.DATE_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.DEVICE_STORAGE_LOW"] = BreadcrumbType.STATE - actions["android.intent.action.DEVICE_STORAGE_OK"] = BreadcrumbType.STATE - actions["android.intent.action.INPUT_METHOD_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.LOCALE_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.REBOOT"] = BreadcrumbType.STATE - actions["android.intent.action.SCREEN_OFF"] = BreadcrumbType.STATE - actions["android.intent.action.SCREEN_ON"] = BreadcrumbType.STATE - actions["android.intent.action.TIMEZONE_CHANGED"] = BreadcrumbType.STATE - actions["android.intent.action.TIME_SET"] = BreadcrumbType.STATE - actions["android.os.action.DEVICE_IDLE_MODE_CHANGED"] = BreadcrumbType.STATE - actions["android.os.action.POWER_SAVE_MODE_CHANGED"] = BreadcrumbType.STATE - } - if (!config.shouldDiscardBreadcrumb(BreadcrumbType.NAVIGATION)) { - actions["android.intent.action.DREAMING_STARTED"] = BreadcrumbType.NAVIGATION - actions["android.intent.action.DREAMING_STOPPED"] = BreadcrumbType.NAVIGATION - } - return actions - } -} diff --git a/app/src/main/java/com/bugsnag/android/Telemetry.kt b/app/src/main/java/com/bugsnag/android/Telemetry.kt deleted file mode 100644 index dde78fd5d6..0000000000 --- a/app/src/main/java/com/bugsnag/android/Telemetry.kt +++ /dev/null @@ -1,21 +0,0 @@ -package com.bugsnag.android - -/** - * Types of telemetry that may be sent to Bugsnag for product improvement purposes. - */ -enum class Telemetry { - - /** - * Errors within the Bugsnag SDK. - */ - INTERNAL_ERRORS, - - /** - * Differences from the default configuration. - */ - USAGE; - - internal companion object { - fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS - } -} diff --git a/app/src/main/java/com/bugsnag/android/Thread.java b/app/src/main/java/com/bugsnag/android/Thread.java deleted file mode 100644 index a6368ab275..0000000000 --- a/app/src/main/java/com/bugsnag/android/Thread.java +++ /dev/null @@ -1,252 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.List; - -/** - * A representation of a thread recorded in an {@link Event} - */ -@SuppressWarnings("ConstantConditions") -public class Thread implements JsonStream.Streamable { - - private final ThreadInternal impl; - private final Logger logger; - - Thread( - String id, - @NonNull String name, - @NonNull ErrorType type, - boolean errorReportingThread, - @NonNull Thread.State state, - @NonNull Logger logger) { - - this.impl = new ThreadInternal( - id, - name, - type, - errorReportingThread, - state.getDescriptor(), - new Stacktrace(new ArrayList()) - ); - - this.logger = logger; - } - - Thread( - String id, - @NonNull String name, - @NonNull ErrorType type, - boolean errorReportingThread, - @NonNull Thread.State state, - @NonNull Stacktrace stacktrace, - @NonNull Logger logger) { - this.impl = new ThreadInternal( - id, name, type, errorReportingThread, state.getDescriptor(), stacktrace); - this.logger = logger; - } - - Thread(@NonNull ThreadInternal impl, @NonNull Logger logger) { - this.impl = impl; - this.logger = logger; - } - - private void logNull(String property) { - logger.e("Invalid null value supplied to thread." + property + ", ignoring"); - } - - /** - * Sets the unique ID of the thread (from {@link java.lang.Thread}) - */ - public void setId(@NonNull String id) { - if (id != null) { - impl.setId(id); - } else { - logNull("id"); - } - } - - /** - * Gets the unique ID of the thread (from {@link java.lang.Thread}) - */ - @NonNull - public String getId() { - return impl.getId(); - } - - /** - * Sets the name of the thread (from {@link java.lang.Thread}) - */ - public void setName(@NonNull String name) { - if (name != null) { - impl.setName(name); - } else { - logNull("name"); - } - } - - /** - * Gets the name of the thread (from {@link java.lang.Thread}) - */ - @NonNull - public String getName() { - return impl.getName(); - } - - /** - * Sets the type of thread based on the originating platform (intended for internal use only) - */ - public void setType(@NonNull ErrorType type) { - if (type != null) { - impl.setType(type); - } else { - logNull("type"); - } - } - - /** - * Gets the type of thread based on the originating platform (intended for internal use only) - */ - @NonNull - public ErrorType getType() { - return impl.getType(); - } - - /** - * Sets the state of thread (from {@link java.lang.Thread}) - */ - public void setState(@NonNull Thread.State threadState) { - if (threadState != null) { - impl.setState(threadState.getDescriptor()); - } else { - logNull("state"); - } - } - - /** - * Gets the state of the thread (from {@link java.lang.Thread}) - */ - @NonNull - public Thread.State getState() { - return Thread.State.byDescriptor(impl.getState()); - } - - /** - * Gets whether the thread was the thread that caused the event - */ - public boolean getErrorReportingThread() { - return impl.isErrorReportingThread(); - } - - /** - * Sets a representation of the thread's stacktrace - */ - public void setStacktrace(@NonNull List stacktrace) { - if (!CollectionUtils.containsNullElements(stacktrace)) { - impl.setStacktrace(stacktrace); - } else { - logNull("stacktrace"); - } - } - - /** - * Gets a representation of the thread's stacktrace - */ - @NonNull - public List getStacktrace() { - return impl.getStacktrace(); - } - - /** - * Add a new stackframe to the end of this thread 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); - } - - /** - * The state of a reported {@link Thread}. These states correspond directly to - * {@link java.lang.Thread.State}, except for {@code UNKNOWN} which indicates that - * a state could not be captured or mapped. - */ - public enum State { - NEW("NEW"), - BLOCKED("BLOCKED"), - RUNNABLE("RUNNABLE"), - TERMINATED("TERMINATED"), - TIMED_WAITING("TIMED_WAITING"), - WAITING("WAITING"), - UNKNOWN("UNKNOWN"); - - private final String descriptor; - - State(String descriptor) { - this.descriptor = descriptor; - } - - @NonNull - public String getDescriptor() { - return descriptor; - } - - @NonNull - public static State forThread(@NonNull java.lang.Thread thread) { - java.lang.Thread.State state = thread.getState(); - return getState(state); - } - - /** - * Lookup the {@code State} for a given {@link #getDescriptor() descriptor} code. Unlike - * {@link #valueOf(String) valueOf}, this method will return {@link #UNKNOWN} is no - * matching {@code State} constant can be found. - * - * @param descriptor a consistent descriptor of the state constant to lookup - * @return the requested {@link State} or {@link #UNKNOWN} - */ - @NonNull - public static State byDescriptor(@Nullable String descriptor) { - if (descriptor == null) { - return UNKNOWN; - } - - for (State state : values()) { - if (state.getDescriptor().equals(descriptor)) { - return state; - } - } - - return UNKNOWN; - } - - @NonNull - private static State getState(java.lang.Thread.State state) { - switch (state) { - case NEW: - return NEW; - case BLOCKED: - return BLOCKED; - case RUNNABLE: - return RUNNABLE; - case TERMINATED: - return TERMINATED; - case TIMED_WAITING: - return TIMED_WAITING; - case WAITING: - return WAITING; - default: - return UNKNOWN; - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt deleted file mode 100644 index 0671ad6ec9..0000000000 --- a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt +++ /dev/null @@ -1,40 +0,0 @@ -package com.bugsnag.android - -import java.io.IOException - -class ThreadInternal internal constructor( - var id: String, - var name: String, - var type: ErrorType, - val isErrorReportingThread: Boolean, - var state: String, - stacktrace: Stacktrace -) : JsonStream.Streamable { - - var stacktrace: MutableList = stacktrace.trace.toMutableList() - - fun addStackframe(method: String?, file: String?, lineNumber: Long): Stackframe { - val frame = Stackframe(method, file, lineNumber, null) - stacktrace.add(frame) - return frame - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - writer.beginObject() - writer.name("id").value(id) - writer.name("name").value(name) - writer.name("type").value(type.desc) - writer.name("state").value(state) - - writer.name("stacktrace") - writer.beginArray() - stacktrace.forEach { writer.value(it) } - writer.endArray() - - if (isErrorReportingThread) { - writer.name("errorReportingThread").value(true) - } - writer.endObject() - } -} diff --git a/app/src/main/java/com/bugsnag/android/ThreadSendPolicy.kt b/app/src/main/java/com/bugsnag/android/ThreadSendPolicy.kt deleted file mode 100644 index beb9bee0dd..0000000000 --- a/app/src/main/java/com/bugsnag/android/ThreadSendPolicy.kt +++ /dev/null @@ -1,27 +0,0 @@ -package com.bugsnag.android - -/** - * Controls whether we should capture and serialize the state of all threads at the time - * of an error. - */ -enum class ThreadSendPolicy { - - /** - * Threads should be captured for all events. - */ - ALWAYS, - - /** - * Threads should be captured for unhandled events only. - */ - UNHANDLED_ONLY, - - /** - * Threads should never be captured. - */ - NEVER; - - internal companion object { - fun fromString(str: String) = values().find { it.name == str } ?: ALWAYS - } -} diff --git a/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt deleted file mode 100644 index abab23d4dd..0000000000 --- a/app/src/main/java/com/bugsnag/android/ThreadState.kt +++ /dev/null @@ -1,179 +0,0 @@ -package com.bugsnag.android - -import android.os.SystemClock -import com.bugsnag.android.internal.ImmutableConfig -import java.io.IOException -import kotlin.math.max -import kotlin.math.min -import java.lang.Thread as JavaThread - -/** - * Capture and serialize the state of all threads at the time of an exception. - */ -internal class ThreadState @Suppress("LongParameterList") constructor( - exc: Throwable?, - isUnhandled: Boolean, - maxThreads: Int, - threadCollectionTimeLimitMillis: Long, - sendThreads: ThreadSendPolicy, - projectPackages: Collection, - logger: Logger, - currentThread: JavaThread = JavaThread.currentThread(), - allThreads: List = allThreads() -) : JsonStream.Streamable { - - internal constructor( - exc: Throwable?, - isUnhandled: Boolean, - config: ImmutableConfig - ) : this( - exc, - isUnhandled, - config.maxReportedThreads, - config.threadCollectionTimeLimitMillis, - config.sendThreads, - config.projectPackages, - config.logger - ) - - val threads: MutableList - - init { - val recordThreads = sendThreads == ThreadSendPolicy.ALWAYS || - (sendThreads == ThreadSendPolicy.UNHANDLED_ONLY && isUnhandled) - - threads = when { - recordThreads -> captureThreadTrace( - allThreads, - currentThread, - exc, - isUnhandled, - maxThreads, - threadCollectionTimeLimitMillis, - projectPackages, - logger - ) - else -> mutableListOf() - } - } - - companion object { - private fun rootThreadGroup(): ThreadGroup { - var group = JavaThread.currentThread().threadGroup!! - - while (group.parent != null) { - group = group.parent - } - - return group - } - - internal fun allThreads(): List { - val rootGroup = rootThreadGroup() - val threadCount = rootGroup.activeCount() - val threads: Array = arrayOfNulls(threadCount) - rootGroup.enumerate(threads) - return threads.filterNotNull() - } - } - - private fun captureThreadTrace( - allThreads: List, - currentThread: JavaThread, - exc: Throwable?, - isUnhandled: Boolean, - maxThreadCount: Int, - threadCollectionTimeLimitMillis: Long, - projectPackages: Collection, - logger: Logger - ): MutableList { - fun toBugsnagThread(thread: JavaThread): Thread { - val isErrorThread = thread.id == currentThread.id - val stackTrace = Stacktrace( - if (isErrorThread) { - if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces - exc.stackTrace - } else { - currentThread.stackTrace - } - } else { - thread.stackTrace - }, - projectPackages, logger - ) - - return Thread( - thread.id.toString(), - thread.name, - ErrorType.ANDROID, - isErrorThread, - Thread.State.forThread(thread), - stackTrace, - logger - ) - } - - // Keep the lowest ID threads (ordered). Anything after maxThreadCount is lost. - // Note: We must ensure that currentThread is always present in the final list regardless. - - val sortedThreads = allThreads.sortedBy { it.id } - val currentThreadIndex = sortedThreads.binarySearch(0, min(maxThreadCount, sortedThreads.size)) { - it.id.compareTo(currentThread.id) - } - - // API 24/25 don't record the currentThread, so add it in manually - // https://issuetracker.google.com/issues/64122757 - // currentThread may also have been removed if its ID occurred after maxThreadCount - // as such we may need to leave a space in new list for currentThread - val keepThreads = sortedThreads.take( - if (currentThreadIndex >= 0) maxThreadCount else max(maxThreadCount - 1, 0) - ) - - val reportThreads = ArrayList(maxThreadCount) - - val timeout = SystemClock.elapsedRealtime() + threadCollectionTimeLimitMillis - for (thread in keepThreads) { - if (SystemClock.elapsedRealtime() >= timeout) { - break - } - reportThreads.add(toBugsnagThread(thread)) - } - - if (currentThreadIndex < 0) { - val expectedIndex = -currentThreadIndex - 1 - if (expectedIndex >= reportThreads.size) { - reportThreads.add(toBugsnagThread(currentThread)) - } else { - reportThreads.add(expectedIndex, toBugsnagThread(currentThread)) - } - } else if (currentThreadIndex >= reportThreads.size) { - // if this is the case we have failed to collect maxThreadCount within the timeout - // so we can safely add currentThread to the end of the list without going over maxThreadCount - reportThreads.add(toBugsnagThread(currentThread)) - } - - if (allThreads.size > maxThreadCount) { - reportThreads.add( - Thread( - "", - "[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]", - ErrorType.UNKNOWN, - false, - Thread.State.UNKNOWN, - Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger), - logger - ) - ) - } - return reportThreads - } - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - writer.beginArray() - for (thread in threads) { - writer.value(thread) - } - writer.endArray() - } -} diff --git a/app/src/main/java/com/bugsnag/android/ThrowableExtensions.kt b/app/src/main/java/com/bugsnag/android/ThrowableExtensions.kt deleted file mode 100644 index cf56fd51c7..0000000000 --- a/app/src/main/java/com/bugsnag/android/ThrowableExtensions.kt +++ /dev/null @@ -1,19 +0,0 @@ -@file:JvmName("ThrowableUtils") -package com.bugsnag.android - -/** - * Unroll the list of causes for this Throwable, handling any recursion that may appear within - * the chain. The first element returned will be this Throwable, and the last will be the root - * cause or last non-recursive Throwable. - */ -internal fun Throwable.safeUnrollCauses(): List { - val causes = LinkedHashSet() - var currentEx: Throwable? = this - - // Set.add will return false if we have already "seen" currentEx - while (currentEx != null && causes.add(currentEx)) { - currentEx = currentEx.cause - } - - return causes.toList() -} diff --git a/app/src/main/java/com/bugsnag/android/TraceCorrelation.kt b/app/src/main/java/com/bugsnag/android/TraceCorrelation.kt deleted file mode 100644 index 4fc01ac60f..0000000000 --- a/app/src/main/java/com/bugsnag/android/TraceCorrelation.kt +++ /dev/null @@ -1,20 +0,0 @@ -package com.bugsnag.android - -import java.util.UUID - -internal data class TraceCorrelation(val traceId: UUID, val spanId: Long) : JsonStream.Streamable { - override fun toStream(writer: JsonStream) { - writer.beginObject() - .name("traceId").value(traceId.toHexString()) - .name("spanId").value(spanId.toHexString()) - writer.endObject() - } - - private fun UUID.toHexString(): String { - return "%016x%016x".format(mostSignificantBits, leastSignificantBits) - } - - private fun Long.toHexString(): String { - return "%016x".format(this) - } -} diff --git a/app/src/main/java/com/bugsnag/android/TrackerModule.kt b/app/src/main/java/com/bugsnag/android/TrackerModule.kt deleted file mode 100644 index 8eb6c47607..0000000000 --- a/app/src/main/java/com/bugsnag/android/TrackerModule.kt +++ /dev/null @@ -1,34 +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 - -/** - * A dependency module which constructs objects that track launch/session related information - * in Bugsnag. - */ -internal class TrackerModule( - configModule: ConfigModule, - storageModule: StorageModule, - client: Client, - bgTaskService: BackgroundTaskService, - callbackState: CallbackState -) : BackgroundDependencyModule(bgTaskService) { - - private val config = configModule.config - - val launchCrashTracker = LaunchCrashTracker(config) - - val sessionTracker = provider { - client.config - SessionTracker( - config, - callbackState, - client, - storageModule.sessionStore.get(), - config.logger, - bgTaskService - ) - } -} diff --git a/app/src/main/java/com/bugsnag/android/User.kt b/app/src/main/java/com/bugsnag/android/User.kt deleted file mode 100644 index a2b2fd3267..0000000000 --- a/app/src/main/java/com/bugsnag/android/User.kt +++ /dev/null @@ -1,83 +0,0 @@ -package com.bugsnag.android - -import android.util.JsonReader -import java.io.IOException - -/** - * Information about the current user of your application. - */ -class User @JvmOverloads internal constructor( - /** - * @return the user ID, by default a UUID generated on installation - */ - val id: String? = null, - - /** - * @return the user's email, if available - */ - val email: String? = null, - - /** - * @return the user's name, if available - */ - val name: String? = null -) : JsonStream.Streamable { - - @Throws(IOException::class) - override fun toStream(writer: JsonStream) { - writer.beginObject() - writer.name(KEY_ID).value(id) - writer.name(KEY_EMAIL).value(email) - writer.name(KEY_NAME).value(name) - writer.endObject() - } - - internal companion object : JsonReadable { - private const val KEY_ID = "id" - private const val KEY_NAME = "name" - private const val KEY_EMAIL = "email" - - override fun fromReader(reader: JsonReader): User { - var user: User - with(reader) { - beginObject() - var id: String? = null - var email: String? = null - var name: String? = null - - while (hasNext()) { - val key = nextName() - val value = nextString() - when (key) { - KEY_ID -> id = value - KEY_EMAIL -> email = value - KEY_NAME -> name = value - } - } - user = User(id, email, name) - endObject() - } - return user - } - } - - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as User - - if (id != other.id) return false - if (email != other.email) return false - if (name != other.name) return false - - return true - } - - override fun hashCode(): Int { - var result = id?.hashCode() ?: 0 - result = 31 * result + (email?.hashCode() ?: 0) - result = 31 * result + (name?.hashCode() ?: 0) - return result - } -} diff --git a/app/src/main/java/com/bugsnag/android/UserAware.kt b/app/src/main/java/com/bugsnag/android/UserAware.kt deleted file mode 100644 index 3798d2a074..0000000000 --- a/app/src/main/java/com/bugsnag/android/UserAware.kt +++ /dev/null @@ -1,6 +0,0 @@ -package com.bugsnag.android - -internal interface UserAware { - fun getUser(): User - fun setUser(id: String?, email: String?, name: String?) -} diff --git a/app/src/main/java/com/bugsnag/android/UserState.kt b/app/src/main/java/com/bugsnag/android/UserState.kt deleted file mode 100644 index 16da106515..0000000000 --- a/app/src/main/java/com/bugsnag/android/UserState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bugsnag.android - -internal class UserState(user: User) : BaseObservable() { - var user = user - set(value) { - field = value - emitObservableEvent() - } - - fun emitObservableEvent() = updateState { StateEvent.UpdateUser(user) } -} diff --git a/app/src/main/java/com/bugsnag/android/UserStore.kt b/app/src/main/java/com/bugsnag/android/UserStore.kt deleted file mode 100644 index 58bf10d7ed..0000000000 --- a/app/src/main/java/com/bugsnag/android/UserStore.kt +++ /dev/null @@ -1,103 +0,0 @@ -package com.bugsnag.android - -import com.bugsnag.android.internal.StateObserver -import com.bugsnag.android.internal.dag.Provider -import java.io.File -import java.util.concurrent.atomic.AtomicReference - -/** - * This class is responsible for persisting and retrieving user information. - */ -internal class UserStore( - private val persist: Boolean, - private val persistentDir: Provider, - private val deviceIdStore: Provider, - file: File = File(persistentDir.get(), "user-info"), - private val sharedPrefMigrator: Provider, - private val logger: Logger -) { - - private val synchronizedStreamableStore: SynchronizedStreamableStore - private val previousUser = AtomicReference(null) - - init { - this.synchronizedStreamableStore = SynchronizedStreamableStore(file) - } - - /** - * Loads the user state which should be used by the [Client]. This is supplied either from - * the [Configuration] value, or a file in the [Configuration.getPersistenceDirectory] if - * [Configuration.getPersistUser] is true. - * - * If no user is stored on disk, then a default [User] is used which uses the device ID - * as its ID (unless the generateAnonymousId config option is set to false, in which case the - * device ID and therefore the user ID is set to - * null). - * - * The [UserState] provides a mechanism for observing value changes to its user property, - * so to avoid interfering with this the method should only be called once for each [Client]. - */ - fun load(initialUser: User): UserState { - val validConfigUser = validUser(initialUser) - - val loadedUser = when { - validConfigUser -> initialUser - persist -> loadPersistedUser() - else -> null - } - - val userState = when { - loadedUser != null && validUser(loadedUser) -> UserState(loadedUser) - // if generateAnonymousId config option is false, the deviceId should already be null - // here - else -> UserState(User(deviceIdStore.get()?.deviceId, null, null)) - } - - userState.addObserver( - StateObserver { event -> - if (event is StateEvent.UpdateUser) { - save(event.user) - } - } - ) - return userState - } - - /** - * Persists the user if [Configuration.getPersistUser] is true and the object is different - * from the previously persisted value. - */ - fun save(user: User) { - if (persist && user != previousUser.getAndSet(user)) { - try { - synchronizedStreamableStore.persist(user) - } catch (exc: Exception) { - logger.w("Failed to persist user info", exc) - } - } - } - - private fun validUser(user: User) = - user.id != null || user.name != null || user.email != null - - private fun loadPersistedUser(): User? { - return if (sharedPrefMigrator.get().hasPrefs()) { - val legacyUser = sharedPrefMigrator.get().loadUser(deviceIdStore.get()?.deviceId) - save(legacyUser) - legacyUser - } else if ( - synchronizedStreamableStore.file.canRead() && - synchronizedStreamableStore.file.length() > 0L && - persist - ) { - try { - synchronizedStreamableStore.load(User.Companion::fromReader) - } catch (exc: Exception) { - logger.w("Failed to load user info", exc) - null - } - } else { - null - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/AbstractStartupProvider.kt b/app/src/main/java/com/bugsnag/android/internal/AbstractStartupProvider.kt deleted file mode 100644 index 768ed1d55e..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/AbstractStartupProvider.kt +++ /dev/null @@ -1,68 +0,0 @@ -package com.bugsnag.android.internal - -import android.annotation.SuppressLint -import android.content.ContentProvider -import android.content.ContentValues -import android.database.Cursor -import android.net.Uri -import android.os.Build -import android.os.Build.VERSION_CODES - -/** - * Empty `ContentProvider` used for early loading / startup processing. - */ -abstract class AbstractStartupProvider : ContentProvider() { - override fun onCreate(): Boolean { - return true - } - - final override fun query( - uri: Uri, - projection: Array?, - selection: String?, - selectionArgs: Array?, - sortOrder: String?, - ): Cursor? { - checkPrivilegeEscalation() - return null - } - - final override fun getType(uri: Uri): String? { - checkPrivilegeEscalation() - return null - } - - final override fun insert(uri: Uri, values: ContentValues?): Uri? { - checkPrivilegeEscalation() - return null - } - - final override fun delete(uri: Uri, selection: String?, selectionArgs: Array?): Int { - checkPrivilegeEscalation() - return 0 - } - - final override fun update( - uri: Uri, - values: ContentValues?, - selection: String?, - selectionArgs: Array?, - ): Int { - checkPrivilegeEscalation() - return 0 - } - - @SuppressLint("NewApi") - protected fun checkPrivilegeEscalation() { - if (Build.VERSION.SDK_INT !in (VERSION_CODES.O..VERSION_CODES.P)) { - return - } - - val caller = callingPackage - if (caller != null && caller == context?.packageName) { - return - } - - throw SecurityException("Provider does not allow Uri permissions to be granted") - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/BackgroundTaskService.kt b/app/src/main/java/com/bugsnag/android/internal/BackgroundTaskService.kt deleted file mode 100644 index 738b2f7922..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/BackgroundTaskService.kt +++ /dev/null @@ -1,233 +0,0 @@ -package com.bugsnag.android.internal - -import androidx.annotation.VisibleForTesting -import com.bugsnag.android.internal.dag.RunnableProvider -import java.util.concurrent.BlockingQueue -import java.util.concurrent.Callable -import java.util.concurrent.ExecutorService -import java.util.concurrent.Executors -import java.util.concurrent.Future -import java.util.concurrent.FutureTask -import java.util.concurrent.LinkedBlockingQueue -import java.util.concurrent.RejectedExecutionException -import java.util.concurrent.ThreadFactory -import java.util.concurrent.ThreadPoolExecutor -import java.util.concurrent.TimeUnit -import java.lang.Thread as JThread - -/** - * The type of task which is being submitted. This determines which execution queue - * the task will be added to. - */ -enum class TaskType { - - /** - * A task that sends an error request. Any filesystem operations - * that persist/delete errors must be submitted using this type. - */ - ERROR_REQUEST, - - /** - * A task that sends a session request. Any filesystem operations - * that persist/delete sessions must be submitted using this type. - */ - SESSION_REQUEST, - - /** - * A task that performs I/O, such as reading a file on disk. This should NOT include operations - * related to error/session storage - use [ERROR_REQUEST] or [SESSION_REQUEST] instead. - */ - IO, - - /** - * A task that sends an internal error report to Bugsnag. - */ - INTERNAL_REPORT, - - /** - * Any other task that needs to run in the background. These will typically be - * short-lived operations that take <100ms, such as registering a - * [android.content.BroadcastReceiver]. - */ - DEFAULT -} - -private const val SHUTDOWN_WAIT_MS = 1500L - -// these values have been loosely adapted from android.os.AsyncTask over the years. -private const val THREAD_POOL_SIZE = 1 -private const val KEEP_ALIVE_SECS = 30L -private const val TASK_QUEUE_SIZE = 128 - -private class TaskTypeThread(runnable: Runnable, name: String, val taskType: TaskType) : - JThread(runnable, name) - -internal val JThread.taskType get() = (this as? TaskTypeThread)?.taskType - -internal fun createExecutor(name: String, type: TaskType, keepAlive: Boolean): ExecutorService { - val queue: BlockingQueue = LinkedBlockingQueue(TASK_QUEUE_SIZE) - val threadFactory = ThreadFactory { TaskTypeThread(it, name, type) } - - // certain executors (error/session/io) should always keep their threads alive, but others - // are less important so are allowed a pool size of 0 that expands on demand. - val coreSize = when { - keepAlive -> THREAD_POOL_SIZE - else -> 0 - } - return ThreadPoolExecutor( - coreSize, - THREAD_POOL_SIZE, - KEEP_ALIVE_SECS, - TimeUnit.SECONDS, - queue, - threadFactory - ) -} - -/** - * Provides a service for submitting lengthy tasks to run on background threads. - * - * A [TaskType] must be submitted with each task, which routes it to the appropriate executor. - * Setting the correct [TaskType] is critical as it can be used to enforce thread confinement. - * It also avoids short-running operations being held up by long-running operations submitted - * to the same executor. - */ -class BackgroundTaskService( - // these executors must remain single-threaded - the SDK makes assumptions - // about synchronization based on this. - @get:VisibleForTesting - internal val errorExecutor: ExecutorService = createExecutor( - "Bugsnag Error thread", - TaskType.ERROR_REQUEST, - true - ), - - @get:VisibleForTesting - internal val sessionExecutor: ExecutorService = createExecutor( - "Bugsnag Session thread", - TaskType.SESSION_REQUEST, - true - ), - - @get:VisibleForTesting - internal val ioExecutor: ExecutorService = createExecutor( - "Bugsnag IO thread", - TaskType.IO, - true - ), - - @get:VisibleForTesting - internal val internalReportExecutor: ExecutorService = createExecutor( - "Bugsnag Internal Report thread", - TaskType.INTERNAL_REPORT, - false - ), - - @get:VisibleForTesting - internal val defaultExecutor: ExecutorService = createExecutor( - "Bugsnag Default thread", - TaskType.DEFAULT, - false - ) -) { - - /** - * Submits a task for execution on a single-threaded executor. It is guaranteed that tasks - * with the same [TaskType] are executed in the order of submission. - * - * The caller is responsible for catching and handling - * [java.util.concurrent.RejectedExecutionException] if the executor is saturated. - * - * On process termination the service will attempt to wait for previously submitted jobs - * with the task type [TaskType.ERROR_REQUEST], [TaskType.SESSION_REQUEST] and [TaskType.IO]. - * This is a best-effort attempt - no guarantee can be made that the operations will complete. - */ - @Throws(RejectedExecutionException::class) - fun submitTask(taskType: TaskType, runnable: Runnable): Future<*> { - return submitTask(taskType, Executors.callable(runnable)) - } - - /** - * @see [submitTask] - */ - @Throws(RejectedExecutionException::class) - fun submitTask(taskType: TaskType, callable: Callable): Future { - val task = FutureTask(callable) - execute(taskType, task) - return SafeFuture(task, taskType) - } - - fun execute(taskType: TaskType, task: Runnable) { - when (taskType) { - TaskType.ERROR_REQUEST -> errorExecutor.execute(task) - TaskType.SESSION_REQUEST -> sessionExecutor.execute(task) - TaskType.IO -> ioExecutor.execute(task) - TaskType.INTERNAL_REPORT -> internalReportExecutor.execute(task) - TaskType.DEFAULT -> defaultExecutor.execute(task) - } - } - - /** - * Notifies the background service that the process is about to terminate. This causes it to - * shutdown submission of tasks to executors, while allowing for in-flight tasks - * to be completed within a reasonable grace period. - */ - fun shutdown() { - // don't wait for existing tasks to complete for these executors, as they are - // less essential - internalReportExecutor.shutdownNow() - defaultExecutor.shutdownNow() - - // Wait a little while for these ones to shut down - errorExecutor.shutdown() - sessionExecutor.shutdown() - ioExecutor.shutdown() - - errorExecutor.awaitTerminationSafe() - sessionExecutor.awaitTerminationSafe() - ioExecutor.awaitTerminationSafe() - } - - inline fun provider( - taskType: TaskType, - crossinline provider: () -> R - ): RunnableProvider { - val task = object : RunnableProvider() { - override fun invoke(): R = provider() - } - - execute(taskType, task) - return task - } - - private fun ExecutorService.awaitTerminationSafe() { - try { - awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) - } catch (ignored: InterruptedException) { - // ignore interrupted exception as the JVM is shutting down - } - } - - private class SafeFuture( - private val delegate: FutureTask, - private val taskType: TaskType - ) : Future by delegate { - override fun get(): V { - ensureTaskGetSafe() - return delegate.get() - } - - override fun get(timeout: Long, unit: TimeUnit?): V { - ensureTaskGetSafe() - return delegate.get(timeout, unit) - } - - private fun ensureTaskGetSafe() { - if (!delegate.isDone && JThread.currentThread().taskType == taskType) { - // if this is the execution queue for the wrapped FutureTask && it is not yet 'done' - // then it has not yet been started, so we run it immediately - delegate.run() - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt deleted file mode 100644 index 14147c69cd..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt +++ /dev/null @@ -1,13 +0,0 @@ -package com.bugsnag.android.internal - -import android.app.Application - -class BugsnagContentProvider : AbstractStartupProvider() { - override fun onCreate(): Boolean { - (context?.applicationContext as? Application)?.let { app -> - ForegroundDetector.registerOn(app) - } - - return true - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt deleted file mode 100644 index c004241bc7..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt +++ /dev/null @@ -1,44 +0,0 @@ -package com.bugsnag.android.internal - -import com.bugsnag.android.BugsnagEventMapper -import com.bugsnag.android.Event -import com.bugsnag.android.JsonStream -import com.bugsnag.android.Logger -import java.io.ByteArrayOutputStream -import com.bugsnag.android.Error as BugsnagError - -class BugsnagMapper(logger: Logger) { - private val eventMapper = BugsnagEventMapper(logger) - - /** - * Convert the given `Map` of data to an `Event` object - */ - fun convertToEvent(data: Map, fallbackApiKey: String): Event { - return eventMapper.convertToEvent(data, fallbackApiKey) - } - - /** - * Convert the given `Map` of data to an `Error` object - */ - fun convertToError(data: Map): BugsnagError { - return eventMapper.convertError(data) - } - - /** - * Convert a given `Event` object to a `Map` - */ - fun convertToMap(event: Event): Map { - val byteStream = ByteArrayOutputStream() - byteStream.writer().use { writer -> JsonStream(writer).value(event) } - return JsonHelper.deserialize(byteStream.toByteArray()) - } - - /** - * Convert a given `Error` object to a `Map` - */ - fun convertToMap(error: BugsnagError): Map { - val byteStream = ByteArrayOutputStream() - byteStream.writer().use { writer -> JsonStream(writer).value(error) } - return JsonHelper.deserialize(byteStream.toByteArray()) - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt deleted file mode 100644 index 7affbf4fca..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.bugsnag.android.internal - -import java.io.File - -internal object BugsnagStoreMigrator { - - @JvmStatic - fun migrateLegacyFiles(persistenceDir: Lazy): File { - val originalDir = persistenceDir.value - val bugsnagDir = File(originalDir, "bugsnag") - if (!bugsnagDir.isDirectory) { - bugsnagDir.mkdirs() - } - val filesToMove = listOf( - "last-run-info" to "last-run-info", - "bugsnag-sessions" to "sessions", - "user-info" to "user-info", - "bugsnag-native" to "native", - "bugsnag-errors" to "errors" - ) - - filesToMove.forEach { (from, to) -> - val fromFile = File(originalDir, from) - if (fromFile.exists()) { - fromFile.renameTo(File(bugsnagDir, to)) - } - } - - return bugsnagDir - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt b/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt deleted file mode 100644 index 1125bec348..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt +++ /dev/null @@ -1,15 +0,0 @@ -package com.bugsnag.android.internal - -private const val HEX_RADIX = 16 - -/** - * Encode this `ByteArray` as a string of lowercase hex-pairs. - */ -internal fun ByteArray.toHexString(): String = buildString(size * 2) { - for (byte in this@toHexString) { - @Suppress("MagicNumber") - val value = byte.toInt() and 0xff - if (value < HEX_RADIX) append('0') - append(value.toString(HEX_RADIX)) - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt b/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt deleted file mode 100644 index fbcbdd95b0..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/DateUtils.kt +++ /dev/null @@ -1,36 +0,0 @@ -package com.bugsnag.android.internal - -import java.text.DateFormat -import java.text.ParseException -import java.text.SimpleDateFormat -import java.util.Date -import java.util.Locale -import java.util.TimeZone - -object DateUtils { - // SimpleDateFormat isn't thread safe, cache one instance per thread as needed. - private val iso8601Holder = object : ThreadLocal() { - override fun initialValue(): DateFormat { - return SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'Z'", Locale.US).apply { - timeZone = TimeZone.getTimeZone("UTC") - } - } - } - - private val iso8601Format: DateFormat - get() = requireNotNull(iso8601Holder.get()) { "Unable to find valid dateformatter" } - - @JvmStatic - fun toIso8601(date: Date): String { - return iso8601Format.format(date) - } - - @JvmStatic - fun fromIso8601(date: String): Date { - return try { - iso8601Format.parse(date) ?: throw ParseException("DateFormat.parse returned null", 0) - } catch (exc: ParseException) { - throw IllegalArgumentException("Failed to parse timestamp", exc) - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt b/app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt deleted file mode 100644 index 8c2142f432..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt +++ /dev/null @@ -1,108 +0,0 @@ -package com.bugsnag.android.internal - -import android.content.pm.ApplicationInfo -import androidx.annotation.VisibleForTesting -import java.io.File -import java.util.zip.ZipEntry -import java.util.zip.ZipFile -import kotlin.experimental.xor - -internal object DexBuildIdGenerator { - private const val MAGIC_NUMBER_BYTE_COUNT = 8 - private const val CHECKSUM_BYTE_COUNT = 4 - private const val SIGNATURE_START_BYTE = MAGIC_NUMBER_BYTE_COUNT + CHECKSUM_BYTE_COUNT - private const val SIGNATURE_BYTE_COUNT = 20 - - private const val HEADER_SIZE = - MAGIC_NUMBER_BYTE_COUNT + CHECKSUM_BYTE_COUNT + SIGNATURE_BYTE_COUNT - - fun generateBuildId(appInfo: ApplicationInfo): String? { - @Suppress("SwallowedException") // this is deliberate - return try { - unsafeGenerateBuildId(appInfo)?.toHexString() - } catch (ex: Throwable) { - null - } - } - - private fun unsafeGenerateBuildId(appInfo: ApplicationInfo): ByteArray? { - val apk = File(appInfo.sourceDir) - - // we can't read the APK - if (!apk.canRead()) { - return null - } - - return generateApkBuildId(apk) - } - - @VisibleForTesting - internal fun generateApkBuildId(apk: File): ByteArray? { - ZipFile(apk, ZipFile.OPEN_READ).use { zip -> - var dexEntry = zip.getEntry("classes.dex") ?: return null - val buildId = signatureFromZipEntry(zip, dexEntry) ?: return null - - // search for any other classes(N).dex files and merge the signatures together - var dexFileIndex = 2 - - // removing the second break would only create noise in this loop - @Suppress("LoopWithTooManyJumpStatements") - while (true) { - dexEntry = zip.getEntry("classes$dexFileIndex.dex") ?: break - val secondarySignature = signatureFromZipEntry(zip, dexEntry) ?: break - mergeSignatureInfoBuildId(buildId, secondarySignature) - - dexFileIndex++ - } - - return buildId - } - } - - private fun mergeSignatureInfoBuildId(buildId: ByteArray, signature: ByteArray) { - for (i in buildId.indices) { - buildId[i] = buildId[i] xor signature[i] - } - } - - private fun signatureFromZipEntry(zip: ZipFile, dexEntry: ZipEntry): ByteArray? { - // read the byte[20] signature from the dex file header, after validating the magic number - // https://source.android.com/docs/core/runtime/dex-format#header-item - - return zip.getInputStream(dexEntry).use { input -> - val header = ByteArray(HEADER_SIZE) - if (input.read(header, 0, HEADER_SIZE) == HEADER_SIZE) { - extractDexSignature(header) - } else { - null - } - } - } - - @VisibleForTesting - internal fun extractDexSignature(header: ByteArray): ByteArray? { - return if (!validateHeader(header)) { - null - } else { - return header.copyOfRange( - SIGNATURE_START_BYTE, - SIGNATURE_START_BYTE + SIGNATURE_BYTE_COUNT - ) - } - } - - @Suppress("MagicNumber", "ReturnCount") - private fun validateHeader(header: ByteArray): Boolean { - // https://source.android.com/docs/core/runtime/dex-format#dex-file-magic - if (header[0].toInt() and 0xff != 0x64) return false - if (header[1].toInt() and 0xff != 0x65) return false - if (header[2].toInt() and 0xff != 0x78) return false - if (header[3].toInt() and 0xff != 0x0a) return false - - // we skip the version digits - // the magic number ends in a 0 - if (header[7].toInt() and 0xff != 0) return false - - return true - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt b/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt deleted file mode 100644 index fd32beeb33..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/FallbackWriter.kt +++ /dev/null @@ -1,29 +0,0 @@ -package com.bugsnag.android.internal - -import com.bugsnag.android.ObjectJsonStreamer -import com.bugsnag.android.repackaged.dslplatform.json.DslJson -import java.io.InputStream -import java.io.OutputStream -import java.lang.reflect.Type - -internal class FallbackWriter : DslJson.Fallback> { - - private val placeholder = "\"${ObjectJsonStreamer.OBJECT_PLACEHOLDER}\"".toByteArray() - - override fun serialize(instance: Any?, stream: OutputStream) { - stream.write(placeholder) - } - - override fun deserialize( - context: MutableMap?, - manifest: Type, - body: ByteArray, - size: Int - ): Any = throw UnsupportedOperationException() - - override fun deserialize( - context: MutableMap?, - manifest: Type, - stream: InputStream - ): Any = throw UnsupportedOperationException() -} diff --git a/app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt b/app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt deleted file mode 100644 index 548c56ebdc..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt +++ /dev/null @@ -1,231 +0,0 @@ -package com.bugsnag.android.internal - -import android.app.Activity -import android.app.Application -import android.app.Application.ActivityLifecycleCallbacks -import android.os.Build -import android.os.Bundle -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.os.SystemClock -import androidx.annotation.VisibleForTesting -import java.lang.ref.WeakReference -import kotlin.math.max - -internal object ForegroundDetector : ActivityLifecycleCallbacks, Handler.Callback { - - /** - * Same as `androidx.lifecycle.ProcessLifecycleOwner` and is used to avoid reporting - * background / foreground changes when there is only 1 Activity being restarted for configuration - * changes. - */ - @VisibleForTesting - internal const val BACKGROUND_TIMEOUT_MS = 700L - - /** - * `Message.what` used to send the "in background" notification event. The `arg1` and `arg2` - * contain the actual timestamp (relative to [SystemClock.elapsedRealtime()]) split into `int` - * values. - */ - @VisibleForTesting - internal const val MSG_SEND_BACKGROUND = 1 - - private const val INT_MASK = 0xffffffffL - - /** - * We weak-ref all of the listeners to avoid keeping Client instances around forever. The - * references are cleaned up each time we iterate over the list to notify the listeners. - */ - private val listeners = ArrayList>() - - private val mainThreadHandler = Handler(Looper.getMainLooper(), this) - - private var observedApplication: Application? = null - - /** - * The number of Activity instances: `onActivityCreated` - `onActivityDestroyed` - */ - private var activityInstanceCount: Int = 0 - - /** - * The number of started Activity instances: `onActivityStarted` - `onActivityStopped` - */ - private var startedActivityCount: Int = 0 - - private var waitingForActivityRestart: Boolean = false - - /** - * Marks the timestamp (relative to [SystemClock.elapsedRealtime]) that we initialised for the - * first time. - */ - internal val startupTime = SystemClock.elapsedRealtime() - - @VisibleForTesting - internal var backgroundSent = true - - @JvmStatic - var isInForeground: Boolean = false - @VisibleForTesting - internal set - - // This most recent time an Activity was stopped. - @Volatile - @JvmStatic - var lastExitedForegroundMs = 0L - - // The first Activity in this 'session' was started at this time. - @Volatile - @JvmStatic - var lastEnteredForegroundMs = 0L - - @JvmStatic - fun registerOn(application: Application) { - if (application === observedApplication) { - return - } - - observedApplication?.unregisterActivityLifecycleCallbacks(this) - observedApplication = application - application.registerActivityLifecycleCallbacks(this) - } - - @JvmStatic - @JvmOverloads - fun registerActivityCallbacks( - callbacks: OnActivityCallback, - notifyCurrentState: Boolean = true, - ) { - synchronized(listeners) { - listeners.add(WeakReference(callbacks)) - } - - if (notifyCurrentState) { - callbacks.onForegroundStatus( - isInForeground, - if (isInForeground) lastEnteredForegroundMs else lastExitedForegroundMs - ) - } - } - - private inline fun notifyListeners(sendCallback: (OnActivityCallback) -> Unit) { - synchronized(listeners) { - if (listeners.isEmpty()) { - return - } - - try { - val iterator = listeners.iterator() - while (iterator.hasNext()) { - val ref = iterator.next() - val listener = ref.get() - if (listener == null) { - iterator.remove() - } else { - sendCallback(listener) - } - } - } catch (e: Exception) { - // ignore callback errors - } - } - } - - override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) { - activityInstanceCount++ - } - - override fun onActivityStarted(activity: Activity) { - if (startedActivityCount == 0 && !waitingForActivityRestart) { - val startedTimestamp = SystemClock.elapsedRealtime() - notifyListeners { it.onForegroundStatus(true, startedTimestamp) } - lastEnteredForegroundMs = startedTimestamp - } - - startedActivityCount++ - mainThreadHandler.removeMessages(MSG_SEND_BACKGROUND) - isInForeground = true - waitingForActivityRestart = false - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - notifyListeners { it.onActivityStarted(activity) } - } - } - - override fun onActivityStopped(activity: Activity) { - startedActivityCount = max(0, startedActivityCount - 1) - - if (startedActivityCount == 0) { - val stoppedTimestamp = SystemClock.elapsedRealtime() - if (activity.isChangingConfigurations) { - // isChangingConfigurations indicates that the Activity will be restarted - // immediately, but we post a slightly delayed Message (with the current timestamp) - // to handle cases where (for whatever reason) that doesn't happen - // this follows the same logic as ProcessLifecycleOwner - waitingForActivityRestart = true - - val backgroundMessage = mainThreadHandler.obtainMessage(MSG_SEND_BACKGROUND) - backgroundMessage.timestamp = stoppedTimestamp - mainThreadHandler.sendMessageDelayed(backgroundMessage, BACKGROUND_TIMEOUT_MS) - } else { - notifyListeners { it.onForegroundStatus(false, stoppedTimestamp) } - isInForeground = false - lastExitedForegroundMs = stoppedTimestamp - } - } - - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - notifyListeners { it.onActivityStopped(activity) } - } - } - - override fun onActivityPostStarted(activity: Activity) { - notifyListeners { it.onActivityStarted(activity) } - } - - override fun onActivityPostStopped(activity: Activity) { - notifyListeners { it.onActivityStopped(activity) } - } - - override fun onActivityDestroyed(activity: Activity) { - activityInstanceCount = max(0, activityInstanceCount - 1) - } - - override fun handleMessage(msg: Message): Boolean { - if (msg.what != MSG_SEND_BACKGROUND) { - return false - } - - waitingForActivityRestart = false - - if (!backgroundSent) { - isInForeground = false - backgroundSent = true - - val backgroundedTimestamp = msg.timestamp - notifyListeners { it.onForegroundStatus(false, backgroundedTimestamp) } - lastExitedForegroundMs = backgroundedTimestamp - } - - return true - } - - private var Message.timestamp: Long - get() = (arg1.toLong() shl Int.SIZE_BITS) or arg2.toLong() - set(timestamp) { - arg1 = ((timestamp ushr Int.SIZE_BITS) and INT_MASK).toInt() - arg2 = (timestamp and INT_MASK).toInt() - } - - override fun onActivityResumed(activity: Activity) = Unit - override fun onActivityPaused(activity: Activity) = Unit - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) = Unit - - interface OnActivityCallback { - fun onForegroundStatus(foreground: Boolean, timestamp: Long) - - fun onActivityStarted(activity: Activity) - - fun onActivityStopped(activity: Activity) - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt deleted file mode 100644 index cb147f1f15..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ /dev/null @@ -1,294 +0,0 @@ -package com.bugsnag.android.internal - -import android.content.Context -import android.content.pm.ApplicationInfo -import android.content.pm.PackageInfo -import android.content.pm.PackageManager -import androidx.annotation.VisibleForTesting -import com.bugsnag.android.BreadcrumbType -import com.bugsnag.android.Configuration -import com.bugsnag.android.Connectivity -import com.bugsnag.android.DebugLogger -import com.bugsnag.android.DefaultDelivery -import com.bugsnag.android.Delivery -import com.bugsnag.android.DeliveryParams -import com.bugsnag.android.EndpointConfiguration -import com.bugsnag.android.ErrorTypes -import com.bugsnag.android.EventPayload -import com.bugsnag.android.Logger -import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID -import com.bugsnag.android.NoopLogger -import com.bugsnag.android.Session -import com.bugsnag.android.Telemetry -import com.bugsnag.android.ThreadSendPolicy -import com.bugsnag.android.errorApiHeaders -import com.bugsnag.android.internal.dag.Provider -import com.bugsnag.android.internal.dag.ValueProvider -import com.bugsnag.android.safeUnrollCauses -import com.bugsnag.android.sessionApiHeaders -import java.io.File -import java.util.regex.Pattern - -data class ImmutableConfig( - val apiKey: String, - val autoDetectErrors: Boolean, - val enabledErrorTypes: ErrorTypes, - val autoTrackSessions: Boolean, - val sendThreads: ThreadSendPolicy, - val discardClasses: Collection, - val enabledReleaseStages: Collection?, - val projectPackages: Collection, - val enabledBreadcrumbTypes: Set?, - val telemetry: Set, - val releaseStage: String?, - val buildUuid: Provider?, - val appVersion: String?, - val versionCode: Int?, - val appType: String?, - val delivery: Delivery, - val endpoints: EndpointConfiguration, - val persistUser: Boolean, - val launchDurationMillis: Long, - val logger: Logger, - val maxBreadcrumbs: Int, - val maxPersistedEvents: Int, - val maxPersistedSessions: Int, - val maxReportedThreads: Int, - val maxStringValueLength: Int, - val threadCollectionTimeLimitMillis: Long, - val persistenceDirectory: Lazy, - val sendLaunchCrashesSynchronously: Boolean, - val attemptDeliveryOnCrash: Boolean, - val generateAnonymousId: Boolean, - - // results cached here to avoid unnecessary lookups in Client. - val packageInfo: PackageInfo?, - val appInfo: ApplicationInfo?, - val redactedKeys: Collection -) { - - @JvmName("getErrorApiDeliveryParams") - internal fun getErrorApiDeliveryParams(payload: EventPayload) = - DeliveryParams(endpoints.notify, errorApiHeaders(payload)) - - @JvmName("getSessionApiDeliveryParams") - internal fun getSessionApiDeliveryParams(session: Session) = - DeliveryParams(endpoints.sessions, sessionApiHeaders(session.apiKey)) - - /** - * Returns whether the given throwable should be discarded - * based on the automatic data capture settings in [Configuration]. - */ - fun shouldDiscardError(exc: Throwable): Boolean { - return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(exc) - } - - /** - * Returns whether the given error should be discarded - * based on the automatic data capture settings in [Configuration]. - */ - fun shouldDiscardError(errorClass: String?): Boolean { - return shouldDiscardByReleaseStage() || shouldDiscardByErrorClass(errorClass) - } - - /** - * Returns whether a session should be discarded based on the - * automatic data capture settings in [Configuration]. - */ - fun shouldDiscardSession(autoCaptured: Boolean): Boolean { - return shouldDiscardByReleaseStage() || (autoCaptured && !autoTrackSessions) - } - - /** - * Returns whether breadcrumbs with the given type should be discarded or not. - */ - fun shouldDiscardBreadcrumb(type: BreadcrumbType): Boolean { - return enabledBreadcrumbTypes != null && !enabledBreadcrumbTypes.contains(type) - } - - /** - * Returns whether errors/sessions should be discarded or not based on the enabled - * release stages. - */ - fun shouldDiscardByReleaseStage(): Boolean { - return enabledReleaseStages != null && !enabledReleaseStages.contains(releaseStage) - } - - /** - * Returns whether errors with the given errorClass should be discarded or not. - */ - @VisibleForTesting - internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean { - return if (!errorClass.isNullOrEmpty()) { - discardClasses.any { it.matcher(errorClass.toString()).matches() } - } else { - false - } - } - - /** - * Returns whether errors should be discarded or not based on the errorClass, as deduced - * by the Throwable's class name. - */ - @VisibleForTesting - internal fun shouldDiscardByErrorClass(exc: Throwable): Boolean { - return exc.safeUnrollCauses().any { throwable -> - val errorClass = throwable.javaClass.name - shouldDiscardByErrorClass(errorClass) - } - } -} - -@JvmOverloads -internal fun convertToImmutableConfig( - config: Configuration, - buildUuid: Provider? = null, - packageInfo: PackageInfo? = null, - appInfo: ApplicationInfo? = null, - persistenceDir: Lazy = lazy { requireNotNull(config.persistenceDirectory) } -): ImmutableConfig { - val errorTypes = when { - config.autoDetectErrors -> config.enabledErrorTypes.copy() - else -> ErrorTypes(false) - } - - return ImmutableConfig( - apiKey = config.apiKey, - autoDetectErrors = config.autoDetectErrors, - enabledErrorTypes = errorTypes, - autoTrackSessions = config.autoTrackSessions, - sendThreads = config.sendThreads, - discardClasses = config.discardClasses.toSet(), - enabledReleaseStages = config.enabledReleaseStages?.toSet(), - projectPackages = config.projectPackages.toSet(), - releaseStage = config.releaseStage, - buildUuid = buildUuid, - appVersion = config.appVersion, - versionCode = config.versionCode, - appType = config.appType, - delivery = config.delivery, - endpoints = config.endpoints, - persistUser = config.persistUser, - generateAnonymousId = config.generateAnonymousId, - launchDurationMillis = config.launchDurationMillis, - logger = config.logger!!, - maxBreadcrumbs = config.maxBreadcrumbs, - maxPersistedEvents = config.maxPersistedEvents, - maxPersistedSessions = config.maxPersistedSessions, - maxReportedThreads = config.maxReportedThreads, - maxStringValueLength = config.maxStringValueLength, - threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis, - enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), - telemetry = config.telemetry.toSet(), - persistenceDirectory = persistenceDir, - sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously, - attemptDeliveryOnCrash = config.isAttemptDeliveryOnCrash, - packageInfo = packageInfo, - appInfo = appInfo, - redactedKeys = config.redactedKeys.toSet() - ) -} - -private fun validateApiKey(value: String?) { - if (isInvalidApiKey(value)) { - DebugLogger.w( - "Invalid configuration. apiKey should be a 32-character hexademical string, got $value" - ) - } -} - -@VisibleForTesting -fun isInvalidApiKey(apiKey: String?): Boolean { - if (apiKey.isNullOrEmpty()) { - throw IllegalArgumentException("No Bugsnag API Key set") - } - if (apiKey.length != VALID_API_KEY_LEN) { - return true - } - // check whether each character is hexadecimal (either a digit or a-f). - // this avoids using a regex to improve startup performance. - return !apiKey.all { it.isDigit() || it in 'a'..'f' } -} - -internal fun sanitiseConfiguration( - appContext: Context, - configuration: Configuration, - connectivity: Connectivity, - backgroundTaskService: BackgroundTaskService -): ImmutableConfig { - validateApiKey(configuration.apiKey) - val packageName = appContext.packageName - val packageManager = appContext.packageManager - val packageInfo = runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull() - val appInfo = runCatching { - packageManager.getApplicationInfo(packageName, PackageManager.GET_META_DATA) - }.getOrNull() - - // populate releaseStage - if (configuration.releaseStage == null) { - configuration.releaseStage = when { - appInfo != null && (appInfo.flags and ApplicationInfo.FLAG_DEBUGGABLE != 0) -> RELEASE_STAGE_DEVELOPMENT - else -> RELEASE_STAGE_PRODUCTION - } - } - - // if the user has set the releaseStage to production manually, disable logging - if (configuration.logger == null || configuration.logger == DebugLogger) { - val releaseStage = configuration.releaseStage - val loggingEnabled = RELEASE_STAGE_PRODUCTION != releaseStage - - if (loggingEnabled) { - configuration.logger = DebugLogger - } else { - configuration.logger = NoopLogger - } - } - - if (configuration.versionCode == null || configuration.versionCode == 0) { - @Suppress("DEPRECATION") - configuration.versionCode = packageInfo?.versionCode - } - - // Set sensible defaults if project packages not already set - if (configuration.projectPackages.isEmpty()) { - configuration.projectPackages = setOf(packageName) - } - - // populate buildUUID from manifest - val buildUuid = collectBuildUuid(appInfo, backgroundTaskService) - - @Suppress("SENSELESS_COMPARISON") - if (configuration.delivery == null) { - configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!) - } - return convertToImmutableConfig( - configuration, - buildUuid, - packageInfo, - appInfo, - lazy { configuration.persistenceDirectory ?: appContext.cacheDir } - ) -} - -private fun collectBuildUuid( - appInfo: ApplicationInfo?, - backgroundTaskService: BackgroundTaskService -): Provider? { - val bundle = appInfo?.metaData - return when { - bundle?.containsKey(BUILD_UUID) == true -> ValueProvider( - (bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString()) - .takeIf { it.isNotEmpty() } - ) - - appInfo != null -> backgroundTaskService.provider(TaskType.IO) { - DexBuildIdGenerator.generateBuildId(appInfo) - } - - else -> null - } -} - -internal const val RELEASE_STAGE_DEVELOPMENT = "development" -internal const val RELEASE_STAGE_PRODUCTION = "production" -internal const val VALID_API_KEY_LEN = 32 diff --git a/app/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt b/app/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt deleted file mode 100644 index 0fbdb53f63..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/InternalMetrics.kt +++ /dev/null @@ -1,28 +0,0 @@ -package com.bugsnag.android.internal - -/** - * Stores internal metrics for Bugsnag use. - */ -interface InternalMetrics { - /** - * Returns a map that can be merged with the top-level JSON report. - */ - fun toJsonableMap(): Map - - fun setConfigDifferences(differences: Map) - - fun setCallbackCounts(newCallbackCounts: Map) - - fun notifyAddCallback(callback: String) - - fun notifyRemoveCallback(callback: String) - - fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) - - fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) -} - -internal data class TrimMetrics( - val itemsTrimmed: Int, // breadcrumbs, strings, whatever - val dataTrimmed: Int // chars, bytes, whatever -) diff --git a/app/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt b/app/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt deleted file mode 100644 index 0900b9166c..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/InternalMetricsImpl.kt +++ /dev/null @@ -1,111 +0,0 @@ -package com.bugsnag.android.internal - -import com.bugsnag.android.NdkPluginCaller - -class InternalMetricsImpl(source: Map? = null) : InternalMetrics { - private val configDifferences: MutableMap - private val callbackCounts: MutableMap - private var metadataStringsTrimmedCount = 0 - private var metadataCharsTruncatedCount = 0 - private var breadcrumbsRemovedCount = 0 - private var breadcrumbBytesRemovedCount = 0 - - init { - if (source != null) { - @Suppress("UNCHECKED_CAST") - configDifferences = (source["config"] as MutableMap?) ?: hashMapOf() - @Suppress("UNCHECKED_CAST") - callbackCounts = (source["callbacks"] as MutableMap?) ?: hashMapOf() - @Suppress("UNCHECKED_CAST") - val system = source["system"] as MutableMap? - if (system != null) { - metadataStringsTrimmedCount = (system["stringsTruncated"] as Number?)?.toInt() ?: 0 - metadataCharsTruncatedCount = (system["stringCharsTruncated"] as Number?)?.toInt() ?: 0 - breadcrumbsRemovedCount = (system["breadcrumbsRemovedCount"] as Number?)?.toInt() ?: 0 - breadcrumbBytesRemovedCount = (system["breadcrumbBytesRemoved"] as Number?)?.toInt() ?: 0 - } - } else { - configDifferences = hashMapOf() - callbackCounts = hashMapOf() - } - } - - override fun toJsonableMap(): Map { - val callbacks = allCallbacks() - - val system = listOfNotNull( - if (metadataStringsTrimmedCount > 0) "stringsTruncated" to metadataStringsTrimmedCount else null, - if (metadataCharsTruncatedCount > 0) "stringCharsTruncated" to metadataCharsTruncatedCount else null, - if (breadcrumbsRemovedCount > 0) "breadcrumbsRemoved" to breadcrumbsRemovedCount else null, - if (breadcrumbBytesRemovedCount > 0) "breadcrumbBytesRemoved" to breadcrumbBytesRemovedCount else null, - ).toMap() - - return listOfNotNull( - if (configDifferences.isNotEmpty()) "config" to configDifferences else null, - if (callbacks.isNotEmpty()) "callbacks" to callbacks else null, - if (system.isNotEmpty()) "system" to system else null, - ).toMap() - } - - override fun setConfigDifferences(differences: Map) { - configDifferences.clear() - configDifferences.putAll(differences) - // This is currently the only place where we set static data. - // When that changes in future, we'll need a StaticData object to properly merge data - // coming from multiple sources. - NdkPluginCaller.setStaticData(mapOf("config" to configDifferences)) - } - - override fun setCallbackCounts(newCallbackCounts: Map) { - callbackCounts.clear() - callbackCounts.putAll(newCallbackCounts) - NdkPluginCaller.initCallbackCounts(newCallbackCounts) - } - - override fun notifyAddCallback(callback: String) { - modifyCallback(callback, 1) - NdkPluginCaller.notifyAddCallback(callback) - } - - override fun notifyRemoveCallback(callback: String) { - modifyCallback(callback, -1) - NdkPluginCaller.notifyRemoveCallback(callback) - } - - private fun modifyCallback(callback: String, delta: Int) { - var currentValue = callbackCounts[callback] ?: 0 - currentValue += delta - callbackCounts[callback] = currentValue.coerceAtLeast(0) - } - - private fun allCallbacks(): Map { - val result = hashMapOf() - result.putAll(callbackCounts) - - val counts = NdkPluginCaller.getCurrentCallbackSetCounts() - if (counts != null) { - // ndkOnError comes from the native side. The rest we already have. - val ndkOnError = counts["ndkOnError"] - if (ndkOnError != null) { - result["ndkOnError"] = ndkOnError - } - } - - val usage = NdkPluginCaller.getCurrentNativeApiCallUsage() - if (usage != null) { - result.putAll(usage) - } - - return result - } - - override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) { - metadataStringsTrimmedCount = stringsTrimmed - metadataCharsTruncatedCount = charsRemoved - } - - override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) { - breadcrumbsRemovedCount = breadcrumbsRemoved - breadcrumbBytesRemovedCount = bytesRemoved - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt b/app/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt deleted file mode 100644 index 743ef8d20d..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/InternalMetricsNoop.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bugsnag.android.internal - -class InternalMetricsNoop : InternalMetrics { - override fun toJsonableMap(): Map = emptyMap() - override fun setConfigDifferences(differences: Map) = Unit - override fun setCallbackCounts(newCallbackCounts: Map) = Unit - override fun notifyAddCallback(callback: String) = Unit - override fun notifyRemoveCallback(callback: String) = Unit - override fun setMetadataTrimMetrics(stringsTrimmed: Int, charsRemoved: Int) = Unit - override fun setBreadcrumbTrimMetrics(breadcrumbsRemoved: Int, bytesRemoved: Int) = Unit -} diff --git a/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt b/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt deleted file mode 100644 index 2453e35974..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/JsonHelper.kt +++ /dev/null @@ -1,160 +0,0 @@ -package com.bugsnag.android.internal - -import com.bugsnag.android.JsonStream -import com.bugsnag.android.repackaged.dslplatform.json.DslJson -import com.bugsnag.android.repackaged.dslplatform.json.JsonWriter -import java.io.ByteArrayOutputStream -import java.io.File -import java.io.FileInputStream -import java.io.FileNotFoundException -import java.io.FileOutputStream -import java.io.IOException -import java.io.InputStream -import java.io.OutputStream -import java.io.PrintWriter -import java.util.Date - -internal object JsonHelper { - - // ignore deprecation warnings as there is no other API that allows - // serializing a placeholder for a type and all its subtypes - @Suppress("deprecation") - private val settings = DslJson.Settings>().fallbackTo(FallbackWriter()) - - // Only one global DslJson is needed, and is thread-safe - // Note: dsl-json adds about 150k to the final binary size. - private val dslJson = DslJson(settings) - - init { - dslJson.registerWriter(Date::class.java) { writer: JsonWriter, value: Date? -> - value?.let { - val timestamp = DateUtils.toIso8601(it) - writer.writeString(timestamp) - } - } - } - - fun serialize(streamable: JsonStream.Streamable): ByteArray { - return ByteArrayOutputStream().use { baos -> - JsonStream(PrintWriter(baos)).use(streamable::toStream) - baos.toByteArray() - } - } - - fun serialize(value: Any): ByteArray { - return ByteArrayOutputStream().use { baos -> - serialize(value, baos) - baos.toByteArray() - } - } - - fun serialize(value: Any, stream: OutputStream) { - dslJson.serialize(value, stream) - } - - fun serialize(value: Any, file: File) { - val parentFile = file.parentFile - if (parentFile != null && !parentFile.exists()) { - if (!parentFile.mkdirs()) { - throw FileSystemException(file, null, "Could not create parent dirs of file") - } - } - try { - FileOutputStream(file).use { stream -> dslJson.serialize(value, stream) } - } catch (ex: IOException) { - throw IOException("Could not serialize JSON document to $file", ex) - } - } - - fun deserialize(bytes: ByteArray): MutableMap { - val document = dslJson.deserialize( - MutableMap::class.java, - bytes, - bytes.size - ) - requireNotNull(document) { "JSON document is invalid" } - @Suppress("UNCHECKED_CAST") - return document as MutableMap - } - - fun deserialize(stream: InputStream): MutableMap { - val document = dslJson.deserialize(MutableMap::class.java, stream) - requireNotNull(document) { "JSON document is invalid" } - @Suppress("UNCHECKED_CAST") - return document as MutableMap - } - - fun deserialize(file: File): MutableMap { - try { - FileInputStream(file).use { stream -> return deserialize(stream) } - } catch (ex: FileNotFoundException) { - throw ex - } catch (ex: IOException) { - throw IOException("Could not deserialize from $file", ex) - } - } - - /** - * Convert a long that technically contains an unsigned long value into its (unsigned) hex string equivalent. - * Negative values are interpreted as if the sign bit is the high bit of an unsigned integer. - * - * Returns null if null is passed in. - */ - fun ulongToHex(value: Long?): String? { - return if (value == null) { - null - } else if (value >= 0) { - "0x%x".format(value) - } else { - return "0x%x%02x".format(value.ushr(8), value.and(0xff)) - } - } - - /** - * Convert a JSON-decoded value into a long. Accepts numeric types, or numeric encoded strings - * (e.g. "1234", "0xb1ff"). - * - * Returns null if null or an empty string is passed in. - */ - fun jsonToLong(value: Any?): Long? { - return when (value) { - null -> null - is Number -> value.toLong() - is String -> { - if (value.length == 0) { - null - } else { - try { - java.lang.Long.decode(value) - } catch (e: NumberFormatException) { - // Check if the value overflows a long, and correct for it. - if (value.startsWith("0x")) { - // All problematic hex values (e.g. 0x8000000000000000) have 18 characters - if (value.length != 18) { - throw e - } - // Decode all but the last byte, then shift and add it. - // This overflows and gives the "correct" signed result. - val headLength = value.length - 2 - java.lang.Long.decode(value.substring(0, headLength)) - .shl(8) - .or(value.substring(headLength, value.length).toLong(16)) - } else { - // The first problematic decimal value (9223372036854775808) has 19 digits - if (value.length < 19) { - throw e - } - // Decode all but the last 3 chars, then multiply and add them. - // This overflows and gives the "correct" signed result. - val headLength = value.length - 3 - java.lang.Long.decode(value.substring(0, headLength)) * - 1000 + - java.lang.Long.decode(value.substring(headLength, value.length)) - } - } - } - } - else -> throw IllegalArgumentException("Cannot convert " + value + " to long") - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt b/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt deleted file mode 100644 index 924d301b2c..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt +++ /dev/null @@ -1,11 +0,0 @@ -package com.bugsnag.android.internal - -import com.bugsnag.android.StateEvent - -fun interface StateObserver { - /** - * This is called whenever the notifier's state is altered, so that observers can react - * appropriately. This is intended for internal use only. - */ - fun onStateChange(event: StateEvent) -} diff --git a/app/src/main/java/com/bugsnag/android/internal/StringUtils.kt b/app/src/main/java/com/bugsnag/android/internal/StringUtils.kt deleted file mode 100644 index 4007caf7c4..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/StringUtils.kt +++ /dev/null @@ -1,107 +0,0 @@ -package com.bugsnag.android.internal - -import java.util.EnumMap -import java.util.Hashtable -import java.util.LinkedList -import java.util.TreeMap -import java.util.Vector -import java.util.WeakHashMap -import java.util.concurrent.ConcurrentMap -import java.util.concurrent.CopyOnWriteArrayList - -internal object StringUtils { - private const val trimMessageLength = "***<9> CHARS TRUNCATED***".length - - fun stringTrimmedTo(maxLength: Int, str: String): String { - val excessCharCount = str.length - maxLength - return when { - excessCharCount < trimMessageLength -> str - else -> "${str.substring(0, maxLength)}***<$excessCharCount> CHARS TRUNCATED***" - } - } - - @Suppress("unchecked_cast") - fun trimStringValuesTo(maxStringLength: Int, list: MutableList): TrimMetrics { - var stringCount = 0 - var charCount = 0 - - repeat(list.size) { index -> - trimValue(maxStringLength, list[index]) { newValue, stringTrimmed, charsTrimmed -> - list[index] = newValue - stringCount += stringTrimmed - charCount += charsTrimmed - } - } - - return TrimMetrics(stringCount, charCount) - } - - @Suppress("unchecked_cast") - fun trimStringValuesTo(maxStringLength: Int, map: MutableMap): TrimMetrics { - var stringCount = 0 - var charCount = 0 - map.entries.forEach { entry -> - trimValue(maxStringLength, entry.value) { newValue, stringTrimmed, charsTrimmed -> - entry.setValue(newValue) - stringCount += stringTrimmed - charCount += charsTrimmed - } - } - - return TrimMetrics(stringCount, charCount) - } - - @Suppress("unchecked_cast") - private inline fun trimValue( - maxStringLength: Int, - value: Any?, - update: (newValue: Any, stringTrimmed: Int, charsTrimmed: Int) -> Unit - ) { - if (value is String && value.length > maxStringLength) { - update(stringTrimmedTo(maxStringLength, value), 1, value.length - maxStringLength) - } else if (value.isDefinitelyMutableMap()) { - val (innerStringCount, innerCharCount) = trimStringValuesTo( - maxStringLength, - value as MutableMap - ) - - update(value, innerStringCount, innerCharCount) - } else if (value.isDefinitelyMutableList()) { - val (innerStringCount, innerCharCount) = trimStringValuesTo( - maxStringLength, - value as MutableList - ) - - update(value, innerStringCount, innerCharCount) - } else if (value is Map<*, *>) { - val newValue = value.toMutableMap() as MutableMap - val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue) - update(newValue, innerStringCount, innerCharCount) - } else if (value is Collection<*>) { - val newValue = value.toMutableList() - val (innerStringCount, innerCharCount) = trimStringValuesTo(maxStringLength, newValue) - update(newValue, innerStringCount, innerCharCount) - } - } - - /** - * In order to avoid surprises we have a small list of commonly used Map types that are known - * to be mutable (avoiding issues around Kotlin trying to determine whether - * `Collections.singletonMap` (and such) is mutable or not). - * - * It is technically possible that a HashMap was extended to be immutable, but it's unlikely. - */ - private fun Any?.isDefinitelyMutableMap() = - this is HashMap<*, *> || - this is TreeMap<*, *> || - this is ConcurrentMap<*, *> || // concurrent automatically implies mutability - this is EnumMap<*, *> || - this is Hashtable<*, *> || - this is WeakHashMap<*, *> - - private fun Any?.isDefinitelyMutableList() = - this is ArrayList<*> || - this is LinkedList<*> || - this is CopyOnWriteArrayList<*> || - this is Vector<*> -} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt deleted file mode 100644 index aece93e476..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.bugsnag.android.internal.dag - -import com.bugsnag.android.Configuration -import com.bugsnag.android.Connectivity -import com.bugsnag.android.internal.BackgroundTaskService -import com.bugsnag.android.internal.sanitiseConfiguration - -/** - * A dependency module which constructs the configuration object that is used to alter - * Bugsnag's default behaviour. - */ -internal class ConfigModule( - contextModule: ContextModule, - configuration: Configuration, - connectivity: Connectivity, - bgTaskExecutor: BackgroundTaskService -) : BackgroundDependencyModule(bgTaskExecutor) { - val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor) -} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt deleted file mode 100644 index 0d36ce64bf..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/dag/ContextModule.kt +++ /dev/null @@ -1,19 +0,0 @@ -package com.bugsnag.android.internal.dag - -import android.content.Context -import com.bugsnag.android.internal.BackgroundTaskService - -/** - * A dependency module which accesses the application context object, falling back to the supplied - * context if it is the base context. - */ -internal class ContextModule( - appContext: Context, - bgTaskService: BackgroundTaskService -) : BackgroundDependencyModule(bgTaskService) { - - val ctx: Context = when (appContext.applicationContext) { - null -> appContext - else -> appContext.applicationContext - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt deleted file mode 100644 index a44ca147f4..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/dag/DependencyModule.kt +++ /dev/null @@ -1,39 +0,0 @@ -package com.bugsnag.android.internal.dag - -import com.bugsnag.android.internal.BackgroundTaskService -import com.bugsnag.android.internal.TaskType - -internal interface DependencyModule - -internal abstract class BackgroundDependencyModule( - @JvmField - val bgTaskService: BackgroundTaskService, - @JvmField - val taskType: TaskType = TaskType.DEFAULT -) : DependencyModule { - /** - * Convenience function to create and schedule a `RunnableProvider` of [taskType] with - * [bgTaskService]. The returned `RunnableProvider` will be implemented using the `provider` - * lambda as its `invoke` implementation. - */ - inline fun provider(crossinline provider: () -> R): RunnableProvider { - return bgTaskService.provider(taskType, provider) - } - - /** - * Return a `RunnableProvider` containing the result of applying the given [mapping] to - * this `Provider`. The `RunnableProvider` will be scheduled with [bgTaskService] as a - * [taskType] when this function returns. - * - * This function behaves similar to `List.map` or `Any.let` but with `Provider` encapsulation - * to handle value reuse and threading. - */ - internal inline fun Provider.map(crossinline mapping: (E) -> R): RunnableProvider { - val task = object : RunnableProvider() { - override fun invoke(): R = mapping(this@map.get()) - } - - bgTaskService.execute(taskType, task) - return task - } -} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/Provider.kt b/app/src/main/java/com/bugsnag/android/internal/dag/Provider.kt deleted file mode 100644 index 6eb4408439..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/dag/Provider.kt +++ /dev/null @@ -1,181 +0,0 @@ -package com.bugsnag.android.internal.dag - -import android.os.Looper -import androidx.annotation.VisibleForTesting -import java.util.concurrent.atomic.AtomicInteger - -/** - * A lightweight abstraction similar to `Lazy` or `Future` allowing values to be calculated on - * separate threads, or to be pre-computed. - */ -interface Provider { - /** - * Same as [get] but will return `null` instead of throwing an exception if the value could - * not be computed. - */ - fun getOrNull(): E? - - /** - * Return the value sourced from this provider, throwing an exception if the provider failed - * to calculate a value. Anything thrown from here will have been captured when attempting - * to calculate the value. - */ - fun get(): E -} - -/** - * The primary implementation of [Provider], usually created using the - * [BackgroundDependencyModule.provider] function. Similar conceptually to - * [java.util.concurrent.FutureTask] but with a more compact implementation. The implementation - * of [RunnableProvider.get] is special because it behaves more like [Lazy.value] in that getting - * a value that is still pending will cause it to be run on the current thread instead of waiting - * for it to be run "sometime in the future". This makes RunnableProviders less bug-prone when - * dealing with single-thread executors (such as those in [BackgroundTaskService]). RunnableProvider - * also has special handling for the main-thread, ensuring no computational work (such as IO) is - * done on the main thread. - */ -abstract class RunnableProvider : Provider, Runnable { - private val state = AtomicInteger(TASK_STATE_PENDING) - - @Volatile - private var value: Any? = null - - /** - * Calculate the value of this [Provider]. This function will be called at-most once by [run]. - * Do not call this function directly, instead use [get] and [getOrNull] which implement the - * correct threading behaviour and will reuse the value if it has been previously calculated. - */ - abstract operator fun invoke(): E - - override fun getOrNull(): E? { - return getOr { return null } - } - - override fun get(): E { - return getOr { throw value as Throwable } - } - - private inline fun getOr(failureHandler: () -> E): E { - while (true) { - when (state.get()) { - TASK_STATE_RUNNING -> awaitResult() - TASK_STATE_PENDING -> { - if (isMainThread()) { - // When the calling thread is the 'main' thread, we *always* wait for the - // background workers to [invoke] this Provider, assuming that the Provider - // is performing some kind of IO that should be kept away from the main - // thread. Ideally this doesn't happen, but this behaviour avoids the - // need for complicated callback mechanisms. - awaitResult() - } else { - // If the Provider has yet to be computed, we will try and run it on the - // current thread. This potentially causes run() to happen on a different - // Thread to the expected worker (TaskType), effectively like work-stealing. - run() - } - } - - TASK_STATE_COMPLETE -> @Suppress("UNCHECKED_CAST") return value as E - TASK_STATE_FAILED -> failureHandler() - } - } - } - - private fun isMainThread(): Boolean { - return Thread.currentThread() === mainThread - } - - /** - * Cause the current thread to wait (block) until this `Provider` [isComplete]. Upon returning - * the [isComplete] function will return `true`. - */ - private fun awaitResult() { - synchronized(this) { - while (!isComplete()) { - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - (this as Object).wait() - } - } - } - - private fun isComplete() = when (state.get()) { - TASK_STATE_PENDING, TASK_STATE_RUNNING -> false - else -> true - } - - /** - * The main entry point for a provider, typically called by a worker thread from - * [BackgroundTaskService]. If [run] has already been called this will be a no-op (including - * a reentrant thread), as such the task state *must* be checked after calling this. - * - * This should not be called, and instead [get] or [getOrNull] should be used to obtain the - * value produced by [invoke]. - */ - final override fun run() { - if (state.compareAndSet(TASK_STATE_PENDING, TASK_STATE_RUNNING)) { - try { - value = invoke() - state.set(TASK_STATE_COMPLETE) - } catch (ex: Throwable) { - value = ex - state.set(TASK_STATE_FAILED) - } finally { - synchronized(this) { - // wakeup any waiting threads - @Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN") - (this as Object).notifyAll() - } - } - } - } - - @VisibleForTesting - internal companion object { - /** - * The `Provider` task state before the provider has started actually running. This state - * indicates that the task has been constructed, has typically been scheduled but has - * not actually started running yet. - */ - private const val TASK_STATE_PENDING = 0 - - /** - * The `Provider` task state when running. Once the [run] function returns the state will - * be either [TASK_STATE_COMPLETE] or [TASK_STATE_FAILED]. - */ - private const val TASK_STATE_RUNNING = 1 - - /** - * The `Provider` state of a successfully completed task. When this is the state the - * provider value can be obtained immediately without error. - */ - private const val TASK_STATE_COMPLETE = 2 - - /** - * The `Provider` state of a task where [invoke] failed with an error or exception. - */ - private const val TASK_STATE_FAILED = 999 - - /** - * We cache the main thread to avoid any locks within [Looper.getMainLooper]. This is - * settable for unit tests, so that there doesn't have to be a valid Looper when they run. - * - * Actually access is done via the [mainThread] property. - */ - @VisibleForTesting - @Suppress("ObjectPropertyNaming") // backing property from 'mainThread' - internal var _mainThread: Thread? = null - get() { - if (field == null) { - field = Looper.getMainLooper().thread - } - return field - } - - internal val mainThread: Thread get() = _mainThread!! - } -} - -data class ValueProvider(private val value: T) : Provider { - override fun getOrNull(): T? = get() - override fun get(): T = value -} diff --git a/app/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt deleted file mode 100644 index 1c7cd6cf02..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/dag/SystemServiceModule.kt +++ /dev/null @@ -1,17 +0,0 @@ -package com.bugsnag.android.internal.dag - -import com.bugsnag.android.getActivityManager -import com.bugsnag.android.getStorageManager -import com.bugsnag.android.internal.BackgroundTaskService - -/** - * A dependency module which provides a reference to Android system services. - */ -internal class SystemServiceModule( - contextModule: ContextModule, - bgTaskService: BackgroundTaskService -) : BackgroundDependencyModule(bgTaskService) { - - val storageManager = contextModule.ctx.getStorageManager() - val activityManager = contextModule.ctx.getActivityManager() -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java deleted file mode 100644 index 64b9e084c9..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Base64.java +++ /dev/null @@ -1,190 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import java.util.Arrays; - -/** A very fast and memory efficient class to encode and decode to and from BASE64 in full accordance - * with RFC 2045.

- * On Windows XP sp1 with 1.4.2_04 and later ;), this encoder and decoder is about 10 times faster - * on small arrays (10 - 1000 bytes) and 2-3 times as fast on larger arrays (10000 - 1000000 bytes) - * compared to sun.misc.Encoder()/Decoder().

- * - * On byte arrays the encoder is about 20% faster than Jakarta Commons Base64 Codec for encode and - * about 50% faster for decoding large arrays. This implementation is about twice as fast on very small - * arrays (< 30 bytes). If source/destination is a String this - * version is about three times as fast due to the fact that the Commons Codec result has to be recoded - * to a String from byte[], which is very expensive.

- * - * This encode/decode algorithm doesn't create any temporary arrays as many other codecs do, it only - * allocates the resulting array. This produces less garbage and it is possible to handle arrays twice - * as large as algorithms that create a temporary array. (E.g. Jakarta Commons Codec). It is unknown - * whether Sun's sun.misc.Encoder()/Decoder() produce temporary arrays but since performance - * is quite low it probably does.

- * - * The encoder produces the same output as the Sun one except that the Sun's encoder appends - * a trailing line separator if the last character isn't a pad. Unclear why but it only adds to the - * length and is probably a side effect. Both are in conformance with RFC 2045 though.
- * Commons codec seem to always att a trailing line separator.

- * - * Note! - * The encode/decode method pairs (types) come in three versions with the exact same algorithm and - * thus a lot of code redundancy. This is to not create any temporary arrays for transcoding to/from different - * format types. The methods not used can simply be commented out.

- * - * There is also a "fast" version of all decode methods that works the same way as the normal ones, but - * har a few demands on the decoded input. Normally though, these fast verions should be used if the source if - * the input is known and it hasn't bee tampered with.

- * - * If you find the code useful or you find a bug, please send me a note at base64 @ miginfocom . com. - * - * Licence (BSD): - * ============== - * - * Copyright (c) 2004, Mikael Grev, MiG InfoCom AB. (base64 @ miginfocom . com) - * All rights reserved. - * - * Redistribution and use in source and binary forms, with or without modification, - * are permitted provided that the following conditions are met: - * Redistributions of source code must retain the above copyright notice, this list - * of conditions and the following disclaimer. - * Redistributions in binary form must reproduce the above copyright notice, this - * list of conditions and the following disclaimer in the documentation and/or other - * materials provided with the distribution. - * Neither the name of the MiG InfoCom AB nor the names of its contributors may be - * used to endorse or promote products derived from this software without specific - * prior written permission. - * - * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND - * ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED - * WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. - * IN NO EVENT SHALL THE COPYRIGHT OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, - * INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, - * BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, - * OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, - * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) - * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY - * OF SUCH DAMAGE. - * - * @version 2.2 - * @author Mikael Grev - * Date: 2004-aug-02 - * Time: 11:31:11 - */ - -abstract class Base64 { - private static final char[] CA = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/".toCharArray(); - private static final byte[] BA; - private static final int[] IA = new int[256]; - static { - Arrays.fill(IA, -1); - for (int i = 0, iS = CA.length; i < iS; i++) { - IA[CA[i]] = i; - } - IA['='] = 0; - BA = new byte[CA.length]; - for (int i = 0; i < CA.length; i++) { - BA[i] = (byte)CA[i]; - } - } - - static int encodeToBytes(byte[] sArr, byte[] dArr, final int start) { - final int sLen = sArr.length; - - final int eLen = (sLen / 3) * 3; // Length of even 24-bits. - final int dLen = ((sLen - 1) / 3 + 1) << 2; // Returned character count - - // Encode even 24-bits - for (int s = 0, d = start; s < eLen;) { - // Copy next three bytes into lower 24 bits of int, paying attension to sign. - int i = (sArr[s++] & 0xff) << 16 | (sArr[s++] & 0xff) << 8 | (sArr[s++] & 0xff); - - // Encode the int into four chars - dArr[d++] = BA[(i >>> 18) & 0x3f]; - dArr[d++] = BA[(i >>> 12) & 0x3f]; - dArr[d++] = BA[(i >>> 6) & 0x3f]; - dArr[d++] = BA[i & 0x3f]; - } - - // Pad and encode last bits if source isn't even 24 bits. - int left = sLen - eLen; // 0 - 2. - if (left > 0) { - // Prepare the int - int i = ((sArr[eLen] & 0xff) << 10) | (left == 2 ? ((sArr[sLen - 1] & 0xff) << 2) : 0); - - // Set last four chars - dArr[start + dLen - 4] = BA[i >> 12]; - dArr[start + dLen - 3] = BA[(i >>> 6) & 0x3f]; - dArr[start + dLen - 2] = left == 2 ? BA[i & 0x3f] : (byte)'='; - dArr[start + dLen - 1] = '='; - } - - return dLen; - } - - static int findEnd(final byte[] sArr, final int start) { - for (int i = start; i < sArr.length; i++) - if (IA[sArr[i] & 0xff] < 0) - return i; - return sArr.length; - } - - private final static byte[] EMPTY_ARRAY = new byte[0]; - - static byte[] decodeFast(final byte[] sArr, final int start, final int end) { - // Check special case - int sLen = end - start; - if (sLen == 0) - return EMPTY_ARRAY; - - int sIx = start, eIx = end - 1; // Start and end index after trimming. - - // Trim illegal chars from start - while (sIx < eIx && IA[sArr[sIx] & 0xff] < 0) { - sIx++; - } - - // Trim illegal chars from end - while (eIx > 0 && IA[sArr[eIx] & 0xff] < 0) { - eIx--; - } - - // get the padding count (=) (0, 1 or 2) - final int pad = sArr[eIx] == '=' ? (sArr[eIx - 1] == '=' ? 2 : 1) : 0; // Count '=' at end. - final int cCnt = eIx - sIx + 1; // Content count including possible separators - final int sepCnt = sLen > 76 ? (sArr[76] == '\r' ? cCnt / 78 : 0) << 1 : 0; - - final int len = ((cCnt - sepCnt) * 6 >> 3) - pad; // The number of decoded bytes - final byte[] dArr = new byte[len]; // Preallocate byte[] of exact length - - // Decode all but the last 0 - 2 bytes. - int d = 0; - for (int cc = 0, eLen = (len / 3) * 3; d < eLen;) { - // Assemble three bytes into an int from four "valid" characters. - int i = IA[sArr[sIx++]] << 18 | IA[sArr[sIx++]] << 12 | IA[sArr[sIx++]] << 6 | IA[sArr[sIx++]]; - - // Add the bytes - dArr[d++] = (byte) (i >> 16); - dArr[d++] = (byte) (i >> 8); - dArr[d++] = (byte) i; - - // If line separator, jump over it. - if (sepCnt > 0 && ++cc == 19) { - sIx += 2; - cc = 0; - } - } - - if (d < len) { - // Decode last 1-3 bytes (incl '=') into 1-3 bytes - int i = 0; - for (int j = 0; sIx <= eIx - pad; j++) { - i |= IA[sArr[sIx++]] << (18 - j * 6); - } - - for (int r = 16; d < len; r -= 8) { - dArr[d++] = (byte) (i >> r); - } - } - - return dArr; - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java deleted file mode 100644 index d3bbe44ef6..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BinaryConverter.java +++ /dev/null @@ -1,57 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class BinaryConverter { - - static final JsonReader.ReadObject Base64Reader = new JsonReader.ReadObject() { - @Nullable - @Override - public byte[] read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserialize(reader); - } - }; - static final JsonWriter.WriteObject Base64Writer = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable byte[] value) { - serialize(value, writer); - } - }; - - public static void serialize(@Nullable final byte[] value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value.length == 0) { - sw.writeAscii("\"\""); - } else { - sw.writeBinary(value); - } - } - - public static byte[] deserialize(final JsonReader reader) throws IOException { - return reader.readBase64(); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(Base64Reader); - } - - public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(Base64Reader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(Base64Reader); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(Base64Reader, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java deleted file mode 100644 index 40dfdd298f..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/BoolConverter.java +++ /dev/null @@ -1,128 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class BoolConverter { - - public final static boolean[] EMPTY_ARRAY = new boolean[0]; - - public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { - @Override - public Boolean read(JsonReader reader) throws IOException { - return deserialize(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Boolean read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserialize(reader); - } - }; - public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Boolean value) { - serializeNullable(value, writer); - } - }; - public static final JsonReader.ReadObject ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public boolean[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for boolean array start"); - reader.getNextToken(); - return deserializeBoolArray(reader); - } - }; - public static final JsonWriter.WriteObject ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable boolean[] value) { - serialize(value, writer); - } - }; - - public static void serializeNullable(@Nullable final Boolean value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value) { - sw.writeAscii("true"); - } else { - sw.writeAscii("false"); - } - } - - public static void serialize(final boolean value, final JsonWriter sw) { - if (value) { - sw.writeAscii("true"); - } else { - sw.writeAscii("false"); - } - } - - public static void serialize(@Nullable final boolean[] value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value.length == 0) { - sw.writeAscii("[]"); - } else { - sw.writeByte(JsonWriter.ARRAY_START); - sw.writeAscii(value[0] ? "true" : "false"); - for(int i = 1; i < value.length; i++) { - sw.writeAscii(value[i] ? ",true" : ",false"); - } - sw.writeByte(JsonWriter.ARRAY_END); - } - } - - public static boolean deserialize(final JsonReader reader) throws IOException { - if (reader.wasTrue()) { - return true; - } else if (reader.wasFalse()) { - return false; - } - throw reader.newParseErrorAt("Found invalid boolean value", 0); - } - - public static boolean[] deserializeBoolArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return EMPTY_ARRAY; - } - boolean[] buffer = new boolean[4]; - buffer[0] = deserialize(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = deserialize(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(READER); - } - - public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(READER); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(READER, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java deleted file mode 100644 index a5ff40f99a..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Configuration.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -/** - * Configuration API for setting up readers/writers during library initialization. - * DslJson will use ServiceLoader.load(Configuration.class) in default constructor. - * This will load services registered in META-INF/services/com.bugsnag.dslplatform.json.Configuration file. - */ -@SuppressWarnings("rawtypes") // suppress pre-existing warnings -public interface Configuration { - /** - * Configure library instance with appropriate readers/writers/etc... - * - * @param json library instance - */ - void configure(DslJson json); -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java deleted file mode 100644 index ae10ac9207..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ConfigurationException.java +++ /dev/null @@ -1,16 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -@SuppressWarnings("serial") // suppress pre-existing warnings -public class ConfigurationException extends RuntimeException { - public ConfigurationException(String reason) { - super(reason); - } - - public ConfigurationException(Throwable cause) { - super(cause); - } - - public ConfigurationException(String reason, Throwable cause) { - super(reason, cause); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java deleted file mode 100644 index d917c829ec..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/DslJson.java +++ /dev/null @@ -1,2908 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import org.w3c.dom.Element; - -import java.io.*; -import java.lang.annotation.Annotation; -import java.lang.reflect.*; -import java.math.BigDecimal; -import java.net.InetAddress; -import java.net.URI; -import java.nio.charset.Charset; -import java.util.*; -import java.util.concurrent.*; - -/** - * Main DSL-JSON class. - * Easiest way to use the library is to create an DslJson<Object> instance and reuse it within application. - * DslJson has optional constructor for specifying default readers/writers. - *

- * During initialization DslJson will use ServiceLoader API to load registered services. - * This is done through `META-INF/services/com.dslplatform.json.CompiledJson` file. - *

- * DslJson can fallback to another serializer in case when it doesn't know how to handle specific type. - * This can be specified by Fallback interface during initialization. - *

- * If you wish to use compile time databinding @CompiledJson annotation must be specified on the target class - * or implicit reference to target class must exists from a class with @CompiledJson annotation. - *

- * Usage example: - *

- *     DslJson<Object> dsl = new DslJson<>();
- *     dsl.serialize(instance, OutputStream);
- *     POJO pojo = dsl.deserialize(POJO.class, InputStream);
- * 
- *

- * For best performance use serialization API with JsonWriter and byte[] as target. - * JsonWriter is reused via thread local variable. When custom JsonWriter's are used, reusing them will yield maximum performance. - * JsonWriter can be reused via reset methods. - * For best deserialization performance prefer byte[] API instead of InputStream API. - * JsonReader is reused via thread local variable. When custom JsonReaders are used, reusing them will yield maximum performance. - * JsonReader can be reused via process methods. - *

- * During deserialization TContext can be used to pass data into deserialized classes. - * This is useful when deserializing domain objects which require state or service provider. - * For example DSL Platform entities require service locator to be able to perform lazy load. - *

- * DslJson doesn't have a String or Reader API since it's optimized for processing bytes and streams. - * If you wish to process String, use String.getBytes("UTF-8") as argument for DslJson. - * Only UTF-8 is supported for encoding and decoding JSON. - *

- *     DslJson<Object> dsl = new DslJson<>();
- *     JsonWriter writer = dsl.newWriter();
- *     dsl.serialize(writer, instance);
- *     String json = writer.toString(); //JSON as string - avoid using JSON as Strings whenever possible
- *     byte[] input = json.getBytes("UTF-8");
- *     POJO pojo = dsl.deserialize(POJO.class, input, input.length);
- * 
- * - * @param used for library specialization. If unsure, use Object - */ -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public class DslJson implements UnknownSerializer, TypeLookup { - - private static final Charset UTF8 = Charset.forName("UTF-8"); - private static final Object unknownValue = new Object(); - - /** - * The context of this instance. - * Can be used for library specialization - */ - @Nullable - public final TContext context; - @Nullable - protected final Fallback fallback; - /** - * Should properties with default values be omitted from the resulting JSON? - * This will leave out nulls, empty collections, zeros and other attributes with default values - * which can be reconstructed from schema information - */ - public final boolean omitDefaults; - /** - * When object supports array format, eg. [prop1, prop2, prop3] this value must be enabled before - * object will be serialized in such a way. Regardless of this value deserialization will support all formats. - */ - public final boolean allowArrayFormat; - - protected final StringCache keyCache; - protected final StringCache valuesCache; - protected final List> writerFactories = new CopyOnWriteArrayList>(); - private final int settingsWriters; - protected final List> readerFactories = new CopyOnWriteArrayList>(); - private final int settingsReaders; - protected final List> binderFactories = new CopyOnWriteArrayList>(); - private final int settingsBinders; - private final JsonReader.ErrorInfo errorInfo; - private final JsonReader.DoublePrecision doublePrecision; - private final JsonReader.UnknownNumberParsing unknownNumbers; - private final int maxNumberDigits; - private final int maxStringSize; - protected final ThreadLocal localWriter; - protected final ThreadLocal localReader; - private final ExternalConverterAnalyzer externalConverterAnalyzer; - private final Map, Boolean> creatorMarkers; - - public interface Fallback { - void serialize(@Nullable Object instance, OutputStream stream) throws IOException; - - @Nullable - Object deserialize(@Nullable TContext context, Type manifest, byte[] body, int size) throws IOException; - - @Nullable - Object deserialize(@Nullable TContext context, Type manifest, InputStream stream) throws IOException; - } - - public interface ConverterFactory { - @Nullable - T tryCreate(Type manifest, DslJson dslJson); - } - - /** - * Configuration for DslJson options. - * By default key cache is enabled. Everything else is not configured. - * To load `META-INF/services` call `includeServiceLoader()` - * - * @param DslJson context - */ - public static class Settings { - private TContext context; - private boolean javaSpecifics; - private Fallback fallback; - private boolean omitDefaults; - private boolean allowArrayFormat; - private StringCache keyCache = new SimpleStringCache(); - private StringCache valuesCache; - private int fromServiceLoader; - private JsonReader.ErrorInfo errorInfo = JsonReader.ErrorInfo.WITH_STACK_TRACE; - private JsonReader.DoublePrecision doublePrecision = JsonReader.DoublePrecision.DEFAULT; - private JsonReader.UnknownNumberParsing unknownNumbers = JsonReader.UnknownNumberParsing.LONG_AND_BIGDECIMAL; - private int maxNumberDigits = 512; - private int maxStringBuffer = 128 * 1024 * 1024; - private final List configurations = new ArrayList(); - private final List> writerFactories = new ArrayList>(); - private final List> readerFactories = new ArrayList>(); - private final List> binderFactories = new ArrayList>(); - private final Set classLoaders = new HashSet(); - private final Map, Boolean> creatorMarkers = new HashMap, Boolean>(); - - /** - * Pass in context for DslJson. - * Context will be available in JsonReader for objects which needs it. - * - * @param context context propagated to JsonReaders - * @return itself - */ - public Settings withContext(@Nullable TContext context) { - this.context = context; - return this; - } - - /** - * Enable converters for Java specific types (Graphics API) not available on Android. - * - * @param javaSpecifics should register Java specific converters - * @return itself - */ - public Settings withJavaConverters(boolean javaSpecifics) { - this.javaSpecifics = javaSpecifics; - return this; - } - - /** - * Will be eventually replaced with writer/reader factories. - * Used by DslJson to call into when trying to serialize/deserialize object which is not supported. - * - * @param fallback how to handle unsupported type - * @return which fallback to use in case of unsupported type - */ - @Deprecated - public Settings fallbackTo(@Nullable Fallback fallback) { - this.fallback = fallback; - return this; - } - - /** - * DslJson can exclude some properties from resulting JSON which it can reconstruct fully from schema information. - * Eg. int with value 0 can be omitted since that is default value for the type. - * Null values can be excluded since they are handled the same way as missing property. - * - * @param omitDefaults should exclude default values from resulting JSON - * @return itself - */ - public Settings skipDefaultValues(boolean omitDefaults) { - this.omitDefaults = omitDefaults; - return this; - } - - /** - * Some encoders/decoders support writing objects in array format. - * For encoder to write objects in such format, Array format must be defined before the Default and minified formats - * and array format must be allowed via this setting. - * If objects support multiple formats decoding will work regardless of this setting. - * - * @param allowArrayFormat allow serialization via array format - * @return itself - */ - public Settings allowArrayFormat(boolean allowArrayFormat) { - this.allowArrayFormat = allowArrayFormat; - return this; - } - - /** - * Use specific key cache implementation. - * Key cache is enabled by default and it's used when deserializing unstructured objects such as Map<String, Object> - * to avoid allocating new String key instance. Instead StringCache will provide a new or an old instance. - * This improves memory usage and performance since there is usually small number of keys. - * It does have some performance overhead, but this is dependant on the implementation. - *

- * To disable key cache, provide null for it. - * - * @param keyCache which key cache to use - * @return itself - */ - public Settings useKeyCache(@Nullable StringCache keyCache) { - this.keyCache = keyCache; - return this; - } - - /** - * Use specific string values cache implementation. - * By default string values cache is disabled. - *

- * To support memory restricted scenarios where there is limited number of string values, - * values cache can be used. - *

- * Not every "JSON string" will use this cache... eg UUID, LocalDate don't create an instance of string - * and therefore don't use this cache. - * - * @param valuesCache which values cache to use - * @return itself - */ - public Settings useStringValuesCache(@Nullable StringCache valuesCache) { - this.valuesCache = valuesCache; - return this; - } - - /** - * DslJson will iterate over converter factories when requested type is unknown. - * Registering writer converter factory allows for constructing JSON converter lazily. - * - * @param writer registered writer factory - * @return itself - */ - @SuppressWarnings("unchecked") - public Settings resolveWriter(ConverterFactory writer) { - if (writer == null) throw new IllegalArgumentException("writer can't be null"); - if (writerFactories.contains(writer)) { - throw new IllegalArgumentException("writer already registered"); - } - writerFactories.add((ConverterFactory) writer); - return this; - } - - /** - * DslJson will iterate over converter factories when requested type is unknown. - * Registering reader converter factory allows for constructing JSON converter lazily. - * - * @param reader registered reader factory - * @return itself - */ - @SuppressWarnings("unchecked") - public Settings resolveReader(ConverterFactory reader) { - if (reader == null) throw new IllegalArgumentException("reader can't be null"); - if (readerFactories.contains(reader)) { - throw new IllegalArgumentException("reader already registered"); - } - readerFactories.add((ConverterFactory) reader); - return this; - } - - /** - * DslJson will iterate over converter factories when requested type is unknown. - * Registering binder converter factory allows for constructing JSON converter lazily. - * - * @param binder registered binder factory - * @return itself - */ - @SuppressWarnings("unchecked") - public Settings resolveBinder(ConverterFactory binder) { - if (binder == null) throw new IllegalArgumentException("binder can't be null"); - if (binderFactories.contains(binder)) { - throw new IllegalArgumentException("binder already registered"); - } - binderFactories.add((ConverterFactory) binder); - return this; - } - - /** - * Load converters using thread local ClassLoader. - * Will scan through `META-INF/services/com.bugsnag.dslplatform.json.Configuration` file and register implementation during startup. - * This will pick up compile time databindings if they are available in specific folder. - *

- * Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. - * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. - * - * @return itself - */ - public Settings includeServiceLoader() { - return includeServiceLoader(Thread.currentThread().getContextClassLoader()); - } - - /** - * Load converters using provided `ClassLoader` instance - * Will scan through `META-INF/services/com.bugsnag.dslplatform.json.Configuration` file and register implementation during startup. - * This will pick up compile time databindings if they are available in specific folder. - *

- * Note that gradle on Android has issues with preserving that file, in which case it can be provided manually. - * DslJson will fall back to "expected" class name if it doesn't find anything during scanning. - * - * @param loader ClassLoader to use - * @return itself - */ - public Settings includeServiceLoader(ClassLoader loader) { - if (loader == null) throw new IllegalArgumentException("loader can't be null"); - classLoaders.add(loader); - for (Configuration c : ServiceLoader.load(Configuration.class, loader)) { - boolean hasConfiguration = false; - Class manifest = c.getClass(); - for (Configuration cur : configurations) { - if (cur.getClass() == manifest) { - hasConfiguration = true; - break; - } - } - if (!hasConfiguration) { - fromServiceLoader++; - configurations.add(c); - } - } - return this; - } - - /** - * By default doubles are not deserialized into an exact value in some rare edge cases. - * - * @param errorInfo information about error in parsing exception - * @return itself - */ - public Settings errorInfo(JsonReader.ErrorInfo errorInfo) { - if (errorInfo == null) throw new IllegalArgumentException("errorInfo can't be null"); - this.errorInfo = errorInfo; - return this; - } - - /** - * By default doubles are not deserialized into an exact value in some rare edge cases. - * - * @param precision type of double deserialization - * @return itself - */ - public Settings doublePrecision(JsonReader.DoublePrecision precision) { - if (precision == null) throw new IllegalArgumentException("precision can't be null"); - this.doublePrecision = precision; - return this; - } - - /** - * When processing JSON without a schema numbers can be deserialized in various ways: - * - * - as longs and decimals - * - as longs and doubles - * - as decimals only - * - as doubles only - * - * Default is as long and BigDecimal - * - * @param unknownNumbers how to deserialize numbers without a schema - * @return itself - */ - public Settings unknownNumbers(JsonReader.UnknownNumberParsing unknownNumbers) { - if (unknownNumbers == null) throw new IllegalArgumentException("unknownNumbers can't be null"); - this.unknownNumbers = unknownNumbers; - return this; - } - - /** - * Specify maximum allowed size for digits buffer. Default is 512. - * Digits buffer is used when processing strange/large input numbers. - * - * @param size maximum allowed size for digit buffer - * @return itself - */ - public Settings limitDigitsBuffer(int size) { - if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); - this.maxNumberDigits = size; - return this; - } - - /** - * Specify maximum allowed size for string buffer. Default is 128MB - * To protect against malicious inputs, maximum allowed string buffer can be reduced. - * - * @param size maximum size of buffer in bytes - * @return itself - */ - public Settings limitStringBuffer(int size) { - if (size < 1) throw new IllegalArgumentException("size can't be smaller than 1"); - this.maxStringBuffer = size; - return this; - } - - /** - * When there are multiple constructors, pick the one marked with annotation. - * When markers is allowed on non public targets, attempt at visibility change will be done in runtime. - * - * @param marker annotation used for marking constructor or static method factory - * @param expandVisibility should consider annotation declared on non public accessor - * @return itself - */ - public Settings creatorMarker(Class marker, boolean expandVisibility) { - if (marker == null) throw new IllegalArgumentException("marker can't be null"); - this.creatorMarkers.put(marker, expandVisibility); - return this; - } - - /** - * Configure DslJson with custom Configuration during startup. - * Configurations are extension points for setting up readers/writers during DslJson initialization. - * - * @param conf custom extensibility point - * @return itself - */ - public Settings with(Configuration conf) { - if (conf == null) throw new IllegalArgumentException("conf can't be null"); - configurations.add(conf); - return this; - } - - private Settings with(Iterable confs) { - if (confs != null) { - for (Configuration c : confs) - configurations.add(c); - } - return this; - } - } - - /** - * Simple initialization entry point. - * Will provide null for TContext - * Java graphics readers/writers will not be registered. - * Fallback will not be configured. - * Key cache will be enables, values cache will be disabled. - * Default ServiceLoader.load method will be used to setup services from META-INF - */ - public DslJson() { - this(new Settings().includeServiceLoader()); - } - - /** - * Will be removed. Use DslJson(Settings) instead. - * Fully configurable entry point. - * - * @param context context instance which can be provided to deserialized objects. Use null if not sure - * @param javaSpecifics register Java graphics specific classes such as java.awt.Point, Image, ... - * @param fallback in case of unsupported type, try serialization/deserialization through external API - * @param omitDefaults should serialization produce minified JSON (omit nulls and default values) - * @param keyCache parsed keys can be cached (this is only used in small subset of parsing) - * @param serializers additional serializers/deserializers which will be immediately registered into readers/writers - */ - @Deprecated - public DslJson( - @Nullable final TContext context, - final boolean javaSpecifics, - @Nullable final Fallback fallback, - final boolean omitDefaults, - @Nullable final StringCache keyCache, - final Iterable serializers) { - this(new Settings() - .withContext(context) - .withJavaConverters(javaSpecifics) - .fallbackTo(fallback) - .skipDefaultValues(omitDefaults) - .useKeyCache(keyCache) - .with(serializers) - ); - } - - /** - * Fully configurable entry point. - * Provide settings for DSL-JSON initialization. - * - * @param settings DSL-JSON configuration - */ - public DslJson(final Settings settings) { - if (settings == null) throw new IllegalArgumentException("settings can't be null"); - final DslJson self = this; - this.localWriter = new ThreadLocal() { - @Override - protected JsonWriter initialValue() { - return new JsonWriter(4096, self); - } - }; - this.localReader = new ThreadLocal() { - @Override - protected JsonReader initialValue() { - return new JsonReader(new byte[4096], 4096, self.context, new char[64], self.keyCache, self.valuesCache, self, self.errorInfo, self.doublePrecision, self.unknownNumbers, self.maxNumberDigits, self.maxStringSize); - } - }; - this.context = settings.context; - this.fallback = settings.fallback; - this.omitDefaults = settings.omitDefaults; - this.allowArrayFormat = settings.allowArrayFormat; - this.keyCache = settings.keyCache; - this.valuesCache = settings.valuesCache; - this.unknownNumbers = settings.unknownNumbers; - this.errorInfo = settings.errorInfo; - this.doublePrecision = settings.doublePrecision; - this.maxNumberDigits = settings.maxNumberDigits; - this.maxStringSize = settings.maxStringBuffer; - this.writerFactories.addAll(settings.writerFactories); - this.settingsWriters = settings.writerFactories.size(); - this.readerFactories.addAll(settings.readerFactories); - this.settingsReaders = settings.readerFactories.size(); - this.binderFactories.addAll(settings.binderFactories); - this.settingsBinders = settings.binderFactories.size(); - this.externalConverterAnalyzer = new ExternalConverterAnalyzer(settings.classLoaders); - this.creatorMarkers = new HashMap, Boolean>(settings.creatorMarkers); - - registerReader(byte[].class, BinaryConverter.Base64Reader); - registerWriter(byte[].class, BinaryConverter.Base64Writer); - registerReader(boolean.class, BoolConverter.READER); - registerWriter(boolean.class, BoolConverter.WRITER); - registerDefault(boolean.class, false); - registerReader(boolean[].class, BoolConverter.ARRAY_READER); - registerWriter(boolean[].class, BoolConverter.ARRAY_WRITER); - registerReader(Boolean.class, BoolConverter.NULLABLE_READER); - registerWriter(Boolean.class, BoolConverter.WRITER); - if (settings.javaSpecifics) { - registerJavaSpecifics(this); - } - registerReader(LinkedHashMap.class, ObjectConverter.MapReader); - registerReader(HashMap.class, ObjectConverter.MapReader); - registerReader(Map.class, ObjectConverter.MapReader); - registerWriter(Map.class, new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Map value) { - if (value == null) { - writer.writeNull(); - } else { - try { - serializeMap(value, writer); - } catch (IOException ex) { - throw new SerializationException(ex); - } - } - } - }); - registerReader(URI.class, NetConverter.UriReader); - registerWriter(URI.class, NetConverter.UriWriter); - registerReader(InetAddress.class, NetConverter.AddressReader); - registerWriter(InetAddress.class, NetConverter.AddressWriter); - registerReader(double.class, NumberConverter.DOUBLE_READER); - registerWriter(double.class, NumberConverter.DOUBLE_WRITER); - registerDefault(double.class, 0.0); - registerReader(double[].class, NumberConverter.DOUBLE_ARRAY_READER); - registerWriter(double[].class, NumberConverter.DOUBLE_ARRAY_WRITER); - registerReader(Double.class, NumberConverter.NULLABLE_DOUBLE_READER); - registerWriter(Double.class, NumberConverter.DOUBLE_WRITER); - registerReader(float.class, NumberConverter.FLOAT_READER); - registerWriter(float.class, NumberConverter.FLOAT_WRITER); - registerDefault(float.class, 0.0f); - registerReader(float[].class, NumberConverter.FLOAT_ARRAY_READER); - registerWriter(float[].class, NumberConverter.FLOAT_ARRAY_WRITER); - registerReader(Float.class, NumberConverter.NULLABLE_FLOAT_READER); - registerWriter(Float.class, NumberConverter.FLOAT_WRITER); - registerReader(int.class, NumberConverter.INT_READER); - registerWriter(int.class, NumberConverter.INT_WRITER); - registerDefault(int.class, 0); - registerReader(int[].class, NumberConverter.INT_ARRAY_READER); - registerWriter(int[].class, NumberConverter.INT_ARRAY_WRITER); - registerReader(Integer.class, NumberConverter.NULLABLE_INT_READER); - registerWriter(Integer.class, NumberConverter.INT_WRITER); - registerReader(short.class, NumberConverter.SHORT_READER); - registerWriter(short.class, NumberConverter.SHORT_WRITER); - registerDefault(short.class, (short)0); - registerReader(short[].class, NumberConverter.SHORT_ARRAY_READER); - registerWriter(short[].class, NumberConverter.SHORT_ARRAY_WRITER); - registerReader(Short.class, NumberConverter.NULLABLE_SHORT_READER); - registerWriter(Short.class, NumberConverter.SHORT_WRITER); - registerReader(long.class, NumberConverter.LONG_READER); - registerWriter(long.class, NumberConverter.LONG_WRITER); - registerDefault(long.class, 0L); - registerReader(long[].class, NumberConverter.LONG_ARRAY_READER); - registerWriter(long[].class, NumberConverter.LONG_ARRAY_WRITER); - registerReader(Long.class, NumberConverter.NULLABLE_LONG_READER); - registerWriter(Long.class, NumberConverter.LONG_WRITER); - registerReader(BigDecimal.class, NumberConverter.DecimalReader); - registerWriter(BigDecimal.class, NumberConverter.DecimalWriter); - registerReader(String.class, StringConverter.READER); - registerWriter(String.class, StringConverter.WRITER); - registerReader(UUID.class, UUIDConverter.READER); - registerWriter(UUID.class, UUIDConverter.WRITER); - registerReader(Number.class, NumberConverter.NumberReader); - registerWriter(CharSequence.class, StringConverter.WRITER_CHARS); - registerReader(StringBuilder.class, StringConverter.READER_BUILDER); - registerReader(StringBuffer.class, StringConverter.READER_BUFFER); - - for (Configuration serializer : settings.configurations) { - serializer.configure(this); - } - if (!settings.classLoaders.isEmpty() && settings.fromServiceLoader == 0) { - //TODO: workaround common issue with failed services registration. try to load common external name if exists - loadDefaultConverters(this, settings.classLoaders, "dsl_json_Annotation_Processor_External_Serialization"); - loadDefaultConverters(this, settings.classLoaders, "dsl_json.json.ExternalSerialization"); - loadDefaultConverters(this, settings.classLoaders, "dsl_json_ExternalSerialization"); - } - } - - /** - * Simplistic string cache implementation. - * It uses a fixed String[] structure in which it caches string value based on it's hash. - * Eg, hash & mask provide index into the structure. Different string with same hash will overwrite the previous one. - */ - public static class SimpleStringCache implements StringCache { - - private final int mask; - private final String[] cache; - - /** - * Will use String[] with 1024 elements. - */ - public SimpleStringCache() { - this(10); - } - - public SimpleStringCache(int log2Size) { - int size = 2; - for (int i = 1; i < log2Size; i++) { - size *= 2; - } - mask = size - 1; - cache = new String[size]; - } - - /** - * Calculates hash of the provided "string" and looks it up from the String[] - * It it doesn't exists of a different string is already there a new String instance is created - * and saved into the String[] - * - * @param chars buffer into which string was parsed - * @param len the string length inside the buffer - * @return String instance matching the char[]/int pair - */ - @Override - public String get(char[] chars, int len) { - long hash = 0x811c9dc5; - for (int i = 0; i < len; i++) { - hash ^= (byte) chars[i]; - hash *= 0x1000193; - } - final int index = (int) hash & mask; - final String value = cache[index]; - if (value == null) return createAndPut(index, chars, len); - if (value.length() != len) return createAndPut(index, chars, len); - for (int i = 0; i < value.length(); i++) { - if (value.charAt(i) != chars[i]) return createAndPut(index, chars, len); - } - return value; - } - - private String createAndPut(int index, char[] chars, int len) { - final String value = new String(chars, 0, len); - cache[index] = value; - return value; - } - } - - /** - * Create a writer bound to this DSL-JSON. - * Ideally it should be reused. - * Bound writer can use lookups to find custom writers. - * This can be used to serialize unknown types such as Object.class - * - * @return bound writer - */ - public JsonWriter newWriter() { - return new JsonWriter(this); - } - - /** - * Create a writer bound to this DSL-JSON. - * Ideally it should be reused. - * Bound writer can use lookups to find custom writers. - * This can be used to serialize unknown types such as Object.class - * - * @param size initial buffer size - * @return bound writer - */ - public JsonWriter newWriter(int size) { - return new JsonWriter(size, this); - } - - /** - * Create a writer bound to this DSL-JSON. - * Ideally it should be reused. - * Bound writer can use lookups to find custom writers. - * This can be used to serialize unknown types such as Object.class - * - * @param buffer initial buffer - * @return bound writer - */ - public JsonWriter newWriter(byte[] buffer) { - if (buffer == null) throw new IllegalArgumentException("null value provided for buffer"); - return new JsonWriter(buffer, this); - } - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * This reader can be reused via process method. - * - * @return bound reader - */ - public JsonReader newReader() { - return new JsonReader(new byte[4096], 4096, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); - } - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * This reader can be reused via process method. - * - * @param bytes input bytes - * @return bound reader - */ - public JsonReader newReader(byte[] bytes) { - return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); - } - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * This reader can be reused via process method. - * - * @param bytes input bytes - * @param length use input bytes up to specified length - * @return bound reader - */ - public JsonReader newReader(byte[] bytes, int length) { - return new JsonReader(bytes, length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); - } - - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * Pass in initial string buffer. - * This reader can be reused via process method. - * - * @param bytes input bytes - * @param length use input bytes up to specified length - * @param tmp string parsing buffer - * @return bound reader - */ - public JsonReader newReader(byte[] bytes, int length, char[] tmp) { - return new JsonReader(bytes, length, context, tmp, keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); - } - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * Created reader can be reused (using process method). - * This is convenience method for creating a new reader and binding it to stream. - * - * @param stream input stream - * @param buffer temporary buffer - * @return bound reader - * @throws java.io.IOException unable to read from stream - */ - public JsonReader newReader(InputStream stream, byte[] buffer) throws IOException { - final JsonReader reader = newReader(buffer); - reader.process(stream); - return reader; - } - - /** - * Create a reader bound to this DSL-JSON. - * Bound reader can reuse key cache (which is used during Map deserialization) - * This method id Deprecated since it should be avoided. - * It's better to use byte[] or InputStream based readers - * - * @param input JSON string - * @return bound reader - */ - @Deprecated - public JsonReader newReader(String input) { - final byte[] bytes = input.getBytes(UTF8); - return new JsonReader(bytes, bytes.length, context, new char[64], keyCache, valuesCache, this, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringSize); - } - - private static void loadDefaultConverters(final DslJson json, Set loaders, final String name) { - for (ClassLoader loader : loaders) { - try { - Class external = loader.loadClass(name); - Configuration instance = (Configuration) external.getDeclaredConstructor().newInstance(); - instance.configure(json); - } catch (NoClassDefFoundError ignore) { - } catch (Exception ignore) { - } - } - } - - static void registerJavaSpecifics(final DslJson json) { - json.registerReader(Element.class, XmlConverter.Reader); - json.registerWriter(Element.class, XmlConverter.Writer); - } - - private final Map defaults = new ConcurrentHashMap(); - - public void registerDefault(Class manifest, T instance) { - defaults.put(manifest, instance); - } - - @SuppressWarnings("unchecked") - public boolean registerWriterFactory(ConverterFactory factory) { - if (factory == null) throw new IllegalArgumentException("factory can't be null"); - if (writerFactories.contains(factory)) return false; - writerFactories.add(writerFactories.size() - settingsWriters, (ConverterFactory) factory); - return true; - } - - @SuppressWarnings("unchecked") - public boolean registerReaderFactory(ConverterFactory factory) { - if (factory == null) throw new IllegalArgumentException("factory can't be null"); - if (readerFactories.contains(factory)) return false; - readerFactories.add(readerFactories.size() - settingsReaders, (ConverterFactory) factory); - return true; - } - - @SuppressWarnings("unchecked") - public boolean registerBinderFactory(ConverterFactory factory) { - if (factory == null) throw new IllegalArgumentException("factory can't be null"); - if (binderFactories.contains(factory)) return false; - binderFactories.add(binderFactories.size() - settingsBinders, (ConverterFactory) factory); - return true; - } - - @Nullable - public final Object getDefault(@Nullable Type manifest) { - if (manifest == null) return null; - Object instance = defaults.get(manifest); - if (instance != null) return instance; - final Class rawType; - if (manifest instanceof Class) { - rawType = (Class) manifest; - } else if (manifest instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) manifest; - rawType = (Class) pt.getRawType(); - } else return null; - if (rawType.isPrimitive()) { - return Array.get(Array.newInstance(rawType, 1), 0); - } - return defaults.get(rawType); - } - - private final ConcurrentMap, JsonReader.ReadJsonObject> objectReaders = - new ConcurrentHashMap, JsonReader.ReadJsonObject>(); - - private final ConcurrentMap readers = new ConcurrentHashMap(); - private final ConcurrentMap binders = new ConcurrentHashMap(); - private final ConcurrentMap writers = new ConcurrentHashMap(); - - - public final Set getRegisteredDecoders() { - return readers.keySet(); - } - - public final Set getRegisteredBinders() { - return binders.keySet(); - } - - public final Set getRegisteredEncoders() { - return writers.keySet(); - } - - public final Map, Boolean> getRegisteredCreatorMarkers() { - return creatorMarkers; - } - - /** - * Register custom reader for specific type (JSON -> instance conversion). - * Reader is used for conversion from input byte[] -> target object instance - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a reader this will disable deserialization of specified type - * - * @param manifest specified type - * @param reader provide custom implementation for reading JSON into an object instance - * @param type - * @param type or subtype - */ - public void registerReader(final Class manifest, @Nullable final JsonReader.ReadObject reader) { - if (reader == null) readers.remove(manifest); - else readers.put(manifest, reader); - } - - /** - * Register custom reader for specific type (JSON -> instance conversion). - * Reader is used for conversion from input byte[] -> target object instance - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a reader this will disable deserialization of specified type - * - * @param manifest specified type - * @param reader provide custom implementation for reading JSON into an object instance - * @return old registered value - */ - @Nullable - public JsonReader.ReadObject registerReader(final Type manifest, @Nullable final JsonReader.ReadObject reader) { - if (reader == null) return readers.remove(manifest); - try { - return readers.get(manifest); - } finally { - readers.put(manifest, reader); - } - } - - /** - * Register custom binder for specific type (JSON -> instance conversion). - * Binder is used for conversion from input byte[] -> existing target object instance. - * It's similar to reader, with the difference that it accepts target instance. - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a binder this will disable binding of specified type - * - * @param manifest specified type - * @param binder provide custom implementation for binding JSON to an object instance - * @param type - * @param type or subtype - */ - public void registerBinder(final Class manifest, @Nullable final JsonReader.BindObject binder) { - if (binder == null) binders.remove(manifest); - else binders.put(manifest, binder); - } - - /** - * Register custom binder for specific type (JSON -> instance conversion). - * Binder is used for conversion from input byte[] -> existing target object instance. - * It's similar to reader, with the difference that it accepts target instance. - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a binder this will disable binding of specified type - * - * @param manifest specified type - * @param binder provide custom implementation for binding JSON to an object instance - */ - public void registerBinder(final Type manifest, @Nullable final JsonReader.BindObject binder) { - if (binder == null) binders.remove(manifest); - else binders.put(manifest, binder); - } - - /** - * Register custom writer for specific type (instance -> JSON conversion). - * Writer is used for conversion from object instance -> output byte[] - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a writer this will disable serialization of specified type - * - * @param manifest specified type - * @param writer provide custom implementation for writing JSON from object instance - * @param type - */ - public void registerWriter(final Class manifest, @Nullable final JsonWriter.WriteObject writer) { - if (writer == null) { - writerMap.remove(manifest); - writers.remove(manifest); - } else { - writerMap.put(manifest, manifest); - writers.put(manifest, writer); - } - } - - /** - * Register custom writer for specific type (instance -> JSON conversion). - * Writer is used for conversion from object instance -> output byte[] - *

- * Types registered through @CompiledJson annotation should be registered automatically through - * ServiceLoader.load method and you should not be registering them manually. - *

- * If null is registered for a writer this will disable serialization of specified type - * - * @param manifest specified type - * @param writer provide custom implementation for writing JSON from object instance - * @return old registered value - */ - @Nullable - public JsonWriter.WriteObject registerWriter(final Type manifest, @Nullable final JsonWriter.WriteObject writer) { - if (writer == null) return writers.remove(manifest); - try { - return writers.get(manifest); - } finally { - writers.put(manifest, writer); - } - } - - private final ConcurrentMap, Class> writerMap = new ConcurrentHashMap, Class>(); - - /** - * Try to find registered writer for provided type. - * If writer is not found, null will be returned. - * If writer for exact type is not found, type hierarchy will be scanned for base writer. - *

- * Writer is used for conversion from object instance into JSON representation. - * - * @param manifest specified type - * @return writer for specified type if found - */ - @Nullable - public JsonWriter.WriteObject tryFindWriter(final Type manifest) { - JsonWriter.WriteObject writer = writers.get(manifest); - if (writer != null) return writer; - final Type actualType = extractActualType(manifest); - if (actualType != manifest) { - writer = writers.get(actualType); - if (writer != null) { - writers.putIfAbsent(manifest, writer); - return writer; - } - } - if (actualType instanceof Class) { - final Class signature = (Class) actualType; - if (JsonObject.class.isAssignableFrom(signature)) { - writers.putIfAbsent(manifest, OBJECT_WRITER); - return OBJECT_WRITER; - } - } - writer = lookupFromFactories(manifest, actualType, writerFactories, writers); - if (writer != null) return writer; - if (!(actualType instanceof Class)) return null; - Class found = writerMap.get(actualType); - if (found != null) { - return writers.get(found); - } - Class container = (Class) actualType; - final ArrayList> signatures = new ArrayList>(); - findAllSignatures(container, signatures); - for (final Class sig : signatures) { - writer = writers.get(sig); - if (writer == null) { - writer = lookupFromFactories(manifest, sig, writerFactories, writers); - } - if (writer != null) { - writerMap.putIfAbsent(container, sig); - return writer; - } - } - return null; - } - - private static Type extractActualType(final Type manifest) { - if (manifest instanceof WildcardType) { - WildcardType wt = (WildcardType) manifest; - if (wt.getUpperBounds().length == 1 && wt.getLowerBounds().length == 0) { - return wt.getUpperBounds()[0]; - } - } - return manifest; - } - - private void checkExternal(final Type manifest, final ConcurrentMap cache) { - if (manifest instanceof Class) { - externalConverterAnalyzer.tryFindConverter((Class) manifest, this); - } else if (manifest instanceof ParameterizedType) { - ParameterizedType pt = (ParameterizedType) manifest; - Type container = pt.getRawType(); - externalConverterAnalyzer.tryFindConverter((Class) container, this); - for (Type arg : pt.getActualTypeArguments()) { - if (!cache.containsKey(arg)) { - Type actualType = extractActualType(arg); - if (actualType != arg && !cache.containsKey(actualType)) { - checkExternal(actualType, cache); - } - } - } - } - } - - @Nullable - private T lookupFromFactories( - final Type signature, - final Type manifest, - final List> factories, - final ConcurrentMap cache) { - if (manifest instanceof Class) { - externalConverterAnalyzer.tryFindConverter((Class) manifest, this); - T found = cache.get(manifest); - if (found != null) return found; - } else if (manifest instanceof ParameterizedType) { - checkExternal(manifest, cache); - } - - for (ConverterFactory wrt : factories) { - final T converter = wrt.tryCreate(manifest, this); - if (converter != null) { - cache.putIfAbsent(signature, converter); - return converter; - } - } - return null; - } - - /** - * Try to find registered reader for provided type. - * If reader is not found, null will be returned. - * Exact match must be found, type hierarchy will not be scanned for alternative readers. - *

- * If you wish to use alternative reader for specific type, register it manually with something along the lines of - *

-	 *     DslJson dslJson = ...
-	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
-	 * 
- * - * @param manifest specified type - * @return found reader for specified type - */ - @Nullable - public JsonReader.ReadObject tryFindReader(final Type manifest) { - JsonReader.ReadObject found = readers.get(manifest); - if (found != null) return found; - final Type actualType = extractActualType(manifest); - if (actualType != manifest) { - found = readers.get(actualType); - if (found != null) { - readers.putIfAbsent(manifest, found); - return found; - } - } - if (actualType instanceof Class) { - final Class signature = (Class) actualType; - if (JsonObject.class.isAssignableFrom(signature)) { - final JsonReader.ReadJsonObject decoder = getObjectReader(signature); - if (decoder != null) { - found = convertToReader(decoder); - readers.putIfAbsent(manifest, found); - return found; - } - } - } - return lookupFromFactories(manifest, actualType, readerFactories, readers); - } - - /** - * Try to find registered binder for provided type. - * If binder is not found, null will be returned. - * Exact match must be found, type hierarchy will not be scanned for alternative binders. - *

- * If you wish to use alternative binder for specific type, register it manually with something along the lines of - *

-	 *     DslJson dslJson = ...
-	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
-	 * 
- * - * @param manifest specified type - * @return found reader for specified type - */ - @Nullable - public JsonReader.BindObject tryFindBinder(final Type manifest) { - JsonReader.BindObject found = binders.get(manifest); - if (found != null) return found; - final Type actualType = extractActualType(manifest); - if (actualType != manifest) { - found = binders.get(actualType); - if (found != null) { - binders.putIfAbsent(manifest, found); - return found; - } - } - return lookupFromFactories(manifest, actualType, binderFactories, binders); - } - - /** - * Try to find registered writer for provided type. - * If writer is not found, null will be returned. - * If writer for exact type is not found, type hierarchy will be scanned for base writer. - *

- * Writer is used for conversion from object instance into JSON representation. - * - * @param manifest specified class - * @param specified type - * @return found writer for specified class or null - */ - @SuppressWarnings("unchecked") - @Nullable - public JsonWriter.WriteObject tryFindWriter(final Class manifest) { - return (JsonWriter.WriteObject) tryFindWriter((Type) manifest); - } - - /** - * Try to find registered reader for provided type. - * If reader is not found, null will be returned. - * Exact match must be found, type hierarchy will not be scanned for alternative reader. - *

- * If you wish to use alternative reader for specific type, register it manually with something along the lines of - *

-	 *     DslJson dslJson = ...
-	 *     dslJson.registerReader(Interface.class, dslJson.tryFindReader(Implementation.class));
-	 * 
- * - * @param manifest specified class - * @param specified type - * @return found reader for specified class or null - */ - @SuppressWarnings("unchecked") - @Nullable - public JsonReader.ReadObject tryFindReader(final Class manifest) { - return (JsonReader.ReadObject) tryFindReader((Type) manifest); - } - - /** - * Try to find registered binder for provided type. - * If binder is not found, null will be returned. - * Exact match must be found, type hierarchy will not be scanned for alternative binder. - *

- * If you wish to use alternative binder for specific type, register it manually with something along the lines of - *

-	 *     DslJson dslJson = ...
-	 *     dslJson.registerBinder(Interface.class, dslJson.tryFindBinder(Implementation.class));
-	 * 
- * - * @param manifest specified class - * @param specified type - * @return found reader for specified class or null - */ - @SuppressWarnings("unchecked") - @Nullable - public JsonReader.BindObject tryFindBinder(final Class manifest) { - return (JsonReader.BindObject) tryFindBinder((Type) manifest); - } - - private static void findAllSignatures(final Class manifest, final ArrayList> found) { - if (found.contains(manifest)) { - return; - } - found.add(manifest); - final Class superClass = manifest.getSuperclass(); - if (superClass != null && superClass != Object.class) { - findAllSignatures(superClass, found); - } - for (final Class iface : manifest.getInterfaces()) { - findAllSignatures(iface, found); - } - } - - @SuppressWarnings("unchecked") - @Nullable - private JsonReader.ReadJsonObject probeForObjectReader(Class manifest, Object instance) { - Object found; - try { - found = manifest.getField("JSON_READER").get(instance); - } catch (Exception ignore) { - try { - found = manifest.getMethod("JSON_READER").invoke(instance); - } catch (Exception ignore2) { - try { - found = manifest.getMethod("getJSON_READER").invoke(instance); - } catch (Exception ignore3) { - return null; - } - } - } - return found instanceof JsonReader.ReadJsonObject - ? (JsonReader.ReadJsonObject)found - : null; - } - - @SuppressWarnings("unchecked") - @Nullable - protected final JsonReader.ReadJsonObject getObjectReader(final Class manifest) { - try { - JsonReader.ReadJsonObject reader = objectReaders.get(manifest); - if (reader == null) { - reader = probeForObjectReader(manifest, null); - if (reader == null) { - //probe in few special places - try { - Object companion = manifest.getField("Companion").get(null); - reader = probeForObjectReader(companion.getClass(), companion); - } catch (Exception ignore) { - return null; - } - } - if (reader != null) { - objectReaders.putIfAbsent(manifest, reader); - } - } - return reader; - } catch (final Exception ignore) { - return null; - } - } - - public void serializeMap(final Map value, final JsonWriter sw) throws IOException { - sw.writeByte(JsonWriter.OBJECT_START); - final int size = value.size(); - if (size > 0) { - final Iterator> iterator = value.entrySet().iterator(); - Map.Entry kv = iterator.next(); - sw.writeString(kv.getKey()); - sw.writeByte(JsonWriter.SEMI); - serialize(sw, kv.getValue()); - for (int i = 1; i < size; i++) { - sw.writeByte(JsonWriter.COMMA); - kv = iterator.next(); - sw.writeString(kv.getKey()); - sw.writeByte(JsonWriter.SEMI); - serialize(sw, kv.getValue()); - } - } - sw.writeByte(JsonWriter.OBJECT_END); - } - - @Deprecated - @Nullable - public static Object deserializeObject(final JsonReader reader) throws IOException { - return ObjectConverter.deserializeObject(reader); - } - - /** - * Will be removed - * @param reader JSON reader - * @return deseralized list - * @throws IOException error during parsing - */ - @Deprecated - public static ArrayList deserializeList(final JsonReader reader) throws IOException { - return ObjectConverter.deserializeList(reader); - } - - /** - * Will be removed - * @param reader JSON reader - * @return deserialized map - * @throws IOException error during parsing - */ - @Deprecated - public static LinkedHashMap deserializeMap(final JsonReader reader) throws IOException { - return ObjectConverter.deserializeMap(reader); - } - - private static Object convertResultToArray(Class elementType, List result) { - if (elementType.isPrimitive()) { - if (boolean.class.equals(elementType)) { - boolean[] array = new boolean[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Boolean) result.get(i); - } - return array; - } else if (int.class.equals(elementType)) { - int[] array = new int[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Integer) result.get(i); - } - return array; - } else if (long.class.equals(elementType)) { - long[] array = new long[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Long) result.get(i); - } - return array; - } else if (short.class.equals(elementType)) { - short[] array = new short[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Short) result.get(i); - } - return array; - } else if (byte.class.equals(elementType)) { - byte[] array = new byte[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Byte) result.get(i); - } - return array; - } else if (float.class.equals(elementType)) { - float[] array = new float[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Float) result.get(i); - } - return array; - } else if (double.class.equals(elementType)) { - double[] array = new double[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Double) result.get(i); - } - return array; - } else if (char.class.equals(elementType)) { - char[] array = new char[result.size()]; - for (int i = 0; i < result.size(); i++) { - array[i] = (Character) result.get(i); - } - return array; - } - } - return result.toArray((Object[]) Array.newInstance(elementType, 0)); - } - - /** - * Check if DslJson knows how to serialize a type. - * It will check if a writer for such type exists or can be used. - * - * @param manifest type to check - * @return can serialize this type into JSON - */ - public final boolean canSerialize(final Type manifest) { - JsonWriter.WriteObject writer = writers.get(manifest); - if (writer != null) return true; - if (manifest instanceof Class) { - final Class content = (Class) manifest; - if (JsonObject.class.isAssignableFrom(content)) { - return true; - } - if (JsonObject[].class.isAssignableFrom(content)) { - return true; - } - if (tryFindWriter(manifest) != null) { - return true; - } - if (content.isArray()) { - return !content.getComponentType().isArray() - && !Collection.class.isAssignableFrom(content.getComponentType()) - && canSerialize(content.getComponentType()); - } - } - if (manifest instanceof ParameterizedType) { - final ParameterizedType pt = (ParameterizedType) manifest; - if (pt.getActualTypeArguments().length == 1) { - final Class container = (Class) pt.getRawType(); - if (container.isArray() || Collection.class.isAssignableFrom(container)) { - final Type content = pt.getActualTypeArguments()[0]; - return content instanceof Class && JsonObject.class.isAssignableFrom((Class) content) - || tryFindWriter(content) != null; - } - } - } else if (manifest instanceof GenericArrayType) { - final GenericArrayType gat = (GenericArrayType) manifest; - return gat.getGenericComponentType() instanceof Class - && JsonObject.class.isAssignableFrom((Class) gat.getGenericComponentType()) - || tryFindWriter(gat.getGenericComponentType()) != null; - } - for (ConverterFactory wrt : writerFactories) { - if (wrt.tryCreate(manifest, this) != null) { - return true; - } - } - return false; - } - - /** - * Check if DslJson knows how to deserialize a type. - * It will check if a reader for such type exists or can be used. - * - * @param manifest type to check - * @return can read this type from JSON - */ - public final boolean canDeserialize(final Type manifest) { - if (tryFindReader(manifest) != null) { - return true; - } - if (manifest instanceof Class) { - final Class objectType = (Class) manifest; - if (objectType.isArray()) { - return !objectType.getComponentType().isArray() - && !Collection.class.isAssignableFrom(objectType.getComponentType()) - && canDeserialize(objectType.getComponentType()); - } - } - if (manifest instanceof ParameterizedType) { - final ParameterizedType pt = (ParameterizedType) manifest; - if (pt.getActualTypeArguments().length == 1) { - final Class container = (Class) pt.getRawType(); - if (container.isArray() || Collection.class.isAssignableFrom(container)) { - final Type content = pt.getActualTypeArguments()[0]; - if (tryFindReader(content) != null) { - return true; - } - } - } - } else if (manifest instanceof GenericArrayType) { - final Type content = ((GenericArrayType) manifest).getGenericComponentType(); - return tryFindReader(content) != null; - } - return false; - } - - /** - * Reusable deserialize API. - * For maximum performance `JsonReader` should be reused (otherwise small buffer will be allocated for processing) - * and `JsonReader.ReadObject` should be prepared (otherwise a lookup will be required). - *

- * This is mostly convenience API since it starts the processing of the JSON by calling getNextToken on JsonReader, - * checks for null and calls converter.read(input). - * - * @param specified type - * @param converter target reader - * @param input input JSON - * @return deserialized instance - * @throws IOException error during deserialization - */ - @Nullable - public T deserialize( - final JsonReader.ReadObject converter, - final JsonReader input) throws IOException { - if (converter == null) { - throw new IllegalArgumentException("converter can't be null"); - } - if (input == null) { - throw new IllegalArgumentException("input can't be null"); - } - input.getNextToken(); - return converter.read(input); - } - - /** - * Convenient deserialize API for working with bytes. - * Deserialize provided byte input into target object. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - * - * @param manifest target type - * @param body input JSON - * @param size length - * @param target type - * @return deserialized instance - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public TResult deserialize( - final Class manifest, - final byte[] body, - final int size) throws IOException { - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (body == null) { - throw new IllegalArgumentException("body can't be null"); - } - final JsonReader json = localReader.get().process(body, size); - try { - json.getNextToken(); - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return (TResult) simpleReader.read(json); - } - if (manifest.isArray()) { - if (json.wasNull()) { - return null; - } else if (json.last() != '[') { - throw json.newParseError("Expecting '[' for array start"); - } - final Class elementManifest = manifest.getComponentType(); - final List list = deserializeList(elementManifest, body, size); - if (list == null) { - return null; - } - return (TResult) convertResultToArray(elementManifest, list); - } - if (fallback != null) { - return (TResult) fallback.deserialize(context, manifest, body, size); - } - throw createErrorMessage(manifest); - } finally { - json.reset(); - } - } - - /** - * Deserialize API for working with bytes. - * Deserialize provided byte input into target object. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - * - * @param manifest target type - * @param body input JSON - * @param size length - * @return deserialized instance - * @throws IOException error during deserialization - */ - @Nullable - public Object deserialize( - final Type manifest, - final byte[] body, - final int size) throws IOException { - if (manifest instanceof Class) { - return deserialize((Class) manifest, body, size); - } - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (body == null) { - throw new IllegalArgumentException("body can't be null"); - } - final JsonReader json = localReader.get().process(body, size); - try { - json.getNextToken(); - final Object result = deserializeWith(manifest, json); - if (result != unknownValue) return result; - if (fallback != null) { - return fallback.deserialize(context, manifest, body, size); - } - throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + - "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); - } finally { - json.reset(); - } - } - - @SuppressWarnings("unchecked") - @Nullable - protected Object deserializeWith(Type manifest, JsonReader json) throws IOException { - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return simpleReader.read(json); - } - if (manifest instanceof ParameterizedType) { - final ParameterizedType pt = (ParameterizedType) manifest; - if (pt.getActualTypeArguments().length == 1) { - final Type content = pt.getActualTypeArguments()[0]; - final Class container = (Class) pt.getRawType(); - if (container.isArray() || Collection.class.isAssignableFrom(container)) { - if (json.wasNull()) { - return null; - } else if (json.last() != '[') { - throw json.newParseError("Expecting '[' for array start"); - } - if (json.getNextToken() == ']') { - if (container.isArray()) { - returnEmptyArray(content); - } - return new ArrayList(0); - } - final JsonReader.ReadObject contentReader = tryFindReader(content); - if (contentReader != null) { - final ArrayList result = json.deserializeNullableCollectionCustom(contentReader); - if (container.isArray()) { - return returnAsArray(content, result); - } - return result; - } - } - } - } else if (manifest instanceof GenericArrayType) { - if (json.wasNull()) { - return null; - } else if (json.last() != '[') { - throw json.newParseError("Expecting '[' for array start"); - } - final Type content = ((GenericArrayType) manifest).getGenericComponentType(); - if (json.getNextToken() == ']') { - return returnEmptyArray(content); - } - final JsonReader.ReadObject contentReader = tryFindReader(content); - if (contentReader != null) { - final ArrayList result = json.deserializeNullableCollectionCustom(contentReader); - return returnAsArray(content, result); - } - } - return unknownValue; - } - - private static Object returnAsArray(final Type content, final ArrayList result) { - if (content instanceof Class) { - return convertResultToArray((Class) content, result); - } - if (content instanceof ParameterizedType) { - final ParameterizedType cpt = (ParameterizedType) content; - return result.toArray((Object[]) Array.newInstance((Class) cpt.getRawType(), 0)); - } - return result.toArray(); - } - - private static Object returnEmptyArray(Type content) { - if (content instanceof Class) { - return Array.newInstance((Class) content, 0); - } - if (content instanceof ParameterizedType) { - final ParameterizedType pt = (ParameterizedType) content; - return Array.newInstance((Class) pt.getRawType(), 0); - } - return new Object[0]; - } - - protected IOException createErrorMessage(final Class manifest) { - final ArrayList> signatures = new ArrayList>(); - findAllSignatures(manifest, signatures); - for (final Class sig : signatures) { - if (readers.containsKey(sig)) { - if (sig.equals(manifest)) { - return new IOException("Reader for provided type: " + manifest + " is disabled and fallback serialization is not registered (converter is registered as null).\n" + - "Try initializing system with custom fallback or don't register null for " + manifest); - } - return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + - "Found reader for: " + sig + " so try deserializing into that instead?\n" + - "Alternatively, try initializing system with custom fallback or register specified type using registerReader into " + getClass()); - } - } - return new IOException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + - "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); - } - - /** - * Convenient deserialize list API for working with bytes. - * Deserialize provided byte input into target object. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - * - * @param manifest target type - * @param body input JSON - * @param size length - * @param target element type - * @return deserialized list instance - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public List deserializeList( - final Class manifest, - final byte[] body, - final int size) throws IOException { - if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); - if (body == null) throw new IllegalArgumentException("body can't be null"); - if (size == 4 && body[0] == 'n' && body[1] == 'u' && body[2] == 'l' && body[3] == 'l') { - return null; - } else if (size == 2 && body[0] == '[' && body[1] == ']') { - return new ArrayList(0); - } - final JsonReader json = localReader.get().process(body, size); - try { - if (json.getNextToken() != '[') { - if (json.wasNull()) { - return null; - } - throw json.newParseError("Expecting '[' for list start"); - } - if (json.getNextToken() == ']') { - return new ArrayList(0); - } - //leave for now in to avoid overhead of going through redirection via generic tryFindReader - if (JsonObject.class.isAssignableFrom(manifest)) { - final JsonReader.ReadJsonObject reader = getObjectReader(manifest); - if (reader != null) { - return (List) json.deserializeNullableCollection(reader); - } - } - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return json.deserializeNullableCollectionCustom(simpleReader); - } - if (fallback != null) { - final Object array = Array.newInstance(manifest, 0); - final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), body, size); - if (result == null) { - return null; - } - final ArrayList list = new ArrayList(result.length); - for (TResult aResult : result) { - list.add(aResult); - } - return list; - } - throw createErrorMessage(manifest); - } finally { - json.reset(); - } - } - - /** - * This is deprecated to avoid using it. - * Use deserializeList method without the buffer argument instead. - * - * Convenient deserialize list API for working with streams. - * Deserialize provided stream input into target object. - * Use buffer for internal conversion from stream into byte[] for partial processing. - * This method creates a new instance of JsonReader. - * There is also deserializeList without the buffer which reuses thread local reader. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - * Provided buffer will be used as input for partial processing. - *

- * For best performance buffer should be reused. - * - * @param manifest target type - * @param stream input JSON - * @param buffer buffer used for InputStream -> byte[] conversion - * @param target element type - * @return deserialized list - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public List deserializeList( - final Class manifest, - final InputStream stream, - final byte[] buffer) throws IOException { - if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); - if (stream == null) throw new IllegalArgumentException("stream can't be null"); - if (buffer == null) throw new IllegalArgumentException("buffer can't be null"); - return deserializeList(manifest, newReader(stream, buffer), stream); - } - - /** - * Convenient deserialize list API for working with streams. - * Deserialize provided stream input into target object. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - *

- * - * @param manifest target type - * @param stream input JSON - * @param target element type - * @return deserialized list - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public List deserializeList( - final Class manifest, - final InputStream stream) throws IOException { - if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); - if (stream == null) throw new IllegalArgumentException("stream can't be null"); - - final JsonReader json = localReader.get().process(stream); - try { - return deserializeList(manifest, json, stream); - } finally { - json.reset(); - } - } - - @SuppressWarnings("unchecked") - @Nullable - protected List deserializeList( - final Class manifest, - JsonReader json, - InputStream stream) throws IOException { - if (json.getNextToken() != '[') { - if (json.wasNull()) { - return null; - } - throw json.newParseError("Expecting '[' for list start"); - } - if (json.getNextToken() == ']') { - return new ArrayList(0); - } - //leave for now in to avoid overhead of going through redirection via generic tryFindReader - if (JsonObject.class.isAssignableFrom(manifest)) { - final JsonReader.ReadJsonObject reader = getObjectReader(manifest); - if (reader != null) { - return (List) json.deserializeNullableCollection(reader); - } - } - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return json.deserializeNullableCollectionCustom(simpleReader); - } - if (fallback != null) { - final Object array = Array.newInstance(manifest, 0); - final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); - if (result == null) { - return null; - } - final ArrayList list = new ArrayList(result.length); - for (TResult aResult : result) { - list.add(aResult); - } - return list; - } - throw createErrorMessage(manifest); - } - - /** - * Convenient deserialize API for working with streams. - * Deserialize provided stream input into target object. - * This method accepts a buffer and will create a new reader using provided buffer. - * This buffer is used for internal conversion from stream into byte[] for partial processing. - * There is also method without the buffer which reuses local thread reader for processing. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - * Provided buffer will be used as input for partial processing. - *

- * For best performance buffer should be reused. - * - * @param manifest target type - * @param stream input JSON - * @param buffer buffer used for InputStream -> byte[] conversion - * @param target type - * @return deserialized instance - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public TResult deserialize( - final Class manifest, - final InputStream stream, - final byte[] buffer) throws IOException { - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - if (buffer == null) { - throw new IllegalArgumentException("buffer can't be null"); - } - return deserialize(manifest, newReader(stream, buffer), stream); - } - - /** - * Convenient deserialize API for working with streams. - * Deserialize provided stream input into target object. - * This method reuses thread local reader for processing input stream. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - *

- * - * @param manifest target type - * @param stream input JSON - * @param target type - * @return deserialized instance - * @throws IOException error during deserialization - */ - @SuppressWarnings("unchecked") - @Nullable - public TResult deserialize( - final Class manifest, - final InputStream stream) throws IOException { - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - final JsonReader json = localReader.get().process(stream); - try { - return deserialize(manifest, json, stream); - } finally { - json.reset(); - } - } - - @SuppressWarnings("unchecked") - @Nullable - protected TResult deserialize( - final Class manifest, - final JsonReader json, - final InputStream stream) throws IOException { - json.getNextToken(); - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return (TResult) simpleReader.read(json); - } - if (manifest.isArray()) { - if (json.wasNull()) { - return null; - } else if (json.last() != '[') { - throw json.newParseError("Expecting '[' for array start"); - } - final Class elementManifest = manifest.getComponentType(); - if (json.getNextToken() == ']') { - return (TResult) Array.newInstance(elementManifest, 0); - } - //leave for now in to avoid overhead of going through redirection via generic tryFindReader - if (JsonObject.class.isAssignableFrom(elementManifest)) { - final JsonReader.ReadJsonObject objectReader = getObjectReader(elementManifest); - if (objectReader != null) { - List list = json.deserializeNullableCollection(objectReader); - return (TResult) convertResultToArray(elementManifest, list); - } - } - final JsonReader.ReadObject simpleElementReader = tryFindReader(elementManifest); - if (simpleElementReader != null) { - List list = json.deserializeNullableCollectionCustom(simpleElementReader); - return (TResult) convertResultToArray(elementManifest, list); - } - } - if (fallback != null) { - return (TResult) fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); - } - throw createErrorMessage(manifest); - } - - /** - * Deserialize API for working with streams. - * Deserialize provided stream input into target object. - * Use buffer for internal conversion from stream into byte[] for partial processing. - * This method creates a new instance of JsonReader for processing the stream. - * There is also a method without the byte[] buffer which reuses thread local reader. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - * DslJson will treat input as a sequence of bytes which allows for various optimizations. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - * Provided buffer will be used as input for partial processing. - *

- * For best performance buffer should be reused. - * - * @param manifest target type - * @param stream input JSON - * @param buffer buffer used for InputStream -> byte[] conversion - * @return deserialized instance - * @throws IOException error during deserialization - */ - @Nullable - public Object deserialize( - final Type manifest, - final InputStream stream, - final byte[] buffer) throws IOException { - if (manifest instanceof Class) { - return deserialize((Class) manifest, stream, buffer); - } - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - if (buffer == null) { - throw new IllegalArgumentException("buffer can't be null"); - } - final JsonReader json = newReader(stream, buffer); - json.getNextToken(); - final Object result = deserializeWith(manifest, json); - if (result != unknownValue) return result; - if (fallback != null) { - return fallback.deserialize(context, manifest, new RereadStream(buffer, stream)); - } - throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + - "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); - } - - /** - * Deserialize API for working with streams. - * Deserialize provided stream input into target object. - * This method reuses thread local reader for processing JSON input. - *

- * Since JSON is often though of as a series of char, - * most libraries will convert inputs into a sequence of chars and do processing on them. - *

- * When working on InputStream DslJson will process JSON in chunks of byte[] inputs. - *

- * - * @param manifest target type - * @param stream input JSON - * @return deserialized instance - * @throws IOException error during deserialization - */ - @Nullable - public Object deserialize( - final Type manifest, - final InputStream stream) throws IOException { - if (manifest instanceof Class) { - return deserialize((Class) manifest, stream); - } - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - final JsonReader json = localReader.get().process(stream); - try { - json.getNextToken(); - final Object result = deserializeWith(manifest, json); - if (result != unknownValue) return result; - if (fallback != null) { - return fallback.deserialize(context, manifest, new RereadStream(json.buffer, stream)); - } - throw new ConfigurationException("Unable to find reader for provided type: " + manifest + " and fallback serialization is not registered.\n" + - "Try initializing DslJson with custom fallback in case of unsupported objects or register specified type using registerReader into " + getClass()); - } finally { - json.reset(); - } - } - - static class RereadStream extends InputStream { - private final byte[] buffer; - private final InputStream stream; - private boolean usingBuffer; - private int position; - - RereadStream(byte[] buffer, InputStream stream) { - this.buffer = buffer; - this.stream = stream; - usingBuffer = true; - } - - @Override - public int read() throws IOException { - if (usingBuffer) { - if (position < buffer.length) { - return buffer[position++]; - } else usingBuffer = false; - } - return stream.read(); - } - - @Override - public int read(byte[] buf) throws IOException { - if (usingBuffer) { - return super.read(buf); - } - return stream.read(buf); - } - - @Override - public int read(byte[] buf, int off, int len) throws IOException { - if (usingBuffer) { - return super.read(buf, off, len); - } - return stream.read(buf, off, len); - } - } - - private static final Iterator EMPTY_ITERATOR = new Iterator() { - @Override - public boolean hasNext() { - return false; - } - - @Override - public void remove() { - } - - @Nullable - @Override - public Object next() { - return null; - } - }; - - /** - * Streaming API for collection deserialization. - * DslJson will create iterator based on provided manifest info. - * It will attempt to deserialize from stream on each next() invocation. - * This method requires buffer instance for partial stream processing. - * It will create a new instance of JsonReader. - * There is also a method without the buffer which will reuse thread local reader. - *

- * Useful for processing very large streams if only one instance from collection is required at once. - *

- * Stream will be processed in chunks of specified buffer byte[]. - * It will block on reading until buffer is full or end of stream is detected. - * - * @param manifest type info - * @param stream JSON data stream - * @param type info - * @return Iterator to instances deserialized from input JSON - * @throws IOException if reader is not found or there is an error processing input stream - */ - @SuppressWarnings("unchecked") - @Nullable - public Iterator iterateOver( - final Class manifest, - final InputStream stream) throws IOException { - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - final JsonReader json = localReader.get(); - json.process(stream); - return iterateOver(manifest, json, stream); - } - - - /** - * Streaming API for collection deserialization. - * DslJson will create iterator based on provided manifest info. - * It will attempt to deserialize from stream on each next() invocation. - * This method requires buffer instance for partial stream processing. - * It will create a new instance of JsonReader. - * There is also a method without the buffer which will reuse thread local reader. - *

- * Useful for processing very large streams if only one instance from collection is required at once. - *

- * Stream will be processed in chunks of specified buffer byte[]. - * It will block on reading until buffer is full or end of stream is detected. - * - * @param manifest type info - * @param stream JSON data stream - * @param buffer size of processing chunk - * @param type info - * @return Iterator to instances deserialized from input JSON - * @throws IOException if reader is not found or there is an error processing input stream - */ - @SuppressWarnings("unchecked") - @Nullable - public Iterator iterateOver( - final Class manifest, - final InputStream stream, - final byte[] buffer) throws IOException { - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - if (buffer == null) { - throw new IllegalArgumentException("buffer can't be null"); - } - return iterateOver(manifest, newReader(stream, buffer), stream); - } - - @SuppressWarnings("unchecked") - @Nullable - protected Iterator iterateOver( - final Class manifest, - final JsonReader json, - final InputStream stream) throws IOException { - if (json.getNextToken() != '[') { - if (json.wasNull()) { - return null; - } - throw json.newParseError("Expecting '[' for iterator start"); - } - if (json.getNextToken() == ']') { - return EMPTY_ITERATOR; - } - //leave for now in to avoid overhead of going through redirection via generic tryFindReader - if (JsonObject.class.isAssignableFrom(manifest)) { - final JsonReader.ReadJsonObject reader = getObjectReader(manifest); - if (reader != null) { - return json.iterateOver(reader); - } - } - final JsonReader.ReadObject simpleReader = tryFindReader(manifest); - if (simpleReader != null) { - return json.iterateOverCustom(simpleReader); - } - if (fallback != null) { - final Object array = Array.newInstance(manifest, 0); - final TResult[] result = (TResult[]) fallback.deserialize(context, array.getClass(), new RereadStream(json.buffer, stream)); - if (result == null) { - return null; - } - final ArrayList list = new ArrayList(result.length); - for (TResult aResult : result) { - list.add(aResult); - } - return list.iterator(); - } - throw createErrorMessage(manifest); - } - - private final JsonWriter.WriteObject OBJECT_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable JsonObject value) { - if (value == null) writer.writeNull(); - else value.serialize(writer, omitDefaults); - } - }; - private JsonReader.ReadObject convertToReader(final JsonReader.ReadJsonObject decoder) { - return new JsonReader.ReadObject() { - @Override - public T read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - else if (reader.last() != '{') throw reader.newParseError("Expecting '{' for object start"); - reader.getNextToken(); - return decoder.deserialize(reader); - } - }; - } - - private final JsonWriter.WriteObject OBJECT_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Object value) { - serialize(writer, (JsonObject[]) value); - } - }; - - private static final JsonWriter.WriteObject CHAR_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Object value) { - StringConverter.serialize(new String((char[]) value), writer); - } - }; - - private final JsonWriter.WriteObject NULL_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Object value) { - writer.writeNull(); - } - }; - - @SuppressWarnings("unchecked") - private JsonWriter.WriteObject getOrCreateWriter(@Nullable final Object instance, final Class instanceManifest) throws IOException { - if (instance instanceof JsonObject) { - return OBJECT_WRITER; - } - if (instance instanceof JsonObject[]) { - return OBJECT_ARRAY_WRITER; - } - final Class manifest = instanceManifest != null ? instanceManifest : instance.getClass(); - if (instanceManifest != null) { - if (JsonObject.class.isAssignableFrom(manifest)) { - return OBJECT_WRITER; - } - } - final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); - if (simpleWriter != null) { - return simpleWriter; - } - if (manifest.isArray()) { - final Class elementManifest = manifest.getComponentType(); - if (char.class == elementManifest) { - return CHAR_ARRAY_WRITER; - } else { - final JsonWriter.WriteObject elementWriter = tryFindWriter(elementManifest); - if (elementWriter != null) { - //TODO: cache writer for next lookup - return new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Object value) { - writer.serialize((Object[]) value, elementWriter); - } - }; - } - } - } - if (instance instanceof Collection || Collection.class.isAssignableFrom(manifest)) { - return new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable final Object value) { - final Collection items = (Collection) value; - Class baseType = null; - final Iterator iterator = items.iterator(); - //TODO: pick lowest common denominator!? - do { - final Object item = iterator.next(); - if (item != null) { - Class elementType = item.getClass(); - if (elementType != baseType) { - if (baseType == null || elementType.isAssignableFrom(baseType)) { - baseType = elementType; - } - } - } - } while (iterator.hasNext()); - if (baseType == null) { - writer.writeByte(JsonWriter.ARRAY_START); - writer.writeNull(); - for (int i = 1; i < items.size(); i++) { - writer.writeAscii(",null"); - } - writer.writeByte(JsonWriter.ARRAY_END); - } else if (JsonObject.class.isAssignableFrom(baseType)) { - serialize(writer, (Collection) items); - } else { - final JsonWriter.WriteObject elementWriter = tryFindWriter(baseType); - if (elementWriter != null) { - writer.serialize(items, elementWriter); - } else if (fallback != null) { - final ByteArrayOutputStream stream = new ByteArrayOutputStream(); - stream.reset(); - try { - fallback.serialize(value, stream); - } catch (IOException ex) { - throw new SerializationException(ex); - } - writer.writeAscii(stream.toByteArray()); - } else { - throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + items.getClass()); - } - } - } - }; - } - throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); - } - - /** - * Streaming API for collection serialization. - *

- * It will iterate over entire iterator and serialize each instance into target output stream. - * After each instance serialization it will copy JSON into target output stream. - * During each serialization reader will be looked up based on next() instance which allows - * serializing collection with different types. - * If collection contains all instances of the same type, prefer the other streaming API. - *

- * If reader is not found an IOException will be thrown - *

- * If JsonWriter is provided it will be used, otherwise a new instance will be internally created. - * - * @param iterator input data - * @param stream target JSON stream - * @param writer temporary buffer for serializing a single item. Can be null - * @param input data type - * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream - */ - @SuppressWarnings("unchecked") - public void iterateOver( - final Iterator iterator, - final OutputStream stream, - @Nullable final JsonWriter writer) throws IOException { - if (iterator == null) { - throw new IllegalArgumentException("iterator can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - stream.write(JsonWriter.ARRAY_START); - if (!iterator.hasNext()) { - stream.write(JsonWriter.ARRAY_END); - return; - } - final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; - T item = iterator.next(); - Class lastManifest = null; - JsonWriter.WriteObject lastWriter = null; - if (item != null) { - lastManifest = item.getClass(); - lastWriter = getOrCreateWriter(item, lastManifest); - buffer.reset(); - try { - lastWriter.write(buffer, item); - } catch (ConfigurationException e) { - throw e; - } catch (Exception e) { - throw new IOException(e); - } - buffer.toStream(stream); - } else { - stream.write(NULL); - } - while (iterator.hasNext()) { - stream.write(JsonWriter.COMMA); - item = iterator.next(); - if (item != null) { - final Class currentManifest = item.getClass(); - if (lastWriter == null || lastManifest == null || !lastManifest.equals(currentManifest)) { - lastManifest = currentManifest; - lastWriter = getOrCreateWriter(item, lastManifest); - } - buffer.reset(); - try { - lastWriter.write(buffer, item); - } catch (ConfigurationException e) { - throw e; - } catch (Exception e) { - throw new IOException(e); - } - buffer.toStream(stream); - } else { - stream.write(NULL); - } - } - stream.write(JsonWriter.ARRAY_END); - } - - /** - * Streaming API for collection serialization. - *

- * It will iterate over entire iterator and serialize each instance into target output stream. - * After each instance serialization it will copy JSON into target output stream. - *

- * If reader is not found an IOException will be thrown - *

- * If JsonWriter is provided it will be used, otherwise a new instance will be internally created. - * - * @param iterator input data - * @param manifest type of elements in collection - * @param stream target JSON stream - * @param writer temporary buffer for serializing a single item. Can be null - * @param input data type - * @throws IOException reader is not found, there is an error during serialization or problem with writing to target stream - */ - @SuppressWarnings("unchecked") - public void iterateOver( - final Iterator iterator, - final Class manifest, - final OutputStream stream, - @Nullable final JsonWriter writer) throws IOException { - if (iterator == null) { - throw new IllegalArgumentException("iterator can't be null"); - } - if (manifest == null) { - throw new IllegalArgumentException("manifest can't be null"); - } - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - final JsonWriter buffer = writer == null ? new JsonWriter(this) : writer; - final JsonWriter.WriteObject instanceWriter = getOrCreateWriter(null, manifest); - stream.write(JsonWriter.ARRAY_START); - T item = iterator.next(); - if (item != null) { - buffer.reset(); - try { - instanceWriter.write(buffer, item); - } catch (ConfigurationException e) { - throw e; - } catch (Exception e) { - throw new IOException(e); - } - buffer.toStream(stream); - } else { - stream.write(NULL); - } - while (iterator.hasNext()) { - stream.write(JsonWriter.COMMA); - item = iterator.next(); - if (item != null) { - buffer.reset(); - try { - instanceWriter.write(buffer, item); - } catch (ConfigurationException e) { - throw e; - } catch (Exception e) { - throw new IOException(e); - } - buffer.toStream(stream); - } else { - stream.write(NULL); - } - } - stream.write(JsonWriter.ARRAY_END); - } - - /** - * Use writer.serialize instead - * - * @param writer writer - * @param array items - * @param type - */ - @Deprecated - public void serialize(final JsonWriter writer, @Nullable final T[] array) { - if (array == null) { - writer.writeNull(); - return; - } - writer.writeByte(JsonWriter.ARRAY_START); - if (array.length != 0) { - T item = array[0]; - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - for (int i = 1; i < array.length; i++) { - writer.writeByte(JsonWriter.COMMA); - item = array[i]; - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - } - } - writer.writeByte(JsonWriter.ARRAY_END); - } - - /** - * Use writer.serialize instead - * - * @param writer writer - * @param array items - * @param len part of array - * @param type - */ - @Deprecated - public void serialize(final JsonWriter writer, final T[] array, final int len) { - if (writer == null) { - throw new IllegalArgumentException("writer can't be null"); - } - if (array == null) { - writer.writeNull(); - return; - } - writer.writeByte(JsonWriter.ARRAY_START); - if (len != 0) { - T item = array[0]; - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - for (int i = 1; i < len; i++) { - writer.writeByte(JsonWriter.COMMA); - item = array[i]; - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - } - } - writer.writeByte(JsonWriter.ARRAY_END); - } - - /** - * Use writer.serialize instead - * - * @param writer writer - * @param list items - * @param type - */ - @Deprecated - public void serialize(final JsonWriter writer, @Nullable final List list) { - if (writer == null) { - throw new IllegalArgumentException("writer can't be null"); - } - if (list == null) { - writer.writeNull(); - return; - } - writer.writeByte(JsonWriter.ARRAY_START); - if (list.size() != 0) { - T item = list.get(0); - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - for (int i = 1; i < list.size(); i++) { - writer.writeByte(JsonWriter.COMMA); - item = list.get(i); - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - } - } - writer.writeByte(JsonWriter.ARRAY_END); - } - - /** - * Use writer.serialize instead - * - * @param writer writer - * @param collection items - * @param type - */ - @Deprecated - public void serialize(final JsonWriter writer, @Nullable final Collection collection) { - if (writer == null) { - throw new IllegalArgumentException("writer can't be null"); - } - if (collection == null) { - writer.writeNull(); - return; - } - writer.writeByte(JsonWriter.ARRAY_START); - if (!collection.isEmpty()) { - final Iterator it = collection.iterator(); - T item = it.next(); - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - while (it.hasNext()) { - writer.writeByte(JsonWriter.COMMA); - item = it.next(); - if (item != null) { - item.serialize(writer, omitDefaults); - } else { - writer.writeNull(); - } - } - } - writer.writeByte(JsonWriter.ARRAY_END); - } - - /** - * Generic serialize API. - * Based on provided type manifest converter will be chosen. - * If converter is not found method will return false. - *

- * Resulting JSON will be written into provided writer argument. - * In case of successful serialization true will be returned. - *

- * For best performance writer argument should be reused. - * - * @param writer where to write resulting JSON - * @param manifest type manifest - * @param value instance to serialize - * @return successful serialization - */ - @SuppressWarnings("unchecked") - public boolean serialize(final JsonWriter writer, final Type manifest, @Nullable final Object value) { - try { - if (writer == null) { - throw new IllegalArgumentException("writer can't be null"); - } - if (value == null) { - writer.writeNull(); - return true; - } else if (value instanceof JsonObject) { - ((JsonObject) value).serialize(writer, omitDefaults); - return true; - } else if (value instanceof JsonObject[]) { - serialize(writer, (JsonObject[]) value); - return true; - } - final JsonWriter.WriteObject simpleWriter = tryFindWriter(manifest); - if (simpleWriter != null) { - simpleWriter.write(writer, value); - return true; - } - Class container = null; - if (manifest instanceof Class) { - container = (Class) manifest; - } - if (container != null && container.isArray()) { - if (Array.getLength(value) == 0) { - writer.writeAscii("[]"); - return true; - } - final Class elementManifest = container.getComponentType(); - if (char.class == elementManifest) { - //TODO? char[] !? - StringConverter.serialize(new String((char[]) value), writer); - return true; - } else { - final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(elementManifest); - if (elementWriter != null) { - writer.serialize((Object[]) value, elementWriter); - return true; - } - } - } - if (value instanceof Collection) { - final Collection items = (Collection) value; - if (items.isEmpty()) { - writer.writeAscii("[]"); - return true; - } - Class baseType = null; - final Iterator iterator = items.iterator(); - final boolean isList = items instanceof List; - final List values = isList ? (List) items : new ArrayList(); - final ArrayList itemWriters = new ArrayList(); - Class lastElementType = null; - JsonWriter.WriteObject lastWriter = null; - boolean hasUnknownWriter = false; - //TODO: pick lowest common denominator!? - do { - final Object item = iterator.next(); - if (!isList) { - values.add(item); - } - if (item != null) { - final Class elementType = item.getClass(); - if (elementType != baseType) { - if (baseType == null || elementType.isAssignableFrom(baseType)) { - baseType = elementType; - } - } - if (lastElementType != elementType) { - lastElementType = elementType; - lastWriter = tryFindWriter(elementType); - } - itemWriters.add(lastWriter); - hasUnknownWriter = hasUnknownWriter || lastWriter == null; - } else { - itemWriters.add(NULL_WRITER); - } - } while (iterator.hasNext()); - if (baseType != null && JsonObject.class.isAssignableFrom(baseType)) { - writer.writeByte(JsonWriter.ARRAY_START); - final Iterator iter = values.iterator(); - final JsonObject first = (JsonObject) iter.next(); - if (first != null) first.serialize(writer, omitDefaults); - else writer.writeNull(); - while (iter.hasNext()) { - writer.writeByte(JsonWriter.COMMA); - final JsonObject next = (JsonObject) iter.next(); - if (next != null) next.serialize(writer, omitDefaults); - else writer.writeNull(); - } - writer.writeByte(JsonWriter.ARRAY_END); - return true; - } - if (!hasUnknownWriter) { - writer.writeByte(JsonWriter.ARRAY_START); - final Iterator iter = values.iterator(); - int cur = 1; - itemWriters.get(0).write(writer, iter.next()); - while (iter.hasNext()) { - writer.writeByte(JsonWriter.COMMA); - itemWriters.get(cur++).write(writer, iter.next()); - } - writer.writeByte(JsonWriter.ARRAY_END); - return true; - } - final JsonWriter.WriteObject elementWriter = (JsonWriter.WriteObject) tryFindWriter(baseType); - if (elementWriter != null) { - writer.serialize(items, elementWriter); - return true; - } - } - return false; - } catch (ClassCastException exc) { // workaround for mixed primitive arrays (PLAT-7551) - return false; - } - } - - private static final byte[] NULL = new byte[]{'n', 'u', 'l', 'l'}; - - /** - * Convenient serialize API. - * In most cases JSON is serialized into target `OutputStream`. - * This method will reuse thread local instance of `JsonWriter` and serialize JSON into it. - * - * @param value instance to serialize - * @param stream where to write resulting JSON - * @throws IOException error when unable to serialize instance - */ - public final void serialize(@Nullable final Object value, final OutputStream stream) throws IOException { - if (stream == null) { - throw new IllegalArgumentException("stream can't be null"); - } - if (value == null) { - stream.write(NULL); - return; - } - final JsonWriter jw = localWriter.get(); - jw.reset(stream); - final Class manifest = value.getClass(); - if (!serialize(jw, manifest, value)) { - if (fallback == null) { - throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); - } - fallback.serialize(value, stream); - } else { - jw.flush(); - jw.reset(null); - } - } - - /** - * Main serialization API. - * Convert object instance into JSON. - *

- * JsonWriter contains a growable byte[] where JSON will be serialized. - * After serialization JsonWriter can be copied into OutputStream or it's byte[] can be obtained - *

- * For best performance reuse `JsonWriter` or even better call `JsonWriter.WriteObject` directly - * - * @param writer where to write resulting JSON - * @param value object instance to serialize - * @throws IOException error when unable to serialize instance - */ - public final void serialize(final JsonWriter writer, @Nullable final Object value) throws IOException { - if (writer == null) { - throw new IllegalArgumentException("writer can't be null"); - } - if (value == null) { - writer.writeNull(); - return; - } - final Class manifest = value.getClass(); - if (!serialize(writer, manifest, value)) { - if (fallback == null) { - throw new ConfigurationException("Unable to serialize provided object. Failed to find serializer for: " + manifest); - } - ByteArrayOutputStream stream = new ByteArrayOutputStream(); - fallback.serialize(value, stream); - writer.writeAscii(stream.toByteArray()); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java deleted file mode 100644 index 0e7b75aacc..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ExternalConverterAnalyzer.java +++ /dev/null @@ -1,49 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import java.lang.reflect.InvocationTargetException; -import java.util.*; - -class ExternalConverterAnalyzer { - private final Set lookedUpClasses = new HashSet(); - private final ClassLoader[] classLoaders; - - ExternalConverterAnalyzer(Collection classLoaders) { - this.classLoaders = classLoaders.toArray(new ClassLoader[0]); - } - - synchronized boolean tryFindConverter(Class manifest, DslJson dslJson) { - final String className = manifest.getName(); - if (!lookedUpClasses.add(className)) return false; - String[] converterClassNames = resolveExternalConverterClassNames(className); - for (ClassLoader cl : classLoaders) { - for (String ccn : converterClassNames) { - try { - Class converterClass = cl.loadClass(ccn); - if (!Configuration.class.isAssignableFrom(converterClass)) continue; - Configuration converter = (Configuration) converterClass.getDeclaredConstructor().newInstance(); - converter.configure(dslJson); - return true; - } catch (ClassNotFoundException ignored) { - } catch (IllegalAccessException ignored) { - } catch (InstantiationException ignored) { - } catch (InvocationTargetException e) { - } catch (NoSuchMethodException e) { - } - } - } - return false; - } - - private String[] resolveExternalConverterClassNames(final String fullClassName) { - int dotIndex = fullClassName.lastIndexOf('.'); - if (dotIndex == -1) { - return new String[]{String.format("_%s_DslJsonConverter", fullClassName)}; - } - String packageName = fullClassName.substring(0, dotIndex); - String className = fullClassName.substring(dotIndex + 1); - return new String[]{ - String.format("%s._%s_DslJsonConverter", packageName, className), - String.format("dsl_json.%s._%s_DslJsonConverter", packageName, className), - String.format("dsl_json.%s.%sDslJsonConverter", packageName, className)}; - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java deleted file mode 100644 index 0b4f8d2957..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/Grisu3.java +++ /dev/null @@ -1,924 +0,0 @@ -// Copyright 2010 the V8 project authors. All rights reserved. -// Redistribution and use in source and binary forms, with or without -// modification, are permitted provided that the following conditions are -// met: -// -// * Redistributions of source code must retain the above copyright -// notice, this list of conditions and the following disclaimer. -// * Redistributions in binary form must reproduce the above -// copyright notice, this list of conditions and the following -// disclaimer in the documentation and/or other materials provided -// with the distribution. -// * Neither the name of Google Inc. nor the names of its -// contributors may be used to endorse or promote products derived -// from this software without specific prior written permission. -// -// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS -// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT -// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR -// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT -// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, -// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT -// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, -// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY -// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT -// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE -// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. - -// Ported to Java from Mozilla's version of V8-dtoa by Hannes Wallnoefer. -// The original revision was 67d1049b0bf9 from the mozilla-central tree. - -// Modified by Rikard Pavelic do avoid allocations -// and unused code paths due to external checks - -package com.bugsnag.android.repackaged.dslplatform.json; - -@SuppressWarnings("fallthrough") // suppress pre-existing warnings -abstract class Grisu3 { - - // FastDtoa will produce at most kFastDtoaMaximalLength digits. - static final int kFastDtoaMaximalLength = 17; - - - // The minimal and maximal target exponent define the range of w's binary - // exponent, where 'w' is the result of multiplying the input by a cached power - // of ten. - // - // A different range might be chosen on a different platform, to optimize digit - // generation, but a smaller range requires more powers of ten to be cached. - static final int minimal_target_exponent = -60; - - private static final class DiyFp { - - long f; - int e; - - static final int kSignificandSize = 64; - static final long kUint64MSB = 0x8000000000000000L; - private static final long kM32 = 0xFFFFFFFFL; - private static final long k10MSBits = 0xFFC00000L << 32; - - DiyFp() { - this.f = 0; - this.e = 0; - } - - // this = this - other. - // The exponents of both numbers must be the same and the significand of this - // must be bigger than the significand of other. - // The result will not be normalized. - void subtract(DiyFp other) { - f -= other.f; - } - - // this = this * other. - void multiply(DiyFp other) { - // Simply "emulates" a 128 bit multiplication. - // However: the resulting number only contains 64 bits. The least - // significant 64 bits are only used for rounding the most significant 64 - // bits. - long a = f >>> 32; - long b = f & kM32; - long c = other.f >>> 32; - long d = other.f & kM32; - long ac = a * c; - long bc = b * c; - long ad = a * d; - long bd = b * d; - long tmp = (bd >>> 32) + (ad & kM32) + (bc & kM32); - // By adding 1U << 31 to tmp we round the final result. - // Halfway cases will be round up. - tmp += 1L << 31; - long result_f = ac + (ad >>> 32) + (bc >>> 32) + (tmp >>> 32); - e += other.e + 64; - f = result_f; - } - - void normalize() { - long f = this.f; - int e = this.e; - - // This method is mainly called for normalizing boundaries. In general - // boundaries need to be shifted by 10 bits. We thus optimize for this case. - while ((f & k10MSBits) == 0) { - f <<= 10; - e -= 10; - } - while ((f & kUint64MSB) == 0) { - f <<= 1; - e--; - } - this.f = f; - this.e = e; - } - - void reset() { - e = 0; - f = 0; - } - - @Override - public String toString() { - return "[DiyFp f:" + f + ", e:" + e + "]"; - } - - } - - private static class CachedPowers { - - static final double kD_1_LOG2_10 = 0.30102999566398114; // 1 / lg(10) - - static class CachedPower { - final long significand; - final short binaryExponent; - final short decimalExponent; - - CachedPower(long significand, short binaryExponent, short decimalExponent) { - this.significand = significand; - this.binaryExponent = binaryExponent; - this.decimalExponent = decimalExponent; - } - } - - static int getCachedPower(int e, int alpha, DiyFp c_mk) { - final int kQ = DiyFp.kSignificandSize; - final double k = Math.ceil((alpha - e + kQ - 1) * kD_1_LOG2_10); - final int index = (GRISU_CACHE_OFFSET + (int) k - 1) / CACHED_POWERS_SPACING + 1; - final CachedPower cachedPower = CACHED_POWERS[index]; - - c_mk.f = cachedPower.significand; - c_mk.e = cachedPower.binaryExponent; - return cachedPower.decimalExponent; - } - - // Code below is converted from GRISU_CACHE_NAME(8) in file "powers-ten.h" - // Regexp to convert this from original C++ source: - // \{GRISU_UINT64_C\((\w+), (\w+)\), (\-?\d+), (\-?\d+)\} - - // interval between entries of the powers cache below - static final int CACHED_POWERS_SPACING = 8; - - static final CachedPower[] CACHED_POWERS = { - new CachedPower(0xe61acf033d1a45dfL, (short) -1087, (short) -308), - new CachedPower(0xab70fe17c79ac6caL, (short) -1060, (short) -300), - new CachedPower(0xff77b1fcbebcdc4fL, (short) -1034, (short) -292), - new CachedPower(0xbe5691ef416bd60cL, (short) -1007, (short) -284), - new CachedPower(0x8dd01fad907ffc3cL, (short) -980, (short) -276), - new CachedPower(0xd3515c2831559a83L, (short) -954, (short) -268), - new CachedPower(0x9d71ac8fada6c9b5L, (short) -927, (short) -260), - new CachedPower(0xea9c227723ee8bcbL, (short) -901, (short) -252), - new CachedPower(0xaecc49914078536dL, (short) -874, (short) -244), - new CachedPower(0x823c12795db6ce57L, (short) -847, (short) -236), - new CachedPower(0xc21094364dfb5637L, (short) -821, (short) -228), - new CachedPower(0x9096ea6f3848984fL, (short) -794, (short) -220), - new CachedPower(0xd77485cb25823ac7L, (short) -768, (short) -212), - new CachedPower(0xa086cfcd97bf97f4L, (short) -741, (short) -204), - new CachedPower(0xef340a98172aace5L, (short) -715, (short) -196), - new CachedPower(0xb23867fb2a35b28eL, (short) -688, (short) -188), - new CachedPower(0x84c8d4dfd2c63f3bL, (short) -661, (short) -180), - new CachedPower(0xc5dd44271ad3cdbaL, (short) -635, (short) -172), - new CachedPower(0x936b9fcebb25c996L, (short) -608, (short) -164), - new CachedPower(0xdbac6c247d62a584L, (short) -582, (short) -156), - new CachedPower(0xa3ab66580d5fdaf6L, (short) -555, (short) -148), - new CachedPower(0xf3e2f893dec3f126L, (short) -529, (short) -140), - new CachedPower(0xb5b5ada8aaff80b8L, (short) -502, (short) -132), - new CachedPower(0x87625f056c7c4a8bL, (short) -475, (short) -124), - new CachedPower(0xc9bcff6034c13053L, (short) -449, (short) -116), - new CachedPower(0x964e858c91ba2655L, (short) -422, (short) -108), - new CachedPower(0xdff9772470297ebdL, (short) -396, (short) -100), - new CachedPower(0xa6dfbd9fb8e5b88fL, (short) -369, (short) -92), - new CachedPower(0xf8a95fcf88747d94L, (short) -343, (short) -84), - new CachedPower(0xb94470938fa89bcfL, (short) -316, (short) -76), - new CachedPower(0x8a08f0f8bf0f156bL, (short) -289, (short) -68), - new CachedPower(0xcdb02555653131b6L, (short) -263, (short) -60), - new CachedPower(0x993fe2c6d07b7facL, (short) -236, (short) -52), - new CachedPower(0xe45c10c42a2b3b06L, (short) -210, (short) -44), - new CachedPower(0xaa242499697392d3L, (short) -183, (short) -36), - new CachedPower(0xfd87b5f28300ca0eL, (short) -157, (short) -28), - new CachedPower(0xbce5086492111aebL, (short) -130, (short) -20), - new CachedPower(0x8cbccc096f5088ccL, (short) -103, (short) -12), - new CachedPower(0xd1b71758e219652cL, (short) -77, (short) -4), - new CachedPower(0x9c40000000000000L, (short) -50, (short) 4), - new CachedPower(0xe8d4a51000000000L, (short) -24, (short) 12), - new CachedPower(0xad78ebc5ac620000L, (short) 3, (short) 20), - new CachedPower(0x813f3978f8940984L, (short) 30, (short) 28), - new CachedPower(0xc097ce7bc90715b3L, (short) 56, (short) 36), - new CachedPower(0x8f7e32ce7bea5c70L, (short) 83, (short) 44), - new CachedPower(0xd5d238a4abe98068L, (short) 109, (short) 52), - new CachedPower(0x9f4f2726179a2245L, (short) 136, (short) 60), - new CachedPower(0xed63a231d4c4fb27L, (short) 162, (short) 68), - new CachedPower(0xb0de65388cc8ada8L, (short) 189, (short) 76), - new CachedPower(0x83c7088e1aab65dbL, (short) 216, (short) 84), - new CachedPower(0xc45d1df942711d9aL, (short) 242, (short) 92), - new CachedPower(0x924d692ca61be758L, (short) 269, (short) 100), - new CachedPower(0xda01ee641a708deaL, (short) 295, (short) 108), - new CachedPower(0xa26da3999aef774aL, (short) 322, (short) 116), - new CachedPower(0xf209787bb47d6b85L, (short) 348, (short) 124), - new CachedPower(0xb454e4a179dd1877L, (short) 375, (short) 132), - new CachedPower(0x865b86925b9bc5c2L, (short) 402, (short) 140), - new CachedPower(0xc83553c5c8965d3dL, (short) 428, (short) 148), - new CachedPower(0x952ab45cfa97a0b3L, (short) 455, (short) 156), - new CachedPower(0xde469fbd99a05fe3L, (short) 481, (short) 164), - new CachedPower(0xa59bc234db398c25L, (short) 508, (short) 172), - new CachedPower(0xf6c69a72a3989f5cL, (short) 534, (short) 180), - new CachedPower(0xb7dcbf5354e9beceL, (short) 561, (short) 188), - new CachedPower(0x88fcf317f22241e2L, (short) 588, (short) 196), - new CachedPower(0xcc20ce9bd35c78a5L, (short) 614, (short) 204), - new CachedPower(0x98165af37b2153dfL, (short) 641, (short) 212), - new CachedPower(0xe2a0b5dc971f303aL, (short) 667, (short) 220), - new CachedPower(0xa8d9d1535ce3b396L, (short) 694, (short) 228), - new CachedPower(0xfb9b7cd9a4a7443cL, (short) 720, (short) 236), - new CachedPower(0xbb764c4ca7a44410L, (short) 747, (short) 244), - new CachedPower(0x8bab8eefb6409c1aL, (short) 774, (short) 252), - new CachedPower(0xd01fef10a657842cL, (short) 800, (short) 260), - new CachedPower(0x9b10a4e5e9913129L, (short) 827, (short) 268), - new CachedPower(0xe7109bfba19c0c9dL, (short) 853, (short) 276), - new CachedPower(0xac2820d9623bf429L, (short) 880, (short) 284), - new CachedPower(0x80444b5e7aa7cf85L, (short) 907, (short) 292), - new CachedPower(0xbf21e44003acdd2dL, (short) 933, (short) 300), - new CachedPower(0x8e679c2f5e44ff8fL, (short) 960, (short) 308), - new CachedPower(0xd433179d9c8cb841L, (short) 986, (short) 316), - new CachedPower(0x9e19db92b4e31ba9L, (short) 1013, (short) 324), - new CachedPower(0xeb96bf6ebadf77d9L, (short) 1039, (short) 332), - new CachedPower(0xaf87023b9bf0ee6bL, (short) 1066, (short) 340) - }; - - // nb elements (8): 82 - - static final int GRISU_CACHE_OFFSET = 308; - } - - private static class DoubleHelper { - - static final long kExponentMask = 0x7FF0000000000000L; - static final long kSignificandMask = 0x000FFFFFFFFFFFFFL; - static final long kHiddenBit = 0x0010000000000000L; - - static void asDiyFp(long d64, DiyFp v) { - v.f = significand(d64); - v.e = exponent(d64); - } - - // this->Significand() must not be 0. - static void asNormalizedDiyFp(long d64, DiyFp w) { - long f = significand(d64); - int e = exponent(d64); - - // The current double could be a denormal. - while ((f & kHiddenBit) == 0) { - f <<= 1; - e--; - } - // Do the final shifts in one go. Don't forget the hidden bit (the '-1'). - f <<= DiyFp.kSignificandSize - kSignificandSize - 1; - e -= DiyFp.kSignificandSize - kSignificandSize - 1; - w.f = f; - w.e = e; - } - - static int exponent(long d64) { - if (isDenormal(d64)) return kDenormalExponent; - - int biased_e = (int) (((d64 & kExponentMask) >>> kSignificandSize) & 0xffffffffL); - return biased_e - kExponentBias; - } - - static long significand(long d64) { - long significand = d64 & kSignificandMask; - if (!isDenormal(d64)) { - return significand + kHiddenBit; - } else { - return significand; - } - } - - // Returns true if the double is a denormal. - private static boolean isDenormal(long d64) { - return (d64 & kExponentMask) == 0L; - } - - // Returns the two boundaries of first argument. - // The bigger boundary (m_plus) is normalized. The lower boundary has the same - // exponent as m_plus. - static void normalizedBoundaries(DiyFp v, long d64, DiyFp m_minus, DiyFp m_plus) { - asDiyFp(d64, v); - final boolean significand_is_zero = (v.f == kHiddenBit); - m_plus.f = (v.f << 1) + 1; - m_plus.e = v.e - 1; - m_plus.normalize(); - if (significand_is_zero && v.e != kDenormalExponent) { - // The boundary is closer. Think of v = 1000e10 and v- = 9999e9. - // Then the boundary (== (v - v-)/2) is not just at a distance of 1e9 but - // at a distance of 1e8. - // The only exception is for the smallest normal: the largest denormal is - // at the same distance as its successor. - // Note: denormals have the same exponent as the smallest normals. - m_minus.f = (v.f << 2) - 1; - m_minus.e = v.e - 2; - } else { - m_minus.f = (v.f << 1) - 1; - m_minus.e = v.e - 1; - } - m_minus.f = m_minus.f << (m_minus.e - m_plus.e); - m_minus.e = m_plus.e; - } - - private static final int kSignificandSize = 52; // Excludes the hidden bit. - private static final int kExponentBias = 0x3FF + kSignificandSize; - private static final int kDenormalExponent = -kExponentBias + 1; - - } - - static class FastDtoa { - - // Adjusts the last digit of the generated number, and screens out generated - // solutions that may be inaccurate. A solution may be inaccurate if it is - // outside the safe interval, or if we ctannot prove that it is closer to the - // input than a neighboring representation of the same length. - // - // Input: * buffer containing the digits of too_high / 10^kappa - // * distance_too_high_w == (too_high - w).f() * unit - // * unsafe_interval == (too_high - too_low).f() * unit - // * rest = (too_high - buffer * 10^kappa).f() * unit - // * ten_kappa = 10^kappa * unit - // * unit = the common multiplier - // Output: returns true if the buffer is guaranteed to contain the closest - // representable number to the input. - // Modifies the generated digits in the buffer to approach (round towards) w. - static boolean roundWeed( - final FastDtoaBuilder buffer, - final long distance_too_high_w, - final long unsafe_interval, - long rest, - final long ten_kappa, - final long unit) { - final long small_distance = distance_too_high_w - unit; - final long big_distance = distance_too_high_w + unit; - // Let w_low = too_high - big_distance, and - // w_high = too_high - small_distance. - // Note: w_low < w < w_high - // - // The real w (* unit) must lie somewhere inside the interval - // ]w_low; w_low[ (often written as "(w_low; w_low)") - - // Basically the buffer currently contains a number in the unsafe interval - // ]too_low; too_high[ with too_low < w < too_high - // - // too_high - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // ^v 1 unit ^ ^ ^ ^ - // boundary_high --------------------- . . . . - // ^v 1 unit . . . . - // - - - - - - - - - - - - - - - - - - - + - - + - - - - - - . . - // . . ^ . . - // . big_distance . . . - // . . . . rest - // small_distance . . . . - // v . . . . - // w_high - - - - - - - - - - - - - - - - - - . . . . - // ^v 1 unit . . . . - // w ---------------------------------------- . . . . - // ^v 1 unit v . . . - // w_low - - - - - - - - - - - - - - - - - - - - - . . . - // . . v - // buffer --------------------------------------------------+-------+-------- - // . . - // safe_interval . - // v . - // - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - . - // ^v 1 unit . - // boundary_low ------------------------- unsafe_interval - // ^v 1 unit v - // too_low - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - // - // - // Note that the value of buffer could lie anywhere inside the range too_low - // to too_high. - // - // boundary_low, boundary_high and w are approximations of the real boundaries - // and v (the input number). They are guaranteed to be precise up to one unit. - // In fact the error is guaranteed to be strictly less than one unit. - // - // Anything that lies outside the unsafe interval is guaranteed not to round - // to v when read again. - // Anything that lies inside the safe interval is guaranteed to round to v - // when read again. - // If the number inside the buffer lies inside the unsafe interval but not - // inside the safe interval then we simply do not know and bail out (returning - // false). - // - // Similarly we have to take into account the imprecision of 'w' when rounding - // the buffer. If we have two potential representations we need to make sure - // that the chosen one is closer to w_low and w_high since v can be anywhere - // between them. - // - // By generating the digits of too_high we got the largest (closest to - // too_high) buffer that is still in the unsafe interval. In the case where - // w_high < buffer < too_high we try to decrement the buffer. - // This way the buffer approaches (rounds towards) w. - // There are 3 conditions that stop the decrementation process: - // 1) the buffer is already below w_high - // 2) decrementing the buffer would make it leave the unsafe interval - // 3) decrementing the buffer would yield a number below w_high and farther - // away than the current number. In other words: - // (buffer{-1} < w_high) && w_high - buffer{-1} > buffer - w_high - // Instead of using the buffer directly we use its distance to too_high. - // Conceptually rest ~= too_high - buffer - while (rest < small_distance && // Negated condition 1 - unsafe_interval - rest >= ten_kappa && // Negated condition 2 - (rest + ten_kappa < small_distance || // buffer{-1} > w_high - small_distance - rest >= rest + ten_kappa - small_distance)) { - buffer.decreaseLast(); - rest += ten_kappa; - } - - // We have approached w+ as much as possible. We now test if approaching w- - // would require changing the buffer. If yes, then we have two possible - // representations close to w, but we cannot decide which one is closer. - if (rest < big_distance && - unsafe_interval - rest >= ten_kappa && - (rest + ten_kappa < big_distance || - big_distance - rest > rest + ten_kappa - big_distance)) { - return false; - } - - // Weeding test. - // The safe interval is [too_low + 2 ulp; too_high - 2 ulp] - // Since too_low = too_high - unsafe_interval this is equivalent to - // [too_high - unsafe_interval + 4 ulp; too_high - 2 ulp] - // Conceptually we have: rest ~= too_high - buffer - return (2 * unit <= rest) && (rest <= unsafe_interval - 4 * unit); - } - - static final int kTen4 = 10000; - static final int kTen5 = 100000; - static final int kTen6 = 1000000; - static final int kTen7 = 10000000; - static final int kTen8 = 100000000; - static final int kTen9 = 1000000000; - - // Returns the biggest power of ten that is less than or equal than the given - // number. We furthermore receive the maximum number of bits 'number' has. - // If number_bits == 0 then 0^-1 is returned - // The number of bits must be <= 32. - // Precondition: (1 << number_bits) <= number < (1 << (number_bits + 1)). - static long biggestPowerTen(int number, int number_bits) { - int power, exponent; - switch (number_bits) { - case 32: - case 31: - case 30: - if (kTen9 <= number) { - power = kTen9; - exponent = 9; - break; - } // else fallthrough - case 29: - case 28: - case 27: - if (kTen8 <= number) { - power = kTen8; - exponent = 8; - break; - } // else fallthrough - case 26: - case 25: - case 24: - if (kTen7 <= number) { - power = kTen7; - exponent = 7; - break; - } // else fallthrough - case 23: - case 22: - case 21: - case 20: - if (kTen6 <= number) { - power = kTen6; - exponent = 6; - break; - } // else fallthrough - case 19: - case 18: - case 17: - if (kTen5 <= number) { - power = kTen5; - exponent = 5; - break; - } // else fallthrough - case 16: - case 15: - case 14: - if (kTen4 <= number) { - power = kTen4; - exponent = 4; - break; - } // else fallthrough - case 13: - case 12: - case 11: - case 10: - if (1000 <= number) { - power = 1000; - exponent = 3; - break; - } // else fallthrough - case 9: - case 8: - case 7: - if (100 <= number) { - power = 100; - exponent = 2; - break; - } // else fallthrough - case 6: - case 5: - case 4: - if (10 <= number) { - power = 10; - exponent = 1; - break; - } // else fallthrough - case 3: - case 2: - case 1: - if (1 <= number) { - power = 1; - exponent = 0; - break; - } // else fallthrough - case 0: - power = 0; - exponent = -1; - break; - default: - // Following assignments are here to silence compiler warnings. - power = 0; - exponent = 0; - // UNREACHABLE(); - } - return ((long) power << 32) | (0xffffffffL & exponent); - } - - // Generates the digits of input number w. - // w is a floating-point number (DiyFp), consisting of a significand and an - // exponent. Its exponent is bounded by minimal_target_exponent and - // maximal_target_exponent. - // Hence -60 <= w.e() <= -32. - // - // Returns false if it fails, in which case the generated digits in the buffer - // should not be used. - // Preconditions: - // * low, w and high are correct up to 1 ulp (unit in the last place). That - // is, their error must be less that a unit of their last digits. - // * low.e() == w.e() == high.e() - // * low < w < high, and taking into account their error: low~ <= high~ - // * minimal_target_exponent <= w.e() <= maximal_target_exponent - // Postconditions: returns false if procedure fails. - // otherwise: - // * buffer is not null-terminated, but len contains the number of digits. - // * buffer contains the shortest possible decimal digit-sequence - // such that LOW < buffer * 10^kappa < HIGH, where LOW and HIGH are the - // correct values of low and high (without their error). - // * if more than one decimal representation gives the minimal number of - // decimal digits then the one closest to W (where W is the correct value - // of w) is chosen. - // Remark: this procedure takes into account the imprecision of its input - // numbers. If the precision is not enough to guarantee all the postconditions - // then false is returned. This usually happens rarely (~0.5%). - // - // Say, for the sake of example, that - // w.e() == -48, and w.f() == 0x1234567890abcdef - // w's value can be computed by w.f() * 2^w.e() - // We can obtain w's integral digits by simply shifting w.f() by -w.e(). - // -> w's integral part is 0x1234 - // w's fractional part is therefore 0x567890abcdef. - // Printing w's integral part is easy (simply print 0x1234 in decimal). - // In order to print its fraction we repeatedly multiply the fraction by 10 and - // get each digit. Example the first digit after the point would be computed by - // (0x567890abcdef * 10) >> 48. -> 3 - // The whole thing becomes slightly more complicated because we want to stop - // once we have enough digits. That is, once the digits inside the buffer - // represent 'w' we can stop. Everything inside the interval low - high - // represents w. However we have to pay attention to low, high and w's - // imprecision. - static boolean digitGen(FastDtoaBuilder buffer, int mk) { - final DiyFp low = buffer.scaled_boundary_minus; - final DiyFp w = buffer.scaled_w; - final DiyFp high = buffer.scaled_boundary_plus; - - // low, w and high are imprecise, but by less than one ulp (unit in the last - // place). - // If we remove (resp. add) 1 ulp from low (resp. high) we are certain that - // the new numbers are outside of the interval we want the final - // representation to lie in. - // Inversely adding (resp. removing) 1 ulp from low (resp. high) would yield - // numbers that are certain to lie in the interval. We will use this fact - // later on. - // We will now start by generating the digits within the uncertain - // interval. Later we will weed out representations that lie outside the safe - // interval and thus _might_ lie outside the correct interval. - long unit = 1; - final DiyFp too_low = buffer.too_low; - too_low.f = low.f - unit; - too_low.e = low.e; - final DiyFp too_high = buffer.too_high; - too_high.f = high.f + unit; - too_high.e = high.e; - // too_low and too_high are guaranteed to lie outside the interval we want the - // generated number in. - final DiyFp unsafe_interval = buffer.unsafe_interval; - unsafe_interval.f = too_high.f; - unsafe_interval.e = too_high.e; - unsafe_interval.subtract(too_low); - // We now cut the input number into two parts: the integral digits and the - // fractionals. We will not write any decimal separator though, but adapt - // kappa instead. - // Reminder: we are currently computing the digits (stored inside the buffer) - // such that: too_low < buffer * 10^kappa < too_high - // We use too_high for the digit_generation and stop as soon as possible. - // If we stop early we effectively round down. - final DiyFp one = buffer.one; - one.f = 1L << -w.e; - one.e = w.e; - // Division by one is a shift. - int integrals = (int) ((too_high.f >>> -one.e) & 0xffffffffL); - // Modulo by one is an and. - long fractionals = too_high.f & (one.f - 1); - long result = biggestPowerTen(integrals, DiyFp.kSignificandSize - (-one.e)); - int divider = (int) ((result >>> 32) & 0xffffffffL); - int divider_exponent = (int) (result & 0xffffffffL); - int kappa = divider_exponent + 1; - // Loop invariant: buffer = too_high / 10^kappa (integer division) - // The invariant holds for the first iteration: kappa has been initialized - // with the divider exponent + 1. And the divider is the biggest power of ten - // that is smaller than integrals. - while (kappa > 0) { - int digit = integrals / divider; - buffer.append((byte) ('0' + digit)); - integrals %= divider; - kappa--; - // Note that kappa now equals the exponent of the divider and that the - // invariant thus holds again. - final long rest = ((long) integrals << -one.e) + fractionals; - // Invariant: too_high = buffer * 10^kappa + DiyFp(rest, one.e()) - // Reminder: unsafe_interval.e() == one.e() - if (rest < unsafe_interval.f) { - // Rounding down (by not emitting the remaining digits) yields a number - // that lies within the unsafe interval. - buffer.point = buffer.end - mk + kappa; - final DiyFp minus_round = buffer.minus_round; - minus_round.f = too_high.f; - minus_round.e = too_high.e; - minus_round.subtract(w); - return roundWeed(buffer, minus_round.f, - unsafe_interval.f, rest, - (long) divider << -one.e, unit); - } - divider /= 10; - } - - // The integrals have been generated. We are at the point of the decimal - // separator. In the following loop we simply multiply the remaining digits by - // 10 and divide by one. We just need to pay attention to multiply associated - // data (like the interval or 'unit'), too. - // Instead of multiplying by 10 we multiply by 5 (cheaper operation) and - // increase its (imaginary) exponent. At the same time we decrease the - // divider's (one's) exponent and shift its significand. - // Basically, if fractionals was a DiyFp (with fractionals.e == one.e): - // fractionals.f *= 10; - // fractionals.f >>= 1; fractionals.e++; // value remains unchanged. - // one.f >>= 1; one.e++; // value remains unchanged. - // and we have again fractionals.e == one.e which allows us to divide - // fractionals.f() by one.f() - // We simply combine the *= 10 and the >>= 1. - while (true) { - fractionals *= 5; - unit *= 5; - unsafe_interval.f = unsafe_interval.f * 5; - unsafe_interval.e = unsafe_interval.e + 1; // Will be optimized out. - one.f = one.f >>> 1; - one.e = one.e + 1; - // Integer division by one. - final int digit = (int) ((fractionals >>> -one.e) & 0xffffffffL); - buffer.append((byte) ('0' + digit)); - fractionals &= one.f - 1; // Modulo by one. - kappa--; - if (fractionals < unsafe_interval.f) { - buffer.point = buffer.end - mk + kappa; - final DiyFp minus_round = buffer.minus_round; - minus_round.f = too_high.f; - minus_round.e = too_high.e; - minus_round.subtract(w); - return roundWeed(buffer, minus_round.f * unit, - unsafe_interval.f, fractionals, one.f, unit); - } - } - } - } - - public static boolean tryConvert(final double value, final FastDtoaBuilder buffer) { - final long bits; - final int firstDigit; - buffer.reset(); - if (value < 0) { - buffer.append((byte) '-'); - bits = Double.doubleToLongBits(-value); - firstDigit = 1; - } else { - bits = Double.doubleToLongBits(value); - firstDigit = 0; - } - - // Provides a decimal representation of v. - // Returns true if it succeeds, otherwise the result cannot be trusted. - // There will be *length digits inside the buffer (not null-terminated). - // If the function returns true then - // v == (double) (buffer * 10^decimal_exponent). - // The digits in the buffer are the shortest representation possible: no - // 0.09999999999999999 instead of 0.1. The shorter representation will even be - // chosen even if the longer one would be closer to v. - // The last digit will be closest to the actual v. That is, even if several - // digits might correctly yield 'v' when read again, the closest will be - // computed. - final int mk = buffer.initialize(bits); - - // DigitGen will generate the digits of scaled_w. Therefore we have - // v == (double) (scaled_w * 10^-mk). - // Set decimal_exponent == -mk and pass it to DigitGen. If scaled_w is not an - // integer than it will be updated. For instance if scaled_w == 1.23 then - // the buffer will be filled with "123" und the decimal_exponent will be - // decreased by 2. - if (FastDtoa.digitGen(buffer, mk)) { - buffer.write(firstDigit); - return true; - } else { - return false; - } - } - - static class FastDtoaBuilder { - - private final DiyFp v = new DiyFp(); - private final DiyFp w = new DiyFp(); - private final DiyFp boundary_minus = new DiyFp(); - private final DiyFp boundary_plus = new DiyFp(); - private final DiyFp ten_mk = new DiyFp(); - private final DiyFp scaled_w = new DiyFp(); - private final DiyFp scaled_boundary_minus = new DiyFp(); - private final DiyFp scaled_boundary_plus = new DiyFp(); - - private final DiyFp too_low = new DiyFp(); - private final DiyFp too_high = new DiyFp(); - private final DiyFp unsafe_interval = new DiyFp(); - private final DiyFp one = new DiyFp(); - private final DiyFp minus_round = new DiyFp(); - - int initialize(final long bits) { - DoubleHelper.asNormalizedDiyFp(bits, w); - // boundary_minus and boundary_plus are the boundaries between v and its - // closest floating-point neighbors. Any number strictly between - // boundary_minus and boundary_plus will round to v when convert to a double. - // Grisu3 will never output representations that lie exactly on a boundary. - boundary_minus.reset(); - boundary_plus.reset(); - DoubleHelper.normalizedBoundaries(v, bits, boundary_minus, boundary_plus); - ten_mk.reset(); // Cached power of ten: 10^-k - final int mk = CachedPowers.getCachedPower(w.e + DiyFp.kSignificandSize, minimal_target_exponent, ten_mk); - // Note that ten_mk is only an approximation of 10^-k. A DiyFp only contains a - // 64 bit significand and ten_mk is thus only precise up to 64 bits. - - // The DiyFp::Times procedure rounds its result, and ten_mk is approximated - // too. The variable scaled_w (as well as scaled_boundary_minus/plus) are now - // off by a small amount. - // In fact: scaled_w - w*10^k < 1ulp (unit in the last place) of scaled_w. - // In other words: let f = scaled_w.f() and e = scaled_w.e(), then - // (f-1) * 2^e < w*10^k < (f+1) * 2^e - scaled_w.f = w.f; - scaled_w.e = w.e; - scaled_w.multiply(ten_mk); - // In theory it would be possible to avoid some recomputations by computing - // the difference between w and boundary_minus/plus (a power of 2) and to - // compute scaled_boundary_minus/plus by subtracting/adding from - // scaled_w. However the code becomes much less readable and the speed - // enhancements are not terriffic. - scaled_boundary_minus.f = boundary_minus.f; - scaled_boundary_minus.e = boundary_minus.e; - scaled_boundary_minus.multiply(ten_mk); - scaled_boundary_plus.f = boundary_plus.f; - scaled_boundary_plus.e = boundary_plus.e; - scaled_boundary_plus.multiply(ten_mk); - - return mk; - } - - // allocate buffer for generated digits + extra notation + padding zeroes - private final byte[] chars = new byte[kFastDtoaMaximalLength + 10]; - private int end = 0; - private int point; - - void reset() { - end = 0; - } - - void append(byte c) { - chars[end++] = c; - } - - void decreaseLast() { - chars[end - 1]--; - } - - @Override - public String toString() { - return "[chars:" + new String(chars, 0, end) + ", point:" + point + "]"; - } - - int copyTo(final byte[] target, final int position) { - for (int i = 0; i < end; i++) { - target[i + position] = chars[i]; - } - return end; - } - - public void write(int firstDigit) { - // check for minus sign - int decPoint = point - firstDigit; - if (decPoint < -5 || decPoint > 21) { - toExponentialFormat(firstDigit, decPoint); - } else { - toFixedFormat(firstDigit, decPoint); - } - } - - private void toFixedFormat(int firstDigit, int decPoint) { - if (point < end) { - // insert decimal point - if (decPoint > 0) { - // >= 1, split decimals and insert point - for (int i = end; i >= point; i--) { - chars[i + 1] = chars[i]; - } - chars[point] = '.'; - end++; - } else { - // < 1, - final int offset = 2 - decPoint; - for (int i = end + firstDigit; i >= firstDigit; i--) { - chars[i + offset] = chars[i]; - } - chars[firstDigit] = '0'; - chars[firstDigit + 1] = '.'; - if (decPoint < 0) { - int target = firstDigit + 2 - decPoint; - for (int i = firstDigit + 2; i < target; i++) { - chars[i] = '0'; - } - } - end += 2 - decPoint; - } - } else if (point > end) { - // large integer, add trailing zeroes - for (int i = end; i < point; i++) { - chars[i] = '0'; - } - end += point - end; - chars[end] = '.'; - chars[end + 1] = '0'; - end += 2; - } else { - chars[end] = '.'; - chars[end + 1] = '0'; - end += 2; - } - } - - private void toExponentialFormat(int firstDigit, int decPoint) { - if (end - firstDigit > 1) { - // insert decimal point if more than one digit was produced - int dot = firstDigit + 1; - System.arraycopy(chars, dot, chars, dot + 1, end - dot); - chars[dot] = '.'; - end++; - } - chars[end++] = 'E'; - byte sign = '+'; - int exp = decPoint - 1; - if (exp < 0) { - sign = '-'; - exp = -exp; - } - chars[end++] = sign; - - int charPos = exp > 99 ? end + 2 : exp > 9 ? end + 1 : end; - end = charPos + 1; - - do { - int r = exp % 10; - chars[charPos--] = digits[r]; - exp = exp / 10; - } while (exp != 0); - } - - final static byte[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9'}; - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java deleted file mode 100644 index f773e2e6b6..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonObject.java +++ /dev/null @@ -1,35 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -/** - * Objects which implement this interface are supported for serialization in DslJson. - * This is used by DSL Platform POJO objects. - * Annotation processor uses a different method, since it can't modify existing objects to add such signature into them. - * - * Objects which implement JsonObject support convention based deserialization in form of public static JSON_READER - * An example: - * - *

- *     public class MyCustomPojo implements JsonObject {
- *       public void serialize(JsonWriter writer, boolean minimal) {
- *         //implement serialization logic, eg: writer.writeAscii("{\"my\":\"object\"}");
- *       }
- *       public static final JsonReader.ReadJsonObject<MyCustomPojo> JSON_READER = new JsonReader.ReadJsonObject<MyCustomPojo>() {
- *         public MyCustomPojo deserialize(JsonReader reader) throws IOException {
- *           //implement deserialization logic, eg: return new MyCustomPojo();
- *         }
- *       }
- *     }
- * 
- * - */ -public interface JsonObject { - /** - * Serialize object instance into JsonWriter. - * In DslJson minimal serialization stands for serialization which omits unnecessary information from JSON. - * An example of such data is false for boolean or null for Integer which can be reconstructed from type definition. - * - * @param writer write JSON into target writer - * @param minimal is minimal serialization requested - */ - void serialize(JsonWriter writer, boolean minimal); -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java deleted file mode 100644 index ba26388c76..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonReader.java +++ /dev/null @@ -1,1787 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.EOFException; -import java.io.IOException; -import java.io.InputStream; -import java.nio.charset.Charset; -import java.util.*; - -/** - * Object for processing JSON from byte[] and InputStream. - * DSL-JSON works on byte level (instead of char level). - * Deserialized instances can obtain TContext information provided with this reader. - *

- * JsonReader can be reused by calling process methods. - * - * @param context passed to deserialized object instances - */ -@SuppressWarnings({"rawtypes", "serial"}) // suppress pre-existing warnings -public final class JsonReader { - - private static final boolean[] WHITESPACE = new boolean[256]; - private static final Charset utf8 = Charset.forName("UTF-8"); - - static { - WHITESPACE[9 + 128] = true; - WHITESPACE[10 + 128] = true; - WHITESPACE[11 + 128] = true; - WHITESPACE[12 + 128] = true; - WHITESPACE[13 + 128] = true; - WHITESPACE[32 + 128] = true; - WHITESPACE[-96 + 128] = true; - WHITESPACE[-31 + 128] = true; - WHITESPACE[-30 + 128] = true; - WHITESPACE[-29 + 128] = true; - } - - private int tokenStart; - private int nameEnd; - private int currentIndex = 0; - private long currentPosition = 0; - private byte last = ' '; - - private int length; - private final char[] tmp; - - public final TContext context; - protected byte[] buffer; - protected char[] chars; - - private InputStream stream; - private int readLimit; - //always leave some room for reading special stuff, so that buffer contains enough padding for such optimizations - private int bufferLenWithExtraSpace; - - private final StringCache keyCache; - private final StringCache valuesCache; - private final TypeLookup typeLookup; - - private final byte[] originalBuffer; - private final int originalBufferLenWithExtraSpace; - - public enum ErrorInfo { - WITH_STACK_TRACE, - DESCRIPTION_AND_POSITION, - DESCRIPTION_ONLY, - MINIMAL - } - - public enum DoublePrecision { - EXACT(0), - HIGH(1), - DEFAULT(3), - LOW(4); - - final int level; - - DoublePrecision(int level) { - this.level = level; - } - } - - public enum UnknownNumberParsing { - LONG_AND_BIGDECIMAL, - LONG_AND_DOUBLE, - BIGDECIMAL, - DOUBLE - } - - protected final ErrorInfo errorInfo; - protected final DoublePrecision doublePrecision; - protected final int doubleLengthLimit; - protected final UnknownNumberParsing unknownNumbers; - protected final int maxNumberDigits; - private final int maxStringBuffer; - - private JsonReader( - final char[] tmp, - final byte[] buffer, - final int length, - @Nullable final TContext context, - @Nullable final StringCache keyCache, - @Nullable final StringCache valuesCache, - @Nullable final TypeLookup typeLookup, - final ErrorInfo errorInfo, - final DoublePrecision doublePrecision, - final UnknownNumberParsing unknownNumbers, - final int maxNumberDigits, - final int maxStringBuffer) { - this.tmp = tmp; - this.buffer = buffer; - this.length = length; - this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid - this.context = context; - this.chars = tmp; - this.keyCache = keyCache; - this.valuesCache = valuesCache; - this.typeLookup = typeLookup; - this.errorInfo = errorInfo; - this.doublePrecision = doublePrecision; - this.unknownNumbers = unknownNumbers; - this.maxNumberDigits = maxNumberDigits; - this.maxStringBuffer = maxStringBuffer; - this.doubleLengthLimit = 15 + doublePrecision.level; - this.originalBuffer = buffer; - this.originalBufferLenWithExtraSpace = bufferLenWithExtraSpace; - } - - /** - * Prefer creating reader through DslJson#newReader since it will pass several arguments (such as key/string value cache) - * First byte will not be read. - * It will allocate new char[64] for string buffer. - * Key and string vales cache will be null. - * - * @param buffer input JSON - * @param context context - */ - @Deprecated - public JsonReader(final byte[] buffer, @Nullable final TContext context) { - this(buffer, context, null, null); - } - - @Deprecated - public JsonReader(final byte[] buffer, @Nullable final TContext context, @Nullable StringCache keyCache, @Nullable StringCache valuesCache) { - this(buffer, buffer.length, context, new char[64], keyCache, valuesCache); - } - - @Deprecated - public JsonReader(final byte[] buffer, final TContext context, final char[] tmp) { - this(buffer, buffer.length, context, tmp); - if (tmp == null) { - throw new IllegalArgumentException("tmp buffer provided as null."); - } - } - - @Deprecated - public JsonReader(final byte[] buffer, final int length, final TContext context) { - this(buffer, length, context, new char[64]); - } - - @Deprecated - public JsonReader(final byte[] buffer, final int length, final TContext context, final char[] tmp) { - this(buffer, length, context, tmp, null, null); - } - - @Deprecated - public JsonReader(final byte[] buffer, final int length, @Nullable final TContext context, final char[] tmp, @Nullable final StringCache keyCache, @Nullable final StringCache valuesCache) { - this(tmp, buffer, length, context, keyCache, valuesCache, null, ErrorInfo.WITH_STACK_TRACE, DoublePrecision.DEFAULT, UnknownNumberParsing.LONG_AND_BIGDECIMAL, 512, 256 * 1024 * 1024); - if (tmp == null) { - throw new IllegalArgumentException("tmp buffer provided as null."); - } - if (length > buffer.length) { - throw new IllegalArgumentException("length can't be longer than buffer.length"); - } else if (length < buffer.length) { - buffer[length] = '\0'; - } - } - - JsonReader( - final byte[] buffer, - final int length, - @Nullable final TContext context, - final char[] tmp, - @Nullable final StringCache keyCache, - @Nullable final StringCache valuesCache, - @Nullable final TypeLookup typeLookup, - final ErrorInfo errorInfo, - final DoublePrecision doublePrecision, - final UnknownNumberParsing unknownNumbers, - final int maxNumberDigits, - final int maxStringBuffer) { - this(tmp, buffer, length, context, keyCache, valuesCache, typeLookup, errorInfo, doublePrecision, unknownNumbers, maxNumberDigits, maxStringBuffer); - if (tmp == null) { - throw new IllegalArgumentException("tmp buffer provided as null."); - } - if (length > buffer.length) { - throw new IllegalArgumentException("length can't be longer than buffer.length"); - } else if (length < buffer.length) { - buffer[length] = '\0'; - } - } - - - /** - * Will be removed. Exists only for backward compatibility - * @param stream process stream - * @throws IOException error reading from stream - */ - @Deprecated - public final void reset(final InputStream stream) throws IOException { - process(stream); - } - - /** - * Will be removed. Exists only for backward compatibility - * @param size size of byte[] input to use - */ - @Deprecated - final void reset(final int size) { - process(null, size); - } - - /** - * Reset reader after processing input - * It will release reference to provided byte[] or InputStream input - */ - final void reset() { - this.buffer = this.originalBuffer; - this.bufferLenWithExtraSpace = this.originalBufferLenWithExtraSpace; - currentIndex = 0; - this.length = 0; - this.readLimit = 0; - this.stream = null; - } - - /** - * Bind input stream for processing. - * Stream will be processed in byte[] chunks. - * If stream is null, reference to stream will be released. - * - * @param stream set input stream - * @return itself - * @throws IOException unable to read from stream - */ - public final JsonReader process(@Nullable final InputStream stream) throws IOException { - this.currentPosition = 0; - this.currentIndex = 0; - this.stream = stream; - if (stream != null) { - this.readLimit = this.length < bufferLenWithExtraSpace ? this.length : bufferLenWithExtraSpace; - final int available = readFully(buffer, stream, 0); - readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; - this.length = available; - } - return this; - } - - /** - * Bind byte[] buffer for processing. - * If this method is used in combination with process(InputStream) this buffer will be used for processing chunks of stream. - * If null is sent for byte[] buffer, new length for valid input will be set for existing buffer. - * - * @param newBuffer new buffer to use for processing - * @param newLength length of buffer which can be used - * @return itself - */ - public final JsonReader process(@Nullable final byte[] newBuffer, final int newLength) { - if (newBuffer != null) { - this.buffer = newBuffer; - this.bufferLenWithExtraSpace = buffer.length - 38; //currently maximum padding is for uuid - } - if (newLength > buffer.length) { - throw new IllegalArgumentException("length can't be longer than buffer.length"); - } - currentIndex = 0; - this.length = newLength; - this.stream = null; - this.readLimit = newLength; - return this; - } - - /** - * Valid length of the input buffer. - * - * @return size of JSON input - */ - public final int length() { - return length; - } - - @Override - public String toString() { - return new String(buffer, 0, length, utf8); - } - - private static int readFully(final byte[] buffer, final InputStream stream, final int offset) throws IOException { - int read; - int position = offset; - while (position < buffer.length - && (read = stream.read(buffer, position, buffer.length - position)) != -1) { - position += read; - } - return position; - } - - private static class EmptyEOFException extends EOFException { - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - } - private static final EOFException eof = new EmptyEOFException(); - - boolean withStackTrace() { - return errorInfo == ErrorInfo.WITH_STACK_TRACE; - } - - /** - * Read next byte from the JSON input. - * If buffer has been read in full IOException will be thrown - * - * @return next byte - * @throws IOException when end of JSON input - */ - public final byte read() throws IOException { - if (stream != null && currentIndex > readLimit) { - prepareNextBlock(); - } - if (currentIndex >= length) { - throw ParsingException.create("Unexpected end of JSON input", eof, withStackTrace()); - } - return last = buffer[currentIndex++]; - } - - private int prepareNextBlock() throws IOException { - final int len = length - currentIndex; - System.arraycopy(buffer, currentIndex, buffer, 0, len); - final int available = readFully(buffer, stream, len); - currentPosition += currentIndex; - if (available == len) { - readLimit = length - currentIndex; - length = readLimit; - currentIndex = 0; - } else { - readLimit = available < bufferLenWithExtraSpace ? available : bufferLenWithExtraSpace; - this.length = available; - currentIndex = 0; - } - return available; - } - - final boolean isEndOfStream() throws IOException { - if (stream == null) { - return length == currentIndex; - } - if (length != currentIndex) { - return false; - } - return prepareNextBlock() == 0; - } - - /** - * Which was last byte read from the JSON input. - * JsonReader doesn't allow to go back, but it remembers previously read byte - * - * @return which was the last byte read - */ - public final byte last() { - return last; - } - - public String positionDescription() { - return positionDescription(0); - } - - public String positionDescription(int offset) { - final StringBuilder error = new StringBuilder(60); - positionDescription(offset, error); - return error.toString(); - } - - private void positionDescription(int offset, StringBuilder error) { - error.append("at position: ").append(positionInStream(offset)); - if (currentIndex > offset) { - try { - int maxLen = Math.min(currentIndex - offset, 20); - String prefix = new String(buffer, currentIndex - offset - maxLen, maxLen, utf8); - error.append(", following: `"); - error.append(prefix); - error.append('`'); - } catch (Exception ignore) { - } - } - if (currentIndex - offset < readLimit) { - try { - int maxLen = Math.min(readLimit - currentIndex + offset, 20); - String suffix = new String(buffer, currentIndex - offset, maxLen, utf8); - error.append(", before: `"); - error.append(suffix); - error.append('`'); - } catch (Exception ignore) { - } - } - } - - private final StringBuilder error = new StringBuilder(0); - private final Formatter errorFormatter = new Formatter(error); - - public final ParsingException newParseError(final String description) { - return newParseError(description, 0); - } - - public final ParsingException newParseError(final String description, final int positionOffset) { - if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(description, false); - error.setLength(0); - error.append(description); - error.append(". Found "); - error.append((char)last); - if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); - error.append(" "); - positionDescription(positionOffset, error); - return ParsingException.create(error.toString(), withStackTrace()); - } - - public final ParsingException newParseErrorAt(final String description, final int positionOffset) { - if (errorInfo == ErrorInfo.MINIMAL || errorInfo == ErrorInfo.DESCRIPTION_ONLY) { - return ParsingException.create(description, false); - } - error.setLength(0); - error.append(description); - error.append(" "); - positionDescription(positionOffset, error); - return ParsingException.create(error.toString(), withStackTrace()); - } - - public final ParsingException newParseErrorAt(final String description, final int positionOffset, final Exception cause) { - if (cause == null) throw new IllegalArgumentException("cause can't be null"); - if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(description, cause, false); - error.setLength(0); - final String msg = cause.getMessage(); - if (msg != null && msg.length() > 0) { - error.append(msg); - if (!msg.endsWith(".")) { - error.append("."); - } - error.append(" "); - } - error.append(description); - if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), cause, false); - error.append(" "); - positionDescription(positionOffset, error); - return ParsingException.create(error.toString(), withStackTrace()); - } - - public final ParsingException newParseErrorFormat(final String shortDescription, final int positionOffset, final String longDescriptionFormat, Object... arguments) { - if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(shortDescription, false); - error.setLength(0); - errorFormatter.format(longDescriptionFormat, arguments); - if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); - error.append(" "); - positionDescription(positionOffset, error); - return ParsingException.create(error.toString(), withStackTrace()); - } - - public final ParsingException newParseErrorWith( - final String description, @Nullable Object argument) { - return newParseErrorWith(description, 0, "", description, argument, ""); - } - - public final ParsingException newParseErrorWith( - final String shortDescription, - final int positionOffset, - final String longDescriptionPrefix, - final String longDescriptionMessage, @Nullable Object argument, - final String longDescriptionSuffix) { - if (errorInfo == ErrorInfo.MINIMAL) return ParsingException.create(shortDescription, false); - error.setLength(0); - error.append(longDescriptionPrefix); - error.append(longDescriptionMessage); - if (argument != null) { - error.append(": '"); - error.append(argument.toString()); - error.append("'"); - } - error.append(longDescriptionSuffix); - if (errorInfo == ErrorInfo.DESCRIPTION_ONLY) return ParsingException.create(error.toString(), false); - error.append(" "); - positionDescription(positionOffset, error); - return ParsingException.create(error.toString(), withStackTrace()); - } - - public final int getTokenStart() { - return tokenStart; - } - - public final int getCurrentIndex() { - return currentIndex; - } - - /** - * will be removed. not used anymore - * - * @return parsed chars from a number - */ - @Deprecated - public final char[] readNumber() { - tokenStart = currentIndex - 1; - tmp[0] = (char) last; - int i = 1; - int ci = currentIndex; - byte bb = last; - while (i < tmp.length && ci < length) { - bb = buffer[ci++]; - if (bb == ',' || bb == '}' || bb == ']') break; - tmp[i++] = (char) bb; - } - currentIndex += i - 1; - last = bb; - return tmp; - } - - public final int scanNumber() { - tokenStart = currentIndex - 1; - int i = 1; - int ci = currentIndex; - byte bb = last; - while (ci < length) { - bb = buffer[ci++]; - if (bb == ',' || bb == '}' || bb == ']') break; - i++; - } - currentIndex += i - 1; - last = bb; - return tokenStart; - } - - final char[] prepareBuffer(final int start, final int len) throws ParsingException { - if (len > maxNumberDigits) { - throw newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", len, ""); - } - while (chars.length < len) { - chars = Arrays.copyOf(chars, chars.length * 2); - } - final char[] _tmp = chars; - final byte[] _buf = buffer; - for (int i = 0; i < len; i++) { - _tmp[i] = (char) _buf[start + i]; - } - return _tmp; - } - - final boolean allWhitespace(final int start, final int end) { - final byte[] _buf = buffer; - for (int i = start; i < end; i++) { - if (!WHITESPACE[_buf[i] + 128]) return false; - } - return true; - } - - final int findNonWhitespace(final int end) { - final byte[] _buf = buffer; - for (int i = end - 1; i > 0; i--) { - if (!WHITESPACE[_buf[i] + 128]) return i + 1; - } - return 0; - } - - /** - * Read simple ascii string. Will not use values cache to create instance. - * - * @return parsed string - * @throws ParsingException unable to parse string - */ - public final String readSimpleString() throws ParsingException { - if (last != '"') throw newParseError("Expecting '\"' for string start"); - int i = 0; - int ci = currentIndex; - try { - while (i < tmp.length) { - final byte bb = buffer[ci++]; - if (bb == '"') break; - tmp[i++] = (char) bb; - } - } catch (ArrayIndexOutOfBoundsException ignore) { - throw newParseErrorAt("JSON string was not closed with a double quote", 0); - } - if (ci > length) throw newParseErrorAt("JSON string was not closed with a double quote", 0); - currentIndex = ci; - return new String(tmp, 0, i); - } - - /** - * Read simple "ascii string" into temporary buffer. - * String length must be obtained through getTokenStart and getCurrentToken - * - * @return temporary buffer - * @throws ParsingException unable to parse string - */ - public final char[] readSimpleQuote() throws ParsingException { - if (last != '"') throw newParseError("Expecting '\"' for string start"); - int ci = tokenStart = currentIndex; - try { - for (int i = 0; i < tmp.length; i++) { - final byte bb = buffer[ci++]; - if (bb == '"') break; - tmp[i] = (char) bb; - } - } catch (ArrayIndexOutOfBoundsException ignore) { - throw newParseErrorAt("JSON string was not closed with a double quote", 0); - } - if (ci > length) throw newParseErrorAt("JSON string was not closed with a double quote", 0); - currentIndex = ci; - return tmp; - } - - /** - * Read string from JSON input. - * If values cache is used, string will be looked up from the cache. - *

- * String value must start and end with a double quote ("). - * - * @return parsed string - * @throws IOException error reading string input - */ - public final String readString() throws IOException { - final int len = parseString(); - return valuesCache == null ? new String(chars, 0, len) : valuesCache.get(chars, len); - } - - public final StringBuilder appendString(StringBuilder builder) throws IOException { - final int len = parseString(); - builder.append(chars, 0, len); - return builder; - } - - public final StringBuffer appendString(StringBuffer buffer) throws IOException { - final int len = parseString(); - buffer.append(chars, 0, len); - return buffer; - } - - final int parseString() throws IOException { - final int startIndex = currentIndex; - if (last != '"') throw newParseError("Expecting '\"' for string start"); - else if (currentIndex == length) throw newParseErrorAt("Premature end of JSON string", 0); - - byte bb; - int ci = currentIndex; - char[] _tmp = chars; - final int remaining = length - currentIndex; - int _tmpLen = _tmp.length < remaining ? _tmp.length : remaining; - int i = 0; - while (i < _tmpLen) { - bb = buffer[ci++]; - if (bb == '"') { - currentIndex = ci; - return i; - } - // If we encounter a backslash, which is a beginning of an escape sequence - // or a high bit was set - indicating an UTF-8 encoded multibyte character, - // there is no chance that we can decode the string without instantiating - // a temporary buffer, so quit this loop - if ((bb ^ '\\') < 1) break; - _tmp[i++] = (char) bb; - } - if (i == _tmp.length) { - final int newSize = chars.length * 2; - if (newSize > maxStringBuffer) { - throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); - } - _tmp = chars = Arrays.copyOf(chars, newSize); - } - _tmpLen = _tmp.length; - currentIndex = ci; - int soFar = --currentIndex - startIndex; - - while (!isEndOfStream()) { - int bc = read(); - if (bc == '"') { - return soFar; - } - - if (bc == '\\') { - if (soFar >= _tmpLen - 6) { - final int newSize = chars.length * 2; - if (newSize > maxStringBuffer) { - throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); - } - _tmp = chars = Arrays.copyOf(chars, newSize); - _tmpLen = _tmp.length; - } - bc = buffer[currentIndex++]; - - switch (bc) { - case 'b': - bc = '\b'; - break; - case 't': - bc = '\t'; - break; - case 'n': - bc = '\n'; - break; - case 'f': - bc = '\f'; - break; - case 'r': - bc = '\r'; - break; - case '"': - case '/': - case '\\': - break; - case 'u': - bc = (hexToInt(buffer[currentIndex++]) << 12) + - (hexToInt(buffer[currentIndex++]) << 8) + - (hexToInt(buffer[currentIndex++]) << 4) + - hexToInt(buffer[currentIndex++]); - break; - - default: - throw newParseErrorWith("Invalid escape combination detected", bc); - } - } else if ((bc & 0x80) != 0) { - if (soFar >= _tmpLen - 4) { - final int newSize = chars.length * 2; - if (newSize > maxStringBuffer) { - throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); - } - _tmp = chars = Arrays.copyOf(chars, newSize); - _tmpLen = _tmp.length; - } - final int u2 = buffer[currentIndex++]; - if ((bc & 0xE0) == 0xC0) { - bc = ((bc & 0x1F) << 6) + (u2 & 0x3F); - } else { - final int u3 = buffer[currentIndex++]; - if ((bc & 0xF0) == 0xE0) { - bc = ((bc & 0x0F) << 12) + ((u2 & 0x3F) << 6) + (u3 & 0x3F); - } else { - final int u4 = buffer[currentIndex++]; - if ((bc & 0xF8) == 0xF0) { - bc = ((bc & 0x07) << 18) + ((u2 & 0x3F) << 12) + ((u3 & 0x3F) << 6) + (u4 & 0x3F); - } else { - // there are legal 5 & 6 byte combinations, but none are _valid_ - throw newParseErrorAt("Invalid unicode character detected", 0); - } - - if (bc >= 0x10000) { - // check if valid unicode - if (bc >= 0x110000) { - throw newParseErrorAt("Invalid unicode character detected", 0); - } - - // split surrogates - final int sup = bc - 0x10000; - _tmp[soFar++] = (char) ((sup >>> 10) + 0xd800); - _tmp[soFar++] = (char) ((sup & 0x3ff) + 0xdc00); - continue; - } - } - } - } else if (soFar >= _tmpLen) { - final int newSize = chars.length * 2; - if (newSize > maxStringBuffer) { - throw newParseErrorWith("Maximum string buffer limit exceeded", maxStringBuffer); - } - _tmp = chars = Arrays.copyOf(chars, newSize); - _tmpLen = _tmp.length; - } - - _tmp[soFar++] = (char) bc; - } - throw newParseErrorAt("JSON string was not closed with a double quote", 0); - } - - private int hexToInt(final byte value) throws ParsingException { - if (value >= '0' && value <= '9') return value - 0x30; - if (value >= 'A' && value <= 'F') return value - 0x37; - if (value >= 'a' && value <= 'f') return value - 0x57; - throw newParseErrorWith("Could not parse unicode escape, expected a hexadecimal digit", value); - } - - private boolean wasWhiteSpace() { - switch (last) { - case 9: - case 10: - case 11: - case 12: - case 13: - case 32: - case -96: - return true; - case -31: - if (currentIndex + 1 < length && buffer[currentIndex] == -102 && buffer[currentIndex + 1] == -128) { - currentIndex += 2; - last = ' '; - return true; - } - return false; - case -30: - if (currentIndex + 1 < length) { - final byte b1 = buffer[currentIndex]; - final byte b2 = buffer[currentIndex + 1]; - if (b1 == -127 && b2 == -97) { - currentIndex += 2; - last = ' '; - return true; - } - if (b1 != -128) return false; - switch (b2) { - case -128: - case -127: - case -126: - case -125: - case -124: - case -123: - case -122: - case -121: - case -120: - case -119: - case -118: - case -88: - case -87: - case -81: - currentIndex += 2; - last = ' '; - return true; - default: - return false; - } - } else { - return false; - } - case -29: - if (currentIndex + 1 < length && buffer[currentIndex] == -128 && buffer[currentIndex + 1] == -128) { - currentIndex += 2; - last = ' '; - return true; - } - return false; - default: - return false; - } - } - - /** - * Read next token (byte) from input JSON. - * Whitespace will be skipped and next non-whitespace byte will be returned. - * - * @return next non-whitespace byte in the JSON input - * @throws IOException unable to get next byte (end of stream, ...) - */ - public final byte getNextToken() throws IOException { - read(); - if (WHITESPACE[last + 128]) { - while (wasWhiteSpace()) { - read(); - } - } - return last; - } - - public final long positionInStream() { - return currentPosition + currentIndex; - } - - public final long positionInStream(final int offset) { - return currentPosition + currentIndex - offset; - } - - public final int fillName() throws IOException { - final int hash = calcHash(); - if (read() != ':') { - if (!wasWhiteSpace() || getNextToken() != ':') { - throw newParseError("Expecting ':' after attribute name"); - } - } - return hash; - } - - public final int fillNameWeakHash() throws IOException { - final int hash = calcWeakHash(); - if (read() != ':') { - if (!wasWhiteSpace() || getNextToken() != ':') { - throw newParseError("Expecting ':' after attribute name"); - } - } - return hash; - } - - public final int calcHash() throws IOException { - if (last != '"') throw newParseError("Expecting '\"' for attribute name start"); - tokenStart = currentIndex; - int ci = currentIndex; - long hash = 0x811c9dc5; - if (stream != null) { - while (ci < readLimit) { - byte b = buffer[ci]; - if (b == '\\') { - if (ci == readLimit - 1) { - return calcHashAndCopyName(hash, ci); - } - b = buffer[++ci]; - } else if (b == '"') { - break; - } - ci++; - hash ^= b; - hash *= 0x1000193; - } - if (ci >= readLimit) { - return calcHashAndCopyName(hash, ci); - } - nameEnd = currentIndex = ci + 1; - } else { - //TODO: use length instead!? this will read data after used buffer size - while (ci < buffer.length) { - byte b = buffer[ci++]; - if (b == '\\') { - if (ci == buffer.length) throw newParseError("Expecting '\"' for attribute name end"); - b = buffer[ci++]; - } else if (b == '"') { - break; - } - hash ^= b; - hash *= 0x1000193; - } - nameEnd = currentIndex = ci; - } - return (int) hash; - } - - public final int calcWeakHash() throws IOException { - if (last != '"') throw newParseError("Expecting '\"' for attribute name start"); - tokenStart = currentIndex; - int ci = currentIndex; - int hash = 0; - if (stream != null) { - while (ci < readLimit) { - byte b = buffer[ci]; - if (b == '\\') { - if (ci == readLimit - 1) { - return calcWeakHashAndCopyName(hash, ci); - } - b = buffer[++ci]; - } else if (b == '"') { - break; - } - ci++; - hash += b; - } - if (ci >= readLimit) { - return calcWeakHashAndCopyName(hash, ci); - } - nameEnd = currentIndex = ci + 1; - } else { - //TODO: use length instead!? this will read data after used buffer size - while (ci < buffer.length) { - byte b = buffer[ci++]; - if (b == '\\') { - if (ci == buffer.length) throw newParseError("Expecting '\"' for attribute name end"); - b = buffer[ci++]; - } else if (b == '"') { - break; - } - hash += b; - } - nameEnd = currentIndex = ci; - } - return hash; - } - - public final int getLastHash() { - long hash = 0x811c9dc5; - if (stream != null && nameEnd == -1) { - int i = 0; - while (i < lastNameLen) { - final byte b = (byte)chars[i++]; - hash ^= b; - hash *= 0x1000193; - } - } else { - int i = tokenStart; - int end = nameEnd - 1; - while (i < end) { - final byte b = buffer[i++]; - hash ^= b; - hash *= 0x1000193; - } - } - return (int)hash; - } - - private int lastNameLen; - - private int calcHashAndCopyName(long hash, int ci) throws IOException { - int soFar = ci - tokenStart; - long startPosition = currentPosition - soFar; - while (chars.length < soFar) { - chars = Arrays.copyOf(chars, chars.length * 2); - } - int i = 0; - for (; i < soFar; i++) { - chars[i] = (char) buffer[i + tokenStart]; - } - currentIndex = ci; - do { - byte b = read(); - if (b == '\\') { - b = read(); - } else if (b == '"') { - nameEnd = -1; - lastNameLen = i; - return (int) hash; - } - if (i == chars.length) { - chars = Arrays.copyOf(chars, chars.length * 2); - } - chars[i++] = (char) b; - hash ^= b; - hash *= 0x1000193; - } while (!isEndOfStream()); - //TODO: check offset - throw newParseErrorAt("JSON string was not closed with a double quote", (int)startPosition); - } - - private int calcWeakHashAndCopyName(int hash, int ci) throws IOException { - int soFar = ci - tokenStart; - long startPosition = currentPosition - soFar; - while (chars.length < soFar) { - chars = Arrays.copyOf(chars, chars.length * 2); - } - int i = 0; - for (; i < soFar; i++) { - chars[i] = (char) buffer[i + tokenStart]; - } - currentIndex = ci; - do { - byte b = read(); - if (b == '\\') { - b = read(); - } else if (b == '"') { - nameEnd = -1; - lastNameLen = i; - return hash; - } - if (i == chars.length) { - chars = Arrays.copyOf(chars, chars.length * 2); - } - chars[i++] = (char) b; - hash += b; - } while (!isEndOfStream()); - //TODO: check offset - throw newParseErrorAt("JSON string was not closed with a double quote", (int)startPosition); - } - - public final boolean wasLastName(final String name) { - if (stream != null && nameEnd == -1) { - if (name.length() != lastNameLen) { - return false; - } - for (int i = 0; i < name.length(); i++) { - if (name.charAt(i) != chars[i]) { - return false; - } - } - return true; - } - if (name.length() != nameEnd - tokenStart - 1) { - return false; - } - //TODO: not correct with escaping - for (int i = 0; i < name.length(); i++) { - if (name.charAt(i) != buffer[tokenStart + i]) { - return false; - } - } - return true; - } - - public final boolean wasLastName(final byte[] name) { - if (stream != null && nameEnd == -1) { - if (name.length != lastNameLen) { - return false; - } - for (int i = 0; i < name.length; i++) { - if (name[i] != chars[i]) { - return false; - } - } - return true; - } - if (name.length != nameEnd - tokenStart - 1) { - return false; - } - for (int i = 0; i < name.length; i++) { - if (name[i] != buffer[tokenStart + i]) { - return false; - } - } - return true; - } - - public final String getLastName() throws IOException { - if (stream != null && nameEnd == -1) { - return new String(chars, 0, lastNameLen); - } - return new String(buffer, tokenStart, nameEnd - tokenStart - 1, "UTF-8"); - } - - private byte skipString() throws IOException { - byte c = read(); - boolean inEscape = false; - while (c != '"' || inEscape) { - inEscape = !inEscape && c == '\\'; - c = read(); - } - return getNextToken(); - } - - /** - * Skip to next non-whitespace token (byte) - * Will not allocate memory while skipping over JSON input. - * - * @return next non-whitespace byte - * @throws IOException unable to read next byte (end of stream, invalid JSON, ...) - */ - public final byte skip() throws IOException { - if (last == '"') return skipString(); - if (last == '{') { - byte nextToken = getNextToken(); - if (nextToken == '}') return getNextToken(); - if (nextToken == '"') { - nextToken = skipString(); - } else { - throw newParseError("Expecting '\"' for attribute name"); - } - if (nextToken != ':') throw newParseError("Expecting ':' after attribute name"); - getNextToken(); - nextToken = skip(); - while (nextToken == ',') { - nextToken = getNextToken(); - if (nextToken == '"') { - nextToken = skipString(); - } else { - throw newParseError("Expecting '\"' for attribute name"); - } - if (nextToken != ':') throw newParseError("Expecting ':' after attribute name"); - getNextToken(); - nextToken = skip(); - } - if (nextToken != '}') throw newParseError("Expecting '}' for object end"); - return getNextToken(); - } - if (last == '[') { - getNextToken(); - byte nextToken = skip(); - while (nextToken == ',') { - getNextToken(); - nextToken = skip(); - } - if (nextToken != ']') throw newParseError("Expecting ']' for array end"); - return getNextToken(); - } - if (last == 'n') { - if (!wasNull()) throw newParseErrorAt("Expecting 'null' for null constant", 0); - return getNextToken(); - } - if (last == 't') { - if (!wasTrue()) throw newParseErrorAt("Expecting 'true' for true constant", 0); - return getNextToken(); - } - if (last == 'f') { - if (!wasFalse()) throw newParseErrorAt("Expecting 'false' for false constant", 0); - return getNextToken(); - } - while (last != ',' && last != '}' && last != ']') { - read(); - } - return last; - } - - /** - * will be removed - * - * @return not used anymore - * @throws IOException throws if invalid JSON detected - */ - @Deprecated - public String readNext() throws IOException { - final int start = currentIndex - 1; - skip(); - return new String(buffer, start, currentIndex - start - 1, "UTF-8"); - } - - public final byte[] readBase64() throws IOException { - if (stream != null && Base64.findEnd(buffer, currentIndex) == buffer.length) { - final int len = parseString(); - final byte[] input = new byte[len]; - for (int i = 0; i < input.length; i++) { - input[i] = (byte) chars[i]; - } - return Base64.decodeFast(input, 0, len); - } - if (last != '"') throw newParseError("Expecting '\"' for base64 start"); - final int start = currentIndex; - currentIndex = Base64.findEnd(buffer, start); - last = buffer[currentIndex++]; - if (last != '"') throw newParseError("Expecting '\"' for base64 end"); - return Base64.decodeFast(buffer, start, currentIndex - 1); - } - - /** - * Read key value of JSON input. - * If key cache is used, it will be looked up from there. - * - * @return parsed key value - * @throws IOException unable to parse string input - */ - public final String readKey() throws IOException { - final int len = parseString(); - final String key = keyCache != null ? keyCache.get(chars, len) : new String(chars, 0, len); - if (getNextToken() != ':') throw newParseError("Expecting ':' after attribute name"); - getNextToken(); - return key; - } - - /** - * Custom objects can be deserialized based on the implementation specified through this interface. - * Annotation processor creates custom deserializers at compile time and registers them into DslJson. - * - * @param type - */ - public interface ReadObject { - @Nullable - T read(JsonReader reader) throws IOException; - } - - /** - * Existing instances can be provided as target for deserialization. - * Annotation processor creates custom deserializers at compile time and registers them into DslJson. - * - * @param type - */ - public interface BindObject { - T bind(JsonReader reader, T instance) throws IOException; - } - - public interface ReadJsonObject { - @Nullable - T deserialize(JsonReader reader) throws IOException; - } - - /** - * Checks if 'null' value is at current position. - * This means last read byte was 'n' and 'ull' are next three bytes. - * If last byte was n but next three are not 'ull' it will throw since that is not a valid JSON construct. - * - * @return true if 'null' value is at current position - * @throws ParsingException invalid 'null' value detected - */ - public final boolean wasNull() throws ParsingException { - if (last == 'n') { - if (currentIndex + 2 < length && buffer[currentIndex] == 'u' - && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 'l') { - currentIndex += 3; - last = 'l'; - return true; - } - throw newParseErrorAt("Invalid null constant found", 0); - } - return false; - } - - /** - * Checks if 'true' value is at current position. - * This means last read byte was 't' and 'rue' are next three bytes. - * If last byte was t but next three are not 'rue' it will throw since that is not a valid JSON construct. - * - * @return true if 'true' value is at current position - * @throws ParsingException invalid 'true' value detected - */ - public final boolean wasTrue() throws ParsingException { - if (last == 't') { - if (currentIndex + 2 < length && buffer[currentIndex] == 'r' - && buffer[currentIndex + 1] == 'u' && buffer[currentIndex + 2] == 'e') { - currentIndex += 3; - last = 'e'; - return true; - } - throw newParseErrorAt("Invalid true constant found", 0); - } - return false; - } - - /** - * Checks if 'false' value is at current position. - * This means last read byte was 'f' and 'alse' are next four bytes. - * If last byte was f but next four are not 'alse' it will throw since that is not a valid JSON construct. - * - * @return true if 'false' value is at current position - * @throws ParsingException invalid 'false' value detected - */ - public final boolean wasFalse() throws ParsingException { - if (last == 'f') { - if (currentIndex + 3 < length && buffer[currentIndex] == 'a' - && buffer[currentIndex + 1] == 'l' && buffer[currentIndex + 2] == 's' - && buffer[currentIndex + 3] == 'e') { - currentIndex += 4; - last = 'e'; - return true; - } - throw newParseErrorAt("Invalid false constant found", 0); - } - return false; - } - - /** - * Will advance to next token and check if it's comma - * - * @throws IOException it's not comma - */ - public final void comma() throws IOException { - if (getNextToken() != ',') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting ','"); - } - } - - /** - * Will advance to next token and check if it's semicolon - * - * @throws IOException it's not semicolon - */ - public final void semicolon() throws IOException { - if (getNextToken() != ':') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting ':'"); - } - } - - /** - * Will advance to next token and check if it's array start - * - * @throws IOException it's not array start - */ - public final void startArray() throws IOException { - if (getNextToken() != '[') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting '[' as array start"); - } - } - - /** - * Will advance to next token and check if it's array end - * - * @throws IOException it's not array end - */ - public final void endArray() throws IOException { - if (getNextToken() != ']') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting ']' as array end"); - } - } - - /** - * Will advance to next token and check if it's object start - * - * @throws IOException it's not object start - */ - public final void startObject() throws IOException { - if (getNextToken() != '{') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting '{' as object start"); - } - } - - /** - * Will advance to next token and check it it's object end - * - * @throws IOException it's not object end - */ - public final void endObject() throws IOException { - if (getNextToken() != '}') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end in JSON", 0, eof); - throw newParseError("Expecting '}' as object end"); - } - } - - public final void startAttribute(final String name) throws IOException { - do { - if (getNextToken() != '"') throw newParseError("Expecting '\"' as attribute start"); - fillNameWeakHash(); - if (wasLastName(name)) return; - getNextToken(); - } while (skip() == ','); - throw newParseErrorWith("Unable to find attribute", name); - } - - /** - * Check if the last read token is an array end - * - * @throws IOException it's not array end - */ - public final void checkArrayEnd() throws IOException { - if (last != ']') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end of JSON in collection", 0, eof); - throw newParseError("Expecting ']' as array end"); - } - } - - /** - * Check if the last read token is an object end - * - * @throws IOException it's not object end - */ - public final void checkObjectEnd() throws IOException { - if (last != '}') { - if (currentIndex >= length) throw newParseErrorAt("Unexpected end of JSON in object", 0, eof); - throw newParseError("Expecting '}' as object end"); - } - } - - @Nullable - private Object readNull(final Class manifest) throws IOException { - if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); - if (manifest.isPrimitive()) { - if (manifest == int.class) return 0; - else if (manifest == long.class) return 0L; - else if (manifest == short.class) return (short)0; - else if (manifest == byte.class) return (byte)0; - else if (manifest == float.class) return 0f; - else if (manifest == double.class) return 0d; - else if (manifest == boolean.class) return false; - else if (manifest == char.class) return '\0'; - } - return null; - } - - /** - * Will advance to next token and read the JSON into specified type - * - * @param manifest type to read into - * @param type - * @return new instance from input JSON - * @throws IOException unable to process JSON - */ - @SuppressWarnings("unchecked") - @Nullable - public final T next(final Class manifest) throws IOException { - if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); - if (typeLookup == null) throw new ConfigurationException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); - if (this.getNextToken() == 'n') { - return (T)readNull(manifest); - } - final ReadObject reader = typeLookup.tryFindReader(manifest); - if (reader == null) { - throw new ConfigurationException("Reader not found for " + manifest + ". Check if reader was registered"); - } - return reader.read(this); - } - - /** - * Will advance to next token and read the JSON into specified type - * - * @param reader reader to use - * @param type - * @return new instance from input JSON - * @throws IOException unable to process JSON - */ - @Nullable - public final T next(final ReadObject reader) throws IOException { - if (reader == null) throw new IllegalArgumentException("reader can't be null"); - if (this.getNextToken() == 'n') { - if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); - return null; - } - return reader.read(this); - } - - /** - * Will advance to next token and bind the JSON to provided instance - * - * @param manifest type to read into - * @param instance instance to bind - * @param type - * @return bound instance - * @throws IOException unable to process JSON - */ - @SuppressWarnings("unchecked") - @Nullable - public final T next(final Class manifest, final T instance) throws IOException { - if (manifest == null) throw new IllegalArgumentException("manifest can't be null"); - if (instance == null) throw new IllegalArgumentException("instance can't be null"); - if (typeLookup == null) throw new ConfigurationException("typeLookup is not defined for this JsonReader. Unable to lookup specified type " + manifest); - if (this.getNextToken() == 'n') { - return (T)readNull(manifest); - } - final BindObject binder = typeLookup.tryFindBinder(manifest); - if (binder == null) throw new ConfigurationException("Binder not found for " + manifest + ". Check if binder was registered"); - return binder.bind(this, instance); - } - - /** - * Will advance to next token and bind the JSON to provided instance - * - * @param binder binder to use - * @param instance instance to bind - * @param type - * @return bound instance - * @throws IOException unable to process JSON - */ - @SuppressWarnings("unchecked") - @Nullable - public final T next(final BindObject binder, final T instance) throws IOException { - if (binder == null) throw new IllegalArgumentException("binder can't be null"); - if (instance == null) throw new IllegalArgumentException("instance can't be null"); - if (this.getNextToken() == 'n') { - if (!wasNull()) throw newParseErrorAt("Expecting 'null' as null constant", 0); - return null; - } - return binder.bind(this, instance); - } - - @Nullable - public final ArrayList readCollection(final ReadObject readObject) throws IOException { - if (wasNull()) return null; - if (last != '[') throw newParseError("Expecting '[' as collection start"); - if (getNextToken() == ']') return new ArrayList(0); - final ArrayList res = new ArrayList(4); - res.add(readObject.read(this)); - while (getNextToken() == ',') { - getNextToken(); - res.add(readObject.read(this)); - } - checkArrayEnd(); - return res; - } - - @Nullable - public final LinkedHashSet readSet(final ReadObject readObject) throws IOException { - if (wasNull()) return null; - if (last != '[') throw newParseError("Expecting '[' as set start"); - if (getNextToken() == ']') return new LinkedHashSet(0); - final LinkedHashSet res = new LinkedHashSet(4); - res.add(readObject.read(this)); - while (getNextToken() == ',') { - getNextToken(); - res.add(readObject.read(this)); - } - checkArrayEnd(); - return res; - } - - @Nullable - public final LinkedHashMap readMap(final ReadObject readKey, final ReadObject readValue) throws IOException { - if (wasNull()) return null; - if (last != '{') throw newParseError("Expecting '{' as map start"); - if (getNextToken() == '}') return new LinkedHashMap(0); - final LinkedHashMap res = new LinkedHashMap(4); - K key = readKey.read(this); - if (key == null) throw newParseErrorAt("Null detected as key", 0); - if (getNextToken() != ':') throw newParseError("Expecting ':' after key attribute"); - getNextToken(); - V value = readValue.read(this); - res.put(key, value); - while (getNextToken() == ',') { - getNextToken(); - key = readKey.read(this); - if (key == null) throw newParseErrorAt("Null detected as key", 0); - if (getNextToken() != ':') throw newParseError("Expecting ':' after key attribute"); - getNextToken(); - value = readValue.read(this); - res.put(key, value); - } - checkObjectEnd(); - return res; - } - - @Nullable - public final T[] readArray(final ReadObject readObject, final T[] emptyArray) throws IOException { - if (wasNull()) return null; - if (last != '[') throw newParseError("Expecting '[' as array start"); - if (getNextToken() == ']') return emptyArray; - final ArrayList res = new ArrayList(4); - res.add(readObject.read(this)); - while (getNextToken() == ',') { - getNextToken(); - res.add(readObject.read(this)); - } - checkArrayEnd(); - return res.toArray(emptyArray); - } - - public final ArrayList deserializeCollectionCustom(final ReadObject readObject) throws IOException { - final ArrayList res = new ArrayList(4); - deserializeCollection(readObject, res); - return res; - } - - @SuppressWarnings("overloads") - public final void deserializeCollection(final ReadObject readObject, final Collection res) throws IOException { - res.add(readObject.read(this)); - while (getNextToken() == ',') { - getNextToken(); - res.add(readObject.read(this)); - } - checkArrayEnd(); - } - - public final ArrayList deserializeNullableCollectionCustom(final ReadObject readObject) throws IOException { - final ArrayList res = new ArrayList(4); - deserializeNullableCollection(readObject, res); - return res; - } - - @SuppressWarnings("overloads") - public final void deserializeNullableCollection(final ReadObject readObject, final Collection res) throws IOException { - if (wasNull()) { - res.add(null); - } else { - res.add(readObject.read(this)); - } - while (getNextToken() == ',') { - getNextToken(); - if (wasNull()) { - res.add(null); - } else { - res.add(readObject.read(this)); - } - } - checkArrayEnd(); - } - - public final ArrayList deserializeCollection(final ReadJsonObject readObject) throws IOException { - final ArrayList res = new ArrayList(4); - deserializeCollection(readObject, res); - return res; - } - - @SuppressWarnings("overloads") - public final void deserializeCollection(final ReadJsonObject readObject, final Collection res) throws IOException { - if (last == '{') { - getNextToken(); - res.add(readObject.deserialize(this)); - } else throw newParseError("Expecting '{' as collection start"); - while (getNextToken() == ',') { - if (getNextToken() == '{') { - getNextToken(); - res.add(readObject.deserialize(this)); - } else throw newParseError("Expecting '{' as object start within a collection"); - } - checkArrayEnd(); - } - - public final ArrayList deserializeNullableCollection(final ReadJsonObject readObject) throws IOException { - final ArrayList res = new ArrayList(4); - deserializeNullableCollection(readObject, res); - return res; - } - - @SuppressWarnings("overloads") - public final void deserializeNullableCollection(final ReadJsonObject readObject, final Collection res) throws IOException { - if (last == '{') { - getNextToken(); - res.add(readObject.deserialize(this)); - } else if (wasNull()) { - res.add(null); - } else throw newParseError("Expecting '{' as collection start"); - while (getNextToken() == ',') { - if (getNextToken() == '{') { - getNextToken(); - res.add(readObject.deserialize(this)); - } else if (wasNull()) { - res.add(null); - } else throw newParseError("Expecting '{' as object start within a collection"); - } - checkArrayEnd(); - } - - public final Iterator iterateOverCustom(final JsonReader.ReadObject reader) { - return new WithReader(reader, this); - } - - public final Iterator iterateOver(final JsonReader.ReadJsonObject reader) { - return new WithObjectReader(reader, this); - } - - private static class WithReader implements Iterator { - private final JsonReader.ReadObject reader; - private final JsonReader json; - - private boolean hasNext; - - WithReader(JsonReader.ReadObject reader, JsonReader json) { - this.reader = reader; - this.json = json; - hasNext = true; - } - - @Override - public boolean hasNext() { - return hasNext; - } - - @Override - public void remove() { - } - - @Nullable - @Override - public T next() { - try { - byte nextToken = json.last(); - final T instance; - if (nextToken == 'n') { - if (!json.wasNull()) throw json.newParseErrorAt("Expecting 'null' as null constant", 0); - instance = null; - } else { - instance = reader.read(json); - } - hasNext = json.getNextToken() == ','; - if (hasNext) { - json.getNextToken(); - } else { - if (json.last() != ']') throw json.newParseError("Expecting ']' for iteration end"); - //TODO: ideally we should release stream bound to reader - } - return instance; - } catch (IOException e) { - throw new SerializationException(e); - } - } - } - - private static class WithObjectReader implements Iterator { - private final JsonReader.ReadJsonObject reader; - private final JsonReader json; - - private boolean hasNext; - - WithObjectReader(JsonReader.ReadJsonObject reader, JsonReader json) { - this.reader = reader; - this.json = json; - hasNext = true; - } - - @Override - public boolean hasNext() { - return hasNext; - } - - @Override - public void remove() { - } - - @Nullable - @Override - public T next() { - try { - byte nextToken = json.last(); - final T instance; - if (nextToken == 'n') { - if (!json.wasNull()) throw json.newParseErrorAt("Expecting 'null' as null constant", 0); - instance = null; - } else if (nextToken == '{') { - json.getNextToken(); - instance = reader.deserialize(json); - } else { - throw json.newParseError("Expecting '{' for object start in iteration"); - } - hasNext = json.getNextToken() == ','; - if (hasNext) { - json.getNextToken(); - } else { - if (json.last() != ']') throw json.newParseError("Expecting ']' for iteration end"); - //TODO: ideally we should release stream bound to reader - } - return instance; - } catch (IOException e) { - throw new SerializationException(e); - } - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java deleted file mode 100644 index b423c85701..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/JsonWriter.java +++ /dev/null @@ -1,909 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.io.OutputStream; -import java.nio.charset.Charset; -import java.util.*; - -/** - * DslJson writes JSON into JsonWriter which has two primary modes of operation: - * - * * targeting specific output stream - * * buffering the entire response in memory - * - * In both cases JsonWriter writes into an byte[] buffer. - * If stream is used as target, it will copy buffer into the stream whenever there is no more room in buffer for new data. - * If stream is not used as target, it will grow the buffer to hold the encoded result. - * To use stream as target reset(OutputStream) must be called before processing. - * This class provides low level methods for JSON serialization. - *

- * After the processing is done, - * in case then stream was used as target, flush() must be called to copy the remaining of the buffer into stream. - * When entire response was buffered in memory, buffer can be copied to stream or resulting byte[] can be used directly. - *

- * For maximum performance JsonWriter instances should be reused (to avoid allocation of new byte[] buffer instances). - * They should not be shared across threads (concurrently) so for Thread reuse it's best to use patterns such as ThreadLocal. - */ -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public final class JsonWriter { - - private static final Charset UTF_8 = Charset.forName("UTF-8"); - - final byte[] ensureCapacity(final int free) { - if (position + free >= buffer.length) { - enlargeOrFlush(position, free); - } - return buffer; - } - - void advance(int size) { - position += size; - } - - private int position; - private long flushed; - private OutputStream target; - private byte[] buffer; - - private final UnknownSerializer unknownSerializer; - private final Grisu3.FastDtoaBuilder doubleBuilder = new Grisu3.FastDtoaBuilder(); - - /** - * Prefer creating JsonWriter through DslJson#newWriter - * This instance is safe to use when all type information is known and lookups to custom writers is not required. - */ - @Deprecated - public JsonWriter() { - this(512, null); - } - - JsonWriter(@Nullable final UnknownSerializer unknownSerializer) { - this(512, unknownSerializer); - } - - JsonWriter(final int size, @Nullable final UnknownSerializer unknownSerializer) { - this(new byte[size], unknownSerializer); - } - - JsonWriter(final byte[] buffer, @Nullable final UnknownSerializer unknownSerializer) { - this.buffer = buffer; - this.unknownSerializer = unknownSerializer; - } - - /** - * Helper for writing JSON object start: { - */ - public static final byte OBJECT_START = '{'; - /** - * Helper for writing JSON object end: } - */ - public static final byte OBJECT_END = '}'; - /** - * Helper for writing JSON array start: [ - */ - public static final byte ARRAY_START = '['; - /** - * Helper for writing JSON array end: ] - */ - public static final byte ARRAY_END = ']'; - /** - * Helper for writing comma separator: , - */ - public static final byte COMMA = ','; - /** - * Helper for writing semicolon: : - */ - public static final byte SEMI = ':'; - /** - * Helper for writing JSON quote: " - */ - public static final byte QUOTE = '"'; - /** - * Helper for writing JSON escape: \\ - */ - public static final byte ESCAPE = '\\'; - - private void enlargeOrFlush(final int size, final int padding) { - if (target != null) { - try { - target.write(buffer, 0, size); - } catch (IOException ex) { - throw new SerializationException("Unable to write to target stream.", ex); - } - position = 0; - flushed += size; - if (padding > buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length + buffer.length / 2 + padding); - } - } else { - buffer = Arrays.copyOf(buffer, buffer.length + buffer.length / 2 + padding); - } - } - - /** - * Optimized method for writing 'null' into the JSON. - */ - public final void writeNull() { - if ((position + 4) >= buffer.length) { - enlargeOrFlush(position, 0); - } - final int s = position; - final byte[] _result = buffer; - _result[s] = 'n'; - _result[s + 1] = 'u'; - _result[s + 2] = 'l'; - _result[s + 3] = 'l'; - position += 4; - } - - /** - * Write a single byte into the JSON. - * - * @param value byte to write into the JSON - */ - public final void writeByte(final byte value) { - if (position == buffer.length) { - enlargeOrFlush(position, 0); - } - buffer[position++] = value; - } - - /** - * Write a quoted string into the JSON. - * String will be appropriately escaped according to JSON escaping rules. - * - * @param value string to write - */ - public final void writeString(final String value) { - final int len = value.length(); - if (position + (len << 2) + (len << 1) + 2 >= buffer.length) { - enlargeOrFlush(position, (len << 2) + (len << 1) + 2); - } - final byte[] _result = buffer; - _result[position] = QUOTE; - int cur = position + 1; - for (int i = 0; i < len; i++) { - final char c = value.charAt(i); - if (c > 31 && c != '"' && c != '\\' && c < 126) { - _result[cur++] = (byte) c; - } else { - writeQuotedString(value, i, cur, len); - return; - } - } - _result[cur] = QUOTE; - position = cur + 1; - } - - /** - * Write a quoted string into the JSON. - * Char sequence will be appropriately escaped according to JSON escaping rules. - * - * @param value char sequence to write - */ - public final void writeString(final CharSequence value) { - final int len = value.length(); - if (position + (len << 2) + (len << 1) + 2 >= buffer.length) { - enlargeOrFlush(position, (len << 2) + (len << 1) + 2); - } - final byte[] _result = buffer; - _result[position] = QUOTE; - int cur = position + 1; - for (int i = 0; i < len; i++) { - final char c = value.charAt(i); - if (c > 31 && c != '"' && c != '\\' && c < 126) { - _result[cur++] = (byte) c; - } else { - writeQuotedString(value, i, cur, len); - return; - } - } - _result[cur] = QUOTE; - position = cur + 1; - } - - private void writeQuotedString(final CharSequence str, int i, int cur, final int len) { - final byte[] _result = this.buffer; - for (; i < len; i++) { - final char c = str.charAt(i); - if (c == '"') { - _result[cur++] = ESCAPE; - _result[cur++] = QUOTE; - } else if (c == '\\') { - _result[cur++] = ESCAPE; - _result[cur++] = ESCAPE; - } else if (c < 32) { - if (c == 8) { - _result[cur++] = ESCAPE; - _result[cur++] = 'b'; - } else if (c == 9) { - _result[cur++] = ESCAPE; - _result[cur++] = 't'; - } else if (c == 10) { - _result[cur++] = ESCAPE; - _result[cur++] = 'n'; - } else if (c == 12) { - _result[cur++] = ESCAPE; - _result[cur++] = 'f'; - } else if (c == 13) { - _result[cur++] = ESCAPE; - _result[cur++] = 'r'; - } else { - _result[cur] = ESCAPE; - _result[cur + 1] = 'u'; - _result[cur + 2] = '0'; - _result[cur + 3] = '0'; - switch (c) { - case 0: - _result[cur + 4] = '0'; - _result[cur + 5] = '0'; - break; - case 1: - _result[cur + 4] = '0'; - _result[cur + 5] = '1'; - break; - case 2: - _result[cur + 4] = '0'; - _result[cur + 5] = '2'; - break; - case 3: - _result[cur + 4] = '0'; - _result[cur + 5] = '3'; - break; - case 4: - _result[cur + 4] = '0'; - _result[cur + 5] = '4'; - break; - case 5: - _result[cur + 4] = '0'; - _result[cur + 5] = '5'; - break; - case 6: - _result[cur + 4] = '0'; - _result[cur + 5] = '6'; - break; - case 7: - _result[cur + 4] = '0'; - _result[cur + 5] = '7'; - break; - case 11: - _result[cur + 4] = '0'; - _result[cur + 5] = 'B'; - break; - case 14: - _result[cur + 4] = '0'; - _result[cur + 5] = 'E'; - break; - case 15: - _result[cur + 4] = '0'; - _result[cur + 5] = 'F'; - break; - case 16: - _result[cur + 4] = '1'; - _result[cur + 5] = '0'; - break; - case 17: - _result[cur + 4] = '1'; - _result[cur + 5] = '1'; - break; - case 18: - _result[cur + 4] = '1'; - _result[cur + 5] = '2'; - break; - case 19: - _result[cur + 4] = '1'; - _result[cur + 5] = '3'; - break; - case 20: - _result[cur + 4] = '1'; - _result[cur + 5] = '4'; - break; - case 21: - _result[cur + 4] = '1'; - _result[cur + 5] = '5'; - break; - case 22: - _result[cur + 4] = '1'; - _result[cur + 5] = '6'; - break; - case 23: - _result[cur + 4] = '1'; - _result[cur + 5] = '7'; - break; - case 24: - _result[cur + 4] = '1'; - _result[cur + 5] = '8'; - break; - case 25: - _result[cur + 4] = '1'; - _result[cur + 5] = '9'; - break; - case 26: - _result[cur + 4] = '1'; - _result[cur + 5] = 'A'; - break; - case 27: - _result[cur + 4] = '1'; - _result[cur + 5] = 'B'; - break; - case 28: - _result[cur + 4] = '1'; - _result[cur + 5] = 'C'; - break; - case 29: - _result[cur + 4] = '1'; - _result[cur + 5] = 'D'; - break; - case 30: - _result[cur + 4] = '1'; - _result[cur + 5] = 'E'; - break; - default: - _result[cur + 4] = '1'; - _result[cur + 5] = 'F'; - break; - } - cur += 6; - } - } else if (c < 0x007F) { - _result[cur++] = (byte) c; - } else { - final int cp = Character.codePointAt(str, i); - if (Character.isSupplementaryCodePoint(cp)) { - i++; - } - if (cp == 0x007F) { - _result[cur++] = (byte) cp; - } else if (cp <= 0x7FF) { - _result[cur++] = (byte) (0xC0 | ((cp >> 6) & 0x1F)); - _result[cur++] = (byte) (0x80 | (cp & 0x3F)); - } else if ((cp < 0xD800) || (cp > 0xDFFF && cp <= 0xFFFF)) { - _result[cur++] = (byte) (0xE0 | ((cp >> 12) & 0x0F)); - _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F)); - _result[cur++] = (byte) (0x80 | (cp & 0x3F)); - } else if (cp >= 0x10000 && cp <= 0x10FFFF) { - _result[cur++] = (byte) (0xF0 | ((cp >> 18) & 0x07)); - _result[cur++] = (byte) (0x80 | ((cp >> 12) & 0x3F)); - _result[cur++] = (byte) (0x80 | ((cp >> 6) & 0x3F)); - _result[cur++] = (byte) (0x80 | (cp & 0x3F)); - } else { - throw new SerializationException("Unknown unicode codepoint in string! " + Integer.toHexString(cp)); - } - } - } - _result[cur] = QUOTE; - position = cur + 1; - } - - /** - * Write string consisting of only ascii characters. - * String will not be escaped according to JSON escaping rules. - * - * @param value ascii string - */ - @SuppressWarnings("deprecation") - public final void writeAscii(final String value) { - final int len = value.length(); - if (position + len >= buffer.length) { - enlargeOrFlush(position, len); - } - value.getBytes(0, len, buffer, position); - position += len; - } - - /** - * Write part of string consisting of only ascii characters. - * String will not be escaped according to JSON escaping rules. - * - * @param value ascii string - * @param len part of the provided string to use - */ - @SuppressWarnings("deprecation") - public final void writeAscii(final String value, final int len) { - if (position + len >= buffer.length) { - enlargeOrFlush(position, len); - } - value.getBytes(0, len, buffer, position); - position += len; - } - - /** - * Copy bytes into JSON as is. - * Provided buffer can't be null. - * - * @param buf byte buffer to copy - */ - public final void writeAscii(final byte[] buf) { - final int len = buf.length; - if (position + len >= buffer.length) { - enlargeOrFlush(position, len); - } - final int p = position; - final byte[] _result = buffer; - for (int i = 0; i < buf.length; i++) { - _result[p + i] = buf[i]; - } - position += len; - } - - /** - * Copy part of byte buffer into JSON as is. - * Provided buffer can't be null. - * - * @param buf byte buffer to copy - * @param len part of buffer to copy - */ - public final void writeAscii(final byte[] buf, final int len) { - if (position + len >= buffer.length) { - enlargeOrFlush(position, len); - } - final int p = position; - final byte[] _result = buffer; - for (int i = 0; i < len; i++) { - _result[p + i] = buf[i]; - } - position += len; - } - - /** - * Copy part of byte buffer into JSON as is. - * Provided buffer can't be null. - * - * @param buf byte buffer to copy - * @param offset in buffer to start from - * @param len part of buffer to copy - */ - public final void writeRaw(final byte[] buf, final int offset, final int len) { - if (position + len >= buffer.length) { - enlargeOrFlush(position, len); - } - System.arraycopy(buf, offset, buffer, position, len); - position += len; - } - - /** - * Encode bytes as Base 64. - * Provided value can't be null. - * - * @param value bytes to encode - */ - public final void writeBinary(final byte[] value) { - if (position + (value.length << 1) + 2 >= buffer.length) { - enlargeOrFlush(position, (value.length << 1) + 2); - } - buffer[position++] = '"'; - position += Base64.encodeToBytes(value, buffer, position); - buffer[position++] = '"'; - } - - final void writeDouble(final double value) { - if (value == Double.POSITIVE_INFINITY) { - writeAscii("\"Infinity\""); - } else if (value == Double.NEGATIVE_INFINITY) { - writeAscii("\"-Infinity\""); - } else if (value != value) { - writeAscii("\"NaN\""); - } else if (value == 0.0) { - writeAscii("0.0"); - } else { - if (Grisu3.tryConvert(value, doubleBuilder)) { - if (position + 24 >= buffer.length) { - enlargeOrFlush(position, 24); - } - final int len = doubleBuilder.copyTo(buffer, position); - position += len; - } else { - writeAscii(Double.toString(value)); - } - } - } - - @Override - public String toString() { - return new String(buffer, 0, position, UTF_8); - } - - /** - * Content of buffer can be copied to another array of appropriate size. - * This method can't be used when targeting output stream. - * Ideally it should be avoided if possible, since it will create an array copy. - * It's better to use getByteBuffer and size instead. - * - * @return copy of the buffer up to the current position - */ - public final byte[] toByteArray() { - if (target != null) { - throw new ConfigurationException("Method is not available when targeting stream"); - } - return Arrays.copyOf(buffer, position); - } - - /** - * When JsonWriter does not target stream, this method should be used to copy content of the buffer into target stream. - * It will also reset the buffer position to 0 so writer can be continued to be used even without a call to reset(). - * - * @param stream target stream - * @throws IOException propagates from stream.write - */ - public final void toStream(final OutputStream stream) throws IOException { - if (target != null) { - throw new ConfigurationException("Method should not be used when targeting streams. Instead use flush() to copy what's remaining in the buffer"); - } - stream.write(buffer, 0, position); - flushed += position; - position = 0; - } - - /** - * Current buffer. - * If buffer grows, a new instance will be created and old one will not be used anymore. - * - * @return current buffer - */ - public final byte[] getByteBuffer() { - return buffer; - } - - /** - * Current position in the buffer. When stream is not used, this is also equivalent - * to the size of the resulting JSON in bytes - * - * @return position in the populated buffer - */ - public final int size() { - return position; - } - - /** - * Total bytes currently flushed to stream - * - * @return bytes flushed - */ - public final long flushed() { - return flushed; - } - - /** - * Resets the writer - same as calling reset(OutputStream = null) - */ - public final void reset() { - reset(null); - } - - /** - * Resets the writer - specifies the target stream and sets the position in buffer to 0. - * If stream is set to null, JsonWriter will work in growing byte[] buffer mode (entire response will be buffered in memory). - * - * @param stream sets/clears the target stream - */ - public final void reset(@Nullable OutputStream stream) { - position = 0; - target = stream; - flushed = 0; - } - - /** - * If stream was used, copies the buffer to stream and resets the position in buffer to 0. - * It will not reset the stream as target, - * meaning new usages of the JsonWriter will try to use the already provided stream. - * It will not do anything if stream was not used - *

- * To reset the stream to null use reset() or reset(OutputStream) methods. - */ - public final void flush() { - if (target != null && position != 0) { - try { - target.write(buffer, 0, position); - } catch (IOException ex) { - throw new SerializationException("Unable to write to target stream.", ex); - } - flushed += position; - position = 0; - } - } - - /** - * This is deprecated method which exists only for backward compatibility - * - * @throws java.io.IOException unable to write to target stream - */ - @Deprecated - public void close() throws IOException { - if (target != null && position != 0) { - target.write(buffer, 0, position); - position = 0; - flushed = 0; - } - } - - /** - * Custom objects can be serialized based on the implementation specified through this interface. - * Annotation processor creates custom deserializers at compile time and registers them into DslJson. - * - * @param type - */ - public interface WriteObject { - void write(JsonWriter writer, @Nullable T value); - } - - /** - * Convenience method for serializing array of JsonObject's. - * Array can't be null nor can't contain null values (it will result in NullPointerException). - * - * @param array input objects - * @param type of objects - */ - public void serialize(final T[] array) { - writeByte(ARRAY_START); - if (array.length != 0) { - array[0].serialize(this, false); - for (int i = 1; i < array.length; i++) { - writeByte(COMMA); - array[i].serialize(this, false); - } - } - writeByte(ARRAY_END); - } - - /** - * Convenience method for serializing only part of JsonObject's array. - * Useful when array is reused and only part of it needs to be serialized. - * Array can't be null nor can't contain null values (it will result in NullPointerException). - * - * @param array input objects - * @param len size of array which should be serialized - * @param type of objects - */ - public void serialize(final T[] array, final int len) { - writeByte(ARRAY_START); - if (array.length != 0 && len != 0) { - array[0].serialize(this, false); - for (int i = 1; i < len; i++) { - writeByte(COMMA); - array[i].serialize(this, false); - } - } - writeByte(ARRAY_END); - } - - /** - * Convenience method for serializing list of JsonObject's. - * List can't be null nor can't contain null values (it will result in NullPointerException). - * It will use list .get(index) method to access the object. - * When using .get(index) is not appropriate, - * it's better to call the serialize(Collection<JsonObject>) method instead. - * - * @param list input objects - * @param type of objects - */ - public void serialize(final List list) { - writeByte(ARRAY_START); - if (list.size() != 0) { - list.get(0).serialize(this, false); - for (int i = 1; i < list.size(); i++) { - writeByte(COMMA); - list.get(i).serialize(this, false); - } - } - writeByte(ARRAY_END); - } - - /** - * Convenience method for serializing array through instance serializer (WriteObject). - * Array can be null and can contain null values. - * Instance serializer will not be invoked for null values - * - * @param array array to serialize - * @param encoder instance serializer - * @param type of object - */ - public void serialize(@Nullable final T[] array, final WriteObject encoder) { - if (array == null) { - writeNull(); - return; - } - writeByte(ARRAY_START); - if (array.length != 0) { - T item = array[0]; - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - for (int i = 1; i < array.length; i++) { - writeByte(COMMA); - item = array[i]; - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - } - } - writeByte(ARRAY_END); - } - - /** - * Convenience method for serializing list through instance serializer (WriteObject). - * List can be null and can contain null values. - * Instance serializer will not be invoked for null values - * It will use list .get(index) method to access the object. - * When using .get(index) is not appropriate, - * it's better to call the serialize(Collection<JsonObject>, WriteObject) method instead. - * - * @param list list to serialize - * @param encoder instance serializer - * @param type of object - */ - public void serialize(@Nullable final List list, final WriteObject encoder) { - if (list == null) { - writeNull(); - return; - } - writeByte(ARRAY_START); - if (!list.isEmpty()) { - if (list instanceof RandomAccess) { - T item = list.get(0); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - for (int i = 1; i < list.size(); i++) { - writeByte(COMMA); - item = list.get(i); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - } - } else { - Iterator iter = list.iterator(); - T item = iter.next(); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - while (iter.hasNext()) { - writeByte(COMMA); - item = iter.next(); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - } - } - } - writeByte(ARRAY_END); - } - - public void serializeRaw(@Nullable final List list, final WriteObject encoder) { - serialize(list, encoder); - } - - /** - * Convenience method for serializing collection through instance serializer (WriteObject). - * Collection can be null and can contain null values. - * Instance serializer will not be invoked for null values - * - * @param collection collection to serialize - * @param encoder instance serializer - * @param type of object - */ - public void serialize(@Nullable final Collection collection, final WriteObject encoder) { - if (collection == null) { - writeNull(); - return; - } - writeByte(ARRAY_START); - if (!collection.isEmpty()) { - final Iterator it = collection.iterator(); - T item = it.next(); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - while (it.hasNext()) { - writeByte(COMMA); - item = it.next(); - if (item != null) { - encoder.write(this, item); - } else { - writeNull(); - } - } - } - writeByte(ARRAY_END); - } - - public void serializeRaw(@Nullable final Collection collection, final WriteObject encoder) { - serialize(collection, encoder); - } - - public void serialize(@Nullable final Map map, final WriteObject keyEncoder, final WriteObject valueEncoder) { - if (map == null) { - writeNull(); - return; - } - writeByte(OBJECT_START); - final int size = map.size(); - if (size > 0) { - final Iterator> iterator = map.entrySet().iterator(); - Map.Entry kv = iterator.next(); - writeQuoted(keyEncoder, kv.getKey()); - writeByte(SEMI); - valueEncoder.write(this, kv.getValue()); - for (int i = 1; i < size; i++) { - writeByte(COMMA); - kv = iterator.next(); - writeQuoted(keyEncoder, kv.getKey()); - writeByte(SEMI); - valueEncoder.write(this, kv.getValue()); - } - } - writeByte(OBJECT_END); - } - - public void serializeRaw(@Nullable final Map map, final WriteObject keyEncoder, final WriteObject valueEncoder) { - serialize(map, keyEncoder, valueEncoder); - } - - public void writeQuoted(final JsonWriter.WriteObject keyWriter, final T key) { - if (key instanceof Double) { - final double value = (Double) key; - if (Double.isNaN(value)) writeAscii("\"NaN\""); - else if (value == Double.POSITIVE_INFINITY) writeAscii("\"Infinity\""); - else if (value == Double.NEGATIVE_INFINITY) writeAscii("\"-Infinity\""); - else { - writeByte(QUOTE); - NumberConverter.serialize(value, this); - writeByte(QUOTE); - } - } else if (key instanceof Float) { - final float value = (Float) key; - if (Float.isNaN(value)) writeAscii("\"NaN\""); - else if (value == Float.POSITIVE_INFINITY) writeAscii("\"Infinity\""); - else if (value == Float.NEGATIVE_INFINITY) writeAscii("\"-Infinity\""); - else { - writeByte(QUOTE); - NumberConverter.serialize(value, this); - writeByte(QUOTE); - } - } else if (key instanceof Number) { - writeByte(QUOTE); - keyWriter.write(this, key); - writeByte(QUOTE); - } else { - keyWriter.write(this, key); - } - } - - /** - * Generic object serializer which is used for "unknown schema" objects. - * It will throw SerializationException in case if it doesn't know how to serialize provided instance. - * Will delegate the serialization to UnknownSerializer, which in most cases is the DslJson instance from which the writer was created. - * This enables it to use DslJson configuration and serialize using custom serializers (when they are provided). - * - * @param value instance to serialize - */ - public void serializeObject(@Nullable final Object value) { - if (value == null) { - writeNull(); - } else if (unknownSerializer != null) { - try { - unknownSerializer.serialize(this, value); - } catch (IOException ex) { //serializing unknown stuff can fail in various ways ;( - throw new SerializationException(ex); - } - } else { - throw new ConfigurationException("Unable to serialize: " + value.getClass() + ".\n" + - "Check that JsonWriter was created through DslJson#newWriter."); - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java deleted file mode 100644 index afc832fb53..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/MapConverter.java +++ /dev/null @@ -1,88 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.*; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class MapConverter { - - private static final JsonReader.ReadObject> TypedMapReader = new JsonReader.ReadObject>() { - @Nullable - @Override - public Map read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserialize(reader); - } - }; - - public static void serializeNullable(@Nullable final Map value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - public static void serialize(final Map value, final JsonWriter sw) { - sw.writeByte(JsonWriter.OBJECT_START); - final int size = value.size(); - if (size > 0) { - final Iterator> iterator = value.entrySet().iterator(); - Map.Entry kv = iterator.next(); - StringConverter.serializeShort(kv.getKey(), sw); - sw.writeByte(JsonWriter.SEMI); - StringConverter.serializeNullable(kv.getValue(), sw); - for (int i = 1; i < size; i++) { - sw.writeByte(JsonWriter.COMMA); - kv = iterator.next(); - StringConverter.serializeShort(kv.getKey(), sw); - sw.writeByte(JsonWriter.SEMI); - StringConverter.serializeNullable(kv.getValue(), sw); - } - } - sw.writeByte(JsonWriter.OBJECT_END); - } - - public static Map deserialize(final JsonReader reader) throws IOException { - if (reader.last() != '{') throw reader.newParseError("Expecting '{' for map start"); - byte nextToken = reader.getNextToken(); - if (nextToken == '}') return new LinkedHashMap(0); - final LinkedHashMap res = new LinkedHashMap(); - String key = StringConverter.deserialize(reader); - nextToken = reader.getNextToken(); - if (nextToken != ':') throw reader.newParseError("Expecting ':' after attribute name"); - reader.getNextToken(); - String value = StringConverter.deserializeNullable(reader); - res.put(key, value); - while ((nextToken = reader.getNextToken()) == ',') { - reader.getNextToken(); - key = StringConverter.deserialize(reader); - nextToken = reader.getNextToken(); - if (nextToken != ':') throw reader.newParseError("Expecting ':' after attribute name"); - reader.getNextToken(); - value = StringConverter.deserializeNullable(reader); - res.put(key, value); - } - if (nextToken != '}') throw reader.newParseError("Expecting '}' for map end"); - return res; - } - - @SuppressWarnings("unchecked") - public static ArrayList> deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(TypedMapReader); - } - - public static void deserializeCollection(final JsonReader reader, final Collection> res) throws IOException { - reader.deserializeCollection(TypedMapReader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList> deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(TypedMapReader); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection> res) throws IOException { - reader.deserializeNullableCollection(TypedMapReader, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java deleted file mode 100644 index b01edbea6e..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NetConverter.java +++ /dev/null @@ -1,110 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.net.InetAddress; -import java.net.URI; -import java.util.ArrayList; -import java.util.Collection; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class NetConverter { - - static final JsonReader.ReadObject UriReader = new JsonReader.ReadObject() { - @Nullable - @Override - public URI read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeUri(reader); - } - }; - static final JsonWriter.WriteObject UriWriter = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable URI value) { - serializeNullable(value, writer); - } - }; - static final JsonReader.ReadObject AddressReader = new JsonReader.ReadObject() { - @Nullable - @Override - public InetAddress read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeIp(reader); - } - }; - static final JsonWriter.WriteObject AddressWriter = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable InetAddress value) { - serializeNullable(value, writer); - } - }; - - public static void serializeNullable(@Nullable final URI value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - public static void serialize(final URI value, final JsonWriter sw) { - StringConverter.serializeShort(value.toString(), sw); - } - - public static URI deserializeUri(final JsonReader reader) throws IOException { - return URI.create(reader.readString()); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeUriCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(UriReader); - } - - public static void deserializeUriCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(UriReader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeUriNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(UriReader); - } - - public static void deserializeUriNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(UriReader, res); - } - - public static void serializeNullable(@Nullable final InetAddress value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - public static void serialize(final InetAddress value, final JsonWriter sw) { - sw.writeByte(JsonWriter.QUOTE); - sw.writeAscii(value.getHostAddress()); - sw.writeByte(JsonWriter.QUOTE); - } - - public static InetAddress deserializeIp(final JsonReader reader) throws IOException { - return InetAddress.getByName(reader.readSimpleString()); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeIpCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(AddressReader); - } - - public static void deserializeIpCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(AddressReader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeIpNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(AddressReader); - } - - public static void deserializeIpNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(AddressReader, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java deleted file mode 100644 index 5d87bab985..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/NumberConverter.java +++ /dev/null @@ -1,1700 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.math.BigDecimal; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class NumberConverter { - - public final static short[] SHORT_EMPTY_ARRAY = new short[0]; - public final static int[] INT_EMPTY_ARRAY = new int[0]; - public final static long[] LONG_EMPTY_ARRAY = new long[0]; - public final static float[] FLOAT_EMPTY_ARRAY = new float[0]; - public final static double[] DOUBLE_EMPTY_ARRAY = new double[0]; - public final static Short SHORT_ZERO = 0; - public final static Integer INT_ZERO = 0; - public final static Long LONG_ZERO = 0L; - public final static Float FLOAT_ZERO = 0f; - public final static Double DOUBLE_ZERO = 0.0; - - private final static int[] DIGITS = new int[1000]; - private final static int[] DIFF = {111, 222, 444, 888, 1776}; - private final static int[] ERROR = {50, 100, 200, 400, 800}; - private final static int[] SCALE_10 = {10000, 1000, 100, 10, 1}; - private final static double[] POW_10 = { - 1e1, 1e2, 1e3, 1e4, 1e5, 1e6, 1e7, 1e8, 1e9, - 1e10, 1e11, 1e12, 1e13, 1e14, 1e15, 1e16, 1e17, 1e18, 1e19, - 1e20, 1e21, 1e22, 1e23, 1e24, 1e25, 1e26, 1e27, 1e28, 1e29, - 1e30, 1e31, 1e32, 1e33, 1e34, 1e35, 1e36, 1e37, 1e38, 1e39, - 1e40, 1e41, 1e42, 1e43, 1e44, 1e45, 1e46, 1e47, 1e48, 1e49, - 1e50, 1e51, 1e52, 1e53, 1e54, 1e55, 1e56, 1e57, 1e58, 1e59, - 1e60, 1e61, 1e62, 1e63, 1e64, 1e65 - }; - public static final JsonReader.ReadObject DOUBLE_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Double read(JsonReader reader) throws IOException { - return deserializeDouble(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_DOUBLE_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Double read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeDouble(reader); - } - }; - public static final JsonWriter.WriteObject DOUBLE_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Double value) { - serializeNullable(value, writer); - } - }; - public static final JsonReader.ReadObject DOUBLE_ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public double[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for double array start"); - reader.getNextToken(); - return deserializeDoubleArray(reader); - } - }; - public static final JsonWriter.WriteObject DOUBLE_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable double[] value) { - serialize(value, writer); - } - }; - - public static final JsonReader.ReadObject FLOAT_READER = new JsonReader.ReadObject() { - @Override - public Float read(JsonReader reader) throws IOException { - return deserializeFloat(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_FLOAT_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Float read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeFloat(reader); - } - }; - public static final JsonWriter.WriteObject FLOAT_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Float value) { - serializeNullable(value, writer); - } - }; - public static final JsonReader.ReadObject FLOAT_ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public float[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for float array start"); - reader.getNextToken(); - return deserializeFloatArray(reader); - } - }; - public static final JsonWriter.WriteObject FLOAT_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable float[] value) { - serialize(value, writer); - } - }; - public static final JsonReader.ReadObject INT_READER = new JsonReader.ReadObject() { - @Override - public Integer read(JsonReader reader) throws IOException { - return deserializeInt(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_INT_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Integer read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeInt(reader); - } - }; - public static final JsonWriter.WriteObject INT_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Integer value) { - serializeNullable(value, writer); - } - }; - public static final JsonReader.ReadObject INT_ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public int[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for int array start"); - reader.getNextToken(); - return deserializeIntArray(reader); - } - }; - public static final JsonWriter.WriteObject INT_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable int[] value) { - serialize(value, writer); - } - }; - public static final JsonReader.ReadObject SHORT_READER = new JsonReader.ReadObject() { - @Override - public Short read(JsonReader reader) throws IOException { - return deserializeShort(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_SHORT_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Short read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeShort(reader); - } - }; - public static final JsonWriter.WriteObject SHORT_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Short value) { - if (value == null) writer.writeNull(); - else serialize(value.intValue(), writer); - } - }; - public static final JsonReader.ReadObject SHORT_ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public short[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for short array start"); - reader.getNextToken(); - return deserializeShortArray(reader); - } - }; - public static final JsonWriter.WriteObject SHORT_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable short[] value) { - serialize(value, writer); - } - }; - - public static final JsonReader.ReadObject LONG_READER = new JsonReader.ReadObject() { - @Override - public Long read(JsonReader reader) throws IOException { - return deserializeLong(reader); - } - }; - public static final JsonReader.ReadObject NULLABLE_LONG_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public Long read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeLong(reader); - } - }; - public static final JsonWriter.WriteObject LONG_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Long value) { - serializeNullable(value, writer); - } - }; - public static final JsonReader.ReadObject LONG_ARRAY_READER = new JsonReader.ReadObject() { - @Nullable - @Override - public long[] read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for long array start"); - reader.getNextToken(); - return deserializeLongArray(reader); - } - }; - public static final JsonWriter.WriteObject LONG_ARRAY_WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable long[] value) { - serialize(value, writer); - } - }; - - public static final JsonReader.ReadObject DecimalReader = new JsonReader.ReadObject() { - @Nullable - @Override - public BigDecimal read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeDecimal(reader); - } - }; - public static final JsonWriter.WriteObject DecimalWriter = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable BigDecimal value) { - serializeNullable(value, writer); - } - }; - static final JsonReader.ReadObject NumberReader = new JsonReader.ReadObject() { - @Nullable - @Override - public Number read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeNumber(reader); - } - }; - - static { - for (int i = 0; i < DIGITS.length; i++) { - DIGITS[i] = (i < 10 ? (2 << 24) : i < 100 ? (1 << 24) : 0) - + (((i / 100) + '0') << 16) - + ((((i / 10) % 10) + '0') << 8) - + i % 10 + '0'; - } - } - - static void write4(final int value, final byte[] buf, final int pos) { - if (value > 9999) { - throw new IllegalArgumentException("Only 4 digits numbers are supported. Provided: " + value); - } - final int q = value / 1000; - final int v = DIGITS[value - q * 1000]; - buf[pos] = (byte) (q + '0'); - buf[pos + 1] = (byte) (v >> 16); - buf[pos + 2] = (byte) (v >> 8); - buf[pos + 3] = (byte) v; - } - - static void write3(final int number, final byte[] buf, int pos) { - final int v = DIGITS[number]; - buf[pos] = (byte) (v >> 16); - buf[pos + 1] = (byte) (v >> 8); - buf[pos + 2] = (byte) v; - } - - static void write2(final int value, final byte[] buf, final int pos) { - final int v = DIGITS[value]; - buf[pos] = (byte) (v >> 8); - buf[pos + 1] = (byte) v; - } - - static int read2(final char[] buf, final int pos) { - final int v1 = buf[pos] - 48; - return (v1 << 3) + (v1 << 1) + buf[pos + 1] - 48; - } - - static int read4(final char[] buf, final int pos) { - final int v2 = buf[pos + 1] - 48; - final int v3 = buf[pos + 2] - 48; - return (buf[pos] - 48) * 1000 - + (v2 << 6) + (v2 << 5) + (v2 << 2) - + (v3 << 3) + (v3 << 1) - + buf[pos + 3] - 48; - } - - static void numberException(final JsonReader reader, final int start, final int end, String message) throws ParsingException { - final int len = end - start; - if (len > reader.maxNumberDigits) { - throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); - } - throw reader.newParseErrorWith("Error parsing number", len, "", message, null, ". Error parsing number"); - } - - static void numberException(final JsonReader reader, final int start, final int end, String message, Object messageArgument) throws ParsingException { - final int len = end - start; - if (len > reader.maxNumberDigits) { - throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); - } - throw reader.newParseErrorWith("Error parsing number", len, "", message, messageArgument, ". Error parsing number"); - } - - public static void serializeNullable(@Nullable final Double value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - private static BigDecimal parseNumberGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws ParsingException { - int end = len; - while (end > 0 && Character.isWhitespace(buf[end - 1])) { - end--; - } - if (end > reader.maxNumberDigits) { - throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); - } - final int offset = buf[0] == '-' ? 1 : 0; - if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { - throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); - } - try { - return new BigDecimal(buf, 0, end); - } catch (NumberFormatException nfe) { - throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); - } - } - - public static void serialize(final double value, final JsonWriter sw) { - sw.writeDouble(value); - } - - public static void serialize(@Nullable final double[] value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value.length == 0) { - sw.writeAscii("[]"); - } else { - sw.writeByte(JsonWriter.ARRAY_START); - serialize(value[0], sw); - for (int i = 1; i < value.length; i++) { - sw.writeByte(JsonWriter.COMMA); - serialize(value[i], sw); - } - sw.writeByte(JsonWriter.ARRAY_END); - } - } - - private static class NumberInfo { - final char[] buffer; - final int length; - - NumberInfo(final char[] buffer, final int length) { - this.buffer = buffer; - this.length = length; - } - } - - private static NumberInfo readLongNumber(final JsonReader reader, final int start) throws IOException { - int len = reader.length() - start; - char[] result = reader.prepareBuffer(start, len); - while (reader.length() == reader.getCurrentIndex()) { - if (reader.isEndOfStream()) break; - reader.scanNumber(); // peek, do not read - int end = reader.getCurrentIndex(); - int oldLen = len; - len += end; - if (len > reader.maxNumberDigits) { - throw reader.newParseErrorFormat("Too many digits detected in number", len, "Number of digits larger than %d. Unable to read number", reader.maxNumberDigits); - } - char[] tmp = result; - result = new char[len]; - System.arraycopy(tmp, 0, result, 0, oldLen); - System.arraycopy(reader.prepareBuffer(0, end), 0, result, oldLen, end); - } - return new NumberInfo(result, len); - } - - public static double deserializeDouble(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int position = reader.getCurrentIndex(); - final char[] buf = reader.readSimpleQuote(); - return parseDoubleGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true); - } - final int start = reader.scanNumber(); - final int end = reader.getCurrentIndex(); - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - if (ch == '-') { - return -parseDouble(buf, reader, start, end, 1); - } - return parseDouble(buf, reader, start, end, 0); - } - - private static double parseDouble(final byte[] buf, final JsonReader reader, final int start, final int end, final int offset) throws IOException { - if (end - start - offset > reader.doubleLengthLimit) { - if (end == reader.length()) { - final NumberInfo tmp = readLongNumber(reader, start + offset); - return parseDoubleGeneric(tmp.buffer, tmp.length, reader, false); - } - return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } - long value = 0; - byte ch = ' '; - int i = start + offset; - final boolean leadingZero = buf[start + offset] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = buf[i] - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + offset + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start + offset && reader.allWhitespace(i, end)) return value; - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == start + offset) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + offset + 1) numberException(reader, start, end, "Leading zero is not allowed"); - else if (i == end) return value; - else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - final int maxLen; - final double preciseDividor; - final int expDiff; - final int decPos = i; - final int decOffset; - if (value == 0) { - maxLen = i + 15; - ch = buf[i]; - if (ch == '0' && end > maxLen) { - return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } else if (ch < '8') { - preciseDividor = 1e14; - expDiff = -1; - decOffset = 1; - } else { - preciseDividor = 1e15; - expDiff = 0; - decOffset = 0; - } - } else { - maxLen = start + offset + 16; - if (buf[start + offset] < '8') { - preciseDividor = 1e14; - expDiff = i - maxLen + 14; - decOffset = 1; - } else { - preciseDividor = 1e15; - expDiff = i - maxLen + 15; - decOffset = 0; - } - } - final int numLimit = maxLen < end ? maxLen : end; - //TODO zeros - for (; i < numLimit; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return value / POW_10[i - decPos - 1]; - numberException(reader, start, end, "Unknown digit", (char)buf[i]); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == end) return value / POW_10[i - decPos - 1]; - else if (ch == 'e' || ch == 'E') { - return doubleExponent(reader, value, i - decPos,0, buf, start, end, offset, i); - } - if (reader.doublePrecision == JsonReader.DoublePrecision.HIGH) { - return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } - int decimals = 0; - final int decLimit = start + offset + 18 < end ? start + offset + 18 : end; - final int remPos = i; - for(;i < decLimit; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) { - return approximateDouble(decimals, value / preciseDividor, i - remPos - decOffset); - } - numberException(reader, start, end, "Unknown digit", (char)buf[i]); - } - decimals = (decimals << 3) + (decimals << 1) + ind; - } - final double number = approximateDouble(decimals, value / preciseDividor, i - remPos - decOffset); - while (i < end && ch >= '0' && ch <= '9') { - ch = buf[i++]; - } - if (ch == 'e' || ch == 'E') { - return doubleExponent(reader, 0, expDiff, number, buf, start, end, offset, i); - } else if (expDiff > 0) { - return number * POW_10[expDiff - 1]; - } else if (expDiff < 0) { - return number / POW_10[-expDiff - 1]; - } else { - return number; - } - } else if (ch == 'e' || ch == 'E') { - return doubleExponent(reader, value, 0, 0, buf, start, end, offset, i); - } - return value; - } - - private static double approximateDouble(final int decimals, final double precise, final int digits) { - final long bits = Double.doubleToRawLongBits(precise); - final int exp = (int)(bits >> 52) - 1022; - final int missing = (decimals * SCALE_10[digits + 1] + ERROR[exp]) / DIFF[exp]; - return Double.longBitsToDouble(bits + missing); - } - - private static double doubleExponent(JsonReader reader, final long whole, final int decimals, double fraction, byte[] buf, int start, int end, int offset, int i) throws IOException { - if (reader.doublePrecision == JsonReader.DoublePrecision.EXACT) { - return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } - byte ch; - ch = buf[++i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end) - decimals; - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1) - decimals; - } else { - exp = parsePositiveInt(buf, reader, i, end, 0) - decimals; - } - if (fraction == 0) { - if (exp == 0 || whole == 0) return whole; - else if (exp > 0 && exp < POW_10.length) return whole * POW_10[exp - 1]; - else if (exp < 0 && -exp < POW_10.length) return whole / POW_10[-exp - 1]; - else if (reader.doublePrecision != JsonReader.DoublePrecision.HIGH) { - if (exp > 0 && exp < 300) return whole * Math.pow(10, exp); - else if (exp > -300 && exp < 0) return whole / Math.pow(10, exp); - } - } else { - if (exp == 0) return whole + fraction; - else if (exp > 0 && exp < POW_10.length) return fraction * POW_10[exp - 1] + whole * POW_10[exp - 1]; - else if (exp < 0 && -exp < POW_10.length) return fraction / POW_10[-exp - 1] + whole / POW_10[-exp - 1]; - else if (reader.doublePrecision != JsonReader.DoublePrecision.HIGH) { - if (exp > 0 && exp < 300) return whole * Math.pow(10, exp); - else if (exp > -300 && exp < 0) return whole / Math.pow(10, exp); - } - } - return parseDoubleGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } - - private static double parseDoubleGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws IOException { - int end = len; - while (end > 0 && Character.isWhitespace(buf[end - 1])) { - end--; - } - if (end > reader.maxNumberDigits) { - throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); - } - final int offset = buf[0] == '-' ? 1 : 0; - if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { - throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); - } - try { - return Double.parseDouble(new String(buf, 0, end)); - } catch (NumberFormatException nfe) { - throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); - } - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeDoubleCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(DOUBLE_READER); - } - - public static void deserializeDoubleCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(DOUBLE_READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeDoubleNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(DOUBLE_READER); - } - - public static void deserializeDoubleNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(DOUBLE_READER, res); - } - - public static void serializeNullable(@Nullable final Float value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - public static void serialize(final float value, final JsonWriter sw) { - if (value == Float.POSITIVE_INFINITY) { - sw.writeAscii("\"Infinity\""); - } else if (value == Float.NEGATIVE_INFINITY) { - sw.writeAscii("\"-Infinity\""); - } else if (value != value) { - sw.writeAscii("\"NaN\""); - } else { - sw.writeAscii(Float.toString(value));//TODO: better implementation required - } - } - - public static void serialize(@Nullable final float[] value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value.length == 0) { - sw.writeAscii("[]"); - } else { - sw.writeByte(JsonWriter.ARRAY_START); - serialize(value[0], sw); - for (int i = 1; i < value.length; i++) { - sw.writeByte(JsonWriter.COMMA); - serialize(value[i], sw); - } - sw.writeByte(JsonWriter.ARRAY_END); - } - } - - public static float deserializeFloat(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int position = reader.getCurrentIndex(); - final char[] buf = reader.readSimpleQuote(); - return parseFloatGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true); - } - final int start = reader.scanNumber(); - final int end = reader.getCurrentIndex(); - if (end == reader.length()) { - final NumberInfo tmp = readLongNumber(reader, start); - return parseFloatGeneric(tmp.buffer, tmp.length, reader, false); - } - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - if (ch == '-') { - return -parseFloat(buf, reader, start, end, 1); - } - return parseFloat(buf, reader, start, end, 0); - } - - private static float parseFloat(byte[] buf, final JsonReader reader, final int start, int end, int offset) throws IOException { - long value = 0; - byte ch = ' '; - int i = start + offset; - final int digitStart = i; - final boolean leadingZero = buf[start + offset] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + offset + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start + offset && reader.allWhitespace(i, end)) return value; - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == digitStart) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + offset + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } else if (i > 18 + digitStart) { - return parseFloatGeneric(reader.prepareBuffer(start + offset, end - start - offset), end - start - offset, reader, false); - } else if (i == end) { - return value; - } else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - final int decPos; - final int maxLen; - final int pointOffset; - if (value == 0) { - pointOffset = 0; - decPos = i + 1; - while (i < end && buf[i] == '0') { - i++; - } - maxLen = i + 17; - } else { - pointOffset = 1; - maxLen = digitStart + 17; - decPos = i; - } - final int numLimit = maxLen < end ? maxLen : end; - boolean foundE = false; - for (; i < numLimit; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') { - foundE = true; - ++i; - break; - } - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return (float) (value / POW_10[i - decPos - pointOffset]); - numberException(reader, start, end, "Unknown digit", (char) ch); - } - value = (value << 3) + (value << 1) + ind; - } - final int endPos; - if (i == numLimit && !foundE) { - endPos = i + 1 - pointOffset; - while (i < end && ch >= '0' && ch <= '9') { - ch = buf[i++]; - } - } else endPos = i - pointOffset; - while (i == end && reader.length() == end) { - i = reader.scanNumber(); - end = reader.getCurrentIndex(); - buf = reader.buffer; - while (i < end && ch >= '0' && ch <= '9') { - ch = buf[i++]; - } - } - if (ch == 'e' || ch == 'E') { - return floatExponent(reader, value, endPos - decPos, buf, end, i); - } - final int expDiff = endPos - decPos; - if (expDiff > 0) { - return (float)(value / POW_10[expDiff - 1]); - } else if (expDiff < 0) { - return (float)(value * POW_10[-expDiff - 1]); - } else { - return value; - } - } else if (ch == 'e' || ch == 'E') { - return floatExponent(reader, value, 0, buf, end, i + 1); - } - return value; - } - - private static float floatExponent(JsonReader reader, final long whole, final int decimals, byte[] buf, int end, int i) throws IOException { - byte ch; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end) - decimals; - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1) - decimals; - } else { - exp = parsePositiveInt(buf, reader, i, end, 0) - decimals; - } - if (exp == 0 || whole == 0) return whole; - else if (exp > 0 && exp < POW_10.length) return (float) (whole * POW_10[exp - 1]); - else if (exp < 0 && -exp < POW_10.length) return (float) (whole / POW_10[-exp - 1]); - else return exp > 0 ? Float.POSITIVE_INFINITY : 0f; - } - - private static float parseFloatGeneric(final char[] buf, final int len, final JsonReader reader, final boolean withQuotes) throws ParsingException { - int end = len; - while (end > 0 && Character.isWhitespace(buf[end - 1])) { - end--; - } - if (end > reader.maxNumberDigits) { - throw reader.newParseErrorWith("Too many digits detected in number", len, "", "Too many digits detected in number", end, ""); - } - final int offset = buf[0] == '-' ? 1 : 0; - if (buf[offset] == '0' && end > offset + 1 && buf[offset + 1] >= '0' && buf[offset + 1] <= '9') { - throw reader.newParseErrorAt("Leading zero is not allowed. Error parsing number", len + (withQuotes ? 2 : 0)); - } - try { - return Float.parseFloat(new String(buf, 0, end)); - } catch (NumberFormatException nfe) { - throw reader.newParseErrorAt("Error parsing number", len + (withQuotes ? 2 : 0), nfe); - } - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeFloatCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(FLOAT_READER); - } - - public static void deserializeFloatCollection(final JsonReader reader, Collection res) throws IOException { - reader.deserializeCollection(FLOAT_READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeFloatNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(FLOAT_READER); - } - - public static void deserializeFloatNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(FLOAT_READER, res); - } - - public static void serializeNullable(@Nullable final Integer value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - private static final byte MINUS = '-'; - private static final byte[] MIN_INT = "-2147483648".getBytes(); - - public static void serialize(final int value, final JsonWriter sw) { - final byte[] buf = sw.ensureCapacity(11); - final int position = sw.size(); - int current = serialize(buf, position, value); - sw.advance(current - position); - } - - private static int serialize(final byte[] buf, int pos, final int value) { - int i; - if (value < 0) { - if (value == Integer.MIN_VALUE) { - for (int x = 0; x < MIN_INT.length; x++) { - buf[pos + x] = MIN_INT[x]; - } - return pos + MIN_INT.length; - } - i = -value; - buf[pos++] = MINUS; - } else { - i = value; - } - final int q1 = i / 1000; - if (q1 == 0) { - pos += writeFirstBuf(buf, DIGITS[i], pos); - return pos; - } - final int r1 = i - q1 * 1000; - final int q2 = q1 / 1000; - if (q2 == 0) { - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[q1]; - int off = writeFirstBuf(buf, v2, pos); - writeBuf(buf, v1, pos + off); - return pos + 3 + off; - } - final int r2 = q1 - q2 * 1000; - final int q3 = q2 / 1000; - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[r2]; - if (q3 == 0) { - pos += writeFirstBuf(buf, DIGITS[q2], pos); - } else { - final int r3 = q2 - q3 * 1000; - buf[pos++] = (byte) (q3 + '0'); - writeBuf(buf, DIGITS[r3], pos); - pos += 3; - } - writeBuf(buf, v2, pos); - writeBuf(buf, v1, pos + 3); - return pos + 6; - } - - public static void serialize(@Nullable final int[] values, final JsonWriter sw) { - if (values == null) { - sw.writeNull(); - } else if (values.length == 0) { - sw.writeAscii("[]"); - } else { - final byte[] buf = sw.ensureCapacity(values.length * 11 + 2); - int position = sw.size(); - buf[position++] = '['; - position = serialize(buf, position, values[0]); - for (int i = 1; i < values.length; i++) { - buf[position++] = ','; - position = serialize(buf, position, values[i]); - } - buf[position++] = ']'; - sw.advance(position - sw.size()); - } - } - - public static void serialize(@Nullable final short[] value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else if (value.length == 0) { - sw.writeAscii("[]"); - } else { - sw.writeByte(JsonWriter.ARRAY_START); - serialize(value[0], sw); - for (int i = 1; i < value.length; i++) { - sw.writeByte(JsonWriter.COMMA); - serialize(value[i], sw); - } - sw.writeByte(JsonWriter.ARRAY_END); - } - } - - public static short deserializeShort(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int position = reader.getCurrentIndex(); - final char[] buf = reader.readSimpleQuote(); - try { - return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).shortValueExact(); - } catch (ArithmeticException ignore) { - throw reader.newParseErrorAt("Short overflow detected", reader.getCurrentIndex() - position); - } - } - final int start = reader.scanNumber(); - final int end = reader.getCurrentIndex(); - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - final int value = ch == '-' - ? parseNegativeInt(buf, reader, start, end) - : parsePositiveInt(buf, reader, start, end, 0); - if (value < Short.MIN_VALUE || value > Short.MAX_VALUE) { - throw reader.newParseErrorAt("Short overflow detected", reader.getCurrentIndex()); - } - return (short)value; - } - - public static int deserializeInt(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int position = reader.getCurrentIndex(); - final char[] buf = reader.readSimpleQuote(); - try { - return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).intValueExact(); - } catch (ArithmeticException ignore) { - throw reader.newParseErrorAt("Integer overflow detected", reader.getCurrentIndex() - position); - } - } - final int start = reader.scanNumber(); - final int end = reader.getCurrentIndex(); - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - if (ch == '-') { - if (end > start + 2 && buf[start + 1] == '0' && buf[start + 2] >= '0' && buf[start + 2] <= '9') { - numberException(reader, start, end, "Leading zero is not allowed"); - } - return parseNegativeInt(buf, reader, start, end); - } else { - if (ch == '0' && end > start + 1 && buf[start + 1] >= '0' && buf[start + 1] <= '9') { - numberException(reader, start, end, "Leading zero is not allowed"); - } - return parsePositiveInt(buf, reader, start, end, 0); - } - } - - private static int parsePositiveInt(final byte[] buf, final JsonReader reader, final int start, final int end, final int offset) throws IOException { - int value = 0; - int i = start + offset; - if (i == end) numberException(reader, start, end, "Digit not found"); - for (; i < end; i++) { - final int ind = buf[i] - 48; - if (ind < 0 || ind > 9) { - if (i > start + offset && reader.allWhitespace(i, end)) return value; - else if (i == end - 1 && buf[i] == '.') numberException(reader, start, end, "Number ends with a dot"); - final BigDecimal v = parseNumberGeneric(reader.prepareBuffer(start, end - start), end - start, reader, false); - if (v.scale() > 0) numberException(reader, start, end, "Expecting int but found decimal value", v); - return v.intValue(); - - } - value = (value << 3) + (value << 1) + ind; - if (value < 0) { - numberException(reader, start, end, "Integer overflow detected"); - } - } - return value; - } - - private static int parseNegativeInt(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { - int value = 0; - int i = start + 1; - if (i == end) numberException(reader, start, end, "Digit not found"); - for (; i < end; i++) { - final int ind = buf[i] - 48; - if (ind < 0 || ind > 9) { - if (i > start + 1 && reader.allWhitespace(i, end)) return value; - else if (i == end - 1 && buf[i] == '.') numberException(reader, start, end, "Number ends with a dot"); - final BigDecimal v = parseNumberGeneric(reader.prepareBuffer(start, end - start), end - start, reader, false); - if (v.scale() > 0) numberException(reader, start, end, "Expecting int but found decimal value", v); - return v.intValue(); - } - value = (value << 3) + (value << 1) - ind; - if (value > 0) { - numberException(reader, start, end, "Integer overflow detected"); - } - } - return value; - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeIntCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(INT_READER); - } - - public static int[] deserializeIntArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return INT_EMPTY_ARRAY; - } - int[] buffer = new int[4]; - buffer[0] = deserializeInt(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = deserializeInt(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - public static short[] deserializeShortArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return SHORT_EMPTY_ARRAY; - } - short[] buffer = new short[4]; - buffer[0] = (short)deserializeInt(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = (short)deserializeInt(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - public static long[] deserializeLongArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return LONG_EMPTY_ARRAY; - } - long[] buffer = new long[4]; - buffer[0] = deserializeLong(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = deserializeLong(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - public static float[] deserializeFloatArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return FLOAT_EMPTY_ARRAY; - } - float[] buffer = new float[4]; - buffer[0] = deserializeFloat(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = deserializeFloat(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - public static double[] deserializeDoubleArray(final JsonReader reader) throws IOException { - if (reader.last() == ']') { - return DOUBLE_EMPTY_ARRAY; - } - double[] buffer = new double[4]; - buffer[0] = deserializeDouble(reader); - int i = 1; - while (reader.getNextToken() == ',') { - reader.getNextToken(); - if (i == buffer.length) { - buffer = Arrays.copyOf(buffer, buffer.length << 1); - } - buffer[i++] = deserializeDouble(reader); - } - reader.checkArrayEnd(); - return Arrays.copyOf(buffer, i); - } - - public static void deserializeShortCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(SHORT_READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeShortNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(SHORT_READER); - } - - public static void deserializeShortNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(SHORT_READER, res); - } - - public static void deserializeIntCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(INT_READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeIntNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(INT_READER); - } - - public static void deserializeIntNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(INT_READER, res); - } - - public static void serializeNullable(@Nullable final Long value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - private static int writeFirstBuf(final byte[] buf, final int v, int pos) { - final int start = v >> 24; - if (start == 0) { - buf[pos++] = (byte) (v >> 16); - buf[pos++] = (byte) (v >> 8); - } else if (start == 1) { - buf[pos++] = (byte) (v >> 8); - } - buf[pos] = (byte) v; - return 3 - start; - } - - private static void writeBuf(final byte[] buf, final int v, int pos) { - buf[pos] = (byte) (v >> 16); - buf[pos + 1] = (byte) (v >> 8); - buf[pos + 2] = (byte) v; - } - - private static final byte[] MIN_LONG = "-9223372036854775808".getBytes(); - - public static void serialize(final long value, final JsonWriter sw) { - final byte[] buf = sw.ensureCapacity(21); - final int position = sw.size(); - int current = serialize(buf, position, value); - sw.advance(current - position); - } - - private static int serialize(final byte[] buf, int pos, final long value) { - long i; - if (value < 0) { - if (value == Long.MIN_VALUE) { - for (int x = 0; x < MIN_LONG.length; x++) { - buf[pos + x] = MIN_LONG[x]; - } - return pos + MIN_LONG.length; - } - i = -value; - buf[pos++] = MINUS; - } else { - i = value; - } - final long q1 = i / 1000; - if (q1 == 0) { - pos += writeFirstBuf(buf, DIGITS[(int) i], pos); - return pos; - } - final int r1 = (int) (i - q1 * 1000); - final long q2 = q1 / 1000; - if (q2 == 0) { - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[(int) q1]; - int off = writeFirstBuf(buf, v2, pos); - writeBuf(buf, v1, pos + off); - return pos + 3 + off; - } - final int r2 = (int) (q1 - q2 * 1000); - final long q3 = q2 / 1000; - if (q3 == 0) { - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[r2]; - final int v3 = DIGITS[(int) q2]; - pos += writeFirstBuf(buf, v3, pos); - writeBuf(buf, v2, pos); - writeBuf(buf, v1, pos + 3); - return pos + 6; - } - final int r3 = (int) (q2 - q3 * 1000); - final int q4 = (int) (q3 / 1000); - if (q4 == 0) { - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[r2]; - final int v3 = DIGITS[r3]; - final int v4 = DIGITS[(int) q3]; - pos += writeFirstBuf(buf, v4, pos); - writeBuf(buf, v3, pos); - writeBuf(buf, v2, pos + 3); - writeBuf(buf, v1, pos + 6); - return pos + 9; - } - final int r4 = (int) (q3 - q4 * 1000); - final int q5 = q4 / 1000; - if (q5 == 0) { - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[r2]; - final int v3 = DIGITS[r3]; - final int v4 = DIGITS[r4]; - final int v5 = DIGITS[q4]; - pos += writeFirstBuf(buf, v5, pos); - writeBuf(buf, v4, pos); - writeBuf(buf, v3, pos + 3); - writeBuf(buf, v2, pos + 6); - writeBuf(buf, v1, pos + 9); - return pos + 12; - } - final int r5 = q4 - q5 * 1000; - final int q6 = q5 / 1000; - final int v1 = DIGITS[r1]; - final int v2 = DIGITS[r2]; - final int v3 = DIGITS[r3]; - final int v4 = DIGITS[r4]; - final int v5 = DIGITS[r5]; - if (q6 == 0) { - pos += writeFirstBuf(buf, DIGITS[q5], pos); - } else { - final int r6 = q5 - q6 * 1000; - buf[pos++] = (byte) (q6 + '0'); - writeBuf(buf, DIGITS[r6], pos); - pos += 3; - } - writeBuf(buf, v5, pos); - writeBuf(buf, v4, pos + 3); - writeBuf(buf, v3, pos + 6); - writeBuf(buf, v2, pos + 9); - writeBuf(buf, v1, pos + 12); - return pos + 15; - } - - public static void serialize(@Nullable final long[] values, final JsonWriter sw) { - if (values == null) { - sw.writeNull(); - } else if (values.length == 0) { - sw.writeAscii("[]"); - } else { - final byte[] buf = sw.ensureCapacity(values.length * 21 + 2); - int position = sw.size(); - buf[position++] = '['; - position = serialize(buf, position, values[0]); - for (int i = 1; i < values.length; i++) { - buf[position++] = ','; - position = serialize(buf, position, values[i]); - } - buf[position++] = ']'; - sw.advance(position - sw.size()); - } - } - - public static long deserializeLong(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int position = reader.getCurrentIndex(); - final char[] buf = reader.readSimpleQuote(); - try { - return parseNumberGeneric(buf, reader.getCurrentIndex() - position - 1, reader, true).longValueExact(); - } catch (ArithmeticException ignore) { - throw reader.newParseErrorAt("Long overflow detected", reader.getCurrentIndex() - position); - } - } - final int start = reader.scanNumber(); - final int end = reader.getCurrentIndex(); - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - int i = start; - long value = 0; - if (ch == '-') { - i = start + 1; - if (i == end) numberException(reader, start, end, "Digit not found"); - final boolean leadingZero = buf[i] == 48; - for (; i < end; i++) { - final int ind = buf[i] - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 2) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start + 1 && reader.allWhitespace(i, end)) return value; - return parseLongGeneric(reader, start, end); - } - value = (value << 3) + (value << 1) - ind; - if (value > 0) { - numberException(reader, start, end, "Long overflow detected"); - } - } - if (leadingZero && i > start + 2) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - return value; - } - if (i == end) numberException(reader, start, end, "Digit not found"); - final boolean leadingZero = buf[i] == 48; - for (; i < end; i++) { - final int ind = buf[i] - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (ch == '+' && i > start + 1 && reader.allWhitespace(i, end)) return value; - else if (ch != '+' && i > start && reader.allWhitespace(i, end)) return value; - return parseLongGeneric(reader, start, end); - } - value = (value << 3) + (value << 1) + ind; - if (value < 0) { - numberException(reader, start, end, "Long overflow detected"); - } - } - if (leadingZero && i > start + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - return value; - } - - private static long parseLongGeneric(final JsonReader reader, final int start, final int end) throws IOException { - final int len = end - start; - final char[] buf = reader.prepareBuffer(start, len); - if (len > 0 && buf[len - 1] == '.') numberException(reader, start, end, "Number ends with a dot"); - final BigDecimal v = parseNumberGeneric(buf, len, reader, false); - if (v.scale() > 0) numberException(reader, start, end, "Expecting long, but found decimal value ", v); - return v.longValue(); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeLongCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(LONG_READER); - } - - public static void deserializeLongCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(LONG_READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeLongNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(LONG_READER); - } - - public static void deserializeLongNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(LONG_READER, res); - } - - public static void serializeNullable(@Nullable final BigDecimal value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - sw.writeAscii(value.toString()); - } - } - - public static void serialize(final BigDecimal value, final JsonWriter sw) { - sw.writeAscii(value.toString()); - } - - public static BigDecimal deserializeDecimal(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - final int len = reader.parseString(); - return parseNumberGeneric(reader.chars, len, reader, true); - } - final int start = reader.scanNumber(); - int end = reader.getCurrentIndex(); - if (end == reader.length()) { - NumberInfo info = readLongNumber(reader, start); - return parseNumberGeneric(info.buffer, info.length, reader, false); - } - int len = end - start; - if (len > 18) { - return parseNumberGeneric(reader.prepareBuffer(start, len), len, reader, false); - } - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - if (ch == '-') { - return parseNegativeDecimal(buf, reader, start, end); - } - return parsePositiveDecimal(buf, reader, start, end); - } - - private static BigDecimal parsePositiveDecimal(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { - long value = 0; - byte ch = ' '; - int i = start; - final boolean leadingZero = buf[start] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start && reader.allWhitespace(i, end)) return BigDecimal.valueOf(value); - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == start) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + 1) numberException(reader, start, end, "Leading zero is not allowed"); - else if (i == end) return BigDecimal.valueOf(value); - else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - int dp = i; - for (; i < end; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == end) return BigDecimal.valueOf(value, end - dp); - else if (ch == 'e' || ch == 'E') { - final int ep = i; - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return BigDecimal.valueOf(value, ep - dp - exp); - } - return BigDecimal.valueOf(value, end - dp); - } else if (ch == 'e' || ch == 'E') { - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return BigDecimal.valueOf(value, -exp); - } - return BigDecimal.valueOf(value); - } - - private static BigDecimal parseNegativeDecimal(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { - long value = 0; - byte ch = ' '; - int i = start + 1; - final boolean leadingZero = buf[start + 1] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 2) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start + 1 && reader.allWhitespace(i, end)) return BigDecimal.valueOf(value); - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) - ind; - } - if (i == start + 1) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + 2) numberException(reader, start, end, "Leading zero is not allowed"); - else if (i == end) return BigDecimal.valueOf(value); - else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - int dp = i; - for (; i < end; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); - numberException(reader, start, end, "Unknown digit", (char)ch); - } - value = (value << 3) + (value << 1) - ind; - } - if (i == end) return BigDecimal.valueOf(value, end - dp); - else if (ch == 'e' || ch == 'E') { - final int ep = i; - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return BigDecimal.valueOf(value, ep - dp - exp); - } - return BigDecimal.valueOf(value, end - dp); - } else if (ch == 'e' || ch == 'E') { - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return BigDecimal.valueOf(value, -exp); - } - return BigDecimal.valueOf(value); - } - - private static final BigDecimal BD_MAX_LONG = BigDecimal.valueOf(Long.MAX_VALUE); - private static final BigDecimal BD_MIN_LONG = BigDecimal.valueOf(Long.MIN_VALUE); - - private static Number bigDecimalOrDouble(BigDecimal num, JsonReader.UnknownNumberParsing unknownNumbers) { - return unknownNumbers == JsonReader.UnknownNumberParsing.LONG_AND_BIGDECIMAL - ? num - : num.doubleValue(); - } - - private static Number tryLongFromBigDecimal(final char[] buf, final int len, JsonReader reader) throws IOException { - final BigDecimal num = parseNumberGeneric(buf, len, reader, false); - if (num.scale() == 0 && num.precision() <= 19) { - if (num.signum() == 1) { - if (num.compareTo(BD_MAX_LONG) <= 0) { - return num.longValue(); - } - } else if (num.compareTo(BD_MIN_LONG) >= 0) { - return num.longValue(); - } - } - return bigDecimalOrDouble(num, reader.unknownNumbers); - } - - public static Number deserializeNumber(final JsonReader reader) throws IOException { - if (reader.unknownNumbers == JsonReader.UnknownNumberParsing.BIGDECIMAL) return deserializeDecimal(reader); - else if (reader.unknownNumbers == JsonReader.UnknownNumberParsing.DOUBLE) return deserializeDouble(reader); - final int start = reader.scanNumber(); - int end = reader.getCurrentIndex(); - if (end == reader.length()) { - NumberInfo info = readLongNumber(reader, start); - return tryLongFromBigDecimal(info.buffer, info.length, reader); - } - int len = end - start; - if (len > 18) { - return tryLongFromBigDecimal(reader.prepareBuffer(start, len), len, reader); - } - final byte[] buf = reader.buffer; - final byte ch = buf[start]; - if (ch == '-') { - return parseNegativeNumber(buf, reader, start, end); - } - return parsePositiveNumber(buf, reader, start, end); - } - - private static Number parsePositiveNumber(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { - long value = 0; - byte ch = ' '; - int i = start; - final boolean leadingZero = buf[start] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 1) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start && reader.allWhitespace(i, end)) return value; - return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == start) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + 1) numberException(reader, start, end, "Leading zero is not allowed"); - else if (i == end) return value; - else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - int dp = i; - for (; i < end; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); - return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); - } - value = (value << 3) + (value << 1) + ind; - } - if (i == end) return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); - else if (ch == 'e' || ch == 'E') { - final int ep = i; - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value, ep - dp - exp), reader.unknownNumbers); - } - return BigDecimal.valueOf(value, end - dp); - } else if (ch == 'e' || ch == 'E') { - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value, -exp), reader.unknownNumbers); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value), reader.unknownNumbers); - } - - private static Number parseNegativeNumber(final byte[] buf, final JsonReader reader, final int start, final int end) throws IOException { - long value = 0; - byte ch = ' '; - int i = start + 1; - final boolean leadingZero = buf[start + 1] == 48; - for (; i < end; i++) { - ch = buf[i]; - if (ch == '.' || ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (leadingZero && i > start + 2) { - numberException(reader, start, end, "Leading zero is not allowed"); - } - if (i > start + 1 && reader.allWhitespace(i, end)) return value; - return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); - } - value = (value << 3) + (value << 1) - ind; - } - if (i == start + 1) numberException(reader, start, end, "Digit not found"); - else if (leadingZero && ch != '.' && i > start + 2) numberException(reader, start, end, "Leading zero is not allowed"); - else if (i == end) return value; - else if (ch == '.') { - i++; - if (i == end) numberException(reader, start, end, "Number ends with a dot"); - int dp = i; - for (; i < end; i++) { - ch = buf[i]; - if (ch == 'e' || ch == 'E') break; - final int ind = ch - 48; - if (ind < 0 || ind > 9) { - if (reader.allWhitespace(i, end)) return BigDecimal.valueOf(value, i - dp); - return tryLongFromBigDecimal(reader.prepareBuffer(start, end - start), end - start, reader); - } - value = (value << 3) + (value << 1) - ind; - } - if (i == end) return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); - else if (ch == 'e' || ch == 'E') { - final int ep = i; - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value, ep - dp - exp), reader.unknownNumbers); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value, end - dp), reader.unknownNumbers); - } else if (ch == 'e' || ch == 'E') { - i++; - ch = buf[i]; - final int exp; - if (ch == '-') { - exp = parseNegativeInt(buf, reader, i, end); - } else if (ch == '+') { - exp = parsePositiveInt(buf, reader, i, end, 1); - } else { - exp = parsePositiveInt(buf, reader, i, end, 0); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value, -exp), reader.unknownNumbers); - } - return bigDecimalOrDouble(BigDecimal.valueOf(value), reader.unknownNumbers); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeDecimalCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(DecimalReader); - } - - public static void deserializeDecimalCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(DecimalReader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeDecimalNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(DecimalReader); - } - - public static void deserializeDecimalNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(DecimalReader, res); - } -} \ No newline at end of file diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java deleted file mode 100644 index 2d9ff534a8..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ObjectConverter.java +++ /dev/null @@ -1,135 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.*; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class ObjectConverter { - - private static final JsonReader.ReadObject> TypedMapReader = new JsonReader.ReadObject>() { - @Nullable - @Override - public Map read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeMap(reader); - } - }; - @SuppressWarnings("rawtypes") - static final JsonReader.ReadObject MapReader = new JsonReader.ReadObject() { - @Nullable - @Override - public LinkedHashMap read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserializeMap(reader); - } - }; - - public static void serializeNullableMap(@Nullable final Map value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serializeMap(value, sw); - } - } - - public static void serializeMap(final Map value, final JsonWriter sw) { - sw.writeByte(JsonWriter.OBJECT_START); - final int size = value.size(); - if (size > 0) { - final Iterator> iterator = value.entrySet().iterator(); - Map.Entry kv = iterator.next(); - sw.writeString(kv.getKey()); - sw.writeByte(JsonWriter.SEMI); - sw.serializeObject(kv.getValue()); - for (int i = 1; i < size; i++) { - sw.writeByte(JsonWriter.COMMA); - kv = iterator.next(); - sw.writeString(kv.getKey()); - sw.writeByte(JsonWriter.SEMI); - sw.serializeObject(kv.getValue()); - } - } - sw.writeByte(JsonWriter.OBJECT_END); - } - - public static void serializeObject(@Nullable final Object value, final JsonWriter sw) throws IOException { - sw.serializeObject(value); - } - - @Nullable - public static Object deserializeObject(final JsonReader reader) throws IOException { - switch (reader.last()) { - case 'n': - if (!reader.wasNull()) { - throw reader.newParseErrorAt("Expecting 'null' for null constant", 0); - } - return null; - case 't': - if (!reader.wasTrue()) { - throw reader.newParseErrorAt("Expecting 'true' for true constant", 0); - } - return true; - case 'f': - if (!reader.wasFalse()) { - throw reader.newParseErrorAt("Expecting 'false' for false constant", 0); - } - return false; - case '"': - return reader.readString(); - case '{': - return deserializeMap(reader); - case '[': - return deserializeList(reader); - default: - return NumberConverter.deserializeNumber(reader); - } - } - - public static ArrayList deserializeList(final JsonReader reader) throws IOException { - if (reader.last() != '[') throw reader.newParseError("Expecting '[' for list start"); - byte nextToken = reader.getNextToken(); - if (nextToken == ']') return new ArrayList(0); - final ArrayList res = new ArrayList(4); - res.add(deserializeObject(reader)); - while ((nextToken = reader.getNextToken()) == ',') { - reader.getNextToken(); - res.add(deserializeObject(reader)); - } - if (nextToken != ']') throw reader.newParseError("Expecting ']' for list end"); - return res; - } - - public static LinkedHashMap deserializeMap(final JsonReader reader) throws IOException { - if (reader.last() != '{') throw reader.newParseError("Expecting '{' for map start"); - byte nextToken = reader.getNextToken(); - if (nextToken == '}') return new LinkedHashMap(0); - final LinkedHashMap res = new LinkedHashMap(); - String key = reader.readKey(); - res.put(key, deserializeObject(reader)); - while ((nextToken = reader.getNextToken()) == ',') { - reader.getNextToken(); - key = reader.readKey(); - res.put(key, deserializeObject(reader)); - } - if (nextToken != '}') throw reader.newParseError("Expecting '}' for map end"); - return res; - } - - @SuppressWarnings("unchecked") - public static ArrayList> deserializeMapCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(TypedMapReader); - } - - public static void deserializeMapCollection(final JsonReader reader, final Collection> res) throws IOException { - reader.deserializeCollection(TypedMapReader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList> deserializeNullableMapCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(TypedMapReader); - } - - public static void deserializeNullableMapCollection(final JsonReader reader, final Collection> res) throws IOException { - reader.deserializeNullableCollection(TypedMapReader, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java deleted file mode 100644 index 21ee52de59..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/ParsingException.java +++ /dev/null @@ -1,44 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import java.io.IOException; - -@SuppressWarnings("serial") // suppress pre-existing warnings -public class ParsingException extends IOException { - - private ParsingException(String reason) { - super(reason); - } - - private ParsingException(String reason, Throwable cause) { - super(reason, cause); - } - - public static ParsingException create(String reason, boolean withStackTrace) { - return withStackTrace - ? new ParsingException(reason) - : new ParsingStacklessException(reason); - } - - - public static ParsingException create(String reason, Throwable cause, boolean withStackTrace) { - return withStackTrace - ? new ParsingException(reason, cause) - : new ParsingStacklessException(reason, cause); - } - - private static class ParsingStacklessException extends ParsingException { - - private ParsingStacklessException(String reason) { - super(reason); - } - - private ParsingStacklessException(String reason, Throwable cause) { - super(reason, cause); - } - - @Override - public synchronized Throwable fillInStackTrace() { - return this; - } - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java deleted file mode 100644 index 1c9a2f2e5b..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/SerializationException.java +++ /dev/null @@ -1,18 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -@SuppressWarnings("serial") // suppress pre-existing warnings -public class SerializationException extends RuntimeException { - public SerializationException(@Nullable String reason) { - super(reason); - } - - public SerializationException(@Nullable Throwable cause) { - super(cause); - } - - public SerializationException(@Nullable String reason, @Nullable Throwable cause) { - super(reason, cause); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java deleted file mode 100644 index 390ac190ba..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringCache.java +++ /dev/null @@ -1,5 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -public interface StringCache { - String get(char[] chars, int len); -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java deleted file mode 100644 index f45af88556..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/StringConverter.java +++ /dev/null @@ -1,119 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class StringConverter { - - public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { - @Nullable - @Override - public String read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - return reader.readString(); - } - }; - public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable String value) { - serializeNullable(value, writer); - } - }; - public static final JsonWriter.WriteObject WRITER_CHARS = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable CharSequence value) { - if (value == null) writer.writeNull(); - else writer.writeString(value); - } - }; - public static final JsonReader.ReadObject READER_BUILDER = new JsonReader.ReadObject() { - @Nullable - @Override - public StringBuilder read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - StringBuilder builder = new StringBuilder(); - return reader.appendString(builder); - } - }; - public static final JsonReader.ReadObject READER_BUFFER = new JsonReader.ReadObject() { - @Nullable - @Override - public StringBuffer read(JsonReader reader) throws IOException { - if (reader.wasNull()) return null; - StringBuffer builder = new StringBuffer(); - return reader.appendString(builder); - } - }; - - public static void serializeShortNullable(@Nullable final String value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - sw.writeString(value); - } - } - - public static void serializeShort(final String value, final JsonWriter sw) { - sw.writeString(value); - } - - public static void serializeNullable(@Nullable final String value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - sw.writeString(value); - } - } - - public static void serialize(final String value, final JsonWriter sw) { - sw.writeString(value); - } - - public static String deserialize(final JsonReader reader) throws IOException { - return reader.readString(); - } - - @Nullable - public static String deserializeNullable(final JsonReader reader) throws IOException { - if (reader.last() == 'n') { - if (!reader.wasNull()) throw reader.newParseErrorAt("Expecting 'null' for null constant", 0); - return null; - } - return reader.readString(); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(READER); - } - - public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(READER); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(READER, res); - } - - public static void serialize(final List list, final JsonWriter writer) { - writer.writeByte(JsonWriter.ARRAY_START); - if (list.size() != 0) { - writer.writeString(list.get(0)); - for (int i = 1; i < list.size(); i++) { - writer.writeByte(JsonWriter.COMMA); - writer.writeString(list.get(i)); - } - } - writer.writeByte(JsonWriter.ARRAY_END); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java deleted file mode 100644 index f8d9f4b7fd..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/TypeLookup.java +++ /dev/null @@ -1,10 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -interface TypeLookup { - @Nullable - JsonReader.ReadObject tryFindReader(Class manifest); - @Nullable - JsonReader.BindObject tryFindBinder(Class manifest); -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java deleted file mode 100644 index d5f80b08f6..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UUIDConverter.java +++ /dev/null @@ -1,198 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; -import java.util.ArrayList; -import java.util.Collection; -import java.util.UUID; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class UUIDConverter { - - public static final UUID MIN_UUID = new java.util.UUID(0L, 0L); - public static final JsonReader.ReadObject READER = new JsonReader.ReadObject() { - @Nullable - @Override - public UUID read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserialize(reader); - } - }; - public static final JsonWriter.WriteObject WRITER = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable UUID value) { - serializeNullable(value, writer); - } - }; - - private static final char[] Lookup; - private static final byte[] Values; - - static { - Lookup = new char[256]; - Values = new byte['f' + 1 - '0']; - for (int i = 0; i < 256; i++) { - int hi = (i >> 4) & 15; - int lo = i & 15; - Lookup[i] = (char) (((hi < 10 ? '0' + hi : 'a' + hi - 10) << 8) + (lo < 10 ? '0' + lo : 'a' + lo - 10)); - } - for (char c = '0'; c <= '9'; c++) { - Values[c - '0'] = (byte) (c - '0'); - } - for (char c = 'a'; c <= 'f'; c++) { - Values[c - '0'] = (byte) (c - 'a' + 10); - } - for (char c = 'A'; c <= 'F'; c++) { - Values[c - '0'] = (byte) (c - 'A' + 10); - } - } - - - public static void serializeNullable(@Nullable final UUID value, final JsonWriter sw) { - if (value == null) { - sw.writeNull(); - } else { - serialize(value, sw); - } - } - - public static void serialize(final UUID value, final JsonWriter sw) { - serialize(value.getMostSignificantBits(), value.getLeastSignificantBits(), sw); - } - - public static void serialize(final long hi, final long lo, final JsonWriter sw) { - final int hi1 = (int) (hi >> 32); - final int hi2 = (int) hi; - final int lo1 = (int) (lo >> 32); - final int lo2 = (int) lo; - final byte[] buf = sw.ensureCapacity(38); - final int pos = sw.size(); - buf[pos] = '"'; - int v = (hi1 >> 24) & 255; - int l = Lookup[v]; - buf[pos + 1] = (byte) (l >> 8); - buf[pos + 2] = (byte) l; - v = (hi1 >> 16) & 255; - l = Lookup[v]; - buf[pos + 3] = (byte) (l >> 8); - buf[pos + 4] = (byte) l; - v = (hi1 >> 8) & 255; - l = Lookup[v]; - buf[pos + 5] = (byte) (l >> 8); - buf[pos + 6] = (byte) l; - v = hi1 & 255; - l = Lookup[v]; - buf[pos + 7] = (byte) (l >> 8); - buf[pos + 8] = (byte) l; - buf[pos + 9] = '-'; - v = (hi2 >> 24) & 255; - l = Lookup[v]; - buf[pos + 10] = (byte) (l >> 8); - buf[pos + 11] = (byte) l; - v = (hi2 >> 16) & 255; - l = Lookup[v]; - buf[pos + 12] = (byte) (l >> 8); - buf[pos + 13] = (byte) l; - buf[pos + 14] = '-'; - v = (hi2 >> 8) & 255; - l = Lookup[v]; - buf[pos + 15] = (byte) (l >> 8); - buf[pos + 16] = (byte) l; - v = hi2 & 255; - l = Lookup[v]; - buf[pos + 17] = (byte) (l >> 8); - buf[pos + 18] = (byte) l; - buf[pos + 19] = '-'; - v = (lo1 >> 24) & 255; - l = Lookup[v]; - buf[pos + 20] = (byte) (l >> 8); - buf[pos + 21] = (byte) l; - v = (lo1 >> 16) & 255; - l = Lookup[v]; - buf[pos + 22] = (byte) (l >> 8); - buf[pos + 23] = (byte) l; - buf[pos + 24] = '-'; - v = (lo1 >> 8) & 255; - l = Lookup[v]; - buf[pos + 25] = (byte) (l >> 8); - buf[pos + 26] = (byte) l; - v = lo1 & 255; - l = Lookup[v]; - buf[pos + 27] = (byte) (l >> 8); - buf[pos + 28] = (byte) l; - v = (lo2 >> 24) & 255; - l = Lookup[v]; - buf[pos + 29] = (byte) (l >> 8); - buf[pos + 30] = (byte) l; - v = (lo2 >> 16) & 255; - l = Lookup[v]; - buf[pos + 31] = (byte) (l >> 8); - buf[pos + 32] = (byte) l; - v = (lo2 >> 8) & 255; - l = Lookup[v]; - buf[pos + 33] = (byte) (l >> 8); - buf[pos + 34] = (byte) l; - v = lo2 & 255; - l = Lookup[v]; - buf[pos + 35] = (byte) (l >> 8); - buf[pos + 36] = (byte) l; - buf[pos + 37] = '"'; - sw.advance(38); - } - - public static UUID deserialize(final JsonReader reader) throws IOException { - final char[] buf = reader.readSimpleQuote(); - final int len = reader.getCurrentIndex() - reader.getTokenStart(); - if (len == 37 && buf[8] == '-' && buf[13] == '-' && buf[18] == '-' && buf[23] == '-') { - try { - long hi = 0; - for (int i = 0; i < 8; i++) - hi = (hi << 4) + Values[buf[i] - '0']; - for (int i = 9; i < 13; i++) - hi = (hi << 4) + Values[buf[i] - '0']; - for (int i = 14; i < 18; i++) - hi = (hi << 4) + Values[buf[i] - '0']; - long lo = 0; - for (int i = 19; i < 23; i++) - lo = (lo << 4) + Values[buf[i] - '0']; - for (int i = 24; i < 36; i++) - lo = (lo << 4) + Values[buf[i] - '0']; - return new UUID(hi, lo); - } catch (ArrayIndexOutOfBoundsException ex) { - return UUID.fromString(new String(buf, 0, 36)); - } - } else if (len == 33) { - try { - long hi = 0; - for (int i = 0; i < 16; i++) - hi = (hi << 4) + Values[buf[i] - '0']; - long lo = 0; - for (int i = 16; i < 32; i++) - lo = (lo << 4) + Values[buf[i] - '0']; - return new UUID(hi, lo); - } catch (ArrayIndexOutOfBoundsException ex) { - return UUID.fromString(new String(buf, 0, 32)); - } - } else { - return UUID.fromString(new String(buf, 0, len - 1)); - } - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(READER); - } - - public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(READER, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(READER); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(READER, res); - } -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java deleted file mode 100644 index 405d9f8eb9..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/UnknownSerializer.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import java.io.IOException; - -interface UnknownSerializer { - void serialize(JsonWriter writer, @Nullable Object unknown) throws IOException; -} diff --git a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java b/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java deleted file mode 100644 index 6c06ee792a..0000000000 --- a/app/src/main/java/com/bugsnag/android/repackaged/dslplatform/json/XmlConverter.java +++ /dev/null @@ -1,217 +0,0 @@ -package com.bugsnag.android.repackaged.dslplatform.json; - -import androidx.annotation.Nullable; - -import org.w3c.dom.*; -import org.w3c.dom.ls.DOMImplementationLS; -import org.w3c.dom.ls.LSOutput; -import org.w3c.dom.ls.LSSerializer; -import org.xml.sax.InputSource; -import org.xml.sax.SAXException; - -import javax.xml.parsers.DocumentBuilder; -import javax.xml.parsers.DocumentBuilderFactory; -import javax.xml.parsers.ParserConfigurationException; -import java.io.IOException; -import java.io.StringReader; -import java.io.StringWriter; -import java.util.*; - -@SuppressWarnings({"rawtypes", "unchecked"}) // suppress pre-existing warnings -public abstract class XmlConverter { - - static final JsonReader.ReadObject Reader = new JsonReader.ReadObject() { - @Nullable - @Override - public Element read(JsonReader reader) throws IOException { - return reader.wasNull() ? null : deserialize(reader); - } - }; - static final JsonWriter.WriteObject Writer = new JsonWriter.WriteObject() { - @Override - public void write(JsonWriter writer, @Nullable Element value) { - serializeNullable(value, writer); - } - }; - - private static final DocumentBuilder documentBuilder; - - static { - DocumentBuilderFactory dbFactory = DocumentBuilderFactory.newInstance(); - try { - documentBuilder = dbFactory.newDocumentBuilder(); - } catch (ParserConfigurationException e) { - throw new RuntimeException(e); - } - } - - public static void serializeNullable(@Nullable final Element value, final JsonWriter sw) { - if (value == null) - sw.writeNull(); - else - serialize(value, sw); - } - - public static void serialize(final Element value, final JsonWriter sw) { - Document document = value.getOwnerDocument(); - DOMImplementationLS domImplLS = (DOMImplementationLS) document.getImplementation(); - LSSerializer serializer = domImplLS.createLSSerializer(); - LSOutput lsOutput = domImplLS.createLSOutput(); - lsOutput.setEncoding("UTF-8"); - StringWriter writer = new StringWriter(); - lsOutput.setCharacterStream(writer); - serializer.write(document, lsOutput); - StringConverter.serialize(writer.toString(), sw); - } - - public static Element deserialize(final JsonReader reader) throws IOException { - if (reader.last() == '"') { - try { - InputSource source = new InputSource(new StringReader(reader.readString())); - return documentBuilder.parse(source).getDocumentElement(); - } catch (SAXException ex) { - throw reader.newParseErrorAt("Invalid XML value", 0, ex); - } - } else { - final Map map = ObjectConverter.deserializeMap(reader); - return mapToXml(map); - } - } - - public static Element mapToXml(final Map map) throws IOException { - final Set xmlRootElementNames = map.keySet(); - if (xmlRootElementNames.size() > 1) { - throw ParsingException.create("Invalid XML. Expecting root element", true); - } - final String rootName = xmlRootElementNames.iterator().next(); - final Document document = createDocument(); - final Element rootElement = document.createElement(rootName); - document.appendChild(rootElement); - buildXmlFromHashMap(document, rootElement, map.get(rootName)); - return rootElement; - } - - private static synchronized Document createDocument() { - try { - final DocumentBuilderFactory factory = DocumentBuilderFactory.newInstance(); - final DocumentBuilder builder = factory.newDocumentBuilder(); - return builder.newDocument(); - } catch (ParserConfigurationException e) { - throw new ConfigurationException(e); - } - } - - private static final String TEXT_NODE_TAG = "#text"; - private static final String COMMENT_NODE_TAG = "#comment"; - private static final String CDATA_NODE_TAG = "#cdata-section"; - - @SuppressWarnings("unchecked") - private static void buildXmlFromHashMap( - final Document doc, - final Element subtreeRootElement, - @Nullable final Object elementContent) { - if (elementContent instanceof HashMap) { - final HashMap elementContentMap = (HashMap) elementContent; - for (final Map.Entry childEntry : elementContentMap.entrySet()) { - final String key = childEntry.getKey(); - if (key.startsWith("@")) { - subtreeRootElement.setAttribute(key.substring(1), childEntry.getValue().toString()); - } else if (key.startsWith("#")) { - if (key.equals(TEXT_NODE_TAG)) { - if (childEntry.getValue() instanceof List) { - buildTextNodeList(doc, subtreeRootElement, (List) childEntry.getValue()); - } else { - final Node textNode = doc.createTextNode(childEntry.getValue().toString()); - subtreeRootElement.appendChild(textNode); - } - } else if (key.equals(CDATA_NODE_TAG)) { - if (childEntry.getValue() instanceof List) { - buildCDataList(doc, subtreeRootElement, (List) childEntry.getValue()); - } else { - final Node cDataNode = doc.createCDATASection(childEntry.getValue().toString()); - subtreeRootElement.appendChild(cDataNode); - } - } else if (key.equals(COMMENT_NODE_TAG)) { - if (childEntry.getValue() instanceof List) { - buildCommentList(doc, subtreeRootElement, (List) childEntry.getValue()); - } else { - final Node commentNode = doc.createComment(childEntry.getValue().toString()); - subtreeRootElement.appendChild(commentNode); - } - } //else if (key.equals(WHITESPACE_NODE_TAG) - // || key.equals(SIGNIFICANT_WHITESPACE_NODE_TAG)) { - // Ignore - //} else { - /* - * All other nodes whose name starts with a '#' are invalid XML - * nodes, and thus ignored: - */ - //} - } else { - final Element newElement = doc.createElement(key); - subtreeRootElement.appendChild(newElement); - buildXmlFromHashMap(doc, newElement, childEntry.getValue()); - } - } - } else if (elementContent instanceof List) { - buildXmlFromJsonArray(doc, subtreeRootElement, (List) elementContent); - } else { - if (elementContent != null) { - subtreeRootElement.setTextContent(elementContent.toString()); - } - } - } - - private static void buildTextNodeList(final Document doc, final Node subtreeRoot, final List nodeValues) { - final StringBuilder sb = new StringBuilder(); - for (final String nodeValue : nodeValues) { - sb.append(nodeValue); - } - subtreeRoot.appendChild(doc.createTextNode(sb.toString())); - } - - private static void buildCDataList(final Document doc, final Node subtreeRoot, final List nodeValues) { - for (final String nodeValue : nodeValues) { - subtreeRoot.appendChild(doc.createCDATASection(nodeValue)); - } - } - - private static void buildCommentList(final Document doc, final Node subtreeRoot, final List nodeValues) { - for (final String nodeValue : nodeValues) { - subtreeRoot.appendChild(doc.createComment(nodeValue)); - } - } - - private static void buildXmlFromJsonArray( - final Document doc, - final Node listHeadNode, - final List elementContentList) { - final Node subtreeRootNode = listHeadNode.getParentNode(); - /* The head node (already exists) */ - buildXmlFromHashMap(doc, (Element) listHeadNode, elementContentList.get(0)); - /* The rest of the list */ - for (final Object elementContent : elementContentList.subList(1, elementContentList.size())) { - final Element newElement = doc.createElement(listHeadNode.getNodeName()); - subtreeRootNode.appendChild(newElement); - buildXmlFromHashMap(doc, newElement, elementContent); - } - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeCollection(final JsonReader reader) throws IOException { - return reader.deserializeCollectionCustom(Reader); - } - - public static void deserializeCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeCollection(Reader, res); - } - - @SuppressWarnings("unchecked") - public static ArrayList deserializeNullableCollection(final JsonReader reader) throws IOException { - return reader.deserializeNullableCollectionCustom(Reader); - } - - public static void deserializeNullableCollection(final JsonReader reader, final Collection res) throws IOException { - reader.deserializeNullableCollection(Reader, res); - } -} diff --git a/app/src/main/java/eu/faircode/email/FragmentMessages.java b/app/src/main/java/eu/faircode/email/FragmentMessages.java index ad6fb076d5..e85a146132 100644 --- a/app/src/main/java/eu/faircode/email/FragmentMessages.java +++ b/app/src/main/java/eu/faircode/email/FragmentMessages.java @@ -6061,7 +6061,7 @@ public class FragmentMessages extends FragmentBase } private boolean checkReporting() { - if (viewType != AdapterMessage.ViewType.UNIFIED) + if (viewType != AdapterMessage.ViewType.UNIFIED || true) return false; SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(getContext()); diff --git a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java index d53b3656e0..9fea8cacf1 100644 --- a/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java +++ b/app/src/main/java/eu/faircode/email/FragmentOptionsMisc.java @@ -870,6 +870,7 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc } }); + swCrashReports.setVisibility(View.GONE); swCrashReports.setOnCheckedChangeListener(new CompoundButton.OnCheckedChangeListener() { @Override public void onCheckedChanged(CompoundButton compoundButton, boolean checked) { @@ -881,6 +882,9 @@ public class FragmentOptionsMisc extends FragmentBase implements SharedPreferenc } }); + tvUuid.setVisibility(View.GONE); + + ibCrashReports.setVisibility(View.GONE); ibCrashReports.setOnClickListener(new View.OnClickListener() { @Override public void onClick(View v) { diff --git a/app/src/main/java/eu/faircode/email/HtmlHelper.java b/app/src/main/java/eu/faircode/email/HtmlHelper.java index d00226ff72..0a14032995 100644 --- a/app/src/main/java/eu/faircode/email/HtmlHelper.java +++ b/app/src/main/java/eu/faircode/email/HtmlHelper.java @@ -585,16 +585,6 @@ public class HtmlHelper { final Document document = new Cleaner(safelist).clean(parsed); - if (BuildConfig.DEBUG) - for (Element e : document.select("span:matchesOwn(^UUID: " + Helper.REGEX_UUID + ")")) { - String t = e.text(); - int sp = t.indexOf(' '); - if (sp < 0) - continue; - String uuid = t.substring(sp + 1); - e.html("UUID: " + uuid + ""); - } - // Remove tracking pixels if (disable_tracking) removeTrackingPixels(context, document); diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java index 45bf5687ba..c449accac7 100644 --- a/app/src/main/java/eu/faircode/email/Log.java +++ b/app/src/main/java/eu/faircode/email/Log.java @@ -32,7 +32,6 @@ import android.os.Build; import android.os.Bundle; import android.os.DeadObjectException; import android.os.DeadSystemException; -import android.os.Debug; import android.os.OperationCanceledException; import android.os.RemoteException; import android.os.TransactionTooLargeException; @@ -56,18 +55,6 @@ import androidx.fragment.app.Fragment; import androidx.fragment.app.FragmentManager; import androidx.preference.PreferenceManager; -import com.bugsnag.android.Breadcrumb; -import com.bugsnag.android.BreadcrumbType; -import com.bugsnag.android.Bugsnag; -import com.bugsnag.android.Client; -import com.bugsnag.android.ErrorTypes; -import com.bugsnag.android.Event; -import com.bugsnag.android.OnBreadcrumbCallback; -import com.bugsnag.android.OnErrorCallback; -import com.bugsnag.android.OnSendCallback; -import com.bugsnag.android.OnSessionCallback; -import com.bugsnag.android.Session; -import com.bugsnag.android.Severity; import com.sun.mail.iap.BadCommandException; import com.sun.mail.iap.ConnectionException; import com.sun.mail.iap.ProtocolException; @@ -76,7 +63,6 @@ import com.sun.mail.util.MailConnectException; import org.bouncycastle.jsse.provider.BouncyCastleJsseProvider; -import java.io.FileNotFoundException; import java.io.IOException; import java.io.UnsupportedEncodingException; import java.lang.reflect.Array; @@ -86,20 +72,14 @@ import java.net.UnknownHostException; import java.nio.charset.StandardCharsets; import java.security.KeyStore; import java.security.Provider; -import java.security.cert.CertPathValidatorException; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; -import java.util.Date; import java.util.HashMap; -import java.util.HashSet; import java.util.List; -import java.util.Locale; import java.util.Map; import java.util.Set; -import java.util.UUID; import java.util.concurrent.TimeoutException; -import java.util.regex.Pattern; import javax.mail.AuthenticationFailedException; import javax.mail.FolderClosedException; @@ -108,8 +88,6 @@ import javax.mail.MessagingException; import javax.mail.StoreClosedException; import javax.mail.internet.InternetAddress; import javax.net.ssl.SSLContext; -import javax.net.ssl.SSLHandshakeException; -import javax.net.ssl.SSLPeerUnverifiedException; import javax.net.ssl.SSLSocket; import javax.net.ssl.TrustManager; import javax.net.ssl.TrustManagerFactory; @@ -186,21 +164,6 @@ public class Log { } public static int e(String msg) { - if (BuildConfig.BETA_RELEASE) - try { - ThrowableWrapper ex = new ThrowableWrapper(); - ex.setMessage(msg); - Bugsnag.notify(ex, new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - event.setSeverity(Severity.ERROR); - return true; - } - }); - } catch (Throwable ex) { - Log.i(ex); - } - org.tinylog.Logger.tag(TAG).error(msg); return 0; } @@ -216,43 +179,11 @@ public class Log { } public static int w(Throwable ex) { - if (BuildConfig.BETA_RELEASE) - try { - final StackTraceElement[] ste = new Throwable().getStackTrace(); - Bugsnag.notify(ex, new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - event.setSeverity(Severity.INFO); - if (ste.length > 1) - event.addMetadata("extra", "caller", ste[1].toString()); - return true; - } - }); - } catch (Throwable ex1) { - Log.i(ex1); - } - org.tinylog.Logger.tag(TAG).warn(ex); return 0; } public static int e(Throwable ex) { - if (BuildConfig.BETA_RELEASE) - try { - final StackTraceElement[] ste = new Throwable().getStackTrace(); - Bugsnag.notify(ex, new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - event.setSeverity(Severity.WARNING); - if (ste.length > 1) - event.addMetadata("extra", "caller", ste[1].toString()); - return true; - } - }); - } catch (Throwable ex1) { - Log.i(ex1); - } - org.tinylog.Logger.tag(TAG).error(ex); return 0; } @@ -263,37 +194,11 @@ public class Log { } public static int w(String prefix, Throwable ex) { - if (BuildConfig.BETA_RELEASE) - try { - Bugsnag.notify(ex, new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - event.setSeverity(Severity.INFO); - return true; - } - }); - } catch (Throwable ex1) { - Log.i(ex1); - } - org.tinylog.Logger.tag(TAG).warn(ex, prefix); return 0; } public static int e(String prefix, Throwable ex) { - if (BuildConfig.BETA_RELEASE) - try { - Bugsnag.notify(ex, new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - event.setSeverity(Severity.WARNING); - return true; - } - }); - } catch (Throwable ex1) { - Log.i(ex1); - } - org.tinylog.Logger.tag(TAG).error(ex, prefix); return 0; } @@ -313,14 +218,6 @@ public class Log { } static void setCrashReporting(boolean enabled) { - try { - if (enabled) - Bugsnag.resumeSession(); - else - Bugsnag.pauseSession(); - } catch (Throwable ex) { - Log.i(ex); - } } static void forceCrashReport(Context context, Throwable fatal) { @@ -382,7 +279,6 @@ public class Log { ocrumb.put(key, val); } Log.i(sb.toString()); - Bugsnag.leaveBreadcrumb(name, ocrumb, BreadcrumbType.LOG); } catch (Throwable ex) { Log.e(ex); } @@ -405,204 +301,6 @@ public class Log { } private static void setupBugsnag(final Context context) { - try { - Log.i("Configuring Bugsnag"); - - // https://docs.bugsnag.com/platforms/android/sdk/ - com.bugsnag.android.Configuration config = - new com.bugsnag.android.Configuration("9d2d57476a0614974449a3ec33f2604a"); - config.setTelemetry(Collections.emptySet()); - - if (BuildConfig.DEBUG) - config.setReleaseStage("Debug"); - else - config.setReleaseStage(getReleaseType(context)); - - config.setAutoTrackSessions(false); - - ErrorTypes etypes = new ErrorTypes(); - etypes.setUnhandledExceptions(true); - etypes.setAnrs(false); - etypes.setNdkCrashes(false); - config.setEnabledErrorTypes(etypes); - config.setMaxBreadcrumbs(BuildConfig.PLAY_STORE_RELEASE ? 250 : 500); - - Set discardClasses = new HashSet<>(); - for (String clazz : IGNORE_CLASSES) - discardClasses.add(Pattern.compile(clazz.replace(".", "\\."))); - config.setDiscardClasses(discardClasses); - - final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); - ActivityManager am = Helper.getSystemService(context, ActivityManager.class); - - String no_internet = context.getString(R.string.title_no_internet); - - String installer = Helper.getInstallerName(context); - config.addMetadata("extra", "revision", BuildConfig.REVISION); - config.addMetadata("extra", "installer", installer == null ? "-" : installer); - config.addMetadata("extra", "installed", new Date(Helper.getInstallTime(context)).toString()); - config.addMetadata("extra", "fingerprint", Helper.hasValidFingerprint(context)); - config.addMetadata("extra", "memory_class", am.getMemoryClass()); - config.addMetadata("extra", "memory_class_large", am.getLargeMemoryClass()); - config.addMetadata("extra", "build_host", Build.HOST); - config.addMetadata("extra", "build_time", new Date(Build.TIME)); - - config.addOnSession(new OnSessionCallback() { - @Override - public boolean onSession(@NonNull Session session) { - // opt-in - // opt-in - boolean crash_reports = prefs.getBoolean("crash_reports", false); - return crash_reports || Log.isTestRelease(); - } - }); - - config.addOnError(new OnErrorCallback() { - @Override - public boolean onError(@NonNull Event event) { - // opt-in - boolean crash_reports = prefs.getBoolean("crash_reports", false); - if (!crash_reports && !Log.isTestRelease()) - return false; - - Throwable ex = event.getOriginalError(); - boolean should = shouldNotify(ex); - - if (should) { - event.addMetadata("extra", "pid", Integer.toString(android.os.Process.myPid())); - event.addMetadata("extra", "thread", Thread.currentThread().getName() + ":" + Thread.currentThread().getId()); - event.addMetadata("extra", "memory_free", getFreeMemMb()); - event.addMetadata("extra", "memory_available", getAvailableMb()); - event.addMetadata("extra", "native_allocated", Debug.getNativeHeapAllocatedSize() / 1024L / 1024L); - event.addMetadata("extra", "native_size", Debug.getNativeHeapSize() / 1024L / 1024L); - event.addMetadata("extra", "classifier_size", MessageClassifier.getSize(context)); - - Boolean ignoringOptimizations = Helper.isIgnoringOptimizations(context); - event.addMetadata("extra", "optimizing", (ignoringOptimizations != null && !ignoringOptimizations)); - - String theme = prefs.getString("theme", "blue_orange_system"); - event.addMetadata("extra", "theme", theme); - event.addMetadata("extra", "package", BuildConfig.APPLICATION_ID); - event.addMetadata("extra", "locale", Locale.getDefault().toString()); - - Boolean foreground = Helper.isOnForeground(); - if (foreground != null) - event.addMetadata("extra", "foreground", Boolean.toString(foreground)); - } - - return should; - } - - private boolean shouldNotify(Throwable ex) { - if (ex instanceof MessagingException && - (ex.getCause() instanceof IOException || - ex.getCause() instanceof ProtocolException)) - // IOException includes SocketException, SocketTimeoutException - // ProtocolException includes ConnectionException - return false; - - if (ex instanceof MessagingException && - ("connection failure".equals(ex.getMessage()) || - "failed to create new store connection".equals(ex.getMessage()) || - "Failed to fetch headers".equals(ex.getMessage()) || - "Failed to load IMAP envelope".equals(ex.getMessage()) || - "Unable to load BODYSTRUCTURE".equals(ex.getMessage()))) - return false; - - if (ex instanceof IllegalStateException && - (no_internet.equals(ex.getMessage()) || - TOKEN_REFRESH_REQUIRED.equals(ex.getMessage()) || - "Not connected".equals(ex.getMessage()) || - "This operation is not allowed on a closed folder".equals(ex.getMessage()))) - return false; - - if (ex instanceof FileNotFoundException && - ex.getMessage() != null && - (ex.getMessage().startsWith("Download image failed") || - ex.getMessage().startsWith("http://") || - ex.getMessage().startsWith("https://") || - ex.getMessage().startsWith("content://"))) - return false; - - if (ex instanceof IOException && - ex.getCause() instanceof MessageRemovedException) - return false; - - if (ex instanceof IOException && - ex.getMessage() != null && - (ex.getMessage().startsWith("HTTP status=") || - "NetworkError".equals(ex.getMessage()) || // account manager - "Resetting to invalid mark".equals(ex.getMessage()) || - "Mark has been invalidated.".equals(ex.getMessage()))) - return false; - - if (ex instanceof SSLPeerUnverifiedException || - ex instanceof EmailService.UntrustedException) - return false; - - if (ex instanceof SSLHandshakeException && - ex.getCause() instanceof CertPathValidatorException) - return false; // checkUpdate! - - if (ex instanceof RuntimeException && - "Illegal meta data value: the child service doesn't exist".equals(ex.getMessage())) - return false; - - if (isDead(ex)) - return false; - - // Rate limit - int count = prefs.getInt("crash_report_count", 0) + 1; - prefs.edit().putInt("crash_report_count", count).apply(); - - return (count <= MAX_CRASH_REPORTS); - } - }); - - config.addOnBreadcrumb(new OnBreadcrumbCallback() { - @Override - public boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb) { - // opt-in - boolean crash_reports = prefs.getBoolean("crash_reports", false); - return crash_reports || Log.isTestRelease(); - } - }); - - config.addOnSend(new OnSendCallback() { - @Override - public boolean onSend(@NonNull Event event) { - // opt-in - boolean crash_reports = prefs.getBoolean("crash_reports", false); - return crash_reports || Log.isTestRelease(); - } - }); - - Bugsnag.start(context, config); - - Client client = Bugsnag.getClient(); - - String uuid = prefs.getString("uuid", null); - if (uuid == null) { - uuid = UUID.randomUUID().toString(); - prefs.edit().putString("uuid", uuid).apply(); - } - Log.i("uuid=" + uuid); - client.setUser(uuid, null, null); - - if (prefs.getBoolean("crash_reports", false) || Log.isTestRelease()) - Bugsnag.startSession(); - } catch (Throwable ex) { - Log.e(ex); - /* - java.lang.AssertionError: No NameTypeIndex match for SHORT_DAYLIGHT - at android.icu.impl.TimeZoneNamesImpl$ZNames.getNameTypeIndex(TimeZoneNamesImpl.java:724) - at android.icu.impl.TimeZoneNamesImpl$ZNames.getName(TimeZoneNamesImpl.java:790) - at android.icu.impl.TimeZoneNamesImpl.getTimeZoneDisplayName(TimeZoneNamesImpl.java:183) - at android.icu.text.TimeZoneNames.getDisplayName(TimeZoneNames.java:261) - at java.util.TimeZone.getDisplayName(TimeZone.java:405) - at java.util.Date.toString(Date.java:1066) - */ - } } static @NonNull String getReleaseType(Context context) { diff --git a/app/src/main/res/layout/fragment_options_misc.xml b/app/src/main/res/layout/fragment_options_misc.xml index 00ae94e143..8e7a64667d 100644 --- a/app/src/main/res/layout/fragment_options_misc.xml +++ b/app/src/main/res/layout/fragment_options_misc.xml @@ -518,6 +518,7 @@ android:layout_marginTop="12dp" android:drawableStart="@drawable/bugsnag" android:drawablePadding="6dp" + android:tag="nosuggest" android:text="@string/title_advanced_crash_reports" app:layout_constraintEnd_toEndOf="parent" app:layout_constraintStart_toStartOf="parent" @@ -532,6 +533,7 @@ android:layout_marginEnd="48dp" android:fontFamily="monospace" android:selectAllOnFocus="true" + android:tag="nosuggest" android:text="UUID" android:textAppearance="@style/TextAppearance.AppCompat.Small" android:textIsSelectable="true" @@ -547,6 +549,7 @@ android:layout_marginStart="12dp" android:layout_marginTop="6dp" android:contentDescription="@string/title_info" + android:tag="nosuggest" android:tooltipText="@string/title_info" app:layout_constraintStart_toStartOf="parent" app:layout_constraintTop_toBottomOf="@id/tvUuid" diff --git a/app/src/main/res/values/strings.xml b/app/src/main/res/values/strings.xml index 1995240a33..e14f262d91 100644 --- a/app/src/main/res/values/strings.xml +++ b/app/src/main/res/values/strings.xml @@ -211,7 +211,7 @@ Setting up an account is just three steps and after that you can send and receive emails There is no need to change any other options FairEmail was developed to help you protect your privacy and represents literally thousands of hours of work. If you have any problems, please contact me first for support before leaving a bad review. I am happy to help! - FairEmail does not display ads and does not track or analyze your behavior. Bugsnag is used for error reporting and is disabled by default. + FairEmail does not display ads and does not track or analyze your behavior. Add or change accounts Some providers make it difficult to add an account. Please do not blame FairEmail for this, but ask for support instead. The entered email address is used to query the email server and autoconfig.thunderbird.net for configuration information diff --git a/app/src/play/AndroidManifest.xml b/app/src/play/AndroidManifest.xml index 9f3c8cf74c..a1eb5408a7 100644 --- a/app/src/play/AndroidManifest.xml +++ b/app/src/play/AndroidManifest.xml @@ -155,9 +155,6 @@ android:resource="@xml/car" tools:node="remove" /> - diff --git a/build.gradle b/build.gradle index b010c8c4b5..c7ad048fb3 100644 --- a/build.gradle +++ b/build.gradle @@ -8,9 +8,6 @@ buildscript { dependencies { // https://developer.android.com/studio/releases/gradle-plugin classpath 'com.android.tools.build:gradle:8.9.2' - // https://github.com/bugsnag/bugsnag-android-gradle-plugin - // https://mvnrepository.com/artifact/com.bugsnag/bugsnag-android-gradle-plugin - classpath "com.bugsnag:bugsnag-android-gradle-plugin:8.1.0" // https://kotlinlang.org/docs/releases.html#release-details // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-gradle-plugin classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.24"