diff --git a/app/build.gradle b/app/build.gradle
index fe45c85d24..5dfb8441db 100644
--- a/app/build.gradle
+++ b/app/build.gradle
@@ -359,7 +359,7 @@ dependencies {
def dnsjava_version = "2.1.9"
def openpgp_version = "12.0"
def badge_version = "1.1.22"
- def bugsnag_version = "5.19.2"
+ def bugsnag_version = "5.23.0"
def biweekly_version = "0.6.6"
def vcard_version = "0.11.3"
def relinker_version = "1.4.3"
diff --git a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt
index 4e763633f0..c171c23d50 100644
--- a/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt
+++ b/app/src/main/java/com/bugsnag/android/BackgroundTaskService.kt
@@ -158,18 +158,13 @@ internal class BackgroundTaskService(
internalReportExecutor.shutdownNow()
defaultExecutor.shutdownNow()
- // shutdown the error/session executors first, waiting for existing tasks to complete.
- // If a request fails it may perform IO to persist the payload for delivery next launch,
- // which would submit tasks to the IO executor - therefore it's critical to
- // shutdown the IO executor last.
+ // Wait a little while for these ones to shut down
errorExecutor.shutdown()
sessionExecutor.shutdown()
+ ioExecutor.shutdown()
errorExecutor.awaitTerminationSafe()
sessionExecutor.awaitTerminationSafe()
-
- // shutdown the IO executor last, waiting for any existing tasks to complete
- ioExecutor.shutdown()
ioExecutor.awaitTerminationSafe()
}
diff --git a/app/src/main/java/com/bugsnag/android/Bugsnag.java b/app/src/main/java/com/bugsnag/android/Bugsnag.java
index f32b49d6ae..11538b580d 100644
--- a/app/src/main/java/com/bugsnag/android/Bugsnag.java
+++ b/app/src/main/java/com/bugsnag/android/Bugsnag.java
@@ -69,6 +69,15 @@ public final class Bugsnag {
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.");
}
@@ -76,18 +85,19 @@ public final class Bugsnag {
/**
* 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() {
+ @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.
*/
@@ -115,15 +125,15 @@ public final class Bugsnag {
/**
* 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);
@@ -140,6 +150,7 @@ public final class Bugsnag {
/**
* Removes a previously added "on error" callback
+ *
* @param onError the callback to remove
*/
public static void removeOnError(@NonNull OnErrorCallback onError) {
@@ -149,12 +160,12 @@ public final class Bugsnag {
/**
* 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
@@ -170,6 +181,7 @@ public final class Bugsnag {
/**
* Removes a previously added "on breadcrumb" callback
+ *
* @param onBreadcrumb the callback to remove
*/
public static void removeOnBreadcrumb(@NonNull OnBreadcrumbCallback onBreadcrumb) {
@@ -179,12 +191,12 @@ public final class Bugsnag {
/**
* 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
@@ -200,6 +212,7 @@ public final class Bugsnag {
/**
* Removes a previously added "on session" callback
+ *
* @param onSession the callback to remove
*/
public static void removeOnSession(@NonNull OnSessionCallback onSession) {
@@ -219,7 +232,7 @@ public final class Bugsnag {
* Notify Bugsnag of a handled exception
*
* @param exception the exception to send to Bugsnag
- * @param onError callback invoked on the generated error report for
+ * @param onError callback invoked on the generated error report for
* additional modification
*/
public static void notify(@NonNull final Throwable exception,
@@ -286,7 +299,8 @@ public final class Bugsnag {
/**
* 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 message A short label
* @param metadata Additional diagnostic information about the app environment
* @param type A category for the breadcrumb
*/
@@ -332,11 +346,10 @@ public final class Bugsnag {
*
* 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)
- *
- * @return true if a previous session was resumed, false if a new session was started.
*/
public static boolean resumeSession() {
return getClient().resumeSession();
@@ -365,7 +378,7 @@ public final class Bugsnag {
* 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
@@ -380,7 +393,7 @@ public final class Bugsnag {
/**
* 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.
@@ -394,7 +407,7 @@ public final class Bugsnag {
* 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.
@@ -462,8 +475,12 @@ public final class Bugsnag {
@NonNull
public static Client getClient() {
if (client == null) {
- throw new IllegalStateException("You must call Bugsnag.start before any"
- + " other Bugsnag methods");
+ 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
index 8f17f4049d..d850562671 100644
--- a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt
+++ b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt
@@ -11,6 +11,10 @@ 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)
@@ -85,7 +89,11 @@ internal class BugsnagEventMapper(
return event
}
- internal fun convertErrorInternal(error: Map): ErrorInternal {
+ 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,
diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java
index a6cdf8d932..c91becd799 100644
--- a/app/src/main/java/com/bugsnag/android/Client.java
+++ b/app/src/main/java/com/bugsnag/android/Client.java
@@ -170,7 +170,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
configModule, systemServiceModule, trackerModule,
- bgTaskService, connectivity, storageModule.getDeviceId(), memoryTrimState);
+ bgTaskService, connectivity, storageModule.getDeviceId(),
+ storageModule.getInternalDeviceId(), memoryTrimState);
dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO);
appDataCollector = dataCollectionModule.getAppDataCollector();
deviceDataCollector = dataCollectionModule.getDeviceDataCollector();
diff --git a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt
index f3ec4939f9..9a967d1853 100644
--- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt
+++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt
@@ -2,6 +2,7 @@ package com.bugsnag.android
import android.content.Context
import java.io.File
+import java.util.EnumSet
internal class ConfigInternal(
var apiKey: String
@@ -40,6 +41,7 @@ internal class ConfigInternal(
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 context: String? = null
var redactedKeys: Set
@@ -51,6 +53,7 @@ internal class ConfigInternal(
var discardClasses: Set = emptySet()
var enabledReleaseStages: Set? = null
var enabledBreadcrumbTypes: Set? = null
+ var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS)
var projectPackages: Set = emptySet()
var persistenceDirectory: File? = null
@@ -99,6 +102,7 @@ internal class ConfigInternal(
private const val DEFAULT_MAX_BREADCRUMBS = 50
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_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
@JvmStatic
diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java
index 3b9e6dc2a2..0be3d0ff9a 100644
--- a/app/src/main/java/com/bugsnag/android/Configuration.java
+++ b/app/src/main/java/com/bugsnag/android/Configuration.java
@@ -561,6 +561,32 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
}
}
+ /**
+ * 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(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);
+ }
+ }
+
/**
* Sets the maximum number of persisted sessions which will be stored. Once the threshold is
* reached, the oldest session will be deleted.
@@ -720,6 +746,26 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
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
diff --git a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt
index 667d536e8a..da0c4c3c9e 100644
--- a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt
+++ b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt
@@ -5,6 +5,7 @@ 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.RemoteException
import android.os.storage.StorageManager
@@ -69,3 +70,7 @@ internal fun Context.getConnectivityManager(): ConnectivityManager? =
@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/DataCollectionModule.kt b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt
index 577b9bf7bc..20bf61229c 100644
--- a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt
+++ b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt
@@ -18,6 +18,7 @@ internal class DataCollectionModule(
bgTaskService: BackgroundTaskService,
connectivity: Connectivity,
deviceId: String?,
+ internalDeviceId: String?,
memoryTrimState: MemoryTrimState
) : DependencyModule() {
@@ -49,6 +50,7 @@ internal class DataCollectionModule(
ctx,
ctx.resources,
deviceId,
+ internalDeviceId,
deviceBuildInfo,
dataDir,
rootDetector,
diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
index 69c4063e97..ed3573f3fe 100644
--- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
+++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
@@ -13,7 +13,6 @@ import android.os.Build
import android.provider.Settings
import java.io.File
import java.util.Date
-import java.util.HashMap
import java.util.Locale
import java.util.concurrent.Callable
import java.util.concurrent.Future
@@ -28,6 +27,7 @@ internal class DeviceDataCollector(
private val appContext: Context,
resources: Resources,
private val deviceId: String?,
+ private val internalDeviceId: String?,
private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File,
rootDetector: RootDetector,
@@ -42,7 +42,7 @@ internal class DeviceDataCollector(
private val screenResolution = getScreenResolution()
private val locale = Locale.getDefault().toString()
private val cpuAbi = getCpuAbi()
- private val runtimeVersions: MutableMap
+ private var runtimeVersions: MutableMap
private val rootedFuture: Future?
private val totalMemoryFuture: Future? = retrieveTotalDeviceMemory()
private var orientation = AtomicInteger(resources.configuration.orientation)
@@ -89,6 +89,19 @@ internal class DeviceDataCollector(
Date(now)
)
+ fun generateInternalDeviceWithState(now: Long) = DeviceWithState(
+ buildInfo,
+ checkIsRooted(),
+ internalDeviceId,
+ locale,
+ totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
+ runtimeVersions.toMutableMap(),
+ calculateFreeDisk(),
+ calculateFreeMemory(),
+ getOrientationAsString(),
+ Date(now)
+ )
+
fun getDeviceMetadata(): Map {
val map = HashMap()
populateBatteryInfo(into = map)
@@ -163,19 +176,24 @@ internal class DeviceDataCollector(
*/
private fun getLocationStatus(): String? {
try {
- val cr = appContext.contentResolver
- @Suppress("DEPRECATION") val providersAllowed =
- Settings.Secure.getString(cr, Settings.Secure.LOCATION_PROVIDERS_ALLOWED)
- return when {
- providersAllowed != null && providersAllowed.isNotEmpty() -> "allowed"
- else -> "disallowed"
- }
+ 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"
*/
@@ -293,6 +311,9 @@ internal class DeviceDataCollector(
}
fun addRuntimeVersionInfo(key: String, value: String) {
- runtimeVersions[key] = value
+ // 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
new file mode 100644
index 0000000000..60fa1f9364
--- /dev/null
+++ b/app/src/main/java/com/bugsnag/android/DeviceIdFilePersistence.kt
@@ -0,0 +1,163 @@
+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
new file mode 100644
index 0000000000..0c9bec8ac5
--- /dev/null
+++ b/app/src/main/java/com/bugsnag/android/DeviceIdPersistence.kt
@@ -0,0 +1,14 @@
+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
index b9db53c296..31b7c7dc28 100644
--- a/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt
+++ b/app/src/main/java/com/bugsnag/android/DeviceIdStore.kt
@@ -1,41 +1,33 @@
package com.bugsnag.android
import android.content.Context
-import android.util.JsonReader
import java.io.File
-import java.io.IOException
-import java.lang.Thread
-import java.nio.channels.FileChannel
-import java.nio.channels.FileLock
-import java.nio.channels.OverlappingFileLockException
import java.util.UUID
/**
- * This class is responsible for persisting and retrieving the device ID which uniquely
- * identifies this device.
- *
- * This class is made multi-process safe through the use of a [FileLock], and thread safe
- * through the use of a [ReadWriteLock] in [SynchronizedStreamableStore].
+ * 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 constructor(
context: Context,
- private val file: File = File(context.filesDir, "device-id"),
+ deviceIdfile: File = File(context.filesDir, "device-id"),
+ deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
+ internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"),
+ internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
private val sharedPrefMigrator: SharedPrefMigrator,
- private val logger: Logger
+ logger: Logger
) {
- private val synchronizedStreamableStore: SynchronizedStreamableStore
+ private val persistence: DeviceIdPersistence
+ private val internalPersistence: DeviceIdPersistence
init {
- try {
- file.createNewFile()
- } catch (exc: Throwable) {
- logger.w("Failed to created device ID file", exc)
- }
- this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
+ persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
+ internalPersistence = DeviceIdFilePersistence(internalDeviceIdfile, internalDeviceIdGenerator, logger)
}
/**
+ * 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.
*
@@ -43,137 +35,18 @@ internal class DeviceIdStore @JvmOverloads constructor(
* be used. If no value is present then a random UUID will be generated and persisted.
*/
fun loadDeviceId(): String? {
- return loadDeviceId {
- when (val legacyDeviceId = sharedPrefMigrator.loadDeviceId()) {
- null -> UUID.randomUUID()
- else -> UUID.fromString(legacyDeviceId)
- }
+ var result = persistence.loadDeviceId(false)
+ if (result != null) {
+ return result
}
+ result = sharedPrefMigrator.loadDeviceId(false)
+ if (result != null) {
+ return result
+ }
+ return persistence.loadDeviceId(true)
}
- internal fun loadDeviceId(uuidProvider: () -> UUID): String? {
- return try {
- // optimistically read device ID without a lock - the majority of the time
- // the device ID will already be present so no synchronization is required.
- val deviceId = loadDeviceIdInternal()
-
- if (deviceId?.id != null) {
- deviceId.id
- } else {
- return persistNewDeviceUuid(uuidProvider)
- }
- } catch (exc: Throwable) {
- logger.w("Failed to load device ID", exc)
- null
- }
- }
-
- /**
- * Loads the device ID from the file.
- *
- * If the file has zero length it can't contain device ID, so reading will be skipped.
- */
- private fun loadDeviceIdInternal(): DeviceId? {
- if (file.length() > 0) {
- try {
- return synchronizedStreamableStore.load(DeviceId.Companion::fromReader)
- } catch (exc: Throwable) { // catch AssertionError which can be thrown by JsonReader
- // on Android 8.0/8.1. see https://issuetracker.google.com/issues/79920590
- logger.w("Failed to load device ID", exc)
- }
- }
- return null
- }
-
- /**
- * Write a new Device ID to the file.
- */
- private fun persistNewDeviceUuid(uuidProvider: () -> UUID): String? {
- return try {
- // acquire a FileLock to prevent Clients in different processes writing
- // to the same file concurrently
- file.outputStream().channel.use { channel ->
- persistNewDeviceIdWithLock(channel, uuidProvider)
- }
- } catch (exc: IOException) {
- logger.w("Failed to persist device ID", exc)
- null
- }
- }
-
- private fun persistNewDeviceIdWithLock(
- channel: FileChannel,
- uuidProvider: () -> UUID
- ): String? {
- val lock = waitForFileLock(channel) ?: return null
-
- return try {
- // read the device ID again as it could have changed
- // between the last read and when the lock was acquired
- val deviceId = loadDeviceIdInternal()
-
- if (deviceId?.id != null) {
- // the device ID changed between the last read
- // and acquiring the lock, so return the generated value
- deviceId.id
- } else {
- // generate a new device ID and persist it
- val newId = DeviceId(uuidProvider().toString())
- synchronizedStreamableStore.persist(newId)
- newId.id
- }
- } finally {
- lock.release()
- }
- }
-
- /**
- * Attempt to acquire a file lock. If [OverlappingFileLockException] is thrown
- * then the method will wait for 50ms then try again, for a maximum of 10 attempts.
- */
- private fun waitForFileLock(channel: FileChannel): FileLock? {
- repeat(MAX_FILE_LOCK_ATTEMPTS) {
- try {
- return channel.tryLock()
- } catch (exc: OverlappingFileLockException) {
- Thread.sleep(FILE_LOCK_WAIT_MS)
- }
- }
- return null
- }
-
- companion object {
- private const val MAX_FILE_LOCK_ATTEMPTS = 20
- private const val FILE_LOCK_WAIT_MS = 25L
- }
-}
-
-/**
- * Serializes and deserializes the device ID to/from JSON.
- */
-private class DeviceId(val id: String?) : JsonStream.Streamable {
-
- override fun toStream(stream: JsonStream) {
- with(stream) {
- beginObject()
- name(KEY_ID)
- value(id)
- endObject()
- }
- }
-
- companion object : JsonReadable {
- 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)
- }
+ fun loadInternalDeviceId(): String? {
+ return internalPersistence.loadDeviceId(true)
}
}
diff --git a/app/src/main/java/com/bugsnag/android/ErrorType.kt b/app/src/main/java/com/bugsnag/android/ErrorType.kt
index 6ab5f98a56..299b1a0b45 100644
--- a/app/src/main/java/com/bugsnag/android/ErrorType.kt
+++ b/app/src/main/java/com/bugsnag/android/ErrorType.kt
@@ -18,9 +18,16 @@ enum class ErrorType(internal val desc: String) {
/**
* An error captured from Android's C layer
*/
- C("c");
+ 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/EventFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt
index 6d9ff766c9..f7cfd5f9af 100644
--- a/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt
+++ b/app/src/main/java/com/bugsnag/android/EventFilenameInfo.kt
@@ -22,12 +22,8 @@ internal data class EventFilenameInfo(
val errorTypes: Set
) {
- /**
- * Generates a filename for the Event in the format
- * "[timestamp]_[apiKey]_[errorTypes]_[UUID]_[startupcrash|not-jvm].json"
- */
fun encode(): String {
- return "${timestamp}_${apiKey}_${serializeErrorTypeHeader(errorTypes)}_${uuid}_$suffix.json"
+ return toFilename(apiKey, uuid, timestamp, suffix, errorTypes)
}
fun isLaunchCrashReport(): Boolean = suffix == STARTUP_CRASH
@@ -36,7 +32,21 @@ internal data class EventFilenameInfo(
private const val STARTUP_CRASH = "startupcrash"
private const val NON_JVM_CRASH = "not-jvm"
- @JvmOverloads
+ /**
+ * 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(),
@@ -63,11 +73,12 @@ internal data class EventFilenameInfo(
/**
* 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
- -1, // ignore timestamp when reading from file as unused
+ findTimestampInFilename(file),
findSuffixInFilename(file),
findErrorTypesInFilename(file)
)
@@ -77,7 +88,7 @@ internal data class EventFilenameInfo(
* Retrieves the api key encoded in the filename, or an empty string if this information
* is not encoded for the given event
*/
- private fun findApiKeyInFilename(file: File, config: ImmutableConfig): String {
+ 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)
@@ -93,7 +104,7 @@ internal data class EventFilenameInfo(
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
- private fun findErrorTypesInFilename(eventFile: File): Set {
+ internal fun findErrorTypesInFilename(eventFile: File): Set {
val name = eventFile.name
val end = name.lastIndexOf("_", name.lastIndexOf("_") - 1)
val start = name.lastIndexOf("_", end - 1) + 1
@@ -111,7 +122,7 @@ internal data class EventFilenameInfo(
* Retrieves the error types encoded in the filename, or an empty string if this
* information is not encoded for the given event
*/
- private fun findSuffixInFilename(eventFile: File): String {
+ internal fun findSuffixInFilename(eventFile: File): String {
val name = eventFile.nameWithoutExtension
val suffix = name.substring(name.lastIndexOf("_") + 1)
return when (suffix) {
@@ -120,10 +131,20 @@ internal data class EventFilenameInfo(
}
}
+ /**
+ * 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
*/
- private fun findErrorTypesForEvent(obj: Any): Set {
+ internal fun findErrorTypesForEvent(obj: Any): Set {
return when (obj) {
is Event -> obj.impl.getErrorTypesFromStackframes()
else -> setOf(ErrorType.C)
@@ -133,7 +154,7 @@ internal data class EventFilenameInfo(
/**
* Calculates the suffix for the given event
*/
- private fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
+ internal fun findSuffixForEvent(obj: Any, launching: Boolean?): String {
return when {
obj is Event && obj.app.isLaunching == true -> STARTUP_CRASH
launching == true -> STARTUP_CRASH
diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt
index 78980d1458..03f01f0a3e 100644
--- a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt
+++ b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt
@@ -22,17 +22,18 @@ internal class EventStorageModule(
private val cfg = configModule.config
private val delegate by future {
- InternalReportDelegate(
- contextModule.ctx,
- cfg.logger,
- cfg,
- systemServiceModule.storageManager,
- dataCollectionModule.appDataCollector,
- dataCollectionModule.deviceDataCollector,
- trackerModule.sessionTracker,
- notifier,
- bgTaskService
- )
+ if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS) == true)
+ InternalReportDelegate(
+ contextModule.ctx,
+ cfg.logger,
+ cfg,
+ systemServiceModule.storageManager,
+ dataCollectionModule.appDataCollector,
+ dataCollectionModule.deviceDataCollector,
+ trackerModule.sessionTracker,
+ notifier,
+ bgTaskService
+ ) else null
}
val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) }
diff --git a/app/src/main/java/com/bugsnag/android/EventStore.java b/app/src/main/java/com/bugsnag/android/EventStore.java
index e1365644ae..5ae0c04c50 100644
--- a/app/src/main/java/com/bugsnag/android/EventStore.java
+++ b/app/src/main/java/com/bugsnag/android/EventStore.java
@@ -7,9 +7,11 @@ import androidx.annotation.Nullable;
import java.io.File;
import java.util.ArrayList;
+import java.util.Calendar;
import java.util.Collection;
import java.util.Collections;
import java.util.Comparator;
+import java.util.Date;
import java.util.List;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Future;
@@ -119,7 +121,7 @@ class EventStore extends FileStore {
List launchCrashes = new ArrayList<>();
for (File file : storedFiles) {
- EventFilenameInfo filenameInfo = EventFilenameInfo.Companion.fromFile(file, config);
+ EventFilenameInfo filenameInfo = EventFilenameInfo.fromFile(file, config);
if (filenameInfo.isLaunchCrashReport()) {
launchCrashes.add(file);
}
@@ -163,7 +165,7 @@ class EventStore extends FileStore {
private void flushEventFile(File eventFile) {
try {
- EventFilenameInfo eventInfo = EventFilenameInfo.Companion.fromFile(eventFile, config);
+ EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config);
String apiKey = eventInfo.getApiKey();
EventPayload payload = createEventPayload(eventFile, apiKey);
@@ -188,9 +190,21 @@ class EventStore extends FileStore {
logger.i("Deleting sent error file " + eventFile.getName());
break;
case UNDELIVERED:
- cancelQueuedFiles(Collections.singleton(eventFile));
- logger.w("Could not send previously saved error(s)"
- + " to Bugsnag, will try again later");
+ if (isTooBig(eventFile)) {
+ logger.w("Discarding over-sized event ("
+ + eventFile.length()
+ + ") after failed delivery");
+ deleteStoredFiles(Collections.singleton(eventFile));
+ } else if (isTooOld(eventFile)) {
+ logger.w("Discarding historical event (from "
+ + getCreationDate(eventFile)
+ + ") after failed delivery");
+ deleteStoredFiles(Collections.singleton(eventFile));
+ } else {
+ cancelQueuedFiles(Collections.singleton(eventFile));
+ logger.w("Could not send previously saved error(s)"
+ + " to Bugsnag, will try again later");
+ }
break;
case FAILURE:
Exception exc = new RuntimeException("Failed to deliver event payload");
@@ -234,13 +248,29 @@ class EventStore extends FileStore {
@Override
String getFilename(Object object) {
EventFilenameInfo eventInfo
- = EventFilenameInfo.Companion.fromEvent(object, null, config);
+ = EventFilenameInfo.fromEvent(object, null, config);
return eventInfo.encode();
}
String getNdkFilename(Object object, String apiKey) {
EventFilenameInfo eventInfo
- = EventFilenameInfo.Companion.fromEvent(object, apiKey, config);
+ = EventFilenameInfo.fromEvent(object, apiKey, config);
return eventInfo.encode();
}
+
+ private static long oneMegabyte = 1024 * 1024;
+
+ public boolean isTooBig(File file) {
+ return file.length() > oneMegabyte;
+ }
+
+ public boolean isTooOld(File file) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DATE, -60);
+ return EventFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
+ }
+
+ public Date getCreationDate(File file) {
+ return new Date(EventFilenameInfo.findTimestampInFilename(file));
+ }
}
diff --git a/app/src/main/java/com/bugsnag/android/FileStore.java b/app/src/main/java/com/bugsnag/android/FileStore.java
index cbafdac431..b613cea988 100644
--- a/app/src/main/java/com/bugsnag/android/FileStore.java
+++ b/app/src/main/java/com/bugsnag/android/FileStore.java
@@ -39,7 +39,7 @@ abstract class FileStore {
private final Lock lock = new ReentrantLock();
private final Collection queuedFiles = new ConcurrentSkipListSet<>();
- private final Logger logger;
+ protected final Logger logger;
private final EventStore.Delegate delegate;
FileStore(@NonNull File storageDir,
diff --git a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
index 7f76a24271..ad166e35b8 100644
--- a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
+++ b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt
@@ -37,6 +37,7 @@ internal class ManifestConfigLoader {
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 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"
@@ -77,6 +78,7 @@ internal class ManifestConfigLoader {
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)
launchDurationMillis = data.getInt(
LAUNCH_CRASH_THRESHOLD_MS,
launchDurationMillis.toInt()
diff --git a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt
index 5c47e1633c..3e82416a85 100644
--- a/app/src/main/java/com/bugsnag/android/NativeStackframe.kt
+++ b/app/src/main/java/com/bugsnag/android/NativeStackframe.kt
@@ -45,7 +45,12 @@ class NativeStackframe internal constructor(
/**
* The type of the error
*/
- var type: ErrorType? = null
+ var type: ErrorType? = null,
+
+ /**
+ * Identifies the exact build this frame originates from.
+ */
+ var codeIdentifier: String? = null,
) : JsonStream.Streamable {
@Throws(IOException::class)
@@ -57,6 +62,7 @@ class NativeStackframe internal constructor(
writer.name("frameAddress").value(frameAddress)
writer.name("symbolAddress").value(symbolAddress)
writer.name("loadAddress").value(loadAddress)
+ writer.name("codeIdentifier").value(codeIdentifier)
writer.name("isPC").value(isPC)
type?.let {
diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt
index b2f08dbab0..d131059fc1 100644
--- a/app/src/main/java/com/bugsnag/android/Notifier.kt
+++ b/app/src/main/java/com/bugsnag/android/Notifier.kt
@@ -7,7 +7,7 @@ import java.io.IOException
*/
class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier",
- var version: String = "5.19.2",
+ var version: String = "5.23.0",
var url: String = "https://bugsnag.com"
) : JsonStream.Streamable {
diff --git a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
new file mode 100644
index 0000000000..04efa4081b
--- /dev/null
+++ b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt
@@ -0,0 +1,55 @@
+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(
+ val timestamp: Long,
+ val uuid: String,
+) {
+
+ fun encode(): String {
+ return toFilename(timestamp, uuid)
+ }
+
+ internal companion object {
+
+ const val uuidLength = 36
+
+ /**
+ * Generates a filename for the session in the format
+ * "[UUID][timestamp]_v2.json"
+ */
+ fun toFilename(timestamp: Long, uuid: String): String {
+ return "${uuid}${timestamp}_v2.json"
+ }
+
+ @JvmStatic
+ fun defaultFilename(): String {
+ return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString())
+ }
+
+ fun fromFile(file: File): SessionFilenameInfo {
+ return SessionFilenameInfo(
+ findTimestampInFilename(file),
+ findUuidInFilename(file)
+ )
+ }
+
+ private fun findUuidInFilename(file: File): String {
+ return file.name.substring(0, uuidLength - 1)
+ }
+
+ @JvmStatic
+ fun findTimestampInFilename(file: File): Long {
+ return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1
+ }
+ }
+}
diff --git a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt b/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt
index b43f860813..2299fa87c9 100644
--- a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt
+++ b/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt
@@ -2,17 +2,32 @@ package com.bugsnag.android
import android.app.Activity
import android.app.Application
+import android.os.Build
import android.os.Bundle
internal class SessionLifecycleCallback(
private val sessionTracker: SessionTracker
) : Application.ActivityLifecycleCallbacks {
- override fun onActivityStarted(activity: Activity) =
- sessionTracker.onActivityStarted(activity.javaClass.simpleName)
+ override fun onActivityStarted(activity: Activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ sessionTracker.onActivityStarted(activity.javaClass.simpleName)
+ }
+ }
- override fun onActivityStopped(activity: Activity) =
+ override fun onActivityPostStarted(activity: Activity) {
+ sessionTracker.onActivityStarted(activity.javaClass.simpleName)
+ }
+
+ override fun onActivityStopped(activity: Activity) {
+ if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) {
+ sessionTracker.onActivityStopped(activity.javaClass.simpleName)
+ }
+ }
+
+ override fun onActivityPostStopped(activity: Activity) {
sessionTracker.onActivityStopped(activity.javaClass.simpleName)
+ }
override fun onActivityCreated(activity: Activity, savedInstanceState: Bundle?) {}
override fun onActivityResumed(activity: Activity) {}
diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java
index 0d84d8a677..a0238a5feb 100644
--- a/app/src/main/java/com/bugsnag/android/SessionStore.java
+++ b/app/src/main/java/com/bugsnag/android/SessionStore.java
@@ -6,7 +6,9 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
import java.io.File;
+import java.util.Calendar;
import java.util.Comparator;
+import java.util.Date;
import java.util.UUID;
/**
@@ -46,7 +48,16 @@ class SessionStore extends FileStore {
@NonNull
@Override
String getFilename(Object object) {
- return UUID.randomUUID().toString() + System.currentTimeMillis() + "_v2.json";
+ return SessionFilenameInfo.defaultFilename();
}
+ public boolean isTooOld(File file) {
+ Calendar cal = Calendar.getInstance();
+ cal.add(Calendar.DATE, -60);
+ return SessionFilenameInfo.findTimestampInFilename(file) < cal.getTimeInMillis();
+ }
+
+ public Date getCreationDate(File file) {
+ return new Date(SessionFilenameInfo.findTimestampInFilename(file));
+ }
}
diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java
index 2a5e95c7f1..ddb2f052d5 100644
--- a/app/src/main/java/com/bugsnag/android/SessionTracker.java
+++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java
@@ -270,8 +270,15 @@ class SessionTracker extends BaseObservable {
logger.d("Sent 1 new session to Bugsnag");
break;
case UNDELIVERED:
- sessionStore.cancelQueuedFiles(Collections.singletonList(storedFile));
- logger.w("Leaving session payload for future delivery");
+ 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
diff --git a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
index e8af658a60..d664576791 100644
--- a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
+++ b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt
@@ -6,12 +6,15 @@ import android.content.Context
/**
* Reads legacy information left in SharedPreferences and migrates it to the new location.
*/
-internal class SharedPrefMigrator(context: Context) {
+internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence {
private val prefs = context
.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE)
- fun loadDeviceId() = prefs.getString(INSTALL_ID_KEY, 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),
diff --git a/app/src/main/java/com/bugsnag/android/Stackframe.kt b/app/src/main/java/com/bugsnag/android/Stackframe.kt
index 3e4e063a76..7ac08889bd 100644
--- a/app/src/main/java/com/bugsnag/android/Stackframe.kt
+++ b/app/src/main/java/com/bugsnag/android/Stackframe.kt
@@ -53,6 +53,11 @@ class Stackframe : JsonStream.Streamable {
*/
var loadAddress: Long? = null
+ /**
+ * Identifies the exact build this frame originates from.
+ */
+ var codeIdentifier: String? = null
+
/**
* Whether this frame identifies the program counter
*/
@@ -90,6 +95,7 @@ class Stackframe : JsonStream.Streamable {
this.frameAddress = nativeFrame.frameAddress
this.symbolAddress = nativeFrame.symbolAddress
this.loadAddress = nativeFrame.loadAddress
+ this.codeIdentifier = nativeFrame.codeIdentifier
this.isPC = nativeFrame.isPC
this.type = nativeFrame.type
}
@@ -103,6 +109,7 @@ class Stackframe : JsonStream.Streamable {
frameAddress = (json["frameAddress"] as? Number)?.toLong()
symbolAddress = (json["symbolAddress"] as? Number)?.toLong()
loadAddress = (json["loadAddress"] as? Number)?.toLong()
+ codeIdentifier = (json["codeIdentifier"] as? String)
isPC = json["isPC"] as? Boolean
@Suppress("UNCHECKED_CAST")
@@ -124,6 +131,7 @@ class Stackframe : JsonStream.Streamable {
frameAddress?.let { writer.name("frameAddress").value(it) }
symbolAddress?.let { writer.name("symbolAddress").value(it) }
loadAddress?.let { writer.name("loadAddress").value(it) }
+ codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
isPC?.let { writer.name("isPC").value(it) }
type?.let {
diff --git a/app/src/main/java/com/bugsnag/android/StorageModule.kt b/app/src/main/java/com/bugsnag/android/StorageModule.kt
index 2d4bff14ee..caa419d7b9 100644
--- a/app/src/main/java/com/bugsnag/android/StorageModule.kt
+++ b/app/src/main/java/com/bugsnag/android/StorageModule.kt
@@ -25,6 +25,8 @@ internal class StorageModule(
val deviceId by future { deviceIdStore.loadDeviceId() }
+ val internalDeviceId by future { deviceIdStore.loadInternalDeviceId() }
+
val userStore by future {
UserStore(
immutableConfig,
diff --git a/app/src/main/java/com/bugsnag/android/Telemetry.kt b/app/src/main/java/com/bugsnag/android/Telemetry.kt
new file mode 100644
index 0000000000..7d5a070a71
--- /dev/null
+++ b/app/src/main/java/com/bugsnag/android/Telemetry.kt
@@ -0,0 +1,16 @@
+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;
+
+ internal companion object {
+ fun fromString(str: String) = values().find { it.name == str } ?: INTERNAL_ERRORS
+ }
+}
diff --git a/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt
index 06e00268ac..536ed89168 100644
--- a/app/src/main/java/com/bugsnag/android/ThreadState.kt
+++ b/app/src/main/java/com/bugsnag/android/ThreadState.kt
@@ -2,25 +2,27 @@ package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException
+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") @JvmOverloads constructor(
+internal class ThreadState @Suppress("LongParameterList") constructor(
exc: Throwable?,
isUnhandled: Boolean,
+ maxThreads: Int,
sendThreads: ThreadSendPolicy,
projectPackages: Collection,
logger: Logger,
- currentThread: java.lang.Thread? = null,
- stackTraces: MutableMap>? = null
+ currentThread: JavaThread = JavaThread.currentThread(),
+ allThreads: List = allThreads()
) : JsonStream.Streamable {
internal constructor(
exc: Throwable?,
isUnhandled: Boolean,
config: ImmutableConfig
- ) : this(exc, isUnhandled, config.sendThreads, config.projectPackages, config.logger)
+ ) : this(exc, isUnhandled, config.maxReportedThreads, config.sendThreads, config.projectPackages, config.logger)
val threads: MutableList
@@ -30,10 +32,11 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
threads = when {
recordThreads -> captureThreadTrace(
- stackTraces ?: java.lang.Thread.getAllStackTraces(),
- currentThread ?: java.lang.Thread.currentThread(),
+ allThreads,
+ currentThread,
exc,
isUnhandled,
+ maxThreads,
projectPackages,
logger
)
@@ -41,37 +44,88 @@ internal class ThreadState @Suppress("LongParameterList") @JvmOverloads construc
}
}
+ 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(
- stackTraces: MutableMap>,
- currentThread: java.lang.Thread,
+ allThreads: List,
+ currentThread: JavaThread,
exc: Throwable?,
isUnhandled: Boolean,
+ maxThreadCount: Int,
projectPackages: Collection,
logger: Logger
): MutableList {
- // API 24/25 don't record the currentThread, add it in manually
- // https://issuetracker.google.com/issues/64122757
- if (!stackTraces.containsKey(currentThread)) {
- stackTraces[currentThread] = currentThread.stackTrace
- }
- if (exc != null && isUnhandled) { // unhandled errors use the exception trace for thread traces
- stackTraces[currentThread] = exc.stackTrace
- }
-
- val currentThreadId = currentThread.id
- return stackTraces.keys
- .sortedBy { it.id }
- .mapNotNull { thread ->
- val trace = stackTraces[thread]
-
- if (trace != null) {
- val stacktrace = Stacktrace(trace, projectPackages, logger)
- val errorThread = thread.id == currentThreadId
- Thread(thread.id, thread.name, ThreadType.ANDROID, errorThread, Thread.State.forThread(thread), stacktrace, logger)
+ 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 {
- null
- }
- }.toMutableList()
+ thread.stackTrace
+ },
+ projectPackages, logger
+ )
+
+ return Thread(
+ thread.id,
+ thread.name,
+ ThreadType.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 keepThreads = allThreads.sortedBy { it.id }.take(maxThreadCount)
+
+ val reportThreads = if (keepThreads.contains(currentThread)) {
+ keepThreads
+ } else {
+ // 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
+ keepThreads.take(Math.max(maxThreadCount - 1, 0)).plus(currentThread).sortedBy { it.id }
+ }.map { toBugsnagThread(it) }.toMutableList()
+
+ if (allThreads.size > maxThreadCount) {
+ reportThreads.add(
+ Thread(
+ -1,
+ "[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]",
+ ThreadType.EMPTY,
+ false,
+ Thread.State.UNKNOWN,
+ Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger),
+ logger
+ )
+ )
+ }
+ return reportThreads
}
@Throws(IOException::class)
diff --git a/app/src/main/java/com/bugsnag/android/ThreadType.kt b/app/src/main/java/com/bugsnag/android/ThreadType.kt
index c1c3cbb5d7..60f834741c 100644
--- a/app/src/main/java/com/bugsnag/android/ThreadType.kt
+++ b/app/src/main/java/com/bugsnag/android/ThreadType.kt
@@ -5,6 +5,11 @@ package com.bugsnag.android
*/
enum class ThreadType(internal val desc: String) {
+ /**
+ * A thread captured from Android's JVM layer
+ */
+ EMPTY(""),
+
/**
* A thread captured from Android's JVM layer
*/
diff --git a/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt
new file mode 100644
index 0000000000..c004241bc7
--- /dev/null
+++ b/app/src/main/java/com/bugsnag/android/internal/BugsnagMapper.kt
@@ -0,0 +1,44 @@
+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/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
index 9ace0f6686..06558cfca2 100644
--- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
+++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt
@@ -18,6 +18,7 @@ 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.Telemetry
import com.bugsnag.android.ThreadSendPolicy
import com.bugsnag.android.errorApiHeaders
import com.bugsnag.android.safeUnrollCauses
@@ -34,6 +35,7 @@ data class ImmutableConfig(
val enabledReleaseStages: Collection?,
val projectPackages: Collection,
val enabledBreadcrumbTypes: Set?,
+ val telemetry: Set,
val releaseStage: String?,
val buildUuid: String?,
val appVersion: String?,
@@ -47,6 +49,7 @@ data class ImmutableConfig(
val maxBreadcrumbs: Int,
val maxPersistedEvents: Int,
val maxPersistedSessions: Int,
+ val maxReportedThreads: Int,
val persistenceDirectory: Lazy,
val sendLaunchCrashesSynchronously: Boolean,
@@ -159,7 +162,9 @@ internal fun convertToImmutableConfig(
maxBreadcrumbs = config.maxBreadcrumbs,
maxPersistedEvents = config.maxPersistedEvents,
maxPersistedSessions = config.maxPersistedSessions,
+ maxReportedThreads = config.maxReportedThreads,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
+ telemetry = config.telemetry.toSet(),
persistenceDirectory = persistenceDir,
sendLaunchCrashesSynchronously = config.sendLaunchCrashesSynchronously,
packageInfo = packageInfo,
diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java
index f115c327f0..2eebf4762f 100644
--- a/app/src/main/java/eu/faircode/email/Log.java
+++ b/app/src/main/java/eu/faircode/email/Log.java
@@ -92,6 +92,7 @@ import com.bugsnag.android.OnErrorCallback;
import com.bugsnag.android.OnSessionCallback;
import com.bugsnag.android.Session;
import com.bugsnag.android.Severity;
+import com.bugsnag.android.Telemetry;
import com.sun.mail.iap.BadCommandException;
import com.sun.mail.iap.ConnectionException;
import com.sun.mail.iap.ProtocolException;
@@ -370,6 +371,7 @@ public class Log {
// 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");
diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
index c762d488a1..a45f6efb2a 100644
--- a/patches/Bugsnag.patch
+++ b/patches/Bugsnag.patch
@@ -1,5 +1,5 @@
diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
-index 0ce2eec8c..e1bac196e 100644
+index 0ce2eec8c4..e1bac196e2 100644
--- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
@@ -66,7 +66,7 @@ internal class DefaultDelivery(
@@ -12,24 +12,15 @@ index 0ce2eec8c..e1bac196e 100644
logger.w("Unexpected error delivering payload", exception)
return DeliveryStatus.FAILURE
diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
-index 25c19fd4c..e69de29bb 100644
+index c762d488a1..e69de29bb2 100644
--- a/patches/Bugsnag.patch
+++ b/patches/Bugsnag.patch
-@@ -1,22 +0,0 @@
--From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001
--From: M66B
--Date: Sat, 15 May 2021 22:07:24 +0200
--Subject: [PATCH] Bugsnag failure on I/O error
--
-----
-- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +-
-- 1 file changed, 1 insertion(+), 1 deletion(-)
--
+@@ -1,40 +0,0 @@
-diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
--index a7995164cb4e..5620f0bacd80 100644
+-index 0ce2eec8c..e1bac196e 100644
---- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
-+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
--@@ -64,7 +64,7 @@ internal class DefaultDelivery(
+-@@ -66,7 +66,7 @@ internal class DefaultDelivery(
- return DeliveryStatus.UNDELIVERED
- } catch (exception: IOException) {
- logger.w("IOException encountered in request", exception)
@@ -38,3 +29,30 @@ index 25c19fd4c..e69de29bb 100644
- } catch (exception: Exception) {
- logger.w("Unexpected error delivering payload", exception)
- return DeliveryStatus.FAILURE
+-diff --git a/patches/Bugsnag.patch b/patches/Bugsnag.patch
+-index 25c19fd4c..e69de29bb 100644
+---- a/patches/Bugsnag.patch
+-+++ b/patches/Bugsnag.patch
+-@@ -1,22 +0,0 @@
+--From 3270faf44aea11754c940ba43ee6db72b7462f14 Mon Sep 17 00:00:00 2001
+--From: M66B
+--Date: Sat, 15 May 2021 22:07:24 +0200
+--Subject: [PATCH] Bugsnag failure on I/O error
+--
+-----
+-- app/src/main/java/com/bugsnag/android/DefaultDelivery.kt | 2 +-
+-- 1 file changed, 1 insertion(+), 1 deletion(-)
+--
+--diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
+--index a7995164cb4e..5620f0bacd80 100644
+----- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
+--+++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt
+--@@ -64,7 +64,7 @@ internal class DefaultDelivery(
+-- return DeliveryStatus.UNDELIVERED
+-- } catch (exception: IOException) {
+-- logger.w("IOException encountered in request", exception)
+--- return DeliveryStatus.UNDELIVERED
+--+ return DeliveryStatus.FAILURE
+-- } catch (exception: Exception) {
+-- logger.w("Unexpected error delivering payload", exception)
+-- return DeliveryStatus.FAILURE