From c8cf13b66fe869b78c6568dc1d00e3925374c72e Mon Sep 17 00:00:00 2001 From: M66B Date: Sun, 31 Dec 2023 11:34:04 +0100 Subject: [PATCH] Updated Bugsnag client --- app/build.gradle | 2 +- .../com/bugsnag/android/BugsnagEventMapper.kt | 4 +- .../java/com/bugsnag/android/CallbackState.kt | 7 +- .../main/java/com/bugsnag/android/Client.java | 24 +- .../com/bugsnag/android/ConfigInternal.kt | 13 +- .../com/bugsnag/android/Configuration.java | 92 ++---- .../java/com/bugsnag/android/ErrorType.kt | 5 + .../main/java/com/bugsnag/android/Event.java | 3 +- .../java/com/bugsnag/android/EventInternal.kt | 20 +- .../com/bugsnag/android/EventStorageModule.kt | 11 +- .../java/com/bugsnag/android/EventStore.java | 299 ------------------ .../java/com/bugsnag/android/EventStore.kt | 256 +++++++++++++++ ...tureFlagAware.java => FeatureFlagAware.kt} | 19 +- .../java/com/bugsnag/android/FeatureFlags.kt | 1 - .../java/com/bugsnag/android/FileStore.java | 234 -------------- .../java/com/bugsnag/android/FileStore.kt | 194 ++++++++++++ .../bugsnag/android/ForegroundDetector.java | 81 ----- .../android/InternalReportDelegate.java | 2 +- .../com/bugsnag/android/LastRunInfoStore.kt | 2 +- .../bugsnag/android/ManifestConfigLoader.kt | 25 +- .../main/java/com/bugsnag/android/Metadata.kt | 3 +- .../com/bugsnag/android/NativeInterface.java | 24 +- .../main/java/com/bugsnag/android/Notifier.kt | 2 +- .../com/bugsnag/android/ObjectJsonStreamer.kt | 5 +- ...bCallback.java => OnBreadcrumbCallback.kt} | 23 +- .../com/bugsnag/android/OnErrorCallback.java | 24 -- .../com/bugsnag/android/OnErrorCallback.kt | 22 ++ .../com/bugsnag/android/OnSendCallback.java | 12 - .../com/bugsnag/android/OnSendCallback.kt | 10 + .../bugsnag/android/OnSessionCallback.java | 23 -- .../com/bugsnag/android/OnSessionCallback.kt | 20 ++ .../bugsnag/android/SessionFilenameInfo.kt | 2 +- .../android/SessionLifecycleCallback.kt | 37 --- .../com/bugsnag/android/SessionStore.java | 67 ---- .../java/com/bugsnag/android/SessionStore.kt | 59 ++++ .../com/bugsnag/android/SessionTracker.java | 72 ++--- .../com/bugsnag/android/SeverityReason.java | 1 + .../java/com/bugsnag/android/Stackframe.kt | 8 +- .../java/com/bugsnag/android/StorageModule.kt | 8 +- .../android/SynchronizedStreamableStore.kt | 2 +- .../main/java/com/bugsnag/android/Thread.java | 40 ++- .../com/bugsnag/android/ThreadInternal.kt | 4 +- .../java/com/bugsnag/android/ThreadState.kt | 68 +++- .../java/com/bugsnag/android/ThreadType.kt | 31 -- .../java/com/bugsnag/android/UserStore.kt | 18 +- .../internal/AbstractStartupProvider.kt | 68 ++++ .../internal/BugsnagContentProvider.kt | 13 + .../android/internal/BugsnagStoreMigrator.kt | 30 ++ .../android/internal/ByteArrayExtensions.kt | 15 + .../android/internal/DexBuildIdGenerator.kt | 108 +++++++ .../android/internal/ForegroundDetector.kt | 225 +++++++++++++ .../android/internal/ImmutableConfig.kt | 64 +++- .../android/internal/StateObserver.java | 14 - .../bugsnag/android/internal/StateObserver.kt | 11 + .../android/internal/dag/ConfigModule.kt | 6 +- app/src/main/java/eu/faircode/email/Log.java | 67 ++-- 56 files changed, 1437 insertions(+), 1063 deletions(-) delete mode 100644 app/src/main/java/com/bugsnag/android/EventStore.java create mode 100644 app/src/main/java/com/bugsnag/android/EventStore.kt rename app/src/main/java/com/bugsnag/android/{FeatureFlagAware.java => FeatureFlagAware.kt} (78%) delete mode 100644 app/src/main/java/com/bugsnag/android/FileStore.java create mode 100644 app/src/main/java/com/bugsnag/android/FileStore.kt delete mode 100644 app/src/main/java/com/bugsnag/android/ForegroundDetector.java rename app/src/main/java/com/bugsnag/android/{OnBreadcrumbCallback.java => OnBreadcrumbCallback.kt} (62%) delete mode 100644 app/src/main/java/com/bugsnag/android/OnErrorCallback.java create mode 100644 app/src/main/java/com/bugsnag/android/OnErrorCallback.kt delete mode 100644 app/src/main/java/com/bugsnag/android/OnSendCallback.java create mode 100644 app/src/main/java/com/bugsnag/android/OnSendCallback.kt delete mode 100644 app/src/main/java/com/bugsnag/android/OnSessionCallback.java create mode 100644 app/src/main/java/com/bugsnag/android/OnSessionCallback.kt delete mode 100644 app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt delete mode 100644 app/src/main/java/com/bugsnag/android/SessionStore.java create mode 100644 app/src/main/java/com/bugsnag/android/SessionStore.kt delete mode 100644 app/src/main/java/com/bugsnag/android/ThreadType.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/AbstractStartupProvider.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt create mode 100644 app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt delete mode 100644 app/src/main/java/com/bugsnag/android/internal/StateObserver.java create mode 100644 app/src/main/java/com/bugsnag/android/internal/StateObserver.kt diff --git a/app/build.gradle b/app/build.gradle index 2ef26e917f..82b56dea87 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -551,7 +551,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.31.3" + def bugsnag_version = "6.1.0" def biweekly_version = "0.6.7" def vcard_version = "0.12.1" def relinker_version = "1.4.5" diff --git a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index 61f74229c5..c8a7763932 100644 --- a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -177,9 +177,9 @@ internal class BugsnagEventMapper( @Suppress("UNCHECKED_CAST") internal fun convertThread(thread: Map): ThreadInternal { return ThreadInternal( - (thread["id"] as? Number)?.toLong() ?: 0, + thread["id"].toString(), thread.readEntry("name"), - ThreadType.fromDescriptor(thread.readEntry("type")) ?: ThreadType.ANDROID, + ErrorType.fromDescriptor(thread.readEntry("type")) ?: ErrorType.ANDROID, thread["errorReportingThread"] == true, thread.readEntry("state"), (thread["stacktrace"] as? List>)?.let { convertStacktrace(it) } diff --git a/app/src/main/java/com/bugsnag/android/CallbackState.kt b/app/src/main/java/com/bugsnag/android/CallbackState.kt index facd203e80..2625130448 100644 --- a/app/src/main/java/com/bugsnag/android/CallbackState.kt +++ b/app/src/main/java/com/bugsnag/android/CallbackState.kt @@ -8,7 +8,7 @@ internal data class CallbackState( val onErrorTasks: MutableCollection = CopyOnWriteArrayList(), val onBreadcrumbTasks: MutableCollection = CopyOnWriteArrayList(), val onSessionTasks: MutableCollection = CopyOnWriteArrayList(), - val onSendTasks: MutableCollection = CopyOnWriteArrayList() + val onSendTasks: MutableList = CopyOnWriteArrayList() ) : CallbackAware { private var internalMetrics: InternalMetrics = InternalMetricsNoop() @@ -67,6 +67,11 @@ internal data class CallbackState( } } + fun addPreOnSend(onSend: OnSendCallback) { + onSendTasks.add(0, onSend) + internalMetrics.notifyAddCallback(onSendName) + } + fun removeOnSend(onSend: OnSendCallback) { if (onSendTasks.remove(onSend)) { internalMetrics.notifyRemoveCallback(onSendName) diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index c62c784cf4..afc6675044 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -3,6 +3,8 @@ package com.bugsnag.android; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; import com.bugsnag.android.internal.BackgroundTaskService; +import com.bugsnag.android.internal.BugsnagStoreMigrator; +import com.bugsnag.android.internal.ForegroundDetector; import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.InternalMetrics; import com.bugsnag.android.internal.InternalMetricsImpl; @@ -33,6 +35,7 @@ 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. @@ -143,7 +146,13 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF }); // set sensible defaults for delivery/project packages etc if not set - ConfigModule configModule = new ConfigModule(contextModule, configuration, connectivity); + ConfigModule configModule = new ConfigModule( + contextModule, + configuration, + connectivity, + bgTaskService + ); + immutableConfig = configModule.getConfig(); logger = immutableConfig.getLogger(); @@ -159,6 +168,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF } + BugsnagStoreMigrator.moveToNewDirectory( + immutableConfig.getPersistenceDirectory().getValue()); + // setup storage as soon as possible final StorageModule storageModule = new StorageModule(appContext, immutableConfig, logger); @@ -314,8 +326,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF void registerLifecycleCallbacks() { if (appContext instanceof Application) { Application application = (Application) appContext; - SessionLifecycleCallback sessionCb = new SessionLifecycleCallback(sessionTracker); - application.registerActivityLifecycleCallbacks(sessionCb); + ForegroundDetector.registerOn(application); + ForegroundDetector.registerActivityCallbacks(sessionTracker); if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector( @@ -792,7 +804,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF @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(); + Collection redactedKeys = metadataState.getMetadata().getRedactedKeys(); event.setRedactedKeys(redactedKeys); // get session for event @@ -1182,4 +1194,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF 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/ConfigInternal.kt b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt index bd1e449895..5f83006426 100644 --- a/app/src/main/java/com/bugsnag/android/ConfigInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ConfigInternal.kt @@ -3,9 +3,10 @@ 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 + var apiKey: String? ) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware { private var user = User() @@ -23,7 +24,7 @@ internal class ConfigInternal( var versionCode: Int? = 0 var releaseStage: String? = null var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS - var persistUser: Boolean = false + var persistUser: Boolean = true var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS @@ -42,16 +43,17 @@ internal class ConfigInternal( 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 + var redactedKeys: Set get() = metadataState.metadata.redactedKeys set(value) { metadataState.metadata.redactedKeys = value } - var discardClasses: Set = emptySet() + var discardClasses: Set = emptySet() var enabledReleaseStages: Set? = null var enabledBreadcrumbTypes: Set? = null var telemetry: Set = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE) @@ -138,6 +140,8 @@ internal class ConfigInternal( "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) @@ -152,6 +156,7 @@ internal class ConfigInternal( 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 diff --git a/app/src/main/java/com/bugsnag/android/Configuration.java b/app/src/main/java/com/bugsnag/android/Configuration.java index f949147306..ebdc8cdc7b 100644 --- a/app/src/main/java/com/bugsnag/android/Configuration.java +++ b/app/src/main/java/com/bugsnag/android/Configuration.java @@ -5,11 +5,11 @@ import android.content.Context; import androidx.annotation.IntRange; import androidx.annotation.NonNull; import androidx.annotation.Nullable; -import androidx.annotation.VisibleForTesting; import java.io.File; import java.util.Map; import java.util.Set; +import java.util.regex.Pattern; /** * User-specified configuration storage object, contains information @@ -20,7 +20,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F private static final int MIN_BREADCRUMBS = 0; private static final int MAX_BREADCRUMBS = 500; - private static final int VALID_API_KEY_LEN = 32; private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0; final ConfigInternal impl; @@ -29,7 +28,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * Constructs a new Configuration object with default values. */ public Configuration(@NonNull String apiKey) { - validateApiKey(apiKey); impl = new ConfigInternal(apiKey); } @@ -47,32 +45,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F return ConfigInternal.load(context, apiKey); } - private void validateApiKey(String value) { - if (isInvalidApiKey(value)) { - DebugLogger.INSTANCE.w("Invalid configuration. " - + "apiKey should be a 32-character hexademical string, got " + value); - } - } - - @VisibleForTesting - static boolean isInvalidApiKey(String apiKey) { - if (Intrinsics.isEmpty(apiKey)) { - throw new 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. - for (int k = 0; k < VALID_API_KEY_LEN; k++) { - char chr = apiKey.charAt(k); - if (!Character.isDigit(chr) && (chr < 'a' || chr > 'f')) { - return true; - } - } - return false; - } - private void logNull(String property) { getLogger().e("Invalid null value supplied to config." + property + ", ignoring"); } @@ -89,7 +61,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * Changes the API key used for events sent to Bugsnag. */ public void setApiKey(@NonNull String apiKey) { - validateApiKey(apiKey); impl.setApiKey(apiKey); } @@ -244,28 +215,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F impl.setPersistenceDirectory(directory); } - /** - * Deprecated. Use {@link #getLaunchDurationMillis()} instead. - */ - @Deprecated - public long getLaunchCrashThresholdMs() { - getLogger().w("The launchCrashThresholdMs configuration option is deprecated " - + "and will be removed in a future release. Please use " - + "launchDurationMillis instead."); - return getLaunchDurationMillis(); - } - - /** - * Deprecated. Use {@link #setLaunchDurationMillis(long)} instead. - */ - @Deprecated - public void setLaunchCrashThresholdMs(long launchCrashThresholdMs) { - getLogger().w("The launchCrashThresholdMs configuration option is deprecated " - + "and will be removed in a future release. Please use " - + "launchDurationMillis instead."); - setLaunchDurationMillis(launchCrashThresholdMs); - } - /** * Sets whether or not Bugsnag should send crashes synchronously that occurred during * the application's launch period. By default this behavior is enabled. @@ -541,8 +490,8 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * * By default, 32 events are persisted. */ - public int getMaxPersistedEvents() { - return impl.getMaxPersistedEvents(); + public int getMaxPersistedEvents() { + return impl.getMaxPersistedEvents(); } /** @@ -587,6 +536,31 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F } } + + /** + * 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. @@ -671,7 +645,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * By default, redactedKeys is set to "password" */ @NonNull - public Set getRedactedKeys() { + public Set getRedactedKeys() { return impl.getRedactedKeys(); } @@ -683,7 +657,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * * By default, redactedKeys is set to "password" */ - public void setRedactedKeys(@NonNull Set redactedKeys) { + public void setRedactedKeys(@NonNull Set redactedKeys) { if (CollectionUtils.containsNullElements(redactedKeys)) { logNull("redactedKeys"); } else { @@ -697,7 +671,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * match against the canonical class name. */ @NonNull - public Set getDiscardClasses() { + public Set getDiscardClasses() { return impl.getDiscardClasses(); } @@ -706,7 +680,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F * 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) { + public void setDiscardClasses(@NonNull Set discardClasses) { if (CollectionUtils.containsNullElements(discardClasses)) { logNull("discardClasses"); } else { @@ -1164,7 +1138,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F /** * Whether Bugsnag should try to send crashing errors prior to app termination. - * + * * @see #setAttemptDeliveryOnCrash(boolean) */ public boolean isAttemptDeliveryOnCrash() { diff --git a/app/src/main/java/com/bugsnag/android/ErrorType.kt b/app/src/main/java/com/bugsnag/android/ErrorType.kt index 299b1a0b45..891b3b308a 100644 --- a/app/src/main/java/com/bugsnag/android/ErrorType.kt +++ b/app/src/main/java/com/bugsnag/android/ErrorType.kt @@ -5,6 +5,11 @@ package com.bugsnag.android */ enum class ErrorType(internal val desc: String) { + /** + * An error with an unknown type or source + */ + UNKNOWN(""), + /** * An error captured from Android's JVM layer */ diff --git a/app/src/main/java/com/bugsnag/android/Event.java b/app/src/main/java/com/bugsnag/android/Event.java index 181afeb75a..ac498bb174 100644 --- a/app/src/main/java/com/bugsnag/android/Event.java +++ b/app/src/main/java/com/bugsnag/android/Event.java @@ -10,6 +10,7 @@ import java.io.IOException; import java.util.Collection; import java.util.List; import java.util.Map; +import java.util.regex.Pattern; /** * An Event object represents a Throwable captured by Bugsnag and is available as a parameter on @@ -418,7 +419,7 @@ public class Event implements JsonStream.Streamable, MetadataAware, UserAware, F return impl; } - void setRedactedKeys(Collection redactedKeys) { + void setRedactedKeys(Collection redactedKeys) { impl.setRedactedKeys(redactedKeys); } diff --git a/app/src/main/java/com/bugsnag/android/EventInternal.kt b/app/src/main/java/com/bugsnag/android/EventInternal.kt index a7224b2f93..4a1881f71d 100644 --- a/app/src/main/java/com/bugsnag/android/EventInternal.kt +++ b/app/src/main/java/com/bugsnag/android/EventInternal.kt @@ -6,6 +6,7 @@ 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.regex.Pattern internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware { @@ -39,7 +40,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata apiKey: String, logger: Logger, breadcrumbs: MutableList = mutableListOf(), - discardClasses: Set = setOf(), + discardClasses: Set = setOf(), errors: MutableList = mutableListOf(), metadata: Metadata = Metadata(), featureFlags: FeatureFlags = FeatureFlags(), @@ -48,7 +49,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), threads: MutableList = mutableListOf(), user: User = User(), - redactionKeys: Set? = null + redactionKeys: Set? = null ) { this.logger = logger this.apiKey = apiKey @@ -74,7 +75,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata val logger: Logger val metadata: Metadata val featureFlags: FeatureFlags - private val discardClasses: Set + private val discardClasses: Set internal var projectPackages: Collection private val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer().apply { @@ -105,7 +106,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata var groupingHash: String? = null var context: String? = null - var redactedKeys: Collection + var redactedKeys: Collection get() = jsonStreamer.redactedKeys set(value) { jsonStreamer.redactedKeys = value.toSet() @@ -125,7 +126,11 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata protected fun shouldDiscardClass(): Boolean { return when { errors.isEmpty() -> true - else -> errors.any { discardClasses.contains(it.errorClass) } + else -> errors.any { error -> + discardClasses.any { pattern -> + pattern.matcher(error.errorClass).matches() + } + } } } @@ -304,9 +309,10 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata override fun addFeatureFlag(name: String) = featureFlags.addFeatureFlag(name) - override fun addFeatureFlag(name: String, variant: String?) = featureFlags.addFeatureFlag(name, variant) + override fun addFeatureFlag(name: String, variant: String?) = + featureFlags.addFeatureFlag(name, variant) - override fun addFeatureFlags(featureFlags: MutableIterable) = + override fun addFeatureFlags(featureFlags: Iterable) = this.featureFlags.addFeatureFlags(featureFlags) override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name) diff --git a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt index c0573d6a3e..bad4b68c39 100644 --- a/app/src/main/java/com/bugsnag/android/EventStorageModule.kt +++ b/app/src/main/java/com/bugsnag/android/EventStorageModule.kt @@ -37,5 +37,14 @@ internal class EventStorageModule( ) else null } - val eventStore by future { EventStore(cfg, cfg.logger, notifier, bgTaskService, delegate, callbackState) } + 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 deleted file mode 100644 index 6d1f1ab574..0000000000 --- a/app/src/main/java/com/bugsnag/android/EventStore.java +++ /dev/null @@ -1,299 +0,0 @@ -package com.bugsnag.android; - -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.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.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 which couldn't be sent immediately due to - * lack of network connectivity. - */ -class EventStore extends FileStore { - - private static final long LAUNCH_CRASH_TIMEOUT_MS = 2000; - - private final ImmutableConfig config; - private final Delegate delegate; - private final Notifier notifier; - private final BackgroundTaskService bgTaskSevice; - private final CallbackState callbackState; - final Logger logger; - - static final Comparator EVENT_COMPARATOR = new Comparator() { - @Override - public int compare(File lhs, File rhs) { - if (lhs == null && rhs == null) { - return 0; - } - if (lhs == null) { - return 1; - } - if (rhs == null) { - return -1; - } - return lhs.compareTo(rhs); - } - }; - - EventStore(@NonNull ImmutableConfig config, - @NonNull Logger logger, - Notifier notifier, - BackgroundTaskService bgTaskSevice, - Delegate delegate, - CallbackState callbackState) { - super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-errors"), - config.getMaxPersistedEvents(), - EVENT_COMPARATOR, - logger, - delegate); - this.config = config; - this.logger = logger; - this.delegate = delegate; - this.notifier = notifier; - this.bgTaskSevice = bgTaskSevice; - this.callbackState = callbackState; - } - - /** - * Flush startup crashes synchronously on the main thread - */ - void flushOnLaunch() { - if (!config.getSendLaunchCrashesSynchronously()) { - return; - } - Future future = null; - try { - future = bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() { - @Override - public void run() { - flushLaunchCrashReport(); - } - }); - } catch (RejectedExecutionException exc) { - logger.d("Failed to flush launch crash reports, continuing.", exc); - } - - try { - if (future != null) { - future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS); - } - } catch (InterruptedException | ExecutionException | TimeoutException exc) { - logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc); - } - } - - void flushLaunchCrashReport() { - List storedFiles = findStoredFiles(); - File launchCrashReport = findLaunchCrashReport(storedFiles); - - // cancel non-launch crash reports - if (launchCrashReport != null) { - storedFiles.remove(launchCrashReport); - } - cancelQueuedFiles(storedFiles); - - if (launchCrashReport != null) { - logger.i("Attempting to send the most recent launch crash report"); - flushReports(Collections.singletonList(launchCrashReport)); - logger.i("Continuing with Bugsnag initialisation"); - } else { - logger.d("No startupcrash events to flush to Bugsnag."); - } - } - - @Nullable - File findLaunchCrashReport(Collection storedFiles) { - List launchCrashes = new ArrayList<>(); - - for (File file : storedFiles) { - EventFilenameInfo filenameInfo = EventFilenameInfo.fromFile(file, config); - if (filenameInfo.isLaunchCrashReport()) { - launchCrashes.add(file); - } - } - - // sort to get most recent timestamp - Collections.sort(launchCrashes, EVENT_COMPARATOR); - return launchCrashes.isEmpty() ? null : launchCrashes.get(launchCrashes.size() - 1); - } - - @Nullable - Future writeAndDeliver(@NonNull final JsonStream.Streamable streamable) { - final String filename = write(streamable); - - if (filename != null) { - try { - return bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Callable() { - public String call() { - flushEventFile(new File(filename)); - return filename; - } - }); - } catch (RejectedExecutionException exception) { - logger.w("Failed to flush all on-disk errors, retaining unsent errors for later."); - } - } - - return null; - } - - /** - * Flush any on-disk errors to Bugsnag - */ - void flushAsync() { - try { - bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Runnable() { - @Override - public void run() { - List storedFiles = findStoredFiles(); - if (storedFiles.isEmpty()) { - logger.d("No regular events to flush to Bugsnag."); - } - flushReports(storedFiles); - } - }); - } catch (RejectedExecutionException exception) { - logger.w("Failed to flush all on-disk errors, retaining unsent errors for later."); - } - } - - void flushReports(Collection storedReports) { - if (!storedReports.isEmpty()) { - int size = storedReports.size(); - logger.i("Sending " + size + " saved error(s) to Bugsnag"); - - for (File eventFile : storedReports) { - flushEventFile(eventFile); - } - } - } - - void flushEventFile(File eventFile) { - try { - EventFilenameInfo eventInfo = EventFilenameInfo.fromFile(eventFile, config); - String apiKey = eventInfo.getApiKey(); - EventPayload payload = createEventPayload(eventFile, apiKey); - - if (payload == null) { - deleteStoredFiles(Collections.singleton(eventFile)); - } else { - deliverEventPayload(eventFile, payload); - } - } catch (Exception exception) { - handleEventFlushFailure(exception, eventFile); - } - } - - private void deliverEventPayload(File eventFile, EventPayload payload) { - DeliveryParams deliveryParams = config.getErrorApiDeliveryParams(payload); - Delivery delivery = config.getDelivery(); - DeliveryStatus deliveryStatus = delivery.deliver(payload, deliveryParams); - - switch (deliveryStatus) { - case DELIVERED: - deleteStoredFiles(Collections.singleton(eventFile)); - logger.i("Deleting sent error file " + eventFile.getName()); - break; - case UNDELIVERED: - 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"); - handleEventFlushFailure(exc, eventFile); - break; - default: - break; - } - } - - @Nullable - private EventPayload createEventPayload(File eventFile, String apiKey) { - MarshalledEventSource eventSource = new MarshalledEventSource(eventFile, apiKey, logger); - - try { - if (!callbackState.runOnSendTasks(eventSource, logger)) { - // do not send the payload at all, we must block sending - return null; - } - } catch (Exception ioe) { - eventSource.clear(); - } - - Event processedEvent = eventSource.getEvent(); - if (processedEvent != null) { - apiKey = processedEvent.getApiKey(); - return new EventPayload(apiKey, processedEvent, null, notifier, config); - } else { - return new EventPayload(apiKey, null, eventFile, notifier, config); - } - } - - private void handleEventFlushFailure(Exception exc, File eventFile) { - if (delegate != null) { - delegate.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization"); - } - deleteStoredFiles(Collections.singleton(eventFile)); - } - - @NonNull - @Override - String getFilename(Object object) { - EventFilenameInfo eventInfo - = EventFilenameInfo.fromEvent(object, null, config); - return eventInfo.encode(); - } - - String getNdkFilename(Object object, String apiKey) { - EventFilenameInfo eventInfo - = 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/EventStore.kt b/app/src/main/java/com/bugsnag/android/EventStore.kt new file mode 100644 index 0000000000..bb1172c769 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/EventStore.kt @@ -0,0 +1,256 @@ +package com.bugsnag.android + +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.ImmutableConfig +import com.bugsnag.android.internal.TaskType +import java.io.File +import java.util.Calendar +import java.util.Comparator +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 which couldn't be sent immediately due to + * lack of network connectivity. + */ +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, + EVENT_COMPARATOR, + 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 + */ + 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 { + future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS) + } catch (exc: InterruptedException) { + logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc) + } catch (exc: ExecutionException) { + logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc) + } catch (exc: TimeoutException) { + logger.d("Failed to send launch crash reports within 2s 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) { + 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) { + delegate?.onErrorIOFailure(exc, eventFile, "Crash Report Deserialization") + 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/FeatureFlagAware.java b/app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt similarity index 78% rename from app/src/main/java/com/bugsnag/android/FeatureFlagAware.java rename to app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt index b2571da1c9..3703eb0596 100644 --- a/app/src/main/java/com/bugsnag/android/FeatureFlagAware.java +++ b/app/src/main/java/com/bugsnag/android/FeatureFlagAware.kt @@ -1,9 +1,6 @@ -package com.bugsnag.android; +package com.bugsnag.android -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -interface FeatureFlagAware { +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. @@ -11,7 +8,7 @@ interface FeatureFlagAware { * @param name the name of the feature flag to add * @see #addFeatureFlag(String, String) */ - void addFeatureFlag(@NonNull String name); + fun addFeatureFlag(name: String) /** * Add a single feature flag with an optional variant. If there is an existing feature @@ -22,7 +19,7 @@ interface FeatureFlagAware { * @param variant the variant to set the feature flag to, or {@code null} to specify a feature * flag with no variant */ - void addFeatureFlag(@NonNull String name, @Nullable String variant); + fun addFeatureFlag(name: String, variant: String?) /** * Add a collection of feature flags. This method behaves exactly the same as calling @@ -31,7 +28,7 @@ interface FeatureFlagAware { * @param featureFlags the feature flags to add * @see #addFeatureFlag(String, String) */ - void addFeatureFlags(@NonNull Iterable featureFlags); + fun addFeatureFlags(featureFlags: Iterable) /** * Remove a single feature flag regardless of its current status. This will stop the specified @@ -40,10 +37,10 @@ interface FeatureFlagAware { * * @param name the name of the feature flag to remove */ - void clearFeatureFlag(@NonNull String name); + fun clearFeatureFlag(name: String) /** * Clear all of the feature flags. This will stop all feature flags from being reported. */ - void clearFeatureFlags(); -} \ No newline at end of file + fun clearFeatureFlags() +} diff --git a/app/src/main/java/com/bugsnag/android/FeatureFlags.kt b/app/src/main/java/com/bugsnag/android/FeatureFlags.kt index 97f3ed47d6..efb57506ec 100644 --- a/app/src/main/java/com/bugsnag/android/FeatureFlags.kt +++ b/app/src/main/java/com/bugsnag/android/FeatureFlags.kt @@ -12,7 +12,6 @@ internal class FeatureFlags( } @Synchronized override fun addFeatureFlag(name: String, variant: String?) { - store.remove(name) store[name] = variant ?: emptyVariant } diff --git a/app/src/main/java/com/bugsnag/android/FileStore.java b/app/src/main/java/com/bugsnag/android/FileStore.java deleted file mode 100644 index b613cea988..0000000000 --- a/app/src/main/java/com/bugsnag/android/FileStore.java +++ /dev/null @@ -1,234 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; -import androidx.annotation.Nullable; - -import java.io.BufferedWriter; -import java.io.File; -import java.io.FileNotFoundException; -import java.io.FileOutputStream; -import java.io.OutputStreamWriter; -import java.io.Writer; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.Collection; -import java.util.Collections; -import java.util.Comparator; -import java.util.List; -import java.util.concurrent.ConcurrentSkipListSet; -import java.util.concurrent.locks.Lock; -import java.util.concurrent.locks.ReentrantLock; - -abstract class FileStore { - - interface Delegate { - - /** - * Invoked when an error report is not (de)serialized correctly - * - * @param exception the error encountered reading/delivering the file - * @param errorFile file which could not be (de)serialized correctly - * @param context the context used to group the exception - */ - void onErrorIOFailure(Exception exception, File errorFile, String context); - } - - private final File storageDir; - private final int maxStoreCount; - private final Comparator comparator; - - private final Lock lock = new ReentrantLock(); - private final Collection queuedFiles = new ConcurrentSkipListSet<>(); - protected final Logger logger; - private final EventStore.Delegate delegate; - - FileStore(@NonNull File storageDir, - int maxStoreCount, - Comparator comparator, - Logger logger, - Delegate delegate) { - this.maxStoreCount = maxStoreCount; - this.comparator = comparator; - this.logger = logger; - this.delegate = delegate; - this.storageDir = storageDir; - isStorageDirValid(storageDir); - } - - /** - * Checks whether the storage directory is a writable directory. If it is not, - * this method will attempt to create the directory. - * - * If the directory could not be created then an error will be logged. - */ - private boolean isStorageDirValid(@NonNull File storageDir) { - try { - storageDir.mkdirs(); - } catch (Exception exception) { - this.logger.e("Could not prepare file storage directory", exception); - return false; - } - return true; - } - - void enqueueContentForDelivery(String content, String filename) { - if (!isStorageDirValid(storageDir)) { - return; - } - discardOldestFileIfNeeded(); - - lock.lock(); - Writer out = null; - String filePath = new File(storageDir, filename).getAbsolutePath(); - try { - FileOutputStream fos = new FileOutputStream(filePath); - out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); - out.write(content); - } catch (Exception exc) { - File eventFile = new File(filePath); - - if (delegate != null) { - delegate.onErrorIOFailure(exc, eventFile, "NDK Crash report copy"); - } - - IOUtils.deleteFile(eventFile, logger); - } finally { - try { - if (out != null) { - out.close(); - } - } catch (Exception exception) { - logger.w("Failed to close unsent payload writer: " + filename, exception); - } - lock.unlock(); - } - } - - @Nullable - String write(@NonNull JsonStream.Streamable streamable) { - if (!isStorageDirValid(storageDir)) { - return null; - } - if (maxStoreCount == 0) { - return null; - } - discardOldestFileIfNeeded(); - String filename = new File(storageDir, getFilename(streamable)).getAbsolutePath(); - - JsonStream stream = null; - lock.lock(); - - try { - FileOutputStream fos = new FileOutputStream(filename); - Writer out = new BufferedWriter(new OutputStreamWriter(fos, "UTF-8")); - stream = new JsonStream(out); - stream.value(streamable); - logger.i("Saved unsent payload to disk: '" + filename + '\''); - return filename; - } catch (FileNotFoundException exc) { - logger.w("Ignoring FileNotFoundException - unable to create file", exc); - } catch (Exception exc) { - File eventFile = new File(filename); - - if (delegate != null) { - delegate.onErrorIOFailure(exc, eventFile, "Crash report serialization"); - } - - IOUtils.deleteFile(eventFile, logger); - } finally { - IOUtils.closeQuietly(stream); - lock.unlock(); - } - return null; - } - - void discardOldestFileIfNeeded() { - // Limit number of saved payloads to prevent disk space issues - if (isStorageDirValid(storageDir)) { - File[] listFiles = storageDir.listFiles(); - - if (listFiles == null) { - return; - } - - List files = new ArrayList<>(Arrays.asList(listFiles)); - - if (files.size() >= maxStoreCount) { - // Sort files then delete the first one (oldest timestamp) - Collections.sort(files, comparator); - - for (int k = 0; k < files.size() && files.size() >= maxStoreCount; k++) { - File oldestFile = files.get(k); - - if (!queuedFiles.contains(oldestFile)) { - logger.w("Discarding oldest error as stored " - + "error limit reached: '" + oldestFile.getPath() + '\''); - deleteStoredFiles(Collections.singleton(oldestFile)); - files.remove(k); - k--; - } - } - } - } - } - - @NonNull - abstract String getFilename(Object object); - - List findStoredFiles() { - lock.lock(); - try { - List files = new ArrayList<>(); - - if (isStorageDirValid(storageDir)) { - File[] values = storageDir.listFiles(); - - if (values != null) { - for (File value : values) { - // delete any tombstoned/empty files, as they contain no useful info - if (value.length() == 0) { - if (!value.delete()) { - value.deleteOnExit(); - } - } else if (value.isFile() && !queuedFiles.contains(value)) { - files.add(value); - } - } - } - } - queuedFiles.addAll(files); - return files; - } finally { - lock.unlock(); - } - } - - void cancelQueuedFiles(Collection files) { - lock.lock(); - try { - if (files != null) { - queuedFiles.removeAll(files); - } - } finally { - lock.unlock(); - } - } - - void deleteStoredFiles(Collection storedFiles) { - lock.lock(); - try { - if (storedFiles != null) { - queuedFiles.removeAll(storedFiles); - - for (File storedFile : storedFiles) { - if (!storedFile.delete()) { - storedFile.deleteOnExit(); - } - } - } - } finally { - lock.unlock(); - } - } - -} diff --git a/app/src/main/java/com/bugsnag/android/FileStore.kt b/app/src/main/java/com/bugsnag/android/FileStore.kt new file mode 100644 index 0000000000..1e0ba13731 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/FileStore.kt @@ -0,0 +1,194 @@ +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.Collections +import java.util.Comparator +import java.util.concurrent.ConcurrentSkipListSet +import java.util.concurrent.locks.Lock +import java.util.concurrent.locks.ReentrantLock + +internal abstract class FileStore( + private val storageDir: File, + private val maxStoreCount: Int, + private val comparator: Comparator, + 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() + + init { + isStorageDirValid(storageDir) + } + + /** + * Checks whether the storage directory is a writable directory. If it is not, + * this method will attempt to create the directory. + * + * If the directory could not be created then an error will be logged. + */ + private 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 + val files: ArrayList = arrayListOf(*listFiles) + if (files.size >= maxStoreCount) { + // Sort files then delete the first one (oldest timestamp) + Collections.sort(files, comparator) + var k = 0 + while (k < files.size && files.size >= maxStoreCount) { + val oldestFile = files[k] + if (!queuedFiles.contains(oldestFile)) { + logger.w( + "Discarding oldest error as stored " + + "error limit reached: '" + oldestFile.path + '\'' + ) + deleteStoredFiles(setOf(oldestFile)) + files.removeAt(k) + k-- + } + k++ + } + } + } + } + + 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/ForegroundDetector.java b/app/src/main/java/com/bugsnag/android/ForegroundDetector.java deleted file mode 100644 index b3475f766c..0000000000 --- a/app/src/main/java/com/bugsnag/android/ForegroundDetector.java +++ /dev/null @@ -1,81 +0,0 @@ -package com.bugsnag.android; - -import static com.bugsnag.android.ContextExtensionsKt.getActivityManagerFrom; - -import android.app.ActivityManager; -import android.content.Context; -import android.os.Build; -import android.os.Process; - -import androidx.annotation.Nullable; - -import java.util.List; - -class ForegroundDetector { - - private static final int IMPORTANCE_FOREGROUND_SERVICE = 125; - - @Nullable - private final ActivityManager activityManager; - - ForegroundDetector(Context context) { - this.activityManager = getActivityManagerFrom(context); - } - - /** - * Determines whether or not the application is in the foreground, by using the process' - * importance as a proxy. - *

- * In the unlikely event that information about the process cannot be retrieved, this method - * will return null, and the 'inForeground' and 'durationInForeground' values will not be - * serialized in API calls. - * - * @return whether the application is in the foreground or not - */ - @Nullable - Boolean isInForeground() { - try { - ActivityManager.RunningAppProcessInfo info = getProcessInfo(); - - if (info != null) { - return info.importance <= IMPORTANCE_FOREGROUND_SERVICE; - } else { - return null; - } - } catch (RuntimeException exc) { - return null; - } - } - - private ActivityManager.RunningAppProcessInfo getProcessInfo() { - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - ActivityManager.RunningAppProcessInfo info = - new ActivityManager.RunningAppProcessInfo(); - ActivityManager.getMyMemoryState(info); - return info; - } else { - return getProcessInfoPreApi16(); - } - } - - @Nullable - private ActivityManager.RunningAppProcessInfo getProcessInfoPreApi16() { - if (activityManager == null) { - return null; - } - - List appProcesses - = activityManager.getRunningAppProcesses(); - - if (appProcesses != null) { - int pid = Process.myPid(); - - for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) { - if (pid == appProcess.pid) { - return appProcess; - } - } - } - return null; - } -} diff --git a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java b/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java index 0bf0d49a05..a9d6a08d93 100644 --- a/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java +++ b/app/src/main/java/com/bugsnag/android/InternalReportDelegate.java @@ -82,7 +82,7 @@ class InternalReportDelegate implements EventStore.Delegate { 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"); + File errDir = new File(cacheDir, "bugsnag/errors"); try { boolean tombstone = storageManager.isCacheBehaviorTombstone(errDir); diff --git a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt index 6309bbdc93..d5b1a93d42 100644 --- a/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt +++ b/app/src/main/java/com/bugsnag/android/LastRunInfoStore.kt @@ -16,7 +16,7 @@ private const val KEY_CRASHED_DURING_LAUNCH = "crashedDuringLaunch" */ internal class LastRunInfoStore(config: ImmutableConfig) { - val file: File = File(config.persistenceDirectory.value, "last-run-info") + val file: File = File(config.persistenceDirectory.value, "bugsnag/last-run-info") private val logger: Logger = config.logger private val lock = ReentrantReadWriteLock() diff --git a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt index b121cbf83a..8214f192df 100644 --- a/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt +++ b/app/src/main/java/com/bugsnag/android/ManifestConfigLoader.kt @@ -5,6 +5,7 @@ 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 { @@ -38,6 +39,7 @@ internal class ManifestConfigLoader { 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" @@ -80,10 +82,10 @@ internal class ManifestConfigLoader { 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() - ).toLong() + threadCollectionTimeLimitMillis = data.getLong( + THREAD_COLLECTION_TIME_LIMIT_MS, + threadCollectionTimeLimitMillis + ) launchDurationMillis = data.getInt( LAUNCH_DURATION_MILLIS, launchDurationMillis.toInt() @@ -135,9 +137,9 @@ internal class ManifestConfigLoader { if (data.containsKey(ENABLED_RELEASE_STAGES)) { enabledReleaseStages = getStrArray(data, ENABLED_RELEASE_STAGES, enabledReleaseStages) } - discardClasses = getStrArray(data, DISCARD_CLASSES, discardClasses) ?: emptySet() + discardClasses = getPatternSet(data, DISCARD_CLASSES, discardClasses) ?: emptySet() projectPackages = getStrArray(data, PROJECT_PACKAGES, emptySet()) ?: emptySet() - redactedKeys = getStrArray(data, REDACTED_KEYS, redactedKeys) ?: emptySet() + redactedKeys = getPatternSet(data, REDACTED_KEYS, redactedKeys) ?: emptySet() } } @@ -153,4 +155,15 @@ internal class ManifestConfigLoader { 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/Metadata.kt b/app/src/main/java/com/bugsnag/android/Metadata.kt index 7333c1185b..a69cdf47e8 100644 --- a/app/src/main/java/com/bugsnag/android/Metadata.kt +++ b/app/src/main/java/com/bugsnag/android/Metadata.kt @@ -6,6 +6,7 @@ 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 @@ -19,7 +20,7 @@ internal data class Metadata @JvmOverloads constructor( val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer() - var redactedKeys: Set + var redactedKeys: Set get() = jsonStreamer.redactedKeys set(value) { jsonStreamer.redactedKeys = value diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index bd81195584..6f622fdf27 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -19,6 +19,7 @@ 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 @@ -84,7 +85,7 @@ public class NativeInterface { } private static @NonNull File getNativeReportPath(@NonNull File persistenceDirectory) { - return new File(persistenceDirectory, "bugsnag-native"); + return new File(persistenceDirectory, "bugsnag/native"); } private static @NonNull File getPersistenceDirectory() { @@ -268,7 +269,7 @@ public class NativeInterface { /** * Add metadata to subsequent exception reports with a Hashmap */ - public static void addMetadata(@NonNull final String tab, + public static void addMetadata(@NonNull final String tab, @NonNull final Map metadata) { getClient().addMetadata(tab, metadata); } @@ -348,21 +349,30 @@ public class NativeInterface { */ @SuppressWarnings("unused") public static boolean isDiscardErrorClass(@NonNull String name) { - return getClient().getConfig().getDiscardClasses().contains(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()) { + 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); + 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); + ((Collection) dstValue).addAll((Collection) srcValue); } else { dst.put(key, srcValue); } @@ -394,7 +404,7 @@ public class NativeInterface { @SuppressWarnings("unchecked") Map staticDataMap = (Map) JsonHelper.INSTANCE.deserialize( - new ByteArrayInputStream(staticDataBytes)); + new ByteArrayInputStream(staticDataBytes)); deepMerge(staticDataMap, payloadMap); ByteArrayOutputStream os = new ByteArrayOutputStream(); JsonHelper.INSTANCE.serialize(payloadMap, os); diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 1e36c1483d..f63f29d49d 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.31.3", + var version: String = "6.1.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt index 9316067b81..5df61b8ae9 100644 --- a/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt +++ b/app/src/main/java/com/bugsnag/android/ObjectJsonStreamer.kt @@ -4,6 +4,7 @@ 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 { @@ -12,7 +13,7 @@ internal class ObjectJsonStreamer { internal const val OBJECT_PLACEHOLDER = "[OBJECT]" } - var redactedKeys = setOf("password") + var redactedKeys = setOf(Pattern.compile(".*password.*", Pattern.CASE_INSENSITIVE)) // Write complex/nested values to a JsonStreamer @Throws(IOException::class) @@ -66,5 +67,5 @@ internal class ObjectJsonStreamer { } // Should this key be redacted - private fun isRedactedKey(key: String) = redactedKeys.any { key.contains(it) } + private fun isRedactedKey(key: String) = redactedKeys.any { it.matcher(key).matches() } } diff --git a/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.java b/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt similarity index 62% rename from app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.java rename to app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt index dc4fb72d2d..fb9dedeb75 100644 --- a/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.java +++ b/app/src/main/java/com/bugsnag/android/OnBreadcrumbCallback.kt @@ -1,32 +1,31 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; +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. - *

+ * 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 * } * }) */ -public interface OnBreadcrumbCallback { - +fun interface OnBreadcrumbCallback { /** * Runs the "on breadcrumb" callback. If the callback returns - * false any further OnBreadcrumbCallback callbacks will not be called + * `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 */ - boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb); - + fun onBreadcrumb(breadcrumb: Breadcrumb): Boolean } diff --git a/app/src/main/java/com/bugsnag/android/OnErrorCallback.java b/app/src/main/java/com/bugsnag/android/OnErrorCallback.java deleted file mode 100644 index 4f20584796..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnErrorCallback.java +++ /dev/null @@ -1,24 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; - -/** - * 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. - */ -public 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 - */ - boolean onError(@NonNull Event event); -} diff --git a/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt b/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt new file mode 100644 index 0000000000..299066495a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/OnErrorCallback.kt @@ -0,0 +1,22 @@ +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.java b/app/src/main/java/com/bugsnag/android/OnSendCallback.java deleted file mode 100644 index 1856a328a3..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnSendCallback.java +++ /dev/null @@ -1,12 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; - -/** - * A callback to be invoked before an {@link Event} is uploaded to a server. Similar to - * {@link OnErrorCallback}, an {@code OnSendCallback} may modify the {@code Event} - * contents or even reject the entire payload by returning {@code false}. - */ -public interface OnSendCallback { - boolean onSend(@NonNull Event event); -} diff --git a/app/src/main/java/com/bugsnag/android/OnSendCallback.kt b/app/src/main/java/com/bugsnag/android/OnSendCallback.kt new file mode 100644 index 0000000000..ca40dc6fdf --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/OnSendCallback.kt @@ -0,0 +1,10 @@ +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.java b/app/src/main/java/com/bugsnag/android/OnSessionCallback.java deleted file mode 100644 index d54ed8fc90..0000000000 --- a/app/src/main/java/com/bugsnag/android/OnSessionCallback.java +++ /dev/null @@ -1,23 +0,0 @@ -package com.bugsnag.android; - -import androidx.annotation.NonNull; - -/** - * 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. - */ -public 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 - */ - boolean onSession(@NonNull Session session); -} diff --git a/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt b/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt new file mode 100644 index 0000000000..87475ef30a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/OnSessionCallback.kt @@ -0,0 +1,20 @@ +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/SessionFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt index 554b1ee0d7..3ea39917cd 100644 --- a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt +++ b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt @@ -35,7 +35,7 @@ internal data class SessionFilenameInfo( @JvmStatic fun defaultFilename( - obj: Any, + obj: Any?, config: ImmutableConfig ): SessionFilenameInfo { val sanitizedApiKey = when (obj) { diff --git a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt b/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt deleted file mode 100644 index 2299fa87c9..0000000000 --- a/app/src/main/java/com/bugsnag/android/SessionLifecycleCallback.kt +++ /dev/null @@ -1,37 +0,0 @@ -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) { - if (Build.VERSION.SDK_INT < Build.VERSION_CODES.Q) { - sessionTracker.onActivityStarted(activity.javaClass.simpleName) - } - } - - 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) {} - override fun onActivityPaused(activity: Activity) {} - override fun onActivitySaveInstanceState(activity: Activity, outState: Bundle) {} - override fun onActivityDestroyed(activity: Activity) {} -} diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java deleted file mode 100644 index 87e3524e28..0000000000 --- a/app/src/main/java/com/bugsnag/android/SessionStore.java +++ /dev/null @@ -1,67 +0,0 @@ -package com.bugsnag.android; - -import com.bugsnag.android.internal.ImmutableConfig; - -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; - -/** - * Store and flush Sessions which couldn't be sent immediately due to - * lack of network connectivity. - */ -class SessionStore extends FileStore { - - private final ImmutableConfig config; - static final Comparator SESSION_COMPARATOR = new Comparator() { - @Override - public int compare(File lhs, File rhs) { - if (lhs == null && rhs == null) { - return 0; - } - if (lhs == null) { - return 1; - } - if (rhs == null) { - return -1; - } - String lhsName = lhs.getName(); - String rhsName = rhs.getName(); - return lhsName.compareTo(rhsName); - } - }; - - SessionStore(@NonNull ImmutableConfig config, - @NonNull Logger logger, - @Nullable Delegate delegate) { - super(new File(config.getPersistenceDirectory().getValue(), "bugsnag-sessions"), - config.getMaxPersistedSessions(), - SESSION_COMPARATOR, - logger, - delegate); - this.config = config; - } - - @NonNull - @Override - String getFilename(Object object) { - SessionFilenameInfo sessionInfo - = SessionFilenameInfo.defaultFilename(object, config); - return sessionInfo.encode(); - } - - 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/SessionStore.kt b/app/src/main/java/com/bugsnag/android/SessionStore.kt new file mode 100644 index 0000000000..86519ac5af --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/SessionStore.kt @@ -0,0 +1,59 @@ +package com.bugsnag.android + +import com.bugsnag.android.SessionFilenameInfo.Companion.defaultFilename +import com.bugsnag.android.SessionFilenameInfo.Companion.findTimestampInFilename +import com.bugsnag.android.internal.ImmutableConfig +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( + private val config: ImmutableConfig, + logger: Logger, + delegate: Delegate? +) : FileStore( + File( + config.persistenceDirectory.value, "bugsnag/sessions" + ), + config.maxPersistedSessions, + SESSION_COMPARATOR, + 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, config) + 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 index cc736e1077..bb72ddc5dd 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -2,10 +2,11 @@ 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.os.SystemClock; +import android.app.Activity; import androidx.annotation.NonNull; import androidx.annotation.Nullable; @@ -19,9 +20,8 @@ import java.util.Deque; import java.util.List; import java.util.UUID; import java.util.concurrent.RejectedExecutionException; -import java.util.concurrent.atomic.AtomicLong; -class SessionTracker extends BaseObservable { +class SessionTracker extends BaseObservable implements ForegroundDetector.OnActivityCallback { private static final int DEFAULT_TIMEOUT_MS = 30000; @@ -33,14 +33,7 @@ class SessionTracker extends BaseObservable { private final CallbackState callbackState; private final Client client; final SessionStore sessionStore; - - // This most recent time an Activity was stopped. - private final AtomicLong lastExitedForegroundMs = new AtomicLong(0); - - // The first Activity in this 'session' was started at this time. - private final AtomicLong lastEnteredForegroundMs = new AtomicLong(0); private volatile Session currentSession = null; - private final ForegroundDetector foregroundDetector; final BackgroundTaskService backgroundTaskService; final Logger logger; @@ -66,10 +59,8 @@ class SessionTracker extends BaseObservable { this.client = client; this.timeoutMs = timeoutMs; this.sessionStore = sessionStore; - this.foregroundDetector = new ForegroundDetector(client.getAppContext()); this.backgroundTaskService = backgroundTaskService; this.logger = logger; - notifyNdkInForeground(); } /** @@ -340,12 +331,18 @@ class SessionTracker extends BaseObservable { return delivery.deliver(payload, params); } - void onActivityStarted(String activityName) { - updateForegroundTracker(activityName, true, SystemClock.elapsedRealtime()); + public void onActivityStarted(Activity activity) { + updateContext( + activity.getClass().getSimpleName(), + true + ); } - void onActivityStopped(String activityName) { - updateForegroundTracker(activityName, false, SystemClock.elapsedRealtime()); + public void onActivityStopped(Activity activity) { + updateContext( + activity.getClass().getSimpleName(), + false + ); } /** @@ -359,47 +356,26 @@ class SessionTracker extends BaseObservable { * * @param activityName the activity name * @param activityStarting whether the activity is being started or not - * @param nowMs The current time in ms */ - void updateForegroundTracker(String activityName, boolean activityStarting, long nowMs) { + void updateContext(String activityName, boolean activityStarting) { if (activityStarting) { - long noActivityRunningForMs = nowMs - lastExitedForegroundMs.get(); synchronized (foregroundActivities) { - if (foregroundActivities.isEmpty()) { - lastEnteredForegroundMs.set(nowMs); - - if (noActivityRunningForMs >= timeoutMs - && configuration.getAutoTrackSessions()) { - startNewSession(new Date(), client.getUser(), true); - } - } foregroundActivities.add(activityName); } } else { synchronized (foregroundActivities) { foregroundActivities.removeLastOccurrence(activityName); - if (foregroundActivities.isEmpty()) { - lastExitedForegroundMs.set(nowMs); - } } } client.getContextState().setAutomaticContext(getContextActivity()); - notifyNdkInForeground(); } - private void notifyNdkInForeground() { - Boolean inForeground = isInForeground(); - final boolean foreground = inForeground != null ? inForeground : false; - updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity())); - } - - @Nullable - Boolean isInForeground() { - return foregroundDetector.isInForeground(); + boolean isInForeground() { + return ForegroundDetector.isInForeground(); } long getLastEnteredForegroundMs() { - return lastEnteredForegroundMs.get(); + return ForegroundDetector.getLastEnteredForegroundMs(); } @Nullable @@ -408,4 +384,18 @@ class SessionTracker extends BaseObservable { 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/SeverityReason.java b/app/src/main/java/com/bugsnag/android/SeverityReason.java index 54be78ae1a..e759bd6d59 100644 --- a/app/src/main/java/com/bugsnag/android/SeverityReason.java +++ b/app/src/main/java/com/bugsnag/android/SeverityReason.java @@ -64,6 +64,7 @@ final class SeverityReason implements JsonStream.Streamable { 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: diff --git a/app/src/main/java/com/bugsnag/android/Stackframe.kt b/app/src/main/java/com/bugsnag/android/Stackframe.kt index ed43d5b7e9..54ca673bd3 100644 --- a/app/src/main/java/com/bugsnag/android/Stackframe.kt +++ b/app/src/main/java/com/bugsnag/android/Stackframe.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import androidx.annotation.RestrictTo import com.bugsnag.android.internal.JsonHelper import java.io.IOException @@ -70,7 +71,8 @@ class Stackframe : JsonStream.Streamable { var type: ErrorType? = null @JvmOverloads - internal constructor( + @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP) + constructor( method: String?, file: String?, lineNumber: Number?, @@ -130,7 +132,9 @@ class Stackframe : JsonStream.Streamable { writer.name("columnNumber").value(columnNumber) frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) } - symbolAddress?.let { writer.name("symbolAddress").value(JsonHelper.ulongToHex(symbolAddress)) } + 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) } diff --git a/app/src/main/java/com/bugsnag/android/StorageModule.kt b/app/src/main/java/com/bugsnag/android/StorageModule.kt index caa419d7b9..5bf1d288eb 100644 --- a/app/src/main/java/com/bugsnag/android/StorageModule.kt +++ b/app/src/main/java/com/bugsnag/android/StorageModule.kt @@ -38,7 +38,13 @@ internal class StorageModule( val lastRunInfoStore by future { LastRunInfoStore(immutableConfig) } - val sessionStore by future { SessionStore(immutableConfig, logger, null) } + val sessionStore by future { + SessionStore( + immutableConfig, + logger, + null + ) + } val lastRunInfo by future { val info = lastRunInfoStore.load() diff --git a/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt b/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt index 66bf136e56..f7d53367c9 100644 --- a/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt +++ b/app/src/main/java/com/bugsnag/android/SynchronizedStreamableStore.kt @@ -13,7 +13,7 @@ import kotlin.concurrent.withLock * This class is made thread safe through the use of a [ReadWriteLock]. */ internal class SynchronizedStreamableStore( - private val file: File + internal val file: File ) { private val lock = ReentrantReadWriteLock() diff --git a/app/src/main/java/com/bugsnag/android/Thread.java b/app/src/main/java/com/bugsnag/android/Thread.java index c93745e613..adf7855e69 100644 --- a/app/src/main/java/com/bugsnag/android/Thread.java +++ b/app/src/main/java/com/bugsnag/android/Thread.java @@ -4,6 +4,7 @@ import androidx.annotation.NonNull; import androidx.annotation.Nullable; import java.io.IOException; +import java.util.ArrayList; import java.util.List; /** @@ -16,9 +17,29 @@ public class Thread implements JsonStream.Streamable { private final Logger logger; Thread( - long id, + String id, @NonNull String name, - @NonNull ThreadType type, + @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, @@ -40,14 +61,19 @@ public class Thread implements JsonStream.Streamable { /** * Sets the unique ID of the thread (from {@link java.lang.Thread}) */ - public void setId(long id) { - impl.setId(id); + 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}) */ - public long getId() { + @NonNull + public String getId() { return impl.getId(); } @@ -73,7 +99,7 @@ public class Thread implements JsonStream.Streamable { /** * Sets the type of thread based on the originating platform (intended for internal use only) */ - public void setType(@NonNull ThreadType type) { + public void setType(@NonNull ErrorType type) { if (type != null) { impl.setType(type); } else { @@ -85,7 +111,7 @@ public class Thread implements JsonStream.Streamable { * Gets the type of thread based on the originating platform (intended for internal use only) */ @NonNull - public ThreadType getType() { + public ErrorType getType() { return impl.getType(); } diff --git a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt index fbb9457b87..df29a8263f 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadInternal.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadInternal.kt @@ -3,9 +3,9 @@ package com.bugsnag.android import java.io.IOException class ThreadInternal internal constructor( - var id: Long, + var id: String, var name: String, - var type: ThreadType, + var type: ErrorType, val isErrorReportingThread: Boolean, var state: String, stacktrace: Stacktrace diff --git a/app/src/main/java/com/bugsnag/android/ThreadState.kt b/app/src/main/java/com/bugsnag/android/ThreadState.kt index 536ed89168..abab23d4dd 100644 --- a/app/src/main/java/com/bugsnag/android/ThreadState.kt +++ b/app/src/main/java/com/bugsnag/android/ThreadState.kt @@ -1,7 +1,10 @@ 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 /** @@ -11,6 +14,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor( exc: Throwable?, isUnhandled: Boolean, maxThreads: Int, + threadCollectionTimeLimitMillis: Long, sendThreads: ThreadSendPolicy, projectPackages: Collection, logger: Logger, @@ -22,7 +26,15 @@ internal class ThreadState @Suppress("LongParameterList") constructor( exc: Throwable?, isUnhandled: Boolean, config: ImmutableConfig - ) : this(exc, isUnhandled, config.maxReportedThreads, config.sendThreads, config.projectPackages, config.logger) + ) : this( + exc, + isUnhandled, + config.maxReportedThreads, + config.threadCollectionTimeLimitMillis, + config.sendThreads, + config.projectPackages, + config.logger + ) val threads: MutableList @@ -37,6 +49,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor( exc, isUnhandled, maxThreads, + threadCollectionTimeLimitMillis, projectPackages, logger ) @@ -70,6 +83,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor( exc: Throwable?, isUnhandled: Boolean, maxThreadCount: Int, + threadCollectionTimeLimitMillis: Long, projectPackages: Collection, logger: Logger ): MutableList { @@ -89,9 +103,9 @@ internal class ThreadState @Suppress("LongParameterList") constructor( ) return Thread( - thread.id, + thread.id.toString(), thread.name, - ThreadType.ANDROID, + ErrorType.ANDROID, isErrorThread, Thread.State.forThread(thread), stackTrace, @@ -101,23 +115,49 @@ internal class ThreadState @Suppress("LongParameterList") constructor( // 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() + 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( - -1, + "", "[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]", - ThreadType.EMPTY, + ErrorType.UNKNOWN, false, Thread.State.UNKNOWN, Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger), diff --git a/app/src/main/java/com/bugsnag/android/ThreadType.kt b/app/src/main/java/com/bugsnag/android/ThreadType.kt deleted file mode 100644 index 60f834741c..0000000000 --- a/app/src/main/java/com/bugsnag/android/ThreadType.kt +++ /dev/null @@ -1,31 +0,0 @@ -package com.bugsnag.android - -/** - * Represents the type of thread captured - */ -enum class ThreadType(internal val desc: String) { - - /** - * A thread captured from Android's JVM layer - */ - EMPTY(""), - - /** - * A thread captured from Android's JVM layer - */ - ANDROID("android"), - - /** - * A thread captured from Android's NDK layer - */ - C("c"), - - /** - * A thread captured from JavaScript - */ - REACTNATIVEJS("reactnativejs"); - - internal companion object { - internal fun fromDescriptor(desc: String) = ThreadType.values().find { it.desc == desc } - } -} diff --git a/app/src/main/java/com/bugsnag/android/UserStore.kt b/app/src/main/java/com/bugsnag/android/UserStore.kt index 0a7b59448e..87adf40e16 100644 --- a/app/src/main/java/com/bugsnag/android/UserStore.kt +++ b/app/src/main/java/com/bugsnag/android/UserStore.kt @@ -3,7 +3,6 @@ package com.bugsnag.android import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.StateObserver import java.io.File -import java.io.IOException import java.util.concurrent.atomic.AtomicReference /** @@ -12,7 +11,7 @@ import java.util.concurrent.atomic.AtomicReference internal class UserStore @JvmOverloads constructor( private val config: ImmutableConfig, private val deviceId: String?, - file: File = File(config.persistenceDirectory.value, "user-info"), + file: File = File(config.persistenceDirectory.value, "bugsnag/user-info"), private val sharedPrefMigrator: SharedPrefMigrator, private val logger: Logger ) { @@ -22,11 +21,6 @@ internal class UserStore @JvmOverloads constructor( private val previousUser = AtomicReference(null) init { - try { - file.createNewFile() - } catch (exc: IOException) { - logger.w("Failed to created device ID file", exc) - } this.synchronizedStreamableStore = SynchronizedStreamableStore(file) } @@ -87,13 +81,19 @@ internal class UserStore @JvmOverloads constructor( val legacyUser = sharedPrefMigrator.loadUser(deviceId) save(legacyUser) legacyUser - } else { - return try { + } 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 new file mode 100644 index 0000000000..768ed1d55e --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/AbstractStartupProvider.kt @@ -0,0 +1,68 @@ +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/BugsnagContentProvider.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt new file mode 100644 index 0000000000..14147c69cd --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/BugsnagContentProvider.kt @@ -0,0 +1,13 @@ +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/BugsnagStoreMigrator.kt b/app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt new file mode 100644 index 0000000000..87b223c511 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/BugsnagStoreMigrator.kt @@ -0,0 +1,30 @@ +package com.bugsnag.android.internal + +import java.io.File + +internal object BugsnagStoreMigrator { + + @JvmStatic + fun moveToNewDirectory(persistenceDir: File) { + val bugsnagDir = File(persistenceDir, "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(persistenceDir, from) + if (fromFile.exists()) { + fromFile.renameTo( + File(bugsnagDir, to) + ) + } + } + } +} diff --git a/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt b/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt new file mode 100644 index 0000000000..1125bec348 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/ByteArrayExtensions.kt @@ -0,0 +1,15 @@ +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/DexBuildIdGenerator.kt b/app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt new file mode 100644 index 0000000000..8c2142f432 --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/DexBuildIdGenerator.kt @@ -0,0 +1,108 @@ +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/ForegroundDetector.kt b/app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt new file mode 100644 index 0000000000..4308c8ee9a --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/ForegroundDetector.kt @@ -0,0 +1,225 @@ +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 + + @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 index 8ff97eab96..eed21dbb50 100644 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -25,6 +25,8 @@ import com.bugsnag.android.errorApiHeaders import com.bugsnag.android.safeUnrollCauses import com.bugsnag.android.sessionApiHeaders import java.io.File +import java.util.concurrent.Callable +import java.util.regex.Pattern data class ImmutableConfig( val apiKey: String, @@ -32,7 +34,7 @@ data class ImmutableConfig( val enabledErrorTypes: ErrorTypes, val autoTrackSessions: Boolean, val sendThreads: ThreadSendPolicy, - val discardClasses: Collection, + val discardClasses: Collection, val enabledReleaseStages: Collection?, val projectPackages: Collection, val enabledBreadcrumbTypes: Set?, @@ -51,6 +53,7 @@ data class ImmutableConfig( val maxPersistedEvents: Int, val maxPersistedSessions: Int, val maxReportedThreads: Int, + val threadCollectionTimeLimitMillis: Long, val persistenceDirectory: Lazy, val sendLaunchCrashesSynchronously: Boolean, val attemptDeliveryOnCrash: Boolean, @@ -58,7 +61,7 @@ data class ImmutableConfig( // results cached here to avoid unnecessary lookups in Client. val packageInfo: PackageInfo?, val appInfo: ApplicationInfo?, - val redactedKeys: Collection + val redactedKeys: Collection ) { @JvmName("getErrorApiDeliveryParams") @@ -113,7 +116,11 @@ data class ImmutableConfig( */ @VisibleForTesting internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean { - return discardClasses.contains(errorClass) + return if (!errorClass.isNullOrEmpty()) { + discardClasses.any { it.matcher(errorClass.toString()).matches() } + } else { + false + } } /** @@ -165,6 +172,7 @@ internal fun convertToImmutableConfig( maxPersistedEvents = config.maxPersistedEvents, maxPersistedSessions = config.maxPersistedSessions, maxReportedThreads = config.maxReportedThreads, + threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis, enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), telemetry = config.telemetry.toSet(), persistenceDirectory = persistenceDir, @@ -176,11 +184,34 @@ internal fun convertToImmutableConfig( ) } +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 + connectivity: Connectivity, + backgroundTaskService: BackgroundTaskService ): ImmutableConfig { + validateApiKey(configuration.apiKey) val packageName = appContext.packageName val packageManager = appContext.packageManager val packageInfo = runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull() @@ -219,7 +250,7 @@ internal fun sanitiseConfiguration( } // populate buildUUID from manifest - val buildUuid = populateBuildUuid(appInfo) + val buildUuid = collectBuildUuid(appInfo, backgroundTaskService) @Suppress("SENSELESS_COMPARISON") if (configuration.delivery == null) { @@ -239,15 +270,34 @@ internal fun sanitiseConfiguration( ) } -private fun populateBuildUuid(appInfo: ApplicationInfo?): String? { +private fun collectBuildUuid( + appInfo: ApplicationInfo?, + backgroundTaskService: BackgroundTaskService +): String? { val bundle = appInfo?.metaData return when { bundle?.containsKey(BUILD_UUID) == true -> { - bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString() + (bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString()) + .takeIf { it.isNotEmpty() } } + + appInfo != null -> { + try { + backgroundTaskService + .submitTask( + TaskType.IO, + Callable { DexBuildIdGenerator.generateBuildId(appInfo) } + ) + .get() + } catch (e: Exception) { + null + } + } + 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/StateObserver.java b/app/src/main/java/com/bugsnag/android/internal/StateObserver.java deleted file mode 100644 index d9230e21e1..0000000000 --- a/app/src/main/java/com/bugsnag/android/internal/StateObserver.java +++ /dev/null @@ -1,14 +0,0 @@ -package com.bugsnag.android.internal; - -import com.bugsnag.android.StateEvent; - -import androidx.annotation.NonNull; - -public 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. - */ - void onStateChange(@NonNull StateEvent event); -} diff --git a/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt b/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt new file mode 100644 index 0000000000..924d301b2c --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/internal/StateObserver.kt @@ -0,0 +1,11 @@ +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/dag/ConfigModule.kt b/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt index e327c8ee70..842057c2bc 100644 --- a/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt +++ b/app/src/main/java/com/bugsnag/android/internal/dag/ConfigModule.kt @@ -2,6 +2,7 @@ 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 /** @@ -11,8 +12,9 @@ import com.bugsnag.android.internal.sanitiseConfiguration internal class ConfigModule( contextModule: ContextModule, configuration: Configuration, - connectivity: Connectivity + connectivity: Connectivity, + bgTaskExecutor: BackgroundTaskService ) : DependencyModule() { - val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity) + val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor) } diff --git a/app/src/main/java/eu/faircode/email/Log.java b/app/src/main/java/eu/faircode/email/Log.java index 5fa17efbab..4bbe53df9b 100644 --- a/app/src/main/java/eu/faircode/email/Log.java +++ b/app/src/main/java/eu/faircode/email/Log.java @@ -96,6 +96,7 @@ 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; @@ -119,6 +120,36 @@ public class Log { static final String TOKEN_REFRESH_REQUIRED = "Token refresh required. Is there a VPN based app running?"; + static final List IGNORE_CLASSES = Collections.unmodifiableList(Arrays.asList( + "com.sun.mail.util.MailConnectException", + + "android.accounts.AuthenticatorException", + "android.accounts.OperationCanceledException", + "android.app.RemoteServiceException", + + "java.lang.NoClassDefFoundError", + "java.lang.UnsatisfiedLinkError", + + "java.nio.charset.MalformedInputException", + + "java.net.ConnectException", + "java.net.SocketException", + "java.net.SocketTimeoutException", + "java.net.UnknownHostException", + + "javax.mail.AuthenticationFailedException", + "javax.mail.internet.AddressException", + "javax.mail.internet.ParseException", + "javax.mail.MessageRemovedException", + "javax.mail.FolderNotFoundException", + "javax.mail.ReadOnlyFolderException", + "javax.mail.FolderClosedException", + "com.sun.mail.util.FolderClosedIOException", + "javax.mail.StoreClosedException", + + "org.xmlpull.v1.XmlPullParserException" + )); + static { System.loadLibrary("fairemail"); } @@ -376,37 +407,11 @@ public class Log { config.setEnabledErrorTypes(etypes); config.setMaxBreadcrumbs(BuildConfig.PLAY_STORE_RELEASE ? 250 : 500); - Set ignore = new HashSet<>(); - - ignore.add("com.sun.mail.util.MailConnectException"); - - ignore.add("android.accounts.AuthenticatorException"); - ignore.add("android.accounts.OperationCanceledException"); - ignore.add("android.app.RemoteServiceException"); - - ignore.add("java.lang.NoClassDefFoundError"); - ignore.add("java.lang.UnsatisfiedLinkError"); - - ignore.add("java.nio.charset.MalformedInputException"); - - ignore.add("java.net.ConnectException"); - ignore.add("java.net.SocketException"); - ignore.add("java.net.SocketTimeoutException"); - ignore.add("java.net.UnknownHostException"); - - ignore.add("javax.mail.AuthenticationFailedException"); - ignore.add("javax.mail.internet.AddressException"); - ignore.add("javax.mail.internet.ParseException"); - ignore.add("javax.mail.MessageRemovedException"); - ignore.add("javax.mail.FolderNotFoundException"); - ignore.add("javax.mail.ReadOnlyFolderException"); - ignore.add("javax.mail.FolderClosedException"); - ignore.add("com.sun.mail.util.FolderClosedIOException"); - ignore.add("javax.mail.StoreClosedException"); - - ignore.add("org.xmlpull.v1.XmlPullParserException"); - - config.setDiscardClasses(ignore); + Set discardClasses = new HashSet<>(); + if (!BuildConfig.DEBUG) + 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);