Updated Bugsnag

pull/217/head
M66B 9 months ago
parent 0c697d9c31
commit 245baa3b13

@ -593,7 +593,7 @@ dependencies {
def minidns_version = "1.0.5" def minidns_version = "1.0.5"
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 = "6.7.0" def bugsnag_version = "6.10.0"
def biweekly_version = "0.6.8" def biweekly_version = "0.6.8"
def vcard_version = "0.12.1" def vcard_version = "0.12.1"
def relinker_version = "1.4.5" def relinker_version = "1.4.5"

@ -79,6 +79,11 @@ internal class ActivityBreadcrumbCollector(
} }
set("hasData", intent.data != null) set("hasData", intent.data != null)
set("hasExtras", intent.extras?.keySet()?.joinToString(", ") ?: false)
try {
set("hasExtras", intent.extras?.keySet()?.joinToString(", ") ?: false)
} catch (re: Exception) {
// deliberately ignore
}
} }
} }

@ -1,6 +1,7 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.dag.Provider
import java.io.IOException import java.io.IOException
/** /**
@ -36,7 +37,7 @@ open class App internal constructor(
/** /**
* The unique identifier for the build of the application set in [Configuration.buildUuid] * The unique identifier for the build of the application set in [Configuration.buildUuid]
*/ */
var buildUuid: String?, buildUuid: Provider<String?>?,
/** /**
* The application type set in [Configuration#version] * The application type set in [Configuration#version]
@ -49,6 +50,15 @@ open class App internal constructor(
var versionCode: Number? var versionCode: Number?
) : JsonStream.Streamable { ) : JsonStream.Streamable {
private var buildUuidProvider: Provider<String?>? = buildUuid
var buildUuid: String? = null
get() = field ?: buildUuidProvider?.getOrNull()
set(value) {
field = value
buildUuidProvider = null
}
internal constructor( internal constructor(
config: ImmutableConfig, config: ImmutableConfig,
binaryArch: String?, binaryArch: String?,

@ -1,18 +1,20 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.dag.Provider
import com.bugsnag.android.internal.dag.ValueProvider
/** /**
* Stateful information set by the notifier about your app can be found on this class. These values * Stateful information set by the notifier about your app can be found on this class. These values
* can be accessed and amended if necessary. * can be accessed and amended if necessary.
*/ */
class AppWithState( class AppWithState internal constructor(
binaryArch: String?, binaryArch: String?,
id: String?, id: String?,
releaseStage: String?, releaseStage: String?,
version: String?, version: String?,
codeBundleId: String?, codeBundleId: String?,
buildUuid: String?, buildUuid: Provider<String?>?,
type: String?, type: String?,
versionCode: Number?, versionCode: Number?,
@ -36,7 +38,61 @@ class AppWithState(
* Whether the application was launching when the event occurred * Whether the application was launching when the event occurred
*/ */
var isLaunching: Boolean? var isLaunching: Boolean?
) : App(binaryArch, id, releaseStage, version, codeBundleId, buildUuid, type, versionCode) { ) : App(
binaryArch,
id,
releaseStage,
version,
codeBundleId,
buildUuid,
type,
versionCode
) {
constructor(
binaryArch: String?,
id: String?,
releaseStage: String?,
version: String?,
codeBundleId: String?,
buildUuid: String?,
type: String?,
versionCode: Number?,
/**
* The number of milliseconds the application was running before the event occurred
*/
duration: Number?,
/**
* The number of milliseconds the application was running in the foreground before the
* event occurred
*/
durationInForeground: Number?,
/**
* Whether the application was in the foreground when the event occurred
*/
inForeground: Boolean?,
/**
* Whether the application was launching when the event occurred
*/
isLaunching: Boolean?
) : this(
binaryArch,
id,
releaseStage,
version,
codeBundleId,
buildUuid?.let(::ValueProvider),
type,
versionCode,
duration,
durationInForeground,
inForeground,
isLaunching
)
internal constructor( internal constructor(
config: ImmutableConfig, config: ImmutableConfig,

@ -2,6 +2,7 @@ package com.bugsnag.android
import com.bugsnag.android.internal.DateUtils import com.bugsnag.android.internal.DateUtils
import com.bugsnag.android.internal.InternalMetricsImpl import com.bugsnag.android.internal.InternalMetricsImpl
import com.bugsnag.android.internal.dag.ValueProvider
import java.text.DateFormat import java.text.DateFormat
import java.text.SimpleDateFormat import java.text.SimpleDateFormat
import java.util.Date import java.util.Date
@ -151,7 +152,7 @@ internal class BugsnagEventMapper(
app["releaseStage"] as? String, app["releaseStage"] as? String,
app["version"] as? String, app["version"] as? String,
app["codeBundleId"] as? String, app["codeBundleId"] as? String,
app["buildUUID"] as? String, (app["buildUUID"] as? String)?.let(::ValueProvider),
app["type"] as? String, app["type"] as? String,
(app["versionCode"] as? Number)?.toInt(), (app["versionCode"] as? Number)?.toInt(),
(app["duration"] as? Number)?.toLong(), (app["duration"] as? Number)?.toLong(),

@ -10,7 +10,7 @@ import com.bugsnag.android.internal.dag.DependencyModule
internal class BugsnagStateModule( internal class BugsnagStateModule(
cfg: ImmutableConfig, cfg: ImmutableConfig,
configuration: Configuration configuration: Configuration
) : DependencyModule() { ) : DependencyModule {
val clientObservable = ClientObservable() val clientObservable = ClientObservable()

@ -3,7 +3,6 @@ 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.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;
@ -13,6 +12,7 @@ import com.bugsnag.android.internal.StateObserver;
import com.bugsnag.android.internal.TaskType; import com.bugsnag.android.internal.TaskType;
import com.bugsnag.android.internal.dag.ConfigModule; import com.bugsnag.android.internal.dag.ConfigModule;
import com.bugsnag.android.internal.dag.ContextModule; import com.bugsnag.android.internal.dag.ContextModule;
import com.bugsnag.android.internal.dag.Provider;
import com.bugsnag.android.internal.dag.SystemServiceModule; import com.bugsnag.android.internal.dag.SystemServiceModule;
import android.app.Application; import android.app.Application;
@ -59,7 +59,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
private final InternalMetrics internalMetrics; private final InternalMetrics internalMetrics;
private final ContextState contextState; private final ContextState contextState;
private final CallbackState callbackState; private final CallbackState callbackState;
private final UserState userState; private final Provider<UserState> userState;
private final Map<String, Object> configDifferences; private final Map<String, Object> configDifferences;
final Context appContext; final Context appContext;
@ -125,7 +125,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
* @param configuration a configuration for the Client * @param configuration a configuration for the Client
*/ */
public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) { public Client(@NonNull Context androidContext, @NonNull final Configuration configuration) {
ContextModule contextModule = new ContextModule(androidContext); ContextModule contextModule = new ContextModule(androidContext, bgTaskService);
appContext = contextModule.getCtx(); appContext = contextModule.getCtx();
notifier = configuration.getNotifier(); notifier = configuration.getNotifier();
@ -165,19 +165,15 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
+ "Bugsnag.start(context.getApplicationContext()). " + "Bugsnag.start(context.getApplicationContext()). "
+ "For further info see: " + "For further info see: "
+ "https://docs.bugsnag.com/platforms/android/#basic-configuration"); + "https://docs.bugsnag.com/platforms/android/#basic-configuration");
} }
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, bgTaskService);
// setup state trackers for bugsnag // setup state trackers for bugsnag
BugsnagStateModule bugsnagStateModule = new BugsnagStateModule( BugsnagStateModule bugsnagStateModule =
immutableConfig, configuration); new BugsnagStateModule(immutableConfig, configuration);
clientObservable = bugsnagStateModule.getClientObservable(); clientObservable = bugsnagStateModule.getClientObservable();
callbackState = bugsnagStateModule.getCallbackState(); callbackState = bugsnagStateModule.getCallbackState();
breadcrumbState = bugsnagStateModule.getBreadcrumbState(); breadcrumbState = bugsnagStateModule.getBreadcrumbState();
@ -186,34 +182,26 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
featureFlagState = bugsnagStateModule.getFeatureFlagState(); featureFlagState = bugsnagStateModule.getFeatureFlagState();
// lookup system services // lookup system services
final SystemServiceModule systemServiceModule = new SystemServiceModule(contextModule); final SystemServiceModule systemServiceModule =
new SystemServiceModule(contextModule, bgTaskService);
// block until storage module has resolved everything
storageModule.resolveDependencies(bgTaskService, TaskType.IO);
// setup further state trackers and data collection // setup further state trackers and data collection
TrackerModule trackerModule = new TrackerModule(configModule, TrackerModule trackerModule = new TrackerModule(configModule,
storageModule, this, bgTaskService, callbackState); storageModule, this, bgTaskService, callbackState);
launchCrashTracker = trackerModule.getLaunchCrashTracker();
sessionTracker = trackerModule.getSessionTracker();
DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule,
configModule, systemServiceModule, trackerModule, configModule, systemServiceModule, trackerModule,
bgTaskService, connectivity, storageModule.getDeviceId(), bgTaskService, connectivity, storageModule.getDeviceIdStore(),
storageModule.getInternalDeviceId(), memoryTrimState); memoryTrimState);
dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO);
appDataCollector = dataCollectionModule.getAppDataCollector();
deviceDataCollector = dataCollectionModule.getDeviceDataCollector();
// load the device + user information // load the device + user information
userState = storageModule.getUserStore().load(configuration.getUser()); userState = storageModule.loadUser(configuration.getUser());
storageModule.getSharedPrefMigrator().deleteLegacyPrefs();
EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule, EventStorageModule eventStorageModule = new EventStorageModule(contextModule, configModule,
dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier, dataCollectionModule, bgTaskService, trackerModule, systemServiceModule, notifier,
callbackState); callbackState);
eventStorageModule.resolveDependencies(bgTaskService, TaskType.IO);
eventStore = eventStorageModule.getEventStore(); eventStore = eventStorageModule.getEventStore().get();
deliveryDelegate = new DeliveryDelegate(logger, eventStore, deliveryDelegate = new DeliveryDelegate(logger, eventStore,
immutableConfig, callbackState, notifier, bgTaskService); immutableConfig, callbackState, notifier, bgTaskService);
@ -221,8 +209,13 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
exceptionHandler = new ExceptionHandler(this, logger); exceptionHandler = new ExceptionHandler(this, logger);
// load last run info // load last run info
lastRunInfoStore = storageModule.getLastRunInfoStore(); lastRunInfoStore = storageModule.getLastRunInfoStore().getOrNull();
lastRunInfo = storageModule.getLastRunInfo(); lastRunInfo = storageModule.getLastRunInfo().getOrNull();
launchCrashTracker = trackerModule.getLaunchCrashTracker();
sessionTracker = trackerModule.getSessionTracker().get();
appDataCollector = dataCollectionModule.getAppDataCollector().get();
deviceDataCollector = dataCollectionModule.getDeviceDataCollector().get();
Set<Plugin> userPlugins = configuration.getPlugins(); Set<Plugin> userPlugins = configuration.getPlugins();
pluginClient = new PluginClient(userPlugins, immutableConfig, logger); pluginClient = new PluginClient(userPlugins, immutableConfig, logger);
@ -245,7 +238,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
MetadataState metadataState, MetadataState metadataState,
ContextState contextState, ContextState contextState,
CallbackState callbackState, CallbackState callbackState,
UserState userState, Provider<UserState> userState,
FeatureFlagState featureFlagState, FeatureFlagState featureFlagState,
ClientObservable clientObservable, ClientObservable clientObservable,
Context appContext, Context appContext,
@ -452,7 +445,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
breadcrumbState.addObserver(observer); breadcrumbState.addObserver(observer);
sessionTracker.addObserver(observer); sessionTracker.addObserver(observer);
clientObservable.addObserver(observer); clientObservable.addObserver(observer);
userState.addObserver(observer); userState.get().addObserver(observer);
contextState.addObserver(observer); contextState.addObserver(observer);
deliveryDelegate.addObserver(observer); deliveryDelegate.addObserver(observer);
launchCrashTracker.addObserver(observer); launchCrashTracker.addObserver(observer);
@ -465,7 +458,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
breadcrumbState.removeObserver(observer); breadcrumbState.removeObserver(observer);
sessionTracker.removeObserver(observer); sessionTracker.removeObserver(observer);
clientObservable.removeObserver(observer); clientObservable.removeObserver(observer);
userState.removeObserver(observer); userState.get().removeObserver(observer);
contextState.removeObserver(observer); contextState.removeObserver(observer);
deliveryDelegate.removeObserver(observer); deliveryDelegate.removeObserver(observer);
launchCrashTracker.removeObserver(observer); launchCrashTracker.removeObserver(observer);
@ -479,7 +472,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
void syncInitialState() { void syncInitialState() {
metadataState.emitObservableEvent(); metadataState.emitObservableEvent();
contextState.emitObservableEvent(); contextState.emitObservableEvent();
userState.emitObservableEvent(); userState.get().emitObservableEvent();
memoryTrimState.emitObservableEvent(); memoryTrimState.emitObservableEvent();
featureFlagState.emitObservableEvent(); featureFlagState.emitObservableEvent();
} }
@ -539,11 +532,10 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
* <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score"> * <a href="https://docs.bugsnag.com/product/releases/releases-dashboard/#stability-score">
* stability score</a>. * stability score</a>.
* *
* @return true if a previous session was resumed, false if a new session was started.
* @see #startSession() * @see #startSession()
* @see #pauseSession() * @see #pauseSession()
* @see Configuration#setAutoTrackSessions(boolean) * @see Configuration#setAutoTrackSessions(boolean)
*
* @return true if a previous session was resumed, false if a new session was started.
*/ */
public boolean resumeSession() { public boolean resumeSession() {
return sessionTracker.resumeSession(); return sessionTracker.resumeSession();
@ -556,7 +548,8 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
* In an android app the "context" is automatically set as the foreground Activity. * In an android app the "context" is automatically set as the foreground Activity.
* If you would like to set this value manually, you should alter this property. * If you would like to set this value manually, you should alter this property.
*/ */
@Nullable public String getContext() { @Nullable
public String getContext() {
return contextState.getContext(); return contextState.getContext();
} }
@ -576,7 +569,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
*/ */
@Override @Override
public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) { public void setUser(@Nullable String id, @Nullable String email, @Nullable String name) {
userState.setUser(new User(id, email, name)); userState.get().setUser(new User(id, email, name));
} }
/** /**
@ -585,7 +578,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
@NonNull @NonNull
@Override @Override
public User getUser() { public User getUser() {
return userState.getUser(); return userState.get().getUser();
} }
/** /**
@ -621,6 +614,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
/** /**
* Removes a previously added "on error" callback * Removes a previously added "on error" callback
*
* @param onError the callback to remove * @param onError the callback to remove
*/ */
@Override @Override
@ -661,6 +655,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
/** /**
* Removes a previously added "on breadcrumb" callback * Removes a previously added "on breadcrumb" callback
*
* @param onBreadcrumb the callback to remove * @param onBreadcrumb the callback to remove
*/ */
@Override @Override
@ -701,6 +696,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
/** /**
* Removes a previously added "on session" callback * Removes a previously added "on session" callback
*
* @param onSession the callback to remove * @param onSession the callback to remove
*/ */
@Override @Override
@ -724,9 +720,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
/** /**
* Notify Bugsnag of a handled exception * Notify Bugsnag of a handled exception
* *
* @param exc the exception to send to Bugsnag * @param exc the exception to send to Bugsnag
* @param onError callback invoked on the generated error report for * @param onError callback invoked on the generated error report for
* additional modification * additional modification
*/ */
public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) { public void notify(@NonNull Throwable exc, @Nullable OnErrorCallback onError) {
if (exc != null) { if (exc != null) {
@ -789,7 +785,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
event.setBreadcrumbs(breadcrumbState.copy()); event.setBreadcrumbs(breadcrumbState.copy());
// Attach user info to the event // Attach user info to the event
User user = userState.getUser(); User user = userState.get().getUser();
event.setUser(user.getId(), user.getEmail(), user.getName()); event.setUser(user.getId(), user.getEmail(), user.getName());
// Attach context to the event // Attach context to the event
@ -959,6 +955,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware, FeatureF
/** /**
* Leave a "breadcrumb" log message representing an action or event which * Leave a "breadcrumb" log message representing an action or event which
* occurred in your app, to aid with debugging * occurred in your app, to aid with debugging
*
* @param message A short label * @param message A short label
* @param metadata Additional diagnostic information about the app environment * @param metadata Additional diagnostic information about the app environment
* @param type A category for the breadcrumb * @param type A category for the breadcrumb

@ -18,7 +18,7 @@ internal class ClientObservable : BaseObservable() {
conf.apiKey, conf.apiKey,
conf.enabledErrorTypes.ndkCrashes, conf.enabledErrorTypes.ndkCrashes,
conf.appVersion, conf.appVersion,
conf.buildUuid, conf.buildUuid?.getOrNull(),
conf.releaseStage, conf.releaseStage,
lastRunInfoPath, lastRunInfoPath,
consecutiveLaunchCrashes, consecutiveLaunchCrashes,

@ -2,9 +2,10 @@ package com.bugsnag.android
import android.os.Environment import android.os.Environment
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
import com.bugsnag.android.internal.dag.ConfigModule import com.bugsnag.android.internal.dag.ConfigModule
import com.bugsnag.android.internal.dag.ContextModule import com.bugsnag.android.internal.dag.ContextModule
import com.bugsnag.android.internal.dag.DependencyModule import com.bugsnag.android.internal.dag.Provider
import com.bugsnag.android.internal.dag.SystemServiceModule import com.bugsnag.android.internal.dag.SystemServiceModule
/** /**
@ -18,10 +19,9 @@ internal class DataCollectionModule(
trackerModule: TrackerModule, trackerModule: TrackerModule,
bgTaskService: BackgroundTaskService, bgTaskService: BackgroundTaskService,
connectivity: Connectivity, connectivity: Connectivity,
deviceId: String?, deviceIdStore: Provider<DeviceIdStore>,
internalDeviceId: String?,
memoryTrimState: MemoryTrimState memoryTrimState: MemoryTrimState
) : DependencyModule() { ) : BackgroundDependencyModule(bgTaskService) {
private val ctx = contextModule.ctx private val ctx = contextModule.ctx
private val cfg = configModule.config private val cfg = configModule.config
@ -29,32 +29,32 @@ internal class DataCollectionModule(
private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo() private val deviceBuildInfo: DeviceBuildInfo = DeviceBuildInfo.defaultInfo()
private val dataDir = Environment.getDataDirectory() private val dataDir = Environment.getDataDirectory()
val appDataCollector by future { val appDataCollector = provider {
AppDataCollector( AppDataCollector(
ctx, ctx,
ctx.packageManager, ctx.packageManager,
cfg, cfg,
trackerModule.sessionTracker, trackerModule.sessionTracker.get(),
systemServiceModule.activityManager, systemServiceModule.activityManager,
trackerModule.launchCrashTracker, trackerModule.launchCrashTracker,
memoryTrimState memoryTrimState
) )
} }
private val rootDetector by future { private val rootDetection = provider {
RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo) val rootDetector = RootDetector(logger = logger, deviceBuildInfo = deviceBuildInfo)
rootDetector.isRooted()
} }
val deviceDataCollector by future { val deviceDataCollector = provider {
DeviceDataCollector( DeviceDataCollector(
connectivity, connectivity,
ctx, ctx,
ctx.resources, ctx.resources,
deviceId, deviceIdStore.map { it.load() },
internalDeviceId,
deviceBuildInfo, deviceBuildInfo,
dataDir, dataDir,
rootDetector, rootDetection,
bgTaskService, bgTaskService,
logger logger
) )

@ -4,69 +4,27 @@ import android.net.TrafficStats
import com.bugsnag.android.internal.JsonHelper import com.bugsnag.android.internal.JsonHelper
import java.io.IOException import java.io.IOException
import java.net.HttpURLConnection import java.net.HttpURLConnection
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
import java.net.HttpURLConnection.HTTP_OK
import java.net.URL import java.net.URL
internal class DefaultDelivery( internal class DefaultDelivery(
private val connectivity: Connectivity?, private val connectivity: Connectivity?,
private val apiKey: String,
private val maxStringValueLength: Int,
private val logger: Logger private val logger: Logger
) : Delivery { ) : Delivery {
companion object {
// 1MB with some fiddle room in case of encoding overhead
const val maxPayloadSize = 999700
}
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus { override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
val status = deliver( val status = deliver(
deliveryParams.endpoint, deliveryParams.endpoint,
JsonHelper.serialize(payload), JsonHelper.serialize(payload),
payload.integrityToken,
deliveryParams.headers deliveryParams.headers
) )
logger.i("Session API request finished with status $status") logger.i("Session API request finished with status $status")
return status return status
} }
private fun serializePayload(payload: EventPayload): ByteArray {
var json = JsonHelper.serialize(payload)
if (json.size <= maxPayloadSize) {
return json
}
var event = payload.event
if (event == null) {
event = MarshalledEventSource(payload.eventFile!!, apiKey, logger).invoke()
payload.event = event
payload.apiKey = apiKey
}
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(maxStringValueLength)
event.impl.internalMetrics.setMetadataTrimMetrics(
itemsTrimmed,
dataTrimmed
)
json = JsonHelper.serialize(payload)
if (json.size <= maxPayloadSize) {
return json
}
val breadcrumbAndBytesRemovedCounts =
event.impl.trimBreadcrumbsBy(json.size - maxPayloadSize)
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
breadcrumbAndBytesRemovedCounts.dataTrimmed
)
return JsonHelper.serialize(payload)
}
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus { override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
val json = serializePayload(payload) val json = payload.trimToSize().toByteArray()
val status = deliver(deliveryParams.endpoint, json, deliveryParams.headers) val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers)
logger.i("Error API request finished with status $status") logger.i("Error API request finished with status $status")
return status return status
} }
@ -74,6 +32,7 @@ internal class DefaultDelivery(
fun deliver( fun deliver(
urlString: String, urlString: String,
json: ByteArray, json: ByteArray,
integrity: String?,
headers: Map<String, String?> headers: Map<String, String?>
): DeliveryStatus { ): DeliveryStatus {
@ -84,11 +43,11 @@ internal class DefaultDelivery(
var conn: HttpURLConnection? = null var conn: HttpURLConnection? = null
try { try {
conn = makeRequest(URL(urlString), json, headers) conn = makeRequest(URL(urlString), json, integrity, headers)
// End the request, get the response code // End the request, get the response code
val responseCode = conn.responseCode val responseCode = conn.responseCode
val status = getDeliveryStatus(responseCode) val status = DeliveryStatus.forHttpResponseCode(responseCode)
logRequestInfo(responseCode, conn, status) logRequestInfo(responseCode, conn, status)
return status return status
} catch (oom: OutOfMemoryError) { } catch (oom: OutOfMemoryError) {
@ -111,6 +70,7 @@ internal class DefaultDelivery(
private fun makeRequest( private fun makeRequest(
url: URL, url: URL,
json: ByteArray, json: ByteArray,
integrity: String?,
headers: Map<String, String?> headers: Map<String, String?>
): HttpURLConnection { ): HttpURLConnection {
val conn = url.openConnection() as HttpURLConnection val conn = url.openConnection() as HttpURLConnection
@ -120,8 +80,7 @@ internal class DefaultDelivery(
// https://developer.android.com/reference/java/net/HttpURLConnection // https://developer.android.com/reference/java/net/HttpURLConnection
conn.setFixedLengthStreamingMode(json.size) conn.setFixedLengthStreamingMode(json.size)
// calculate the SHA-1 digest and add all other headers integrity?.let { digest ->
computeSha1Digest(json)?.let { digest ->
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest) conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
} }
headers.forEach { (key, value) -> headers.forEach { (key, value) ->
@ -159,17 +118,4 @@ internal class DefaultDelivery(
} }
} }
} }
internal fun getDeliveryStatus(responseCode: Int): DeliveryStatus {
return when {
responseCode in HTTP_OK..299 -> DeliveryStatus.DELIVERED
isUnrecoverableStatusCode(responseCode) -> DeliveryStatus.FAILURE
else -> DeliveryStatus.UNDELIVERED
}
}
private fun isUnrecoverableStatusCode(responseCode: Int) =
responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable
responseCode != HTTP_CLIENT_TIMEOUT && // except for 408
responseCode != 429 // and 429
} }

@ -0,0 +1,121 @@
package com.bugsnag.android
import android.net.TrafficStats
import com.bugsnag.android.internal.JsonHelper
import java.io.IOException
import java.net.HttpURLConnection
import java.net.URL
internal class DefaultDelivery(
private val connectivity: Connectivity?,
private val logger: Logger
) : Delivery {
override fun deliver(payload: Session, deliveryParams: DeliveryParams): DeliveryStatus {
val status = deliver(
deliveryParams.endpoint,
JsonHelper.serialize(payload),
payload.integrityToken,
deliveryParams.headers
)
logger.i("Session API request finished with status $status")
return status
}
override fun deliver(payload: EventPayload, deliveryParams: DeliveryParams): DeliveryStatus {
val json = payload.trimToSize().toByteArray()
val status = deliver(deliveryParams.endpoint, json, payload.integrityToken, deliveryParams.headers)
logger.i("Error API request finished with status $status")
return status
}
fun deliver(
urlString: String,
json: ByteArray,
integrity: String?,
headers: Map<String, String?>
): DeliveryStatus {
TrafficStats.setThreadStatsTag(1)
if (connectivity != null && !connectivity.hasNetworkConnection()) {
return DeliveryStatus.UNDELIVERED
}
var conn: HttpURLConnection? = null
try {
conn = makeRequest(URL(urlString), json, integrity, headers)
// End the request, get the response code
val responseCode = conn.responseCode
val status = DeliveryStatus.forHttpResponseCode(responseCode)
logRequestInfo(responseCode, conn, status)
return status
} catch (oom: OutOfMemoryError) {
// attempt to persist the payload on disk. This approach uses streams to write to a
// file, which takes less memory than serializing the payload into a ByteArray, and
// therefore has a reasonable chance of retaining the payload for future delivery.
logger.w("Encountered OOM delivering payload, falling back to persist on disk", oom)
return DeliveryStatus.UNDELIVERED
} catch (exception: IOException) {
logger.w("IOException encountered in request", exception)
return DeliveryStatus.UNDELIVERED
} catch (exception: Exception) {
logger.w("Unexpected error delivering payload", exception)
return DeliveryStatus.FAILURE
} finally {
conn?.disconnect()
}
}
private fun makeRequest(
url: URL,
json: ByteArray,
integrity: String?,
headers: Map<String, String?>
): HttpURLConnection {
val conn = url.openConnection() as HttpURLConnection
conn.doOutput = true
// avoids creating a buffer within HttpUrlConnection, see
// https://developer.android.com/reference/java/net/HttpURLConnection
conn.setFixedLengthStreamingMode(json.size)
integrity?.let { digest ->
conn.addRequestProperty(HEADER_BUGSNAG_INTEGRITY, digest)
}
headers.forEach { (key, value) ->
if (value != null) {
conn.addRequestProperty(key, value)
}
}
// write the JSON payload
conn.outputStream.use {
it.write(json)
}
return conn
}
private fun logRequestInfo(code: Int, conn: HttpURLConnection, status: DeliveryStatus) {
runCatching {
logger.i(
"Request completed with code $code, " +
"message: ${conn.responseMessage}, " +
"headers: ${conn.headerFields}"
)
}
runCatching {
conn.inputStream.bufferedReader().use {
logger.d("Received request response: ${it.readText()}")
}
}
runCatching {
if (status != DeliveryStatus.DELIVERED) {
conn.errorStream.bufferedReader().use {
logger.w("Request error details: ${it.readText()}")
}
}
}
}
}

@ -0,0 +1,39 @@
package com.bugsnag.android
import java.io.IOException
import java.security.DigestOutputStream
import java.security.MessageDigest
/**
* Denotes objects that are expected to be delivered over a network.
*/
interface Deliverable {
/**
* Return the byte representation of this `Deliverable`.
*/
@Throws(IOException::class)
fun toByteArray(): ByteArray
/**
* The value of the "Bugsnag-Integrity" HTTP header returned as a String. This value is used
* to validate the payload and is expected by the standard BugSnag servers.
*/
val integrityToken: String?
get() {
runCatching {
val shaDigest = MessageDigest.getInstance("SHA-1")
val builder = StringBuilder("sha1 ")
// Pipe the object through a no-op output stream
DigestOutputStream(NullOutputStream(), shaDigest).use { stream ->
stream.buffered().use { writer ->
writer.write(toByteArray())
}
shaDigest.digest().forEach { byte ->
builder.append(String.format("%02x", byte))
}
}
return builder.toString()
}.getOrElse { return null }
}
}

@ -2,8 +2,6 @@ package com.bugsnag.android
import com.bugsnag.android.internal.DateUtils import com.bugsnag.android.internal.DateUtils
import java.io.OutputStream import java.io.OutputStream
import java.security.DigestOutputStream
import java.security.MessageDigest
import java.util.Date import java.util.Date
private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version" private const val HEADER_API_PAYLOAD_VERSION = "Bugsnag-Payload-Version"
@ -60,24 +58,6 @@ internal fun sessionApiHeaders(apiKey: String): Map<String, String?> = mapOf(
HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date()) HEADER_BUGSNAG_SENT_AT to DateUtils.toIso8601(Date())
) )
internal fun computeSha1Digest(payload: ByteArray): String? {
runCatching {
val shaDigest = MessageDigest.getInstance("SHA-1")
val builder = StringBuilder("sha1 ")
// Pipe the object through a no-op output stream
DigestOutputStream(NullOutputStream(), shaDigest).use { stream ->
stream.buffered().use { writer ->
writer.write(payload)
}
shaDigest.digest().forEach { byte ->
builder.append(String.format("%02x", byte))
}
}
return builder.toString()
}.getOrElse { return null }
}
internal class NullOutputStream : OutputStream() { internal class NullOutputStream : OutputStream() {
override fun write(b: Int) = Unit override fun write(b: Int) = Unit
} }

@ -1,5 +1,9 @@
package com.bugsnag.android package com.bugsnag.android
import java.net.HttpURLConnection.HTTP_BAD_REQUEST
import java.net.HttpURLConnection.HTTP_CLIENT_TIMEOUT
import java.net.HttpURLConnection.HTTP_OK
/** /**
* Return value for the status of a payload delivery. * Return value for the status of a payload delivery.
*/ */
@ -19,5 +23,19 @@ enum class DeliveryStatus {
* *
* The payload was not delivered and should be deleted without attempting retry. * The payload was not delivered and should be deleted without attempting retry.
*/ */
FAILURE FAILURE;
companion object {
@JvmStatic
fun forHttpResponseCode(responseCode: Int): DeliveryStatus {
return when {
responseCode in HTTP_OK..299 -> DELIVERED
responseCode in HTTP_BAD_REQUEST..499 && // 400-499 are considered unrecoverable
responseCode != HTTP_CLIENT_TIMEOUT && // except for 408
responseCode != 429 -> FAILURE
else -> UNDELIVERED
}
}
}
} }

@ -13,6 +13,7 @@ import android.os.Build
import android.provider.Settings import android.provider.Settings
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.TaskType import com.bugsnag.android.internal.TaskType
import com.bugsnag.android.internal.dag.Provider
import java.io.File import java.io.File
import java.util.Date import java.util.Date
import java.util.Locale import java.util.Locale
@ -28,11 +29,10 @@ internal class DeviceDataCollector(
private val connectivity: Connectivity, private val connectivity: Connectivity,
private val appContext: Context, private val appContext: Context,
resources: Resources, resources: Resources,
private val deviceId: String?, private val deviceIdStore: Provider<DeviceIdStore.DeviceIds?>,
private val internalDeviceId: String?,
private val buildInfo: DeviceBuildInfo, private val buildInfo: DeviceBuildInfo,
private val dataDirectory: File, private val dataDirectory: File,
rootDetector: RootDetector, private val rootedFuture: Provider<Boolean>?,
private val bgTaskService: BackgroundTaskService, private val bgTaskService: BackgroundTaskService,
private val logger: Logger private val logger: Logger
) { ) {
@ -45,7 +45,6 @@ internal class DeviceDataCollector(
private val locale = Locale.getDefault().toString() private val locale = Locale.getDefault().toString()
private val cpuAbi = getCpuAbi() private val cpuAbi = getCpuAbi()
private var runtimeVersions: MutableMap<String, Any> private var runtimeVersions: MutableMap<String, Any>
private val rootedFuture: Future<Boolean>?
private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory() private val totalMemoryFuture: Future<Long?>? = retrieveTotalDeviceMemory()
private var orientation = AtomicInteger(resources.configuration.orientation) private var orientation = AtomicInteger(resources.configuration.orientation)
@ -54,25 +53,13 @@ internal class DeviceDataCollector(
buildInfo.apiLevel?.let { map["androidApiLevel"] = it } buildInfo.apiLevel?.let { map["androidApiLevel"] = it }
buildInfo.osBuild?.let { map["osBuild"] = it } buildInfo.osBuild?.let { map["osBuild"] = it }
runtimeVersions = map runtimeVersions = map
rootedFuture = try {
bgTaskService.submitTask(
TaskType.IO,
Callable {
rootDetector.isRooted()
}
)
} catch (exc: RejectedExecutionException) {
logger.w("Failed to perform root detection checks", exc)
null
}
} }
fun generateDevice() = Device( fun generateDevice() = Device(
buildInfo, buildInfo,
cpuAbi, cpuAbi,
checkIsRooted(), checkIsRooted(),
deviceId, deviceIdStore.get()?.deviceId,
locale, locale,
totalMemoryFuture.runCatching { this?.get() }.getOrNull(), totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
runtimeVersions.toMutableMap() runtimeVersions.toMutableMap()
@ -81,7 +68,7 @@ internal class DeviceDataCollector(
fun generateDeviceWithState(now: Long) = DeviceWithState( fun generateDeviceWithState(now: Long) = DeviceWithState(
buildInfo, buildInfo,
checkIsRooted(), checkIsRooted(),
deviceId, deviceIdStore.get()?.deviceId,
locale, locale,
totalMemoryFuture.runCatching { this?.get() }.getOrNull(), totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
runtimeVersions.toMutableMap(), runtimeVersions.toMutableMap(),
@ -94,7 +81,7 @@ internal class DeviceDataCollector(
fun generateInternalDeviceWithState(now: Long) = DeviceWithState( fun generateInternalDeviceWithState(now: Long) = DeviceWithState(
buildInfo, buildInfo,
checkIsRooted(), checkIsRooted(),
internalDeviceId, deviceIdStore.get()?.internalDeviceId,
locale, locale,
totalMemoryFuture.runCatching { this?.get() }.getOrNull(), totalMemoryFuture.runCatching { this?.get() }.getOrNull(),
runtimeVersions.toMutableMap(), runtimeVersions.toMutableMap(),

@ -2,6 +2,7 @@ package com.bugsnag.android
import android.content.Context import android.content.Context
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.dag.Provider
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -11,23 +12,19 @@ import java.util.UUID
*/ */
internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor( internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constructor(
context: Context, context: Context,
deviceIdfile: File = File(context.filesDir, "device-id"), private val deviceIdFile: File = File(context.filesDir, "device-id"),
deviceIdGenerator: () -> UUID = { UUID.randomUUID() }, private val deviceIdGenerator: () -> UUID = { UUID.randomUUID() },
internalDeviceIdfile: File = File(context.filesDir, "internal-device-id"), private val internalDeviceIdFile: File = File(context.filesDir, "internal-device-id"),
internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() }, private val internalDeviceIdGenerator: () -> UUID = { UUID.randomUUID() },
private val sharedPrefMigrator: SharedPrefMigrator, private val sharedPrefMigrator: Provider<SharedPrefMigrator>,
config: ImmutableConfig, config: ImmutableConfig,
logger: Logger private val logger: Logger
) { ) {
private val persistence: DeviceIdPersistence private lateinit var persistence: DeviceIdPersistence
private val internalPersistence: DeviceIdPersistence private lateinit var internalPersistence: DeviceIdPersistence
private val generateId = config.generateAnonymousId private val generateId = config.generateAnonymousId
private var deviceIds: DeviceIds? = null
init {
persistence = DeviceIdFilePersistence(deviceIdfile, deviceIdGenerator, logger)
internalPersistence = DeviceIdFilePersistence(internalDeviceIdfile, internalDeviceIdGenerator, logger)
}
/** /**
* Loads the device ID from * Loads the device ID from
@ -37,7 +34,7 @@ internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constr
* If no device ID exists then the legacy value stored in [SharedPreferences] will * If no device ID exists then the legacy value stored in [SharedPreferences] will
* be used. If no value is present then a random UUID will be generated and persisted. * be used. If no value is present then a random UUID will be generated and persisted.
*/ */
fun loadDeviceId(): String? { private fun loadDeviceId(): String? {
// If generateAnonymousId = false, return null // If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned, // so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted // or a new one is not generated and persisted
@ -48,14 +45,14 @@ internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constr
if (result != null) { if (result != null) {
return result return result
} }
result = sharedPrefMigrator.loadDeviceId(false) result = sharedPrefMigrator.get().loadDeviceId(false)
if (result != null) { if (result != null) {
return result return result
} }
return persistence.loadDeviceId(true) return persistence.loadDeviceId(true)
} }
fun loadInternalDeviceId(): String? { private fun loadInternalDeviceId(): String? {
// If generateAnonymousId = false, return null // If generateAnonymousId = false, return null
// so that a previously persisted device ID is not returned, // so that a previously persisted device ID is not returned,
// or a new one is not generated and persisted // or a new one is not generated and persisted
@ -64,4 +61,28 @@ internal class DeviceIdStore @JvmOverloads @Suppress("LongParameterList") constr
} }
return internalPersistence.loadDeviceId(true) return internalPersistence.loadDeviceId(true)
} }
fun load(): DeviceIds? {
if (deviceIds != null) {
return deviceIds
}
persistence = DeviceIdFilePersistence(deviceIdFile, deviceIdGenerator, logger)
internalPersistence =
DeviceIdFilePersistence(internalDeviceIdFile, internalDeviceIdGenerator, logger)
val deviceId = loadDeviceId()
val internalDeviceId = loadInternalDeviceId()
if (deviceId != null || internalDeviceId != null) {
deviceIds = DeviceIds(deviceId, internalDeviceId)
}
return deviceIds
}
data class DeviceIds(
val deviceId: String?,
val internalDeviceId: String?
)
} }

@ -1,6 +1,8 @@
package com.bugsnag.android package com.bugsnag.android
import androidx.annotation.VisibleForTesting
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.JsonHelper
import java.io.File import java.io.File
import java.io.IOException import java.io.IOException
@ -13,13 +15,20 @@ import java.io.IOException
class EventPayload @JvmOverloads internal constructor( class EventPayload @JvmOverloads internal constructor(
var apiKey: String?, var apiKey: String?,
event: Event? = null, event: Event? = null,
internal val eventFile: File? = null, eventFile: File? = null,
notifier: Notifier, notifier: Notifier,
private val config: ImmutableConfig private val config: ImmutableConfig
) : JsonStream.Streamable { ) : JsonStream.Streamable, Deliverable {
var event = event var event: Event? = event
internal set(value) { field = value } internal set
internal var eventFile: File? = eventFile
private set
private var cachedBytes: ByteArray? = null
private val logger: Logger get() = config.logger
internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply { internal val notifier = Notifier(notifier.name, notifier.version, notifier.url).apply {
dependencies = notifier.dependencies.toMutableList() dependencies = notifier.dependencies.toMutableList()
@ -27,11 +36,64 @@ class EventPayload @JvmOverloads internal constructor(
internal fun getErrorTypes(): Set<ErrorType> { internal fun getErrorTypes(): Set<ErrorType> {
val event = this.event val event = this.event
return when {
event != null -> event.impl.getErrorTypesFromStackframes() return event?.impl?.getErrorTypesFromStackframes() ?: (
eventFile != null -> EventFilenameInfo.fromFile(eventFile, config).errorTypes eventFile?.let { EventFilenameInfo.fromFile(it, config).errorTypes }
else -> emptySet() ?: emptySet()
)
}
private fun decodedEvent(): Event {
val localEvent = event
if (localEvent != null) {
return localEvent
} }
val eventSource = MarshalledEventSource(eventFile!!, apiKey ?: config.apiKey, logger)
val decodedEvent = eventSource()
// cache the decoded Event object
event = decodedEvent
return decodedEvent
}
/**
* If required trim this `EventPayload` so that its [encoded data](toByteArray) will usually be
* less-than or equal to [maxSizeBytes]. This function may make no changes to the payload, and
* may also not achieve the requested [maxSizeBytes]. The default use of the function is
* configured to [DEFAULT_MAX_PAYLOAD_SIZE].
*
* @return `this` for call chaining
*/
@JvmOverloads
fun trimToSize(maxSizeBytes: Int = DEFAULT_MAX_PAYLOAD_SIZE): EventPayload {
var json = toByteArray()
if (json.size <= maxSizeBytes) {
return this
}
val event = decodedEvent()
val (itemsTrimmed, dataTrimmed) = event.impl.trimMetadataStringsTo(config.maxStringValueLength)
event.impl.internalMetrics.setMetadataTrimMetrics(
itemsTrimmed,
dataTrimmed
)
json = rebuildPayloadCache()
if (json.size <= maxSizeBytes) {
return this
}
val breadcrumbAndBytesRemovedCounts =
event.impl.trimBreadcrumbsBy(json.size - maxSizeBytes)
event.impl.internalMetrics.setBreadcrumbTrimMetrics(
breadcrumbAndBytesRemovedCounts.itemsTrimmed,
breadcrumbAndBytesRemovedCounts.dataTrimmed
)
rebuildPayloadCache()
return this
} }
@Throws(IOException::class) @Throws(IOException::class)
@ -51,4 +113,33 @@ class EventPayload @JvmOverloads internal constructor(
writer.endArray() writer.endArray()
writer.endObject() writer.endObject()
} }
/**
* Transform this `EventPayload` to a byte array suitable for delivery to a BugSnag event
* endpoint (typically configured using [EndpointConfiguration.notify]).
*/
@Throws(IOException::class)
override fun toByteArray(): ByteArray {
var payload = cachedBytes
if (payload == null) {
payload = JsonHelper.serialize(this)
cachedBytes = payload
}
return payload
}
@VisibleForTesting
internal fun rebuildPayloadCache(): ByteArray {
cachedBytes = null
return toByteArray()
}
companion object {
/**
* The default maximum payload size for [trimToSize], payloads larger than this will
* typically be rejected by BugSnag.
*/
// 1MB with some fiddle room in case of encoding overhead
const val DEFAULT_MAX_PAYLOAD_SIZE = 999700
}
} }

@ -1,9 +1,9 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
import com.bugsnag.android.internal.dag.ConfigModule import com.bugsnag.android.internal.dag.ConfigModule
import com.bugsnag.android.internal.dag.ContextModule import com.bugsnag.android.internal.dag.ContextModule
import com.bugsnag.android.internal.dag.DependencyModule
import com.bugsnag.android.internal.dag.SystemServiceModule import com.bugsnag.android.internal.dag.SystemServiceModule
/** /**
@ -18,32 +18,32 @@ internal class EventStorageModule(
systemServiceModule: SystemServiceModule, systemServiceModule: SystemServiceModule,
notifier: Notifier, notifier: Notifier,
callbackState: CallbackState callbackState: CallbackState
) : DependencyModule() { ) : BackgroundDependencyModule(bgTaskService) {
private val cfg = configModule.config private val cfg = configModule.config
private val delegate by future { private val delegate = provider {
if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS) == true) if (cfg.telemetry.contains(Telemetry.INTERNAL_ERRORS))
InternalReportDelegate( InternalReportDelegate(
contextModule.ctx, contextModule.ctx,
cfg.logger, cfg.logger,
cfg, cfg,
systemServiceModule.storageManager, systemServiceModule.storageManager,
dataCollectionModule.appDataCollector, dataCollectionModule.appDataCollector.get(),
dataCollectionModule.deviceDataCollector, dataCollectionModule.deviceDataCollector,
trackerModule.sessionTracker, trackerModule.sessionTracker.get(),
notifier, notifier,
bgTaskService bgTaskService
) else null ) else null
} }
val eventStore by future { val eventStore = provider {
EventStore( EventStore(
cfg, cfg,
cfg.logger, cfg.logger,
notifier, notifier,
bgTaskService, bgTaskService,
delegate, delegate.getOrNull(),
callbackState callbackState
) )
} }

@ -1,15 +1,16 @@
package com.bugsnag.android package com.bugsnag.android
import android.os.SystemClock
import com.bugsnag.android.EventFilenameInfo.Companion.findTimestampInFilename import com.bugsnag.android.EventFilenameInfo.Companion.findTimestampInFilename
import com.bugsnag.android.EventFilenameInfo.Companion.fromEvent import com.bugsnag.android.EventFilenameInfo.Companion.fromEvent
import com.bugsnag.android.EventFilenameInfo.Companion.fromFile import com.bugsnag.android.EventFilenameInfo.Companion.fromFile
import com.bugsnag.android.JsonStream.Streamable import com.bugsnag.android.JsonStream.Streamable
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
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 java.io.File import java.io.File
import java.util.Calendar import java.util.Calendar
import java.util.Comparator
import java.util.Date import java.util.Date
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.ExecutionException import java.util.concurrent.ExecutionException
@ -19,8 +20,7 @@ import java.util.concurrent.TimeUnit
import java.util.concurrent.TimeoutException import java.util.concurrent.TimeoutException
/** /**
* Store and flush Event reports which couldn't be sent immediately due to * Store and flush Event reports.
* lack of network connectivity.
*/ */
internal class EventStore( internal class EventStore(
private val config: ImmutableConfig, private val config: ImmutableConfig,
@ -32,7 +32,6 @@ internal class EventStore(
) : FileStore( ) : FileStore(
File(config.persistenceDirectory.value, "bugsnag/errors"), File(config.persistenceDirectory.value, "bugsnag/errors"),
config.maxPersistedEvents, config.maxPersistedEvents,
EVENT_COMPARATOR,
logger, logger,
delegate delegate
) { ) {
@ -42,7 +41,8 @@ internal class EventStore(
override val logger: Logger override val logger: Logger
/** /**
* Flush startup crashes synchronously on the main thread * Flush startup crashes synchronously on the main thread. Startup crashes block the main thread
* when being sent (subject to [Configuration.setSendLaunchCrashesSynchronously])
*/ */
fun flushOnLaunch() { fun flushOnLaunch() {
if (!config.sendLaunchCrashesSynchronously) { if (!config.sendLaunchCrashesSynchronously) {
@ -58,13 +58,28 @@ internal class EventStore(
return return
} }
try { try {
future.get(LAUNCH_CRASH_TIMEOUT_MS, TimeUnit.MILLISECONDS) // Calculate the maximum amount of time we are prepared to block while sending
// startup crashes, based on how long we think startup has taken so-far.
// This attempts to mitigate possible startup ANRs that can occur when other SDKs
// have blocked the main thread before this code is reached.
val currentStartupDuration =
SystemClock.elapsedRealtime() - ForegroundDetector.startupTime
var timeout = LAUNCH_CRASH_TIMEOUT_MS - currentStartupDuration
if (timeout <= 0) {
// if Bugsnag.start is called too long after Application.onCreate is expected to
// have returned, we use a full LAUNCH_CRASH_TIMEOUT_MS instead of a calculated one
// assuming that the app is already fully started
timeout = LAUNCH_CRASH_TIMEOUT_MS
}
future.get(timeout, TimeUnit.MILLISECONDS)
} catch (exc: InterruptedException) { } catch (exc: InterruptedException) {
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc) logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
} catch (exc: ExecutionException) { } catch (exc: ExecutionException) {
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc) logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
} catch (exc: TimeoutException) { } catch (exc: TimeoutException) {
logger.d("Failed to send launch crash reports within 2s timeout, continuing.", exc) logger.d("Failed to send launch crash reports within timeout, continuing.", exc)
} }
} }
@ -159,6 +174,7 @@ internal class EventStore(
deleteStoredFiles(setOf(eventFile)) deleteStoredFiles(setOf(eventFile))
logger.i("Deleting sent error file $eventFile.name") logger.i("Deleting sent error file $eventFile.name")
} }
DeliveryStatus.UNDELIVERED -> undeliveredEventPayload(eventFile) DeliveryStatus.UNDELIVERED -> undeliveredEventPayload(eventFile)
DeliveryStatus.FAILURE -> { DeliveryStatus.FAILURE -> {
val exc: Exception = RuntimeException("Failed to deliver event payload") val exc: Exception = RuntimeException("Failed to deliver event payload")

@ -7,8 +7,6 @@ import java.io.FileNotFoundException
import java.io.FileOutputStream import java.io.FileOutputStream
import java.io.OutputStreamWriter import java.io.OutputStreamWriter
import java.io.Writer import java.io.Writer
import java.util.Collections
import java.util.Comparator
import java.util.concurrent.ConcurrentSkipListSet import java.util.concurrent.ConcurrentSkipListSet
import java.util.concurrent.locks.Lock import java.util.concurrent.locks.Lock
import java.util.concurrent.locks.ReentrantLock import java.util.concurrent.locks.ReentrantLock
@ -16,7 +14,6 @@ import java.util.concurrent.locks.ReentrantLock
internal abstract class FileStore( internal abstract class FileStore(
val storageDir: File, val storageDir: File,
private val maxStoreCount: Int, private val maxStoreCount: Int,
private val comparator: Comparator<in File?>,
protected open val logger: Logger, protected open val logger: Logger,
protected val delegate: Delegate? protected val delegate: Delegate?
) { ) {
@ -34,10 +31,6 @@ internal abstract class FileStore(
private val lock: Lock = ReentrantLock() private val lock: Lock = ReentrantLock()
private val queuedFiles: MutableCollection<File> = ConcurrentSkipListSet() private val queuedFiles: MutableCollection<File> = ConcurrentSkipListSet()
init {
isStorageDirValid(storageDir)
}
/** /**
* Checks whether the storage directory is a writable directory. If it is not, * Checks whether the storage directory is a writable directory. If it is not,
* this method will attempt to create the directory. * this method will attempt to create the directory.
@ -115,23 +108,21 @@ internal abstract class FileStore(
// Limit number of saved payloads to prevent disk space issues // Limit number of saved payloads to prevent disk space issues
if (isStorageDirValid(storageDir)) { if (isStorageDirValid(storageDir)) {
val listFiles = storageDir.listFiles() ?: return val listFiles = storageDir.listFiles() ?: return
val files: ArrayList<File> = arrayListOf(*listFiles) if (listFiles.size < maxStoreCount) return
if (files.size >= maxStoreCount) { val sortedListFiles = listFiles.sortedBy { it.lastModified() }
// Sort files then delete the first one (oldest timestamp) // Number of files to discard takes into account that a new file may need to be written
Collections.sort(files, comparator) val numberToDiscard = listFiles.size - maxStoreCount + 1
var k = 0 var discardedCount = 0
while (k < files.size && files.size >= maxStoreCount) { for (file in sortedListFiles) {
val oldestFile = files[k] if (discardedCount == numberToDiscard) {
if (!queuedFiles.contains(oldestFile)) { return
logger.w( } else if (!queuedFiles.contains(file)) {
"Discarding oldest error as stored " + logger.w(
"error limit reached: '" + oldestFile.path + '\'' "Discarding oldest error as stored error limit reached: '" +
) file.path + '\''
deleteStoredFiles(setOf(oldestFile)) )
files.removeAt(k) deleteStoredFiles(setOf(file))
k-- discardedCount++
}
k++
} }
} }
} }

@ -5,8 +5,8 @@ import static com.bugsnag.android.SeverityReason.REASON_UNHANDLED_EXCEPTION;
import com.bugsnag.android.internal.BackgroundTaskService; import com.bugsnag.android.internal.BackgroundTaskService;
import com.bugsnag.android.internal.ImmutableConfig; import com.bugsnag.android.internal.ImmutableConfig;
import com.bugsnag.android.internal.JsonHelper;
import com.bugsnag.android.internal.TaskType; import com.bugsnag.android.internal.TaskType;
import com.bugsnag.android.internal.dag.Provider;
import android.annotation.SuppressLint; import android.annotation.SuppressLint;
import android.content.Context; import android.content.Context;
@ -33,7 +33,7 @@ class InternalReportDelegate implements EventStore.Delegate {
final StorageManager storageManager; final StorageManager storageManager;
final AppDataCollector appDataCollector; final AppDataCollector appDataCollector;
final DeviceDataCollector deviceDataCollector; final Provider<DeviceDataCollector> deviceDataCollector;
final Context appContext; final Context appContext;
final SessionTracker sessionTracker; final SessionTracker sessionTracker;
final Notifier notifier; final Notifier notifier;
@ -44,7 +44,7 @@ class InternalReportDelegate implements EventStore.Delegate {
ImmutableConfig immutableConfig, ImmutableConfig immutableConfig,
@Nullable StorageManager storageManager, @Nullable StorageManager storageManager,
AppDataCollector appDataCollector, AppDataCollector appDataCollector,
DeviceDataCollector deviceDataCollector, Provider<DeviceDataCollector> deviceDataCollector,
SessionTracker sessionTracker, SessionTracker sessionTracker,
Notifier notifier, Notifier notifier,
BackgroundTaskService backgroundTaskService) { BackgroundTaskService backgroundTaskService) {
@ -102,7 +102,7 @@ class InternalReportDelegate implements EventStore.Delegate {
*/ */
void reportInternalBugsnagError(@NonNull Event event) { void reportInternalBugsnagError(@NonNull Event event) {
event.setApp(appDataCollector.generateAppWithState()); event.setApp(appDataCollector.generateAppWithState());
event.setDevice(deviceDataCollector.generateDeviceWithState(new Date().getTime())); event.setDevice(deviceDataCollector.get().generateDeviceWithState(new Date().getTime()));
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierName", notifier.getName()); event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierName", notifier.getName());
event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierVersion", notifier.getVersion()); event.addMetadata(INTERNAL_DIAGNOSTICS_TAB, "notifierVersion", notifier.getVersion());
@ -126,7 +126,8 @@ class InternalReportDelegate implements EventStore.Delegate {
DefaultDelivery defaultDelivery = (DefaultDelivery) delivery; DefaultDelivery defaultDelivery = (DefaultDelivery) delivery;
defaultDelivery.deliver( defaultDelivery.deliver(
params.getEndpoint(), params.getEndpoint(),
JsonHelper.INSTANCE.serialize(payload), payload.toByteArray(),
payload.getIntegrityToken(),
headers headers
); );
} }

@ -64,7 +64,11 @@ public class JsonStream extends JsonWriter {
* Collections, Maps, and arrays. * Collections, Maps, and arrays.
*/ */
public void value(@Nullable Object object) throws IOException { public void value(@Nullable Object object) throws IOException {
value(object, false); if (object instanceof File) {
value((File) object);
} else {
value(object, false);
}
} }
/** /**

@ -36,6 +36,7 @@ internal class LastRunInfoStore(config: ImmutableConfig) {
add(KEY_CRASHED, lastRunInfo.crashed) add(KEY_CRASHED, lastRunInfo.crashed)
add(KEY_CRASHED_DURING_LAUNCH, lastRunInfo.crashedDuringLaunch) add(KEY_CRASHED_DURING_LAUNCH, lastRunInfo.crashedDuringLaunch)
}.toString() }.toString()
file.parentFile?.mkdirs()
file.writeText(text) file.writeText(text)
logger.d("Persisted: $text") logger.d("Persisted: $text")
} }

@ -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 = "6.7.0", var version: String = "6.10.0",
var url: String = "https://bugsnag.com" var url: String = "https://bugsnag.com"
) : JsonStream.Streamable { ) : JsonStream.Streamable {

@ -1,6 +1,7 @@
package com.bugsnag.android; package com.bugsnag.android;
import com.bugsnag.android.internal.DateUtils; import com.bugsnag.android.internal.DateUtils;
import com.bugsnag.android.internal.JsonHelper;
import androidx.annotation.NonNull; import androidx.annotation.NonNull;
import androidx.annotation.Nullable; import androidx.annotation.Nullable;
@ -17,7 +18,7 @@ import java.util.concurrent.atomic.AtomicInteger;
* Represents a contiguous session in an application. * Represents a contiguous session in an application.
*/ */
@SuppressWarnings("ConstantConditions") @SuppressWarnings("ConstantConditions")
public final class Session implements JsonStream.Streamable, UserAware { public final class Session implements JsonStream.Streamable, Deliverable, UserAware {
private final File file; private final File file;
private final Notifier notifier; private final Notifier notifier;
@ -258,6 +259,17 @@ public final class Session implements JsonStream.Streamable, UserAware {
} }
} }
@NonNull
public byte[] toByteArray() throws IOException {
return JsonHelper.INSTANCE.serialize(this);
}
@Nullable
@Override
public String getIntegrityToken() {
return Deliverable.DefaultImpls.getIntegrityToken(this);
}
private void serializePayload(@NonNull JsonStream writer) throws IOException { private void serializePayload(@NonNull JsonStream writer) throws IOException {
writer.value(file); writer.value(file);
} }

@ -1,6 +1,5 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File import java.io.File
import java.util.UUID import java.util.UUID
@ -34,13 +33,10 @@ internal data class SessionFilenameInfo(
} }
@JvmStatic @JvmStatic
fun defaultFilename( fun defaultFilename(obj: Any?, apiKey: String): SessionFilenameInfo {
obj: Any?,
config: ImmutableConfig
): SessionFilenameInfo {
val sanitizedApiKey = when (obj) { val sanitizedApiKey = when (obj) {
is Session -> obj.apiKey is Session -> obj.apiKey
else -> config.apiKey else -> apiKey
} }
return SessionFilenameInfo( return SessionFilenameInfo(

@ -2,7 +2,6 @@ package com.bugsnag.android
import com.bugsnag.android.SessionFilenameInfo.Companion.defaultFilename import com.bugsnag.android.SessionFilenameInfo.Companion.defaultFilename
import com.bugsnag.android.SessionFilenameInfo.Companion.findTimestampInFilename import com.bugsnag.android.SessionFilenameInfo.Companion.findTimestampInFilename
import com.bugsnag.android.internal.ImmutableConfig
import java.io.File import java.io.File
import java.util.Calendar import java.util.Calendar
import java.util.Comparator import java.util.Comparator
@ -13,15 +12,14 @@ import java.util.Date
* lack of network connectivity. * lack of network connectivity.
*/ */
internal class SessionStore( internal class SessionStore(
private val config: ImmutableConfig, bugsnagDir: File,
maxPersistedSessions: Int,
private val apiKey: String,
logger: Logger, logger: Logger,
delegate: Delegate? delegate: Delegate?
) : FileStore( ) : FileStore(
File( File(bugsnagDir, "sessions"),
config.persistenceDirectory.value, "bugsnag/sessions" maxPersistedSessions,
),
config.maxPersistedSessions,
SESSION_COMPARATOR,
logger, logger,
delegate delegate
) { ) {
@ -53,7 +51,7 @@ internal class SessionStore(
} }
override fun getFilename(obj: Any?): String { override fun getFilename(obj: Any?): String {
val sessionInfo = defaultFilename(obj, config) val sessionInfo = defaultFilename(obj, apiKey)
return sessionInfo.encode() return sessionInfo.encode()
} }
} }

@ -1,56 +1,73 @@
package com.bugsnag.android package com.bugsnag.android
import android.content.Context import android.content.Context
import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.BugsnagStoreMigrator.migrateLegacyFiles
import com.bugsnag.android.internal.ImmutableConfig import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.dag.DependencyModule import com.bugsnag.android.internal.TaskType
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
import com.bugsnag.android.internal.dag.Provider
/** /**
* A dependency module which constructs the objects that store information to disk in Bugsnag. * A dependency module which constructs the objects that store information to disk in Bugsnag.
*/ */
internal class StorageModule( internal class StorageModule(
appContext: Context, appContext: Context,
immutableConfig: ImmutableConfig, private val immutableConfig: ImmutableConfig,
logger: Logger bgTaskService: BackgroundTaskService
) : DependencyModule() { ) : BackgroundDependencyModule(bgTaskService, TaskType.IO) {
val sharedPrefMigrator by future { SharedPrefMigrator(appContext) } val bugsnagDir = provider {
migrateLegacyFiles(immutableConfig.persistenceDirectory)
}
val sharedPrefMigrator = provider {
SharedPrefMigrator(appContext)
}
private val deviceIdStore by future { val deviceIdStore = provider {
DeviceIdStore( DeviceIdStore(
appContext, appContext,
sharedPrefMigrator = sharedPrefMigrator, sharedPrefMigrator = sharedPrefMigrator,
logger = logger, logger = immutableConfig.logger,
config = immutableConfig config = immutableConfig
) )
} }
val deviceId by future { deviceIdStore.loadDeviceId() } val userStore = provider {
val internalDeviceId by future { deviceIdStore.loadInternalDeviceId() }
val userStore by future {
UserStore( UserStore(
immutableConfig, immutableConfig.persistUser,
deviceId, bugsnagDir,
deviceIdStore.map { it.load() },
sharedPrefMigrator = sharedPrefMigrator, sharedPrefMigrator = sharedPrefMigrator,
logger = logger logger = immutableConfig.logger
) )
} }
val lastRunInfoStore by future { LastRunInfoStore(immutableConfig) } val lastRunInfoStore = provider {
LastRunInfoStore(immutableConfig)
}
val sessionStore by future { val sessionStore = provider {
SessionStore( SessionStore(
immutableConfig, bugsnagDir.get(),
logger, immutableConfig.maxPersistedSessions,
immutableConfig.apiKey,
immutableConfig.logger,
null null
) )
} }
val lastRunInfo by future { val lastRunInfo = lastRunInfoStore.map { lastRunInfoStore ->
val info = lastRunInfoStore.load() val info = lastRunInfoStore.load()
val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false) val currentRunInfo = LastRunInfo(0, crashed = false, crashedDuringLaunch = false)
lastRunInfoStore.persist(currentRunInfo) lastRunInfoStore.persist(currentRunInfo)
info return@map info
}
fun loadUser(initialUser: User): Provider<UserState> = provider {
val userState = userStore.get().load(initialUser)
sharedPrefMigrator.getOrNull()?.deleteLegacyPrefs()
return@provider userState
} }
} }

@ -1,8 +1,8 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.dag.BackgroundDependencyModule
import com.bugsnag.android.internal.dag.ConfigModule import com.bugsnag.android.internal.dag.ConfigModule
import com.bugsnag.android.internal.dag.DependencyModule
/** /**
* A dependency module which constructs objects that track launch/session related information * A dependency module which constructs objects that track launch/session related information
@ -14,18 +14,21 @@ internal class TrackerModule(
client: Client, client: Client,
bgTaskService: BackgroundTaskService, bgTaskService: BackgroundTaskService,
callbackState: CallbackState callbackState: CallbackState
) : DependencyModule() { ) : BackgroundDependencyModule(bgTaskService) {
private val config = configModule.config private val config = configModule.config
val launchCrashTracker = LaunchCrashTracker(config) val launchCrashTracker = LaunchCrashTracker(config)
val sessionTracker = SessionTracker( val sessionTracker = provider {
config, client.config
callbackState, SessionTracker(
client, config,
storageModule.sessionStore, callbackState,
config.logger, client,
bgTaskService storageModule.sessionStore.get(),
) config.logger,
bgTaskService
)
}
} }

@ -1,23 +1,23 @@
package com.bugsnag.android package com.bugsnag.android
import com.bugsnag.android.internal.ImmutableConfig
import com.bugsnag.android.internal.StateObserver import com.bugsnag.android.internal.StateObserver
import com.bugsnag.android.internal.dag.Provider
import java.io.File import java.io.File
import java.util.concurrent.atomic.AtomicReference import java.util.concurrent.atomic.AtomicReference
/** /**
* This class is responsible for persisting and retrieving user information. * This class is responsible for persisting and retrieving user information.
*/ */
internal class UserStore @JvmOverloads constructor( internal class UserStore(
private val config: ImmutableConfig, private val persist: Boolean,
private val deviceId: String?, private val persistentDir: Provider<File>,
file: File = File(config.persistenceDirectory.value, "bugsnag/user-info"), private val deviceIdStore: Provider<DeviceIdStore.DeviceIds?>,
private val sharedPrefMigrator: SharedPrefMigrator, file: File = File(persistentDir.get(), "user-info"),
private val sharedPrefMigrator: Provider<SharedPrefMigrator>,
private val logger: Logger private val logger: Logger
) { ) {
private val synchronizedStreamableStore: SynchronizedStreamableStore<User> private val synchronizedStreamableStore: SynchronizedStreamableStore<User>
private val persist = config.persistUser
private val previousUser = AtomicReference<User?>(null) private val previousUser = AtomicReference<User?>(null)
init { init {
@ -50,7 +50,7 @@ internal class UserStore @JvmOverloads constructor(
loadedUser != null && validUser(loadedUser) -> UserState(loadedUser) loadedUser != null && validUser(loadedUser) -> UserState(loadedUser)
// if generateAnonymousId config option is false, the deviceId should already be null // if generateAnonymousId config option is false, the deviceId should already be null
// here // here
else -> UserState(User(deviceId, null, null)) else -> UserState(User(deviceIdStore.get()?.deviceId, null, null))
} }
userState.addObserver( userState.addObserver(
@ -81,8 +81,8 @@ internal class UserStore @JvmOverloads constructor(
user.id != null || user.name != null || user.email != null user.id != null || user.name != null || user.email != null
private fun loadPersistedUser(): User? { private fun loadPersistedUser(): User? {
return if (sharedPrefMigrator.hasPrefs()) { return if (sharedPrefMigrator.get().hasPrefs()) {
val legacyUser = sharedPrefMigrator.loadUser(deviceId) val legacyUser = sharedPrefMigrator.get().loadUser(deviceIdStore.get()?.deviceId)
save(legacyUser) save(legacyUser)
legacyUser legacyUser
} else if ( } else if (

@ -1,6 +1,7 @@
package com.bugsnag.android.internal package com.bugsnag.android.internal
import androidx.annotation.VisibleForTesting import androidx.annotation.VisibleForTesting
import com.bugsnag.android.internal.dag.RunnableProvider
import java.util.concurrent.BlockingQueue import java.util.concurrent.BlockingQueue
import java.util.concurrent.Callable import java.util.concurrent.Callable
import java.util.concurrent.ExecutorService import java.util.concurrent.ExecutorService
@ -152,7 +153,11 @@ class BackgroundTaskService(
@Throws(RejectedExecutionException::class) @Throws(RejectedExecutionException::class)
fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> { fun <T> submitTask(taskType: TaskType, callable: Callable<T>): Future<T> {
val task = FutureTask(callable) val task = FutureTask(callable)
execute(taskType, task)
return SafeFuture(task, taskType)
}
fun execute(taskType: TaskType, task: Runnable) {
when (taskType) { when (taskType) {
TaskType.ERROR_REQUEST -> errorExecutor.execute(task) TaskType.ERROR_REQUEST -> errorExecutor.execute(task)
TaskType.SESSION_REQUEST -> sessionExecutor.execute(task) TaskType.SESSION_REQUEST -> sessionExecutor.execute(task)
@ -160,8 +165,6 @@ class BackgroundTaskService(
TaskType.INTERNAL_REPORT -> internalReportExecutor.execute(task) TaskType.INTERNAL_REPORT -> internalReportExecutor.execute(task)
TaskType.DEFAULT -> defaultExecutor.execute(task) TaskType.DEFAULT -> defaultExecutor.execute(task)
} }
return SafeFuture(task, taskType)
} }
/** /**
@ -185,6 +188,18 @@ class BackgroundTaskService(
ioExecutor.awaitTerminationSafe() ioExecutor.awaitTerminationSafe()
} }
inline fun <R> provider(
taskType: TaskType,
crossinline provider: () -> R
): RunnableProvider<R> {
val task = object : RunnableProvider<R>() {
override fun invoke(): R = provider()
}
execute(taskType, task)
return task
}
private fun ExecutorService.awaitTerminationSafe() { private fun ExecutorService.awaitTerminationSafe() {
try { try {
awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS) awaitTermination(SHUTDOWN_WAIT_MS, TimeUnit.MILLISECONDS)

@ -5,8 +5,9 @@ import java.io.File
internal object BugsnagStoreMigrator { internal object BugsnagStoreMigrator {
@JvmStatic @JvmStatic
fun moveToNewDirectory(persistenceDir: File) { fun migrateLegacyFiles(persistenceDir: Lazy<File>): File {
val bugsnagDir = File(persistenceDir, "bugsnag") val originalDir = persistenceDir.value
val bugsnagDir = File(originalDir, "bugsnag")
if (!bugsnagDir.isDirectory) { if (!bugsnagDir.isDirectory) {
bugsnagDir.mkdirs() bugsnagDir.mkdirs()
} }
@ -19,12 +20,12 @@ internal object BugsnagStoreMigrator {
) )
filesToMove.forEach { (from, to) -> filesToMove.forEach { (from, to) ->
val fromFile = File(persistenceDir, from) val fromFile = File(originalDir, from)
if (fromFile.exists()) { if (fromFile.exists()) {
fromFile.renameTo( fromFile.renameTo(File(bugsnagDir, to))
File(bugsnagDir, to)
)
} }
} }
return bugsnagDir
} }
} }

@ -55,6 +55,12 @@ internal object ForegroundDetector : ActivityLifecycleCallbacks, Handler.Callbac
private var waitingForActivityRestart: Boolean = false private var waitingForActivityRestart: Boolean = false
/**
* Marks the timestamp (relative to [SystemClock.elapsedRealtime]) that we initialised for the
* first time.
*/
internal val startupTime = SystemClock.elapsedRealtime()
@VisibleForTesting @VisibleForTesting
internal var backgroundSent = true internal var backgroundSent = true

@ -22,10 +22,11 @@ import com.bugsnag.android.Session
import com.bugsnag.android.Telemetry import com.bugsnag.android.Telemetry
import com.bugsnag.android.ThreadSendPolicy import com.bugsnag.android.ThreadSendPolicy
import com.bugsnag.android.errorApiHeaders import com.bugsnag.android.errorApiHeaders
import com.bugsnag.android.internal.dag.Provider
import com.bugsnag.android.internal.dag.ValueProvider
import com.bugsnag.android.safeUnrollCauses import com.bugsnag.android.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 import java.util.regex.Pattern
data class ImmutableConfig( data class ImmutableConfig(
@ -40,7 +41,7 @@ data class ImmutableConfig(
val enabledBreadcrumbTypes: Set<BreadcrumbType>?, val enabledBreadcrumbTypes: Set<BreadcrumbType>?,
val telemetry: Set<Telemetry>, val telemetry: Set<Telemetry>,
val releaseStage: String?, val releaseStage: String?,
val buildUuid: String?, val buildUuid: Provider<String?>?,
val appVersion: String?, val appVersion: String?,
val versionCode: Int?, val versionCode: Int?,
val appType: String?, val appType: String?,
@ -53,6 +54,7 @@ data class ImmutableConfig(
val maxPersistedEvents: Int, val maxPersistedEvents: Int,
val maxPersistedSessions: Int, val maxPersistedSessions: Int,
val maxReportedThreads: Int, val maxReportedThreads: Int,
val maxStringValueLength: Int,
val threadCollectionTimeLimitMillis: Long, val threadCollectionTimeLimitMillis: Long,
val persistenceDirectory: Lazy<File>, val persistenceDirectory: Lazy<File>,
val sendLaunchCrashesSynchronously: Boolean, val sendLaunchCrashesSynchronously: Boolean,
@ -140,7 +142,7 @@ data class ImmutableConfig(
@JvmOverloads @JvmOverloads
internal fun convertToImmutableConfig( internal fun convertToImmutableConfig(
config: Configuration, config: Configuration,
buildUuid: String? = null, buildUuid: Provider<String?>? = null,
packageInfo: PackageInfo? = null, packageInfo: PackageInfo? = null,
appInfo: ApplicationInfo? = null, appInfo: ApplicationInfo? = null,
persistenceDir: Lazy<File> = lazy { requireNotNull(config.persistenceDirectory) } persistenceDir: Lazy<File> = lazy { requireNotNull(config.persistenceDirectory) }
@ -174,6 +176,7 @@ internal fun convertToImmutableConfig(
maxPersistedEvents = config.maxPersistedEvents, maxPersistedEvents = config.maxPersistedEvents,
maxPersistedSessions = config.maxPersistedSessions, maxPersistedSessions = config.maxPersistedSessions,
maxReportedThreads = config.maxReportedThreads, maxReportedThreads = config.maxReportedThreads,
maxStringValueLength = config.maxStringValueLength,
threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis, threadCollectionTimeLimitMillis = config.threadCollectionTimeLimitMillis,
enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(), enabledBreadcrumbTypes = config.enabledBreadcrumbTypes?.toSet(),
telemetry = config.telemetry.toSet(), telemetry = config.telemetry.toSet(),
@ -256,12 +259,7 @@ internal fun sanitiseConfiguration(
@Suppress("SENSELESS_COMPARISON") @Suppress("SENSELESS_COMPARISON")
if (configuration.delivery == null) { if (configuration.delivery == null) {
configuration.delivery = DefaultDelivery( configuration.delivery = DefaultDelivery(connectivity, configuration.logger!!)
connectivity,
configuration.apiKey,
configuration.maxStringValueLength,
configuration.logger!!
)
} }
return convertToImmutableConfig( return convertToImmutableConfig(
configuration, configuration,
@ -275,25 +273,16 @@ internal fun sanitiseConfiguration(
private fun collectBuildUuid( private fun collectBuildUuid(
appInfo: ApplicationInfo?, appInfo: ApplicationInfo?,
backgroundTaskService: BackgroundTaskService backgroundTaskService: BackgroundTaskService
): String? { ): Provider<String?>? {
val bundle = appInfo?.metaData val bundle = appInfo?.metaData
return when { return when {
bundle?.containsKey(BUILD_UUID) == true -> { bundle?.containsKey(BUILD_UUID) == true -> ValueProvider(
(bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString()) (bundle.getString(BUILD_UUID) ?: bundle.getInt(BUILD_UUID).toString())
.takeIf { it.isNotEmpty() } .takeIf { it.isNotEmpty() }
} )
appInfo != null -> { appInfo != null -> backgroundTaskService.provider(TaskType.IO) {
try { DexBuildIdGenerator.generateBuildId(appInfo)
backgroundTaskService
.submitTask(
TaskType.IO,
Callable { DexBuildIdGenerator.generateBuildId(appInfo) }
)
.get()
} catch (e: Exception) {
null
}
} }
else -> null else -> null

@ -14,7 +14,6 @@ internal class ConfigModule(
configuration: Configuration, configuration: Configuration,
connectivity: Connectivity, connectivity: Connectivity,
bgTaskExecutor: BackgroundTaskService bgTaskExecutor: BackgroundTaskService
) : DependencyModule() { ) : BackgroundDependencyModule(bgTaskExecutor) {
val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor) val config = sanitiseConfiguration(contextModule.ctx, configuration, connectivity, bgTaskExecutor)
} }

@ -1,14 +1,16 @@
package com.bugsnag.android.internal.dag package com.bugsnag.android.internal.dag
import android.content.Context import android.content.Context
import com.bugsnag.android.internal.BackgroundTaskService
/** /**
* A dependency module which accesses the application context object, falling back to the supplied * A dependency module which accesses the application context object, falling back to the supplied
* context if it is the base context. * context if it is the base context.
*/ */
internal class ContextModule( internal class ContextModule(
appContext: Context appContext: Context,
) : DependencyModule() { bgTaskService: BackgroundTaskService
) : BackgroundDependencyModule(bgTaskService) {
val ctx: Context = when (appContext.applicationContext) { val ctx: Context = when (appContext.applicationContext) {
null -> appContext null -> appContext

@ -3,35 +3,37 @@ package com.bugsnag.android.internal.dag
import com.bugsnag.android.internal.BackgroundTaskService import com.bugsnag.android.internal.BackgroundTaskService
import com.bugsnag.android.internal.TaskType import com.bugsnag.android.internal.TaskType
internal abstract class DependencyModule { internal interface DependencyModule
private val properties = mutableListOf<Lazy<*>>()
internal abstract class BackgroundDependencyModule(
@JvmField
val bgTaskService: BackgroundTaskService,
@JvmField
val taskType: TaskType = TaskType.DEFAULT
) : DependencyModule {
/** /**
* Creates a new [Lazy] property that is marked as an object that should be resolved off the * Convenience function to create and schedule a `RunnableProvider` of [taskType] with
* main thread when [resolveDependencies] is called. * [bgTaskService]. The returned `RunnableProvider` will be implemented using the `provider`
* lambda as its `invoke` implementation.
*/ */
fun <T> future(initializer: () -> T): Lazy<T> { inline fun <R> provider(crossinline provider: () -> R): RunnableProvider<R> {
val lazy = lazy { return bgTaskService.provider(taskType, provider)
initializer()
}
properties.add(lazy)
return lazy
} }
/** /**
* Blocks until all dependencies in the module have been constructed. This provides the option * Return a `RunnableProvider` containing the result of applying the given [mapping] to
* for modules to construct objects in a background thread, then have a user block on another * this `Provider`. The `RunnableProvider` will be scheduled with [bgTaskService] as a
* thread until all the objects have been constructed. * [taskType] when this function returns.
*
* This function behaves similar to `List.map` or `Any.let` but with `Provider` encapsulation
* to handle value reuse and threading.
*/ */
fun resolveDependencies(bgTaskService: BackgroundTaskService, taskType: TaskType) { internal inline fun <E, R> Provider<E>.map(crossinline mapping: (E) -> R): RunnableProvider<R> {
kotlin.runCatching { val task = object : RunnableProvider<R>() {
bgTaskService.submitTask( override fun invoke(): R = mapping(this@map.get())
taskType,
Runnable {
properties.forEach { it.value }
}
).get()
} }
bgTaskService.execute(taskType, task)
return task
} }
} }

@ -0,0 +1,181 @@
package com.bugsnag.android.internal.dag
import android.os.Looper
import androidx.annotation.VisibleForTesting
import java.util.concurrent.atomic.AtomicInteger
/**
* A lightweight abstraction similar to `Lazy` or `Future` allowing values to be calculated on
* separate threads, or to be pre-computed.
*/
interface Provider<E> {
/**
* Same as [get] but will return `null` instead of throwing an exception if the value could
* not be computed.
*/
fun getOrNull(): E?
/**
* Return the value sourced from this provider, throwing an exception if the provider failed
* to calculate a value. Anything thrown from here will have been captured when attempting
* to calculate the value.
*/
fun get(): E
}
/**
* The primary implementation of [Provider], usually created using the
* [BackgroundDependencyModule.provider] function. Similar conceptually to
* [java.util.concurrent.FutureTask] but with a more compact implementation. The implementation
* of [RunnableProvider.get] is special because it behaves more like [Lazy.value] in that getting
* a value that is still pending will cause it to be run on the current thread instead of waiting
* for it to be run "sometime in the future". This makes RunnableProviders less bug-prone when
* dealing with single-thread executors (such as those in [BackgroundTaskService]). RunnableProvider
* also has special handling for the main-thread, ensuring no computational work (such as IO) is
* done on the main thread.
*/
abstract class RunnableProvider<E> : Provider<E>, Runnable {
private val state = AtomicInteger(TASK_STATE_PENDING)
@Volatile
private var value: Any? = null
/**
* Calculate the value of this [Provider]. This function will be called at-most once by [run].
* Do not call this function directly, instead use [get] and [getOrNull] which implement the
* correct threading behaviour and will reuse the value if it has been previously calculated.
*/
abstract operator fun invoke(): E
override fun getOrNull(): E? {
return getOr { return null }
}
override fun get(): E {
return getOr { throw value as Throwable }
}
private inline fun getOr(failureHandler: () -> E): E {
while (true) {
when (state.get()) {
TASK_STATE_RUNNING -> awaitResult()
TASK_STATE_PENDING -> {
if (isMainThread()) {
// When the calling thread is the 'main' thread, we *always* wait for the
// background workers to [invoke] this Provider, assuming that the Provider
// is performing some kind of IO that should be kept away from the main
// thread. Ideally this doesn't happen, but this behaviour avoids the
// need for complicated callback mechanisms.
awaitResult()
} else {
// If the Provider has yet to be computed, we will try and run it on the
// current thread. This potentially causes run() to happen on a different
// Thread to the expected worker (TaskType), effectively like work-stealing.
run()
}
}
TASK_STATE_COMPLETE -> @Suppress("UNCHECKED_CAST") return value as E
TASK_STATE_FAILED -> failureHandler()
}
}
}
private fun isMainThread(): Boolean {
return Thread.currentThread() === mainThread
}
/**
* Cause the current thread to wait (block) until this `Provider` [isComplete]. Upon returning
* the [isComplete] function will return `true`.
*/
private fun awaitResult() {
synchronized(this) {
while (!isComplete()) {
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
(this as Object).wait()
}
}
}
private fun isComplete() = when (state.get()) {
TASK_STATE_PENDING, TASK_STATE_RUNNING -> false
else -> true
}
/**
* The main entry point for a provider, typically called by a worker thread from
* [BackgroundTaskService]. If [run] has already been called this will be a no-op (including
* a reentrant thread), as such the task state *must* be checked after calling this.
*
* This should not be called, and instead [get] or [getOrNull] should be used to obtain the
* value produced by [invoke].
*/
final override fun run() {
if (state.compareAndSet(TASK_STATE_PENDING, TASK_STATE_RUNNING)) {
try {
value = invoke()
state.set(TASK_STATE_COMPLETE)
} catch (ex: Throwable) {
value = ex
state.set(TASK_STATE_FAILED)
} finally {
synchronized(this) {
// wakeup any waiting threads
@Suppress("PLATFORM_CLASS_MAPPED_TO_KOTLIN")
(this as Object).notifyAll()
}
}
}
}
@VisibleForTesting
internal companion object {
/**
* The `Provider` task state before the provider has started actually running. This state
* indicates that the task has been constructed, has typically been scheduled but has
* not actually started running yet.
*/
private const val TASK_STATE_PENDING = 0
/**
* The `Provider` task state when running. Once the [run] function returns the state will
* be either [TASK_STATE_COMPLETE] or [TASK_STATE_FAILED].
*/
private const val TASK_STATE_RUNNING = 1
/**
* The `Provider` state of a successfully completed task. When this is the state the
* provider value can be obtained immediately without error.
*/
private const val TASK_STATE_COMPLETE = 2
/**
* The `Provider` state of a task where [invoke] failed with an error or exception.
*/
private const val TASK_STATE_FAILED = 999
/**
* We cache the main thread to avoid any locks within [Looper.getMainLooper]. This is
* settable for unit tests, so that there doesn't have to be a valid Looper when they run.
*
* Actually access is done via the [mainThread] property.
*/
@VisibleForTesting
@Suppress("ObjectPropertyNaming") // backing property from 'mainThread'
internal var _mainThread: Thread? = null
get() {
if (field == null) {
field = Looper.getMainLooper().thread
}
return field
}
internal val mainThread: Thread get() = _mainThread!!
}
}
data class ValueProvider<T>(private val value: T) : Provider<T> {
override fun getOrNull(): T? = get()
override fun get(): T = value
}

@ -2,13 +2,15 @@ package com.bugsnag.android.internal.dag
import com.bugsnag.android.getActivityManager import com.bugsnag.android.getActivityManager
import com.bugsnag.android.getStorageManager import com.bugsnag.android.getStorageManager
import com.bugsnag.android.internal.BackgroundTaskService
/** /**
* A dependency module which provides a reference to Android system services. * A dependency module which provides a reference to Android system services.
*/ */
internal class SystemServiceModule( internal class SystemServiceModule(
contextModule: ContextModule contextModule: ContextModule,
) : DependencyModule() { bgTaskService: BackgroundTaskService
) : BackgroundDependencyModule(bgTaskService) {
val storageManager = contextModule.ctx.getStorageManager() val storageManager = contextModule.ctx.getStorageManager()
val activityManager = contextModule.ctx.getActivityManager() val activityManager = contextModule.ctx.getActivityManager()

Loading…
Cancel
Save