Updated Bugsnag client

pull/214/head
M66B 2 years ago
parent 7f6104562f
commit c8cf13b66f

@ -551,7 +551,7 @@ dependencies {
def dnsjava_version = "2.1.9" def dnsjava_version = "2.1.9"
def openpgp_version = "12.0" def openpgp_version = "12.0"
def badge_version = "1.1.22" 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 biweekly_version = "0.6.7"
def vcard_version = "0.12.1" def vcard_version = "0.12.1"
def relinker_version = "1.4.5" def relinker_version = "1.4.5"

@ -177,9 +177,9 @@ internal class BugsnagEventMapper(
@Suppress("UNCHECKED_CAST") @Suppress("UNCHECKED_CAST")
internal fun convertThread(thread: Map<String, Any?>): ThreadInternal { internal fun convertThread(thread: Map<String, Any?>): ThreadInternal {
return ThreadInternal( return ThreadInternal(
(thread["id"] as? Number)?.toLong() ?: 0, thread["id"].toString(),
thread.readEntry("name"), thread.readEntry("name"),
ThreadType.fromDescriptor(thread.readEntry("type")) ?: ThreadType.ANDROID, ErrorType.fromDescriptor(thread.readEntry("type")) ?: ErrorType.ANDROID,
thread["errorReportingThread"] == true, thread["errorReportingThread"] == true,
thread.readEntry("state"), thread.readEntry("state"),
(thread["stacktrace"] as? List<Map<String, Any?>>)?.let { convertStacktrace(it) } (thread["stacktrace"] as? List<Map<String, Any?>>)?.let { convertStacktrace(it) }

@ -8,7 +8,7 @@ internal data class CallbackState(
val onErrorTasks: MutableCollection<OnErrorCallback> = CopyOnWriteArrayList(), val onErrorTasks: MutableCollection<OnErrorCallback> = CopyOnWriteArrayList(),
val onBreadcrumbTasks: MutableCollection<OnBreadcrumbCallback> = CopyOnWriteArrayList(), val onBreadcrumbTasks: MutableCollection<OnBreadcrumbCallback> = CopyOnWriteArrayList(),
val onSessionTasks: MutableCollection<OnSessionCallback> = CopyOnWriteArrayList(), val onSessionTasks: MutableCollection<OnSessionCallback> = CopyOnWriteArrayList(),
val onSendTasks: MutableCollection<OnSendCallback> = CopyOnWriteArrayList() val onSendTasks: MutableList<OnSendCallback> = CopyOnWriteArrayList()
) : CallbackAware { ) : CallbackAware {
private var internalMetrics: InternalMetrics = InternalMetricsNoop() 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) { fun removeOnSend(onSend: OnSendCallback) {
if (onSendTasks.remove(onSend)) { if (onSendTasks.remove(onSend)) {
internalMetrics.notifyRemoveCallback(onSendName) internalMetrics.notifyRemoveCallback(onSendName)

@ -3,6 +3,8 @@ package com.bugsnag.android;
import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION; import static com.bugsnag.android.SeverityReason.REASON_HANDLED_EXCEPTION;
import com.bugsnag.android.internal.BackgroundTaskService; 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.ImmutableConfig;
import com.bugsnag.android.internal.InternalMetrics; import com.bugsnag.android.internal.InternalMetrics;
import com.bugsnag.android.internal.InternalMetricsImpl; import com.bugsnag.android.internal.InternalMetricsImpl;
@ -33,6 +35,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.concurrent.Callable; import java.util.concurrent.Callable;
import java.util.concurrent.RejectedExecutionException; import java.util.concurrent.RejectedExecutionException;
import java.util.regex.Pattern;
/** /**
* A Bugsnag Client instance allows you to use Bugsnag in your Android app. * 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 // 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(); immutableConfig = configModule.getConfig();
logger = immutableConfig.getLogger(); 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 // setup storage as soon as possible
final StorageModule storageModule = new StorageModule(appContext, final StorageModule storageModule = new StorageModule(appContext,
immutableConfig, logger); immutableConfig, logger);
@ -314,8 +326,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
void registerLifecycleCallbacks() { void registerLifecycleCallbacks() {
if (appContext instanceof Application) { if (appContext instanceof Application) {
Application application = (Application) appContext; Application application = (Application) appContext;
SessionLifecycleCallback sessionCb = new SessionLifecycleCallback(sessionTracker); ForegroundDetector.registerOn(application);
application.registerActivityLifecycleCallbacks(sessionCb); ForegroundDetector.registerActivityCallbacks(sessionTracker);
if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) { if (!immutableConfig.shouldDiscardBreadcrumb(BreadcrumbType.STATE)) {
ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector( ActivityBreadcrumbCollector activityCb = new ActivityBreadcrumbCollector(
@ -792,7 +804,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
@Nullable OnErrorCallback onError) { @Nullable OnErrorCallback onError) {
// set the redacted keys on the event as this // set the redacted keys on the event as this
// will not have been set for RN/Unity events // will not have been set for RN/Unity events
Collection<String> redactedKeys = metadataState.getMetadata().getRedactedKeys(); Collection<Pattern> redactedKeys = metadataState.getMetadata().getRedactedKeys();
event.setRedactedKeys(redactedKeys); event.setRedactedKeys(redactedKeys);
// get session for event // get session for event
@ -1182,4 +1194,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
void setAutoDetectAnrs(boolean autoDetectAnrs) { void setAutoDetectAnrs(boolean autoDetectAnrs) {
pluginClient.setAutoDetectAnrs(this, autoDetectAnrs); pluginClient.setAutoDetectAnrs(this, autoDetectAnrs);
} }
void addOnSend(OnSendCallback callback) {
callbackState.addPreOnSend(callback);
}
} }

@ -3,9 +3,10 @@ package com.bugsnag.android
import android.content.Context import android.content.Context
import java.io.File import java.io.File
import java.util.EnumSet import java.util.EnumSet
import java.util.regex.Pattern
internal class ConfigInternal( internal class ConfigInternal(
var apiKey: String var apiKey: String?
) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware { ) : CallbackAware, MetadataAware, UserAware, FeatureFlagAware {
private var user = User() private var user = User()
@ -23,7 +24,7 @@ internal class ConfigInternal(
var versionCode: Int? = 0 var versionCode: Int? = 0
var releaseStage: String? = null var releaseStage: String? = null
var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS var sendThreads: ThreadSendPolicy = ThreadSendPolicy.ALWAYS
var persistUser: Boolean = false var persistUser: Boolean = true
var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS var launchDurationMillis: Long = DEFAULT_LAUNCH_CRASH_THRESHOLD_MS
@ -42,16 +43,17 @@ internal class ConfigInternal(
var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS var maxPersistedEvents: Int = DEFAULT_MAX_PERSISTED_EVENTS
var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS var maxPersistedSessions: Int = DEFAULT_MAX_PERSISTED_SESSIONS
var maxReportedThreads: Int = DEFAULT_MAX_REPORTED_THREADS 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 maxStringValueLength: Int = DEFAULT_MAX_STRING_VALUE_LENGTH
var context: String? = null var context: String? = null
var redactedKeys: Set<String> var redactedKeys: Set<Pattern>
get() = metadataState.metadata.redactedKeys get() = metadataState.metadata.redactedKeys
set(value) { set(value) {
metadataState.metadata.redactedKeys = value metadataState.metadata.redactedKeys = value
} }
var discardClasses: Set<String> = emptySet() var discardClasses: Set<Pattern> = emptySet()
var enabledReleaseStages: Set<String>? = null var enabledReleaseStages: Set<String>? = null
var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null var enabledBreadcrumbTypes: Set<BreadcrumbType>? = null
var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE) var telemetry: Set<Telemetry> = EnumSet.of(Telemetry.INTERNAL_ERRORS, Telemetry.USAGE)
@ -138,6 +140,8 @@ internal class ConfigInternal(
"maxPersistedSessions" to maxPersistedSessions else null, "maxPersistedSessions" to maxPersistedSessions else null,
if (maxReportedThreads != defaultConfig.maxReportedThreads) if (maxReportedThreads != defaultConfig.maxReportedThreads)
"maxReportedThreads" to maxReportedThreads else null, "maxReportedThreads" to maxReportedThreads else null,
if (threadCollectionTimeLimitMillis != defaultConfig.threadCollectionTimeLimitMillis)
"threadCollectionTimeLimitMillis" to threadCollectionTimeLimitMillis else null,
if (persistenceDirectory != null) if (persistenceDirectory != null)
"persistenceDirectorySet" to true else null, "persistenceDirectorySet" to true else null,
if (sendThreads != defaultConfig.sendThreads) 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_SESSIONS = 128
private const val DEFAULT_MAX_PERSISTED_EVENTS = 32 private const val DEFAULT_MAX_PERSISTED_EVENTS = 32
private const val DEFAULT_MAX_REPORTED_THREADS = 200 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_LAUNCH_CRASH_THRESHOLD_MS: Long = 5000
private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000 private const val DEFAULT_MAX_STRING_VALUE_LENGTH = 10000

@ -5,11 +5,11 @@ import android.content.Context;
import androidx.annotation.IntRange; import androidx.annotation.IntRange;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
import java.io.File; import java.io.File;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.regex.Pattern;
/** /**
* User-specified configuration storage object, contains information * 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 MIN_BREADCRUMBS = 0;
private static final int MAX_BREADCRUMBS = 500; 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; private static final long MIN_LAUNCH_CRASH_THRESHOLD_MS = 0;
final ConfigInternal impl; final ConfigInternal impl;
@ -29,7 +28,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
* Constructs a new Configuration object with default values. * Constructs a new Configuration object with default values.
*/ */
public Configuration(@NonNull String apiKey) { public Configuration(@NonNull String apiKey) {
validateApiKey(apiKey);
impl = new ConfigInternal(apiKey); impl = new ConfigInternal(apiKey);
} }
@ -47,32 +45,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
return ConfigInternal.load(context, apiKey); 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) { private void logNull(String property) {
getLogger().e("Invalid null value supplied to config." + property + ", ignoring"); 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. * Changes the API key used for events sent to Bugsnag.
*/ */
public void setApiKey(@NonNull String apiKey) { public void setApiKey(@NonNull String apiKey) {
validateApiKey(apiKey);
impl.setApiKey(apiKey); impl.setApiKey(apiKey);
} }
@ -244,28 +215,6 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
impl.setPersistenceDirectory(directory); 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 * Sets whether or not Bugsnag should send crashes synchronously that occurred during
* the application's launch period. By default this behavior is enabled. * the application's launch period. By default this behavior is enabled.
@ -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 * Sets the maximum number of persisted sessions which will be stored. Once the threshold is
* reached, the oldest session will be deleted. * 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" * By default, redactedKeys is set to "password"
*/ */
@NonNull @NonNull
public Set<String> getRedactedKeys() { public Set<Pattern> getRedactedKeys() {
return impl.getRedactedKeys(); return impl.getRedactedKeys();
} }
@ -683,7 +657,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
* *
* By default, redactedKeys is set to "password" * By default, redactedKeys is set to "password"
*/ */
public void setRedactedKeys(@NonNull Set<String> redactedKeys) { public void setRedactedKeys(@NonNull Set<Pattern> redactedKeys) {
if (CollectionUtils.containsNullElements(redactedKeys)) { if (CollectionUtils.containsNullElements(redactedKeys)) {
logNull("redactedKeys"); logNull("redactedKeys");
} else { } else {
@ -697,7 +671,7 @@ public class Configuration implements CallbackAware, MetadataAware, UserAware, F
* match against the canonical class name. * match against the canonical class name.
*/ */
@NonNull @NonNull
public Set<String> getDiscardClasses() { public Set<Pattern> getDiscardClasses() {
return impl.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 * before being sent to Bugsnag if they are detected. The notifier performs an exact
* match against the canonical class name. * match against the canonical class name.
*/ */
public void setDiscardClasses(@NonNull Set<String> discardClasses) { public void setDiscardClasses(@NonNull Set<Pattern> discardClasses) {
if (CollectionUtils.containsNullElements(discardClasses)) { if (CollectionUtils.containsNullElements(discardClasses)) {
logNull("discardClasses"); logNull("discardClasses");
} else { } else {

@ -5,6 +5,11 @@ package com.bugsnag.android
*/ */
enum class ErrorType(internal val desc: String) { enum class ErrorType(internal val desc: String) {
/**
* An error with an unknown type or source
*/
UNKNOWN(""),
/** /**
* An error captured from Android's JVM layer * An error captured from Android's JVM layer
*/ */

@ -10,6 +10,7 @@ import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
import java.util.Map; 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 * 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; return impl;
} }
void setRedactedKeys(Collection<String> redactedKeys) { void setRedactedKeys(Collection<Pattern> redactedKeys) {
impl.setRedactedKeys(redactedKeys); impl.setRedactedKeys(redactedKeys);
} }

@ -6,6 +6,7 @@ import com.bugsnag.android.internal.InternalMetricsNoop
import com.bugsnag.android.internal.JsonHelper import com.bugsnag.android.internal.JsonHelper
import com.bugsnag.android.internal.TrimMetrics import com.bugsnag.android.internal.TrimMetrics
import java.io.IOException import java.io.IOException
import java.util.regex.Pattern
internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware { internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, MetadataAware, UserAware {
@ -39,7 +40,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
apiKey: String, apiKey: String,
logger: Logger, logger: Logger,
breadcrumbs: MutableList<Breadcrumb> = mutableListOf(), breadcrumbs: MutableList<Breadcrumb> = mutableListOf(),
discardClasses: Set<String> = setOf(), discardClasses: Set<Pattern> = setOf(),
errors: MutableList<Error> = mutableListOf(), errors: MutableList<Error> = mutableListOf(),
metadata: Metadata = Metadata(), metadata: Metadata = Metadata(),
featureFlags: FeatureFlags = FeatureFlags(), featureFlags: FeatureFlags = FeatureFlags(),
@ -48,7 +49,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION), severityReason: SeverityReason = SeverityReason.newInstance(SeverityReason.REASON_HANDLED_EXCEPTION),
threads: MutableList<Thread> = mutableListOf(), threads: MutableList<Thread> = mutableListOf(),
user: User = User(), user: User = User(),
redactionKeys: Set<String>? = null redactionKeys: Set<Pattern>? = null
) { ) {
this.logger = logger this.logger = logger
this.apiKey = apiKey this.apiKey = apiKey
@ -74,7 +75,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
val logger: Logger val logger: Logger
val metadata: Metadata val metadata: Metadata
val featureFlags: FeatureFlags val featureFlags: FeatureFlags
private val discardClasses: Set<String> private val discardClasses: Set<Pattern>
internal var projectPackages: Collection<String> internal var projectPackages: Collection<String>
private val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer().apply { private val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer().apply {
@ -105,7 +106,7 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
var groupingHash: String? = null var groupingHash: String? = null
var context: String? = null var context: String? = null
var redactedKeys: Collection<String> var redactedKeys: Collection<Pattern>
get() = jsonStreamer.redactedKeys get() = jsonStreamer.redactedKeys
set(value) { set(value) {
jsonStreamer.redactedKeys = value.toSet() jsonStreamer.redactedKeys = value.toSet()
@ -125,7 +126,11 @@ internal class EventInternal : FeatureFlagAware, JsonStream.Streamable, Metadata
protected fun shouldDiscardClass(): Boolean { protected fun shouldDiscardClass(): Boolean {
return when { return when {
errors.isEmpty() -> true 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) = 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<FeatureFlag>) = override fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>) =
this.featureFlags.addFeatureFlags(featureFlags) this.featureFlags.addFeatureFlags(featureFlags)
override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name) override fun clearFeatureFlag(name: String) = featureFlags.clearFeatureFlag(name)

@ -37,5 +37,14 @@ internal class EventStorageModule(
) else null ) 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
)
}
} }

@ -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<File> EVENT_COMPARATOR = new Comparator<File>() {
@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<File> 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<File> storedFiles) {
List<File> 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<String> writeAndDeliver(@NonNull final JsonStream.Streamable streamable) {
final String filename = write(streamable);
if (filename != null) {
try {
return bgTaskSevice.submitTask(TaskType.ERROR_REQUEST, new Callable<String>() {
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<File> 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<File> 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));
}
}

@ -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>): File? {
return storedFiles
.asSequence()
.filter { fromFile(it, config).isLaunchCrashReport() }
.maxWithOrNull(EVENT_COMPARATOR)
}
fun writeAndDeliver(streamable: Streamable): Future<String>? {
val filename = write(streamable) ?: return null
try {
return bgTaskService.submitTask(
TaskType.ERROR_REQUEST,
Callable {
flushEventFile(File(filename))
filename
}
)
} catch (exception: RejectedExecutionException) {
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
}
return null
}
/**
* Flush any on-disk errors to Bugsnag
*/
fun flushAsync() {
try {
bgTaskService.submitTask(
TaskType.ERROR_REQUEST,
Runnable {
val storedFiles = findStoredFiles()
if (storedFiles.isEmpty()) {
logger.d("No regular events to flush to Bugsnag.")
}
flushReports(storedFiles)
}
)
} catch (exception: RejectedExecutionException) {
logger.w("Failed to flush all on-disk errors, retaining unsent errors for later.")
}
}
private fun flushReports(storedReports: Collection<File>) {
if (!storedReports.isEmpty()) {
val size = storedReports.size
logger.i("Sending $size saved error(s) to Bugsnag")
for (eventFile in storedReports) {
flushEventFile(eventFile)
}
}
}
private fun flushEventFile(eventFile: File) {
try {
val (apiKey) = fromFile(eventFile, config)
val payload = createEventPayload(eventFile, apiKey)
if (payload == null) {
deleteStoredFiles(setOf(eventFile))
} else {
deliverEventPayload(eventFile, payload)
}
} catch (exception: Exception) {
handleEventFlushFailure(exception, eventFile)
}
}
private fun deliverEventPayload(eventFile: File, payload: EventPayload) {
val deliveryParams = config.getErrorApiDeliveryParams(payload)
val delivery = config.delivery
when (delivery.deliver(payload, deliveryParams)) {
DeliveryStatus.DELIVERED -> {
deleteStoredFiles(setOf(eventFile))
logger.i("Deleting sent error file $eventFile.name")
}
DeliveryStatus.UNDELIVERED -> undeliveredEventPayload(eventFile)
DeliveryStatus.FAILURE -> {
val exc: Exception = RuntimeException("Failed to deliver event payload")
handleEventFlushFailure(exc, eventFile)
}
}
}
private fun undeliveredEventPayload(eventFile: File) {
if (isTooBig(eventFile)) {
logger.w(
"Discarding over-sized event (${eventFile.length()}) after failed delivery"
)
deleteStoredFiles(setOf(eventFile))
} else if (isTooOld(eventFile)) {
logger.w(
"Discarding historical event (from ${getCreationDate(eventFile)}) after failed delivery"
)
deleteStoredFiles(setOf(eventFile))
} else {
cancelQueuedFiles(setOf(eventFile))
logger.w(
"Could not send previously saved error(s) to Bugsnag, will try again later"
)
}
}
private fun createEventPayload(eventFile: File, apiKey: String): EventPayload? {
@Suppress("NAME_SHADOWING")
var apiKey: String? = apiKey
val eventSource = MarshalledEventSource(eventFile, apiKey!!, logger)
try {
if (!callbackState.runOnSendTasks(eventSource, logger)) {
// do not send the payload at all, we must block sending
return null
}
} catch (ioe: Exception) {
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<in File?> = Comparator { lhs, rhs ->
when {
lhs == null && rhs == null -> 0
lhs == null -> 1
rhs == null -> -1
else -> lhs.compareTo(rhs)
}
}
private const val oneMegabyte = 1024L * 1024L
}
}

@ -1,9 +1,6 @@
package com.bugsnag.android; package com.bugsnag.android
import androidx.annotation.NonNull; internal interface FeatureFlagAware {
import androidx.annotation.Nullable;
interface FeatureFlagAware {
/** /**
* Add a single feature flag with no variant. If there is an existing feature flag with the * 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. * 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 * @param name the name of the feature flag to add
* @see #addFeatureFlag(String, String) * @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 * 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 * @param variant the variant to set the feature flag to, or {@code null} to specify a feature
* flag with no variant * 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 * 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 * @param featureFlags the feature flags to add
* @see #addFeatureFlag(String, String) * @see #addFeatureFlag(String, String)
*/ */
void addFeatureFlags(@NonNull Iterable<FeatureFlag> featureFlags); fun addFeatureFlags(featureFlags: Iterable<FeatureFlag>)
/** /**
* Remove a single feature flag regardless of its current status. This will stop the specified * 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 * @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. * Clear all of the feature flags. This will stop all feature flags from being reported.
*/ */
void clearFeatureFlags(); fun clearFeatureFlags()
} }

@ -12,7 +12,6 @@ internal class FeatureFlags(
} }
@Synchronized override fun addFeatureFlag(name: String, variant: String?) { @Synchronized override fun addFeatureFlag(name: String, variant: String?) {
store.remove(name)
store[name] = variant ?: emptyVariant store[name] = variant ?: emptyVariant
} }

@ -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<File> comparator;
private final Lock lock = new ReentrantLock();
private final Collection<File> queuedFiles = new ConcurrentSkipListSet<>();
protected final Logger logger;
private final EventStore.Delegate delegate;
FileStore(@NonNull File storageDir,
int maxStoreCount,
Comparator<File> 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<File> 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<File> findStoredFiles() {
lock.lock();
try {
List<File> 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<File> files) {
lock.lock();
try {
if (files != null) {
queuedFiles.removeAll(files);
}
} finally {
lock.unlock();
}
}
void deleteStoredFiles(Collection<File> storedFiles) {
lock.lock();
try {
if (storedFiles != null) {
queuedFiles.removeAll(storedFiles);
for (File storedFile : storedFiles) {
if (!storedFile.delete()) {
storedFile.deleteOnExit();
}
}
}
} finally {
lock.unlock();
}
}
}

@ -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<in File?>,
protected open val logger: Logger,
protected val delegate: Delegate?
) {
internal fun interface Delegate {
/**
* Invoked when an error report is not (de)serialized correctly
*
* @param exception the error encountered reading/delivering the file
* @param errorFile file which could not be (de)serialized correctly
* @param context the context used to group the exception
*/
fun onErrorIOFailure(exception: Exception?, errorFile: File?, context: String?)
}
private val lock: Lock = ReentrantLock()
private val queuedFiles: MutableCollection<File> = ConcurrentSkipListSet()
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<File> = 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<File> {
lock.lock()
return try {
val files: MutableList<File> = ArrayList()
if (isStorageDirValid(storageDir)) {
val values = storageDir.listFiles()
if (values != null) {
for (value in values) {
// delete any tombstoned/empty files, as they contain no useful info
if (value.length() == 0L) {
if (!value.delete()) {
value.deleteOnExit()
}
} else if (value.isFile && !queuedFiles.contains(value)) {
files.add(value)
}
}
}
}
queuedFiles.addAll(files)
files
} finally {
lock.unlock()
}
}
fun cancelQueuedFiles(files: Collection<File>?) {
lock.lock()
try {
if (files != null) {
queuedFiles.removeAll(files)
}
} finally {
lock.unlock()
}
}
fun deleteStoredFiles(storedFiles: Collection<File>?) {
lock.lock()
try {
if (storedFiles != null) {
queuedFiles.removeAll(storedFiles)
for (storedFile in storedFiles) {
if (!storedFile.delete()) {
storedFile.deleteOnExit()
}
}
}
} finally {
lock.unlock()
}
}
}

@ -1,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.
* <p/>
* 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<ActivityManager.RunningAppProcessInfo> appProcesses
= activityManager.getRunningAppProcesses();
if (appProcesses != null) {
int pid = Process.myPid();
for (ActivityManager.RunningAppProcessInfo appProcess : appProcesses) {
if (pid == appProcess.pid) {
return appProcess;
}
}
}
return null;
}
}

@ -82,7 +82,7 @@ class InternalReportDelegate implements EventStore.Delegate {
void recordStorageCacheBehavior(Event event) { void recordStorageCacheBehavior(Event event) {
if (storageManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) { if (storageManager != null && Build.VERSION.SDK_INT >= Build.VERSION_CODES.O) {
File cacheDir = appContext.getCacheDir(); File cacheDir = appContext.getCacheDir();
File errDir = new File(cacheDir, "bugsnag-errors"); File errDir = new File(cacheDir, "bugsnag/errors");
try { try {
boolean tombstone = storageManager.isCacheBehaviorTombstone(errDir); boolean tombstone = storageManager.isCacheBehaviorTombstone(errDir);

@ -16,7 +16,7 @@ private const val KEY_CRASHED_DURING_LAUNCH = "crashedDuringLaunch"
*/ */
internal class LastRunInfoStore(config: ImmutableConfig) { 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 logger: Logger = config.logger
private val lock = ReentrantReadWriteLock() private val lock = ReentrantReadWriteLock()

@ -5,6 +5,7 @@ import android.content.pm.PackageManager
import android.os.Bundle import android.os.Bundle
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import java.lang.IllegalArgumentException import java.lang.IllegalArgumentException
import java.util.regex.Pattern
internal class ManifestConfigLoader { 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_EVENTS = "$BUGSNAG_NS.MAX_PERSISTED_EVENTS"
private const val MAX_PERSISTED_SESSIONS = "$BUGSNAG_NS.MAX_PERSISTED_SESSIONS" 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 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_CRASH_THRESHOLD_MS = "$BUGSNAG_NS.LAUNCH_CRASH_THRESHOLD_MS"
private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS" private const val LAUNCH_DURATION_MILLIS = "$BUGSNAG_NS.LAUNCH_DURATION_MILLIS"
private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY" private const val SEND_LAUNCH_CRASHES_SYNCHRONOUSLY = "$BUGSNAG_NS.SEND_LAUNCH_CRASHES_SYNCHRONOUSLY"
@ -80,10 +82,10 @@ internal class ManifestConfigLoader {
maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents) maxPersistedEvents = data.getInt(MAX_PERSISTED_EVENTS, maxPersistedEvents)
maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions) maxPersistedSessions = data.getInt(MAX_PERSISTED_SESSIONS, maxPersistedSessions)
maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads) maxReportedThreads = data.getInt(MAX_REPORTED_THREADS, maxReportedThreads)
launchDurationMillis = data.getInt( threadCollectionTimeLimitMillis = data.getLong(
LAUNCH_CRASH_THRESHOLD_MS, THREAD_COLLECTION_TIME_LIMIT_MS,
launchDurationMillis.toInt() threadCollectionTimeLimitMillis
).toLong() )
launchDurationMillis = data.getInt( launchDurationMillis = data.getInt(
LAUNCH_DURATION_MILLIS, LAUNCH_DURATION_MILLIS,
launchDurationMillis.toInt() launchDurationMillis.toInt()
@ -135,9 +137,9 @@ internal class ManifestConfigLoader {
if (data.containsKey(ENABLED_RELEASE_STAGES)) { if (data.containsKey(ENABLED_RELEASE_STAGES)) {
enabledReleaseStages = getStrArray(data, ENABLED_RELEASE_STAGES, enabledReleaseStages) 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() 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() else -> ary.toSet()
} }
} }
private fun getPatternSet(
data: Bundle,
key: String,
default: Set<Pattern>?
): Set<Pattern>? {
val delimitedStr = data.getString(key) ?: return default
return delimitedStr.splitToSequence(',')
.map { Pattern.compile(it) }
.toSet()
}
} }

@ -6,6 +6,7 @@ import com.bugsnag.android.internal.StringUtils
import com.bugsnag.android.internal.TrimMetrics import com.bugsnag.android.internal.TrimMetrics
import java.io.IOException import java.io.IOException
import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ConcurrentHashMap
import java.util.regex.Pattern
/** /**
* A container for additional diagnostic information you'd like to send with * 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() val jsonStreamer: ObjectJsonStreamer = ObjectJsonStreamer()
var redactedKeys: Set<String> var redactedKeys: Set<Pattern>
get() = jsonStreamer.redactedKeys get() = jsonStreamer.redactedKeys
set(value) { set(value) {
jsonStreamer.redactedKeys = value jsonStreamer.redactedKeys = value

@ -19,6 +19,7 @@ import java.util.HashMap;
import java.util.List; import java.util.List;
import java.util.Locale; import java.util.Locale;
import java.util.Map; 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 * 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) { 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() { private static @NonNull File getPersistenceDirectory() {
@ -348,21 +349,30 @@ public class NativeInterface {
*/ */
@SuppressWarnings("unused") @SuppressWarnings("unused")
public static boolean isDiscardErrorClass(@NonNull String name) { public static boolean isDiscardErrorClass(@NonNull String name) {
return getClient().getConfig().getDiscardClasses().contains(name); Collection<Pattern> discardClasses = getClient().getConfig().getDiscardClasses();
if (discardClasses.isEmpty()) {
return false;
}
for (Pattern pattern : discardClasses) {
if (pattern.matcher(name).matches()) {
return true;
}
}
return false;
} }
@SuppressWarnings("unchecked") @SuppressWarnings("unchecked")
private static void deepMerge(Map<String, Object> src, Map<String, Object> dst) { private static void deepMerge(Map<String, Object> src, Map<String, Object> dst) {
for (Map.Entry<String, Object> entry: src.entrySet()) { for (Map.Entry<String, Object> entry : src.entrySet()) {
String key = entry.getKey(); String key = entry.getKey();
Object srcValue = entry.getValue(); Object srcValue = entry.getValue();
Object dstValue = dst.get(key); Object dstValue = dst.get(key);
if (srcValue instanceof Map && (dstValue instanceof Map)) { if (srcValue instanceof Map && (dstValue instanceof Map)) {
deepMerge((Map<String, Object>)srcValue, (Map<String, Object>)dstValue); deepMerge((Map<String, Object>) srcValue, (Map<String, Object>) dstValue);
} else if (srcValue instanceof Collection && dstValue instanceof Collection) { } else if (srcValue instanceof Collection && dstValue instanceof Collection) {
// Just append everything because we don't know enough about the context or // Just append everything because we don't know enough about the context or
// provenance of the data to make an intelligent decision about this. // provenance of the data to make an intelligent decision about this.
((Collection<Object>)dstValue).addAll((Collection<Object>)srcValue); ((Collection<Object>) dstValue).addAll((Collection<Object>) srcValue);
} else { } else {
dst.put(key, srcValue); dst.put(key, srcValue);
} }

@ -7,7 +7,7 @@ import java.io.IOException
*/ */
class Notifier @JvmOverloads constructor( class Notifier @JvmOverloads constructor(
var name: String = "Android Bugsnag Notifier", var name: String = "Android Bugsnag Notifier",
var version: String = "5.31.3", var version: String = "6.1.0",
var url: String = "https://bugsnag.com" var url: String = "https://bugsnag.com"
) : JsonStream.Streamable { ) : JsonStream.Streamable {

@ -4,6 +4,7 @@ import com.bugsnag.android.internal.DateUtils
import java.io.IOException import java.io.IOException
import java.lang.reflect.Array import java.lang.reflect.Array
import java.util.Date import java.util.Date
import java.util.regex.Pattern
internal class ObjectJsonStreamer { internal class ObjectJsonStreamer {
@ -12,7 +13,7 @@ internal class ObjectJsonStreamer {
internal const val OBJECT_PLACEHOLDER = "[OBJECT]" 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 // Write complex/nested values to a JsonStreamer
@Throws(IOException::class) @Throws(IOException::class)
@ -66,5 +67,5 @@ internal class ObjectJsonStreamer {
} }
// Should this key be redacted // 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() }
} }

@ -1,32 +1,31 @@
package com.bugsnag.android; package com.bugsnag.android
import androidx.annotation.NonNull;
/** /**
* Add a "on breadcrumb" callback, to execute code before every * Add a "on breadcrumb" callback, to execute code before every
* breadcrumb captured by Bugsnag. * breadcrumb captured by Bugsnag.
* <p> *
*
* You can use this to modify breadcrumbs before they are stored by Bugsnag. * You can use this to modify breadcrumbs before they are stored by Bugsnag.
* You can also return <code>false</code> from any callback to ignore a breadcrumb. * You can also return `false` from any callback to ignore a breadcrumb.
* <p> *
*
* For example: * For example:
* <p> *
*
* Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() { * Bugsnag.onBreadcrumb(new OnBreadcrumbCallback() {
* public boolean onBreadcrumb(Breadcrumb breadcrumb) { * public boolean onBreadcrumb(Breadcrumb breadcrumb) {
* return false; // ignore the breadcrumb * return false; // ignore the breadcrumb
* } * }
* }) * })
*/ */
public interface OnBreadcrumbCallback { fun interface OnBreadcrumbCallback {
/** /**
* Runs the "on breadcrumb" callback. If the callback returns * Runs the "on breadcrumb" callback. If the callback returns
* <code>false</code> 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. * and the breadcrumb will not be captured by Bugsnag.
* *
* @param breadcrumb the breadcrumb to be captured by Bugsnag * @param breadcrumb the breadcrumb to be captured by Bugsnag
* @see Breadcrumb * @see Breadcrumb
*/ */
boolean onBreadcrumb(@NonNull Breadcrumb breadcrumb); fun onBreadcrumb(breadcrumb: Breadcrumb): Boolean
} }

@ -1,24 +0,0 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
/**
* A callback to be run before error reports are sent to Bugsnag.
* <p>
* <p>You can use this to add or modify information attached to an error
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to halt execution.
* <p>"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
* <code>false</code> 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);
}

@ -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
}

@ -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);
}

@ -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
}

@ -1,23 +0,0 @@
package com.bugsnag.android;
import androidx.annotation.NonNull;
/**
* A callback to be run before sessions are sent to Bugsnag.
* <p>
* <p>You can use this to add or modify information attached to a session
* before it is sent to your dashboard. You can also return
* <code>false</code> from any callback to halt execution.
*/
public interface OnSessionCallback {
/**
* Runs the "on session" callback. If the callback returns
* <code>false</code> 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);
}

@ -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
}

@ -35,7 +35,7 @@ internal data class SessionFilenameInfo(
@JvmStatic @JvmStatic
fun defaultFilename( fun defaultFilename(
obj: Any, obj: Any?,
config: ImmutableConfig config: ImmutableConfig
): SessionFilenameInfo { ): SessionFilenameInfo {
val sanitizedApiKey = when (obj) { val sanitizedApiKey = when (obj) {

@ -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) {}
}

@ -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<File> SESSION_COMPARATOR = new Comparator<File>() {
@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));
}
}

@ -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<in File?> = Comparator { lhs, rhs ->
if (lhs == null && rhs == null) {
return@Comparator 0
}
if (lhs == null) {
return@Comparator 1
}
if (rhs == null) {
return@Comparator -1
}
val lhsName = lhs.name
val rhsName = rhs.name
lhsName.compareTo(rhsName)
}
}
override fun getFilename(obj: Any?): String {
val sessionInfo = defaultFilename(obj, config)
return sessionInfo.encode()
}
}

@ -2,10 +2,11 @@ package com.bugsnag.android;
import com.bugsnag.android.internal.BackgroundTaskService; import com.bugsnag.android.internal.BackgroundTaskService;
import com.bugsnag.android.internal.DateUtils; import com.bugsnag.android.internal.DateUtils;
import com.bugsnag.android.internal.ForegroundDetector;
import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.TaskType; import com.bugsnag.android.internal.TaskType;
import android.os.SystemClock; import android.app.Activity;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -19,9 +20,8 @@ import java.util.Deque;
import java.util.List; import java.util.List;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.RejectedExecutionException; 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; private static final int DEFAULT_TIMEOUT_MS = 30000;
@ -33,14 +33,7 @@ class SessionTracker extends BaseObservable {
private final CallbackState callbackState; private final CallbackState callbackState;
private final Client client; private final Client client;
final SessionStore sessionStore; 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 volatile Session currentSession = null;
private final ForegroundDetector foregroundDetector;
final BackgroundTaskService backgroundTaskService; final BackgroundTaskService backgroundTaskService;
final Logger logger; final Logger logger;
@ -66,10 +59,8 @@ class SessionTracker extends BaseObservable {
this.client = client; this.client = client;
this.timeoutMs = timeoutMs; this.timeoutMs = timeoutMs;
this.sessionStore = sessionStore; this.sessionStore = sessionStore;
this.foregroundDetector = new ForegroundDetector(client.getAppContext());
this.backgroundTaskService = backgroundTaskService; this.backgroundTaskService = backgroundTaskService;
this.logger = logger; this.logger = logger;
notifyNdkInForeground();
} }
/** /**
@ -340,12 +331,18 @@ class SessionTracker extends BaseObservable {
return delivery.deliver(payload, params); return delivery.deliver(payload, params);
} }
void onActivityStarted(String activityName) { public void onActivityStarted(Activity activity) {
updateForegroundTracker(activityName, true, SystemClock.elapsedRealtime()); updateContext(
activity.getClass().getSimpleName(),
true
);
} }
void onActivityStopped(String activityName) { public void onActivityStopped(Activity activity) {
updateForegroundTracker(activityName, false, SystemClock.elapsedRealtime()); updateContext(
activity.getClass().getSimpleName(),
false
);
} }
/** /**
@ -359,47 +356,26 @@ class SessionTracker extends BaseObservable {
* *
* @param activityName the activity name * @param activityName the activity name
* @param activityStarting whether the activity is being started or not * @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) { if (activityStarting) {
long noActivityRunningForMs = nowMs - lastExitedForegroundMs.get();
synchronized (foregroundActivities) { synchronized (foregroundActivities) {
if (foregroundActivities.isEmpty()) {
lastEnteredForegroundMs.set(nowMs);
if (noActivityRunningForMs >= timeoutMs
&& configuration.getAutoTrackSessions()) {
startNewSession(new Date(), client.getUser(), true);
}
}
foregroundActivities.add(activityName); foregroundActivities.add(activityName);
} }
} else { } else {
synchronized (foregroundActivities) { synchronized (foregroundActivities) {
foregroundActivities.removeLastOccurrence(activityName); foregroundActivities.removeLastOccurrence(activityName);
if (foregroundActivities.isEmpty()) {
lastExitedForegroundMs.set(nowMs);
}
} }
} }
client.getContextState().setAutomaticContext(getContextActivity()); client.getContextState().setAutomaticContext(getContextActivity());
notifyNdkInForeground();
} }
private void notifyNdkInForeground() { boolean isInForeground() {
Boolean inForeground = isInForeground(); return ForegroundDetector.isInForeground();
final boolean foreground = inForeground != null ? inForeground : false;
updateState(new StateEvent.UpdateInForeground(foreground, getContextActivity()));
}
@Nullable
Boolean isInForeground() {
return foregroundDetector.isInForeground();
} }
long getLastEnteredForegroundMs() { long getLastEnteredForegroundMs() {
return lastEnteredForegroundMs.get(); return ForegroundDetector.getLastEnteredForegroundMs();
} }
@Nullable @Nullable
@ -408,4 +384,18 @@ class SessionTracker extends BaseObservable {
return foregroundActivities.peekLast(); 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()));
}
} }

@ -64,6 +64,7 @@ final class SeverityReason implements JsonStream.Streamable {
switch (reason) { switch (reason) {
case REASON_UNHANDLED_EXCEPTION: case REASON_UNHANDLED_EXCEPTION:
case REASON_PROMISE_REJECTION: case REASON_PROMISE_REJECTION:
case REASON_SIGNAL:
case REASON_ANR: case REASON_ANR:
return new SeverityReason(reason, ERROR, true, true, null, null); return new SeverityReason(reason, ERROR, true, true, null, null);
case REASON_STRICT_MODE: case REASON_STRICT_MODE:

@ -1,5 +1,6 @@
package com.bugsnag.android package com.bugsnag.android
import androidx.annotation.RestrictTo
import com.bugsnag.android.internal.JsonHelper import com.bugsnag.android.internal.JsonHelper
import java.io.IOException import java.io.IOException
@ -70,7 +71,8 @@ class Stackframe : JsonStream.Streamable {
var type: ErrorType? = null var type: ErrorType? = null
@JvmOverloads @JvmOverloads
internal constructor( @RestrictTo(RestrictTo.Scope.LIBRARY_GROUP)
constructor(
method: String?, method: String?,
file: String?, file: String?,
lineNumber: Number?, lineNumber: Number?,
@ -130,7 +132,9 @@ class Stackframe : JsonStream.Streamable {
writer.name("columnNumber").value(columnNumber) writer.name("columnNumber").value(columnNumber)
frameAddress?.let { writer.name("frameAddress").value(JsonHelper.ulongToHex(frameAddress)) } 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)) } loadAddress?.let { writer.name("loadAddress").value(JsonHelper.ulongToHex(loadAddress)) }
codeIdentifier?.let { writer.name("codeIdentifier").value(it) } codeIdentifier?.let { writer.name("codeIdentifier").value(it) }
isPC?.let { writer.name("isPC").value(it) } isPC?.let { writer.name("isPC").value(it) }

@ -38,7 +38,13 @@ internal class StorageModule(
val lastRunInfoStore by future { LastRunInfoStore(immutableConfig) } 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 lastRunInfo by future {
val info = lastRunInfoStore.load() val info = lastRunInfoStore.load()

@ -13,7 +13,7 @@ import kotlin.concurrent.withLock
* This class is made thread safe through the use of a [ReadWriteLock]. * This class is made thread safe through the use of a [ReadWriteLock].
*/ */
internal class SynchronizedStreamableStore<T : JsonStream.Streamable>( internal class SynchronizedStreamableStore<T : JsonStream.Streamable>(
private val file: File internal val file: File
) { ) {
private val lock = ReentrantReadWriteLock() private val lock = ReentrantReadWriteLock()

@ -4,6 +4,7 @@ import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
import java.io.IOException; import java.io.IOException;
import java.util.ArrayList;
import java.util.List; import java.util.List;
/** /**
@ -16,9 +17,29 @@ public class Thread implements JsonStream.Streamable {
private final Logger logger; private final Logger logger;
Thread( Thread(
long id, String id,
@NonNull String name, @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<Stackframe>())
);
this.logger = logger;
}
Thread(
String id,
@NonNull String name,
@NonNull ErrorType type,
boolean errorReportingThread, boolean errorReportingThread,
@NonNull Thread.State state, @NonNull Thread.State state,
@NonNull Stacktrace stacktrace, @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}) * Sets the unique ID of the thread (from {@link java.lang.Thread})
*/ */
public void setId(long id) { public void setId(@NonNull String id) {
if (id != null) {
impl.setId(id); impl.setId(id);
} else {
logNull("id");
}
} }
/** /**
* Gets the unique ID of the thread (from {@link java.lang.Thread}) * Gets the unique ID of the thread (from {@link java.lang.Thread})
*/ */
public long getId() { @NonNull
public String getId() {
return impl.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) * 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) { if (type != null) {
impl.setType(type); impl.setType(type);
} else { } 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) * Gets the type of thread based on the originating platform (intended for internal use only)
*/ */
@NonNull @NonNull
public ThreadType getType() { public ErrorType getType() {
return impl.getType(); return impl.getType();
} }

@ -3,9 +3,9 @@ package com.bugsnag.android
import java.io.IOException import java.io.IOException
class ThreadInternal internal constructor( class ThreadInternal internal constructor(
var id: Long, var id: String,
var name: String, var name: String,
var type: ThreadType, var type: ErrorType,
val isErrorReportingThread: Boolean, val isErrorReportingThread: Boolean,
var state: String, var state: String,
stacktrace: Stacktrace stacktrace: Stacktrace

@ -1,7 +1,10 @@
package com.bugsnag.android package com.bugsnag.android
import android.os.SystemClock
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import java.io.IOException import java.io.IOException
import kotlin.math.max
import kotlin.math.min
import java.lang.Thread as JavaThread import java.lang.Thread as JavaThread
/** /**
@ -11,6 +14,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
exc: Throwable?, exc: Throwable?,
isUnhandled: Boolean, isUnhandled: Boolean,
maxThreads: Int, maxThreads: Int,
threadCollectionTimeLimitMillis: Long,
sendThreads: ThreadSendPolicy, sendThreads: ThreadSendPolicy,
projectPackages: Collection<String>, projectPackages: Collection<String>,
logger: Logger, logger: Logger,
@ -22,7 +26,15 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
exc: Throwable?, exc: Throwable?,
isUnhandled: Boolean, isUnhandled: Boolean,
config: ImmutableConfig 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<Thread> val threads: MutableList<Thread>
@ -37,6 +49,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
exc, exc,
isUnhandled, isUnhandled,
maxThreads, maxThreads,
threadCollectionTimeLimitMillis,
projectPackages, projectPackages,
logger logger
) )
@ -70,6 +83,7 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
exc: Throwable?, exc: Throwable?,
isUnhandled: Boolean, isUnhandled: Boolean,
maxThreadCount: Int, maxThreadCount: Int,
threadCollectionTimeLimitMillis: Long,
projectPackages: Collection<String>, projectPackages: Collection<String>,
logger: Logger logger: Logger
): MutableList<Thread> { ): MutableList<Thread> {
@ -89,9 +103,9 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
) )
return Thread( return Thread(
thread.id, thread.id.toString(),
thread.name, thread.name,
ThreadType.ANDROID, ErrorType.ANDROID,
isErrorThread, isErrorThread,
Thread.State.forThread(thread), Thread.State.forThread(thread),
stackTrace, stackTrace,
@ -101,23 +115,49 @@ internal class ThreadState @Suppress("LongParameterList") constructor(
// Keep the lowest ID threads (ordered). Anything after maxThreadCount is lost. // 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. // 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)) { val sortedThreads = allThreads.sortedBy { it.id }
keepThreads val currentThreadIndex = sortedThreads.binarySearch(0, min(maxThreadCount, sortedThreads.size)) {
} else { it.id.compareTo(currentThread.id)
}
// API 24/25 don't record the currentThread, so add it in manually // API 24/25 don't record the currentThread, so add it in manually
// https://issuetracker.google.com/issues/64122757 // https://issuetracker.google.com/issues/64122757
// currentThread may also have been removed if its ID occurred after maxThreadCount // currentThread may also have been removed if its ID occurred after maxThreadCount
keepThreads.take(Math.max(maxThreadCount - 1, 0)).plus(currentThread).sortedBy { it.id } // as such we may need to leave a space in new list for currentThread
}.map { toBugsnagThread(it) }.toMutableList() val keepThreads = sortedThreads.take(
if (currentThreadIndex >= 0) maxThreadCount else max(maxThreadCount - 1, 0)
)
val reportThreads = ArrayList<Thread>(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) { if (allThreads.size > maxThreadCount) {
reportThreads.add( reportThreads.add(
Thread( Thread(
-1, "",
"[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]", "[${allThreads.size - maxThreadCount} threads omitted as the maxReportedThreads limit ($maxThreadCount) was exceeded]",
ThreadType.EMPTY, ErrorType.UNKNOWN,
false, false,
Thread.State.UNKNOWN, Thread.State.UNKNOWN,
Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger), Stacktrace(arrayOf(StackTraceElement("", "", "-", 0)), projectPackages, logger),

@ -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 }
}
}

@ -3,7 +3,6 @@ package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.StateObserver import com.bugsnag.android.internal.StateObserver
import java.io.File import java.io.File
import java.io.IOException
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
/** /**
@ -12,7 +11,7 @@ import java.util.concurrent.atomic.AtomicReference
internal class UserStore @JvmOverloads constructor( internal class UserStore @JvmOverloads constructor(
private val config: ImmutableConfig, private val config: ImmutableConfig,
private val deviceId: String?, 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 sharedPrefMigrator: SharedPrefMigrator,
private val logger: Logger private val logger: Logger
) { ) {
@ -22,11 +21,6 @@ internal class UserStore @JvmOverloads constructor(
private val previousUser = AtomicReference<User?>(null) private val previousUser = AtomicReference<User?>(null)
init { init {
try {
file.createNewFile()
} catch (exc: IOException) {
logger.w("Failed to created device ID file", exc)
}
this.synchronizedStreamableStore = SynchronizedStreamableStore(file) this.synchronizedStreamableStore = SynchronizedStreamableStore(file)
} }
@ -87,13 +81,19 @@ internal class UserStore @JvmOverloads constructor(
val legacyUser = sharedPrefMigrator.loadUser(deviceId) val legacyUser = sharedPrefMigrator.loadUser(deviceId)
save(legacyUser) save(legacyUser)
legacyUser legacyUser
} else { } else if (
return try { synchronizedStreamableStore.file.canRead() &&
synchronizedStreamableStore.file.length() > 0L &&
persist
) {
try {
synchronizedStreamableStore.load(User.Companion::fromReader) synchronizedStreamableStore.load(User.Companion::fromReader)
} catch (exc: Exception) { } catch (exc: Exception) {
logger.w("Failed to load user info", exc) logger.w("Failed to load user info", exc)
null null
} }
} else {
null
} }
} }
} }

@ -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<out String>?,
selection: String?,
selectionArgs: Array<out String>?,
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<out String>?): Int {
checkPrivilegeEscalation()
return 0
}
final override fun update(
uri: Uri,
values: ContentValues?,
selection: String?,
selectionArgs: Array<out String>?,
): 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")
}
}

@ -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
}
}

@ -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)
)
}
}
}
}

@ -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))
}
}

@ -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
}
}

@ -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<WeakReference<OnActivityCallback>>()
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)
}
}

@ -25,6 +25,8 @@ import com.bugsnag.android.errorApiHeaders
import com.bugsnag.android.safeUnrollCauses import com.bugsnag.android.safeUnrollCauses
import com.bugsnag.android.sessionApiHeaders import com.bugsnag.android.sessionApiHeaders
import java.io.File import java.io.File
import java.util.concurrent.Callable
import java.util.regex.Pattern
data class ImmutableConfig( data class ImmutableConfig(
val apiKey: String, val apiKey: String,
@ -32,7 +34,7 @@ data class ImmutableConfig(
val enabledErrorTypes: ErrorTypes, val enabledErrorTypes: ErrorTypes,
val autoTrackSessions: Boolean, val autoTrackSessions: Boolean,
val sendThreads: ThreadSendPolicy, val sendThreads: ThreadSendPolicy,
val discardClasses: Collection<String>, val discardClasses: Collection<Pattern>,
val enabledReleaseStages: Collection<String>?, val enabledReleaseStages: Collection<String>?,
val projectPackages: Collection<String>, val projectPackages: Collection<String>,
val enabledBreadcrumbTypes: Set<BreadcrumbType>?, val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
@ -51,6 +53,7 @@ data class ImmutableConfig(
val maxPersistedEvents: Int, val maxPersistedEvents: Int,
val maxPersistedSessions: Int, val maxPersistedSessions: Int,
val maxReportedThreads: Int, val maxReportedThreads: Int,
val threadCollectionTimeLimitMillis: Long,
val persistenceDirectory: Lazy<File>, val persistenceDirectory: Lazy<File>,
val sendLaunchCrashesSynchronously: Boolean, val sendLaunchCrashesSynchronously: Boolean,
val attemptDeliveryOnCrash: Boolean, val attemptDeliveryOnCrash: Boolean,
@ -58,7 +61,7 @@ data class ImmutableConfig(
// results cached here to avoid unnecessary lookups in Client. // results cached here to avoid unnecessary lookups in Client.
val packageInfo: PackageInfo?, val packageInfo: PackageInfo?,
val appInfo: ApplicationInfo?, val appInfo: ApplicationInfo?,
val redactedKeys: Collection<String> val redactedKeys: Collection<Pattern>
) { ) {
@JvmName("getErrorApiDeliveryParams") @JvmName("getErrorApiDeliveryParams")
@ -113,7 +116,11 @@ data class ImmutableConfig(
*/ */
@VisibleForTesting @VisibleForTesting
internal fun shouldDiscardByErrorClass(errorClass: String?): Boolean { 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, maxPersistedEvents = config.maxPersistedEvents,
maxPersistedSessions = config.maxPersistedSessions, maxPersistedSessions = config.maxPersistedSessions,
maxReportedThreads = config.maxReportedThreads, maxReportedThreads = config.maxReportedThreads,
threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
telemetry = config.telemetry.toSet(), telemetry = config.telemetry.toSet(),
persistenceDirectory = persistenceDir, 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( internal fun sanitiseConfiguration(
appContext: Context, appContext: Context,
configuration: Configuration, configuration: Configuration,
connectivity: Connectivity connectivity: Connectivity,
backgroundTaskService: BackgroundTaskService
): ImmutableConfig { ): ImmutableConfig {
validateApiKey(configuration.apiKey)
val packageName = appContext.packageName val packageName = appContext.packageName
val packageManager = appContext.packageManager val packageManager = appContext.packageManager
val packageInfo = runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull() val packageInfo = runCatching { packageManager.getPackageInfo(packageName, 0) }.getOrNull()
@ -219,7 +250,7 @@ internal fun sanitiseConfiguration(
} }
// populate buildUUID from manifest // populate buildUUID from manifest
val buildUuid = populateBuildUuid(appInfo) val buildUuid = collectBuildUuid(appInfo, backgroundTaskService)
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (configuration.delivery == null) { 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 val bundle = appInfo?.metaData
return when { return when {
bundle?.containsKey(BUILD_UUID) == true -> { 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 else -> null
} }
} }
internal const val RELEASE_STAGE_DEVELOPMENT = "development" internal const val RELEASE_STAGE_DEVELOPMENT = "development"
internal const val RELEASE_STAGE_PRODUCTION = "production" internal const val RELEASE_STAGE_PRODUCTION = "production"
internal const val VALID_API_KEY_LEN = 32

@ -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);
}

@ -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)
}

@ -2,6 +2,7 @@ package com.bugsnag.android.internal.dag
import com.bugsnag.android.Configuration import com.bugsnag.android.Configuration
import com.bugsnag.android.Connectivity import com.bugsnag.android.Connectivity
import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.sanitiseConfiguration import com.bugsnag.android.internal.sanitiseConfiguration
/** /**
@ -11,8 +12,9 @@ import com.bugsnag.android.internal.sanitiseConfiguration
internal class ConfigModule( internal class ConfigModule(
contextModule: ContextModule, contextModule: ContextModule,
configuration: Configuration, configuration: Configuration,
connectivity: Connectivity connectivity: Connectivity,
bgTaskExecutor: BackgroundTaskService
) : DependencyModule() { ) : DependencyModule() {
val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity) val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor)
} }

@ -96,6 +96,7 @@ import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID; import java.util.UUID;
import java.util.concurrent.TimeoutException; import java.util.concurrent.TimeoutException;
import java.util.regex.Pattern;
import javax.mail.AuthenticationFailedException; import javax.mail.AuthenticationFailedException;
import javax.mail.FolderClosedException; import javax.mail.FolderClosedException;
@ -119,6 +120,36 @@ public class Log {
static final String TOKEN_REFRESH_REQUIRED = static final String TOKEN_REFRESH_REQUIRED =
"Token refresh required. Is there a VPN based app running?"; "Token refresh required. Is there a VPN based app running?";
static final List<String> 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 { static {
System.loadLibrary("fairemail"); System.loadLibrary("fairemail");
} }
@ -376,37 +407,11 @@ public class Log {
config.setEnabledErrorTypes(etypes); config.setEnabledErrorTypes(etypes);
config.setMaxBreadcrumbs(BuildConfig.PLAY_STORE_RELEASE ? 250 : 500); config.setMaxBreadcrumbs(BuildConfig.PLAY_STORE_RELEASE ? 250 : 500);
Set<String> ignore = new HashSet<>(); Set<Pattern> discardClasses = new HashSet<>();
if (!BuildConfig.DEBUG)
ignore.add("com.sun.mail.util.MailConnectException"); for (String clazz : IGNORE_CLASSES)
discardClasses.add(Pattern.compile(clazz.replace(".", "\\.")));
ignore.add("android.accounts.AuthenticatorException"); config.setDiscardClasses(discardClasses);
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);
final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context); final SharedPreferences prefs = PreferenceManager.getDefaultSharedPreferences(context);
ActivityManager am = Helper.getSystemService(context, ActivityManager.class); ActivityManager am = Helper.getSystemService(context, ActivityManager.class);

Loading…
Cancel
Save