diff --git a/app/build.gradle b/app/build.gradle index cdd003b1a8..888206cb33 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -506,7 +506,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.28.2" + def bugsnag_version = "5.31.3" def biweekly_version = "0.6.7" def vcard_version = "0.12.1" def relinker_version = "1.4.5" diff --git a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt index 696935490b..ee23a07a72 100644 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -33,6 +33,7 @@ internal class AppDataCollector( private val processName = findProcessName() private val releaseStage = config.releaseStage private val versionName = config.appVersion ?: config.packageInfo?.versionName + private val installerPackage = getInstallerPackageName() fun generateApp(): App = App(config, binaryArch, packageName, releaseStage, versionName, codeBundleId) @@ -74,6 +75,7 @@ internal class AppDataCollector( map["totalMemory"] = totalMemory map["freeMemory"] = freeMemory map["memoryLimit"] = runtime.maxMemory() + map["installerPackage"] = installerPackage } /** @@ -130,6 +132,20 @@ internal class AppDataCollector( } } + /** + * The name of installer / vendor package of the app + */ + fun getInstallerPackageName(): String? { + try { + if (VERSION.SDK_INT >= VERSION_CODES.R) + return packageManager?.getInstallSourceInfo(packageName)?.installingPackageName + @Suppress("DEPRECATION") + return packageManager?.getInstallerPackageName(packageName) + } catch (e: Exception) { + return null + } + } + /** * Finds the name of the current process, or null if this cannot be found. */ diff --git a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt index 1cfdfd90f0..61f74229c5 100644 --- a/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt +++ b/app/src/main/java/com/bugsnag/android/BugsnagEventMapper.kt @@ -66,7 +66,7 @@ internal class BugsnagEventMapper( // populate session val sessionMap = map["session"] as? Map sessionMap?.let { - event.session = Session(it, logger) + event.session = Session(it, logger, apiKey) } // populate threads diff --git a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt index da0c4c3c9e..99e8d87b18 100644 --- a/app/src/main/java/com/bugsnag/android/ContextExtensions.kt +++ b/app/src/main/java/com/bugsnag/android/ContextExtensions.kt @@ -7,6 +7,7 @@ import android.content.Intent import android.content.IntentFilter import android.location.LocationManager import android.net.ConnectivityManager +import android.os.Build import android.os.RemoteException import android.os.storage.StorageManager import java.lang.RuntimeException @@ -21,7 +22,11 @@ internal fun Context.registerReceiverSafe( logger: Logger? = null ): Intent? { try { - return registerReceiver(receiver, filter) + return if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE) { + registerReceiver(receiver, filter, Context.RECEIVER_EXPORTED) + } else { + registerReceiver(receiver, filter) + } } catch (exc: SecurityException) { logger?.w("Failed to register receiver", exc) } catch (exc: RemoteException) { diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index a7755a2edc..493763a493 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -242,21 +242,26 @@ internal class DeviceDataCollector( /** * Get the amount of memory remaining on the device */ - private fun calculateFreeMemory(): Long? { + fun calculateFreeMemory(): Long? { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { - val freeMemory = appContext.getActivityManager() - ?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } } - ?.availMem - - if (freeMemory != null) { - return freeMemory + try { + val freeMemory = appContext.getActivityManager() + ?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } } + ?.availMem + if (freeMemory != null) { + return freeMemory + } + } catch (e: Throwable) { + return null } } - return runCatching { + return try { @Suppress("PrivateApi") AndroidProcess::class.java.getDeclaredMethod("getFreeMemory").invoke(null) as Long? - }.getOrNull() + } catch (e: Throwable) { + null + } } /** diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index d7321a88c0..1e36c1483d 100644 --- a/app/src/main/java/com/bugsnag/android/Notifier.kt +++ b/app/src/main/java/com/bugsnag/android/Notifier.kt @@ -7,7 +7,7 @@ import java.io.IOException */ class Notifier @JvmOverloads constructor( var name: String = "Android Bugsnag Notifier", - var version: String = "5.30.0", + var version: String = "5.31.3", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/PluginClient.kt b/app/src/main/java/com/bugsnag/android/PluginClient.kt index 46abba1da9..a9a46886ad 100644 --- a/app/src/main/java/com/bugsnag/android/PluginClient.kt +++ b/app/src/main/java/com/bugsnag/android/PluginClient.kt @@ -34,7 +34,7 @@ internal class PluginClient( private fun instantiatePlugin(clz: String, isWarningEnabled: Boolean): Plugin? { return try { val pluginClz = Class.forName(clz) - pluginClz.newInstance() as Plugin + pluginClz.getDeclaredConstructor().newInstance() as Plugin } catch (exc: ClassNotFoundException) { if (isWarningEnabled) { logger.d("Plugin '$clz' is not on the classpath - functionality will not be enabled.") diff --git a/app/src/main/java/com/bugsnag/android/Session.java b/app/src/main/java/com/bugsnag/android/Session.java index 23dc782e2c..408d6fc0f7 100644 --- a/app/src/main/java/com/bugsnag/android/Session.java +++ b/app/src/main/java/com/bugsnag/android/Session.java @@ -34,17 +34,19 @@ public final class Session implements JsonStream.Streamable, UserAware { private final AtomicBoolean tracked = new AtomicBoolean(false); final AtomicBoolean isPaused = new AtomicBoolean(false); + private String apiKey; + static Session copySession(Session session) { Session copy = new Session(session.id, session.startedAt, session.user, session.unhandledCount.get(), session.handledCount.get(), session.notifier, - session.logger); + session.logger, session.getApiKey()); copy.tracked.set(session.tracked.get()); copy.autoCaptured.set(session.isAutoCaptured()); return copy; } - Session(Map map, Logger logger) { - this(null, null, logger); + Session(Map map, Logger logger, String apiKey) { + this(null, null, logger, apiKey); setId((String) map.get("id")); String timestamp = (String) map.get("startedAt"); @@ -61,25 +63,28 @@ public final class Session implements JsonStream.Streamable, UserAware { } Session(String id, Date startedAt, User user, boolean autoCaptured, - Notifier notifier, Logger logger) { - this(null, notifier, logger); + Notifier notifier, Logger logger, String apiKey) { + this(null, notifier, logger, apiKey); this.id = id; this.startedAt = new Date(startedAt.getTime()); this.user = user; this.autoCaptured.set(autoCaptured); + this.apiKey = apiKey; } Session(String id, Date startedAt, User user, int unhandledCount, int handledCount, - Notifier notifier, Logger logger) { - this(id, startedAt, user, false, notifier, logger); + Notifier notifier, Logger logger, String apiKey) { + this(id, startedAt, user, false, notifier, logger, apiKey); this.unhandledCount.set(unhandledCount); this.handledCount.set(handledCount); this.tracked.set(true); + this.apiKey = apiKey; } - Session(File file, Notifier notifier, Logger logger) { + Session(File file, Notifier notifier, Logger logger, String apiKey) { this.file = file; this.logger = logger; + this.apiKey = SessionFilenameInfo.findApiKeyInFilename(file, apiKey); if (notifier != null) { Notifier copy = new Notifier(notifier.getName(), notifier.getVersion(), notifier.getUrl()); @@ -211,8 +216,10 @@ public final class Session implements JsonStream.Streamable, UserAware { * * @return whether the payload is v2 */ - boolean isV2Payload() { - return file != null && file.getName().endsWith("_v2.json"); + + boolean isLegacyPayload() { + return !(file != null + && (file.getName().endsWith("_v2.json") || file.getName().endsWith("_v3.json"))); } Notifier getNotifier() { @@ -222,10 +229,10 @@ public final class Session implements JsonStream.Streamable, UserAware { @Override public void toStream(@NonNull JsonStream writer) throws IOException { if (file != null) { - if (isV2Payload()) { - serializeV2Payload(writer); + if (!isLegacyPayload()) { + serializePayload(writer); } else { - serializeV1Payload(writer); + serializeLegacyPayload(writer); } } else { writer.beginObject(); @@ -239,11 +246,11 @@ public final class Session implements JsonStream.Streamable, UserAware { } } - private void serializeV2Payload(@NonNull JsonStream writer) throws IOException { + private void serializePayload(@NonNull JsonStream writer) throws IOException { writer.value(file); } - private void serializeV1Payload(@NonNull JsonStream writer) throws IOException { + private void serializeLegacyPayload(@NonNull JsonStream writer) throws IOException { writer.beginObject(); writer.name("notifier").value(notifier); writer.name("app").value(app); @@ -261,4 +268,25 @@ public final class Session implements JsonStream.Streamable, UserAware { writer.name("user").value(user); writer.endObject(); } + + /** + * The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag + * is initialized, you may choose to send certain sessions to a different Bugsnag project. + */ + public void setApiKey(@NonNull String apiKey) { + if (apiKey != null) { + this.apiKey = apiKey; + } else { + logNull("apiKey"); + } + } + + /** + * The API key used for session sent to Bugsnag. Even though the API key is set when Bugsnag + * is initialized, you may choose to send certain sessions to a different Bugsnag project. + */ + @NonNull + public String getApiKey() { + return apiKey; + } } diff --git a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt index 04efa4081b..554b1ee0d7 100644 --- a/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt +++ b/app/src/main/java/com/bugsnag/android/SessionFilenameInfo.kt @@ -1,5 +1,6 @@ package com.bugsnag.android +import com.bugsnag.android.internal.ImmutableConfig import java.io.File import java.util.UUID @@ -11,12 +12,13 @@ import java.util.UUID * timestamp - to sort error reports by time of capture */ internal data class SessionFilenameInfo( + var apiKey: String, val timestamp: Long, - val uuid: String, + val uuid: String ) { fun encode(): String { - return toFilename(timestamp, uuid) + return toFilename(apiKey, timestamp, uuid) } internal companion object { @@ -27,29 +29,63 @@ internal data class SessionFilenameInfo( * Generates a filename for the session in the format * "[UUID][timestamp]_v2.json" */ - fun toFilename(timestamp: Long, uuid: String): String { - return "${uuid}${timestamp}_v2.json" + fun toFilename(apiKey: String, timestamp: Long, uuid: String): String { + return "${apiKey}_${uuid}${timestamp}_v3.json" } @JvmStatic - fun defaultFilename(): String { - return toFilename(System.currentTimeMillis(), UUID.randomUUID().toString()) + fun defaultFilename( + obj: Any, + config: ImmutableConfig + ): SessionFilenameInfo { + val sanitizedApiKey = when (obj) { + is Session -> obj.apiKey + else -> config.apiKey + } + + return SessionFilenameInfo( + sanitizedApiKey, + System.currentTimeMillis(), + UUID.randomUUID().toString() + ) } - fun fromFile(file: File): SessionFilenameInfo { + fun fromFile(file: File, defaultApiKey: String): SessionFilenameInfo { return SessionFilenameInfo( + findApiKeyInFilename(file, defaultApiKey), findTimestampInFilename(file), findUuidInFilename(file) ) } - private fun findUuidInFilename(file: File): String { - return file.name.substring(0, uuidLength - 1) + @JvmStatic + fun findUuidInFilename(file: File): String { + var fileName = file.name + if (isFileV3(file)) { + fileName = file.name.substringAfter('_') + } + return fileName.takeIf { it.length >= uuidLength }?.take(uuidLength) ?: "" } @JvmStatic fun findTimestampInFilename(file: File): Long { - return file.name.substring(uuidLength, file.name.indexOf("_")).toLongOrNull() ?: -1 + var fileName = file.name + if (isFileV3(file)) { + fileName = file.name.substringAfter('_') + } + return fileName.drop(findUuidInFilename(file).length) + .substringBefore('_') + .toLongOrNull() ?: -1 } + + @JvmStatic + fun findApiKeyInFilename(file: File?, defaultApiKey: String): String { + if (file == null || !isFileV3(file)) { + return defaultApiKey + } + return file.name.substringBefore('_').takeUnless { it.isEmpty() } ?: defaultApiKey + } + + internal fun isFileV3(file: File): Boolean = file.name.endsWith("_v3.json") } } diff --git a/app/src/main/java/com/bugsnag/android/SessionStore.java b/app/src/main/java/com/bugsnag/android/SessionStore.java index a0238a5feb..87e3524e28 100644 --- a/app/src/main/java/com/bugsnag/android/SessionStore.java +++ b/app/src/main/java/com/bugsnag/android/SessionStore.java @@ -17,6 +17,7 @@ import java.util.UUID; */ class SessionStore extends FileStore { + private final ImmutableConfig config; static final Comparator SESSION_COMPARATOR = new Comparator() { @Override public int compare(File lhs, File rhs) { @@ -43,12 +44,15 @@ class SessionStore extends FileStore { SESSION_COMPARATOR, logger, delegate); + this.config = config; } @NonNull @Override String getFilename(Object object) { - return SessionFilenameInfo.defaultFilename(); + SessionFilenameInfo sessionInfo + = SessionFilenameInfo.defaultFilename(object, config); + return sessionInfo.encode(); } public boolean isTooOld(File file) { diff --git a/app/src/main/java/com/bugsnag/android/SessionTracker.java b/app/src/main/java/com/bugsnag/android/SessionTracker.java index f620b32416..cc736e1077 100644 --- a/app/src/main/java/com/bugsnag/android/SessionTracker.java +++ b/app/src/main/java/com/bugsnag/android/SessionTracker.java @@ -89,7 +89,10 @@ class SessionTracker extends BaseObservable { return null; } String id = UUID.randomUUID().toString(); - Session session = new Session(id, date, user, autoCaptured, client.getNotifier(), logger); + Session session = new Session( + id, date, user, autoCaptured, + client.getNotifier(), logger, configuration.getApiKey() + ); if (trackSessionIfNeeded(session)) { return session; } else { @@ -157,7 +160,7 @@ class SessionTracker extends BaseObservable { Session session = null; if (date != null && sessionId != null) { session = new Session(sessionId, date, user, unhandledCount, handledCount, - client.getNotifier(), logger); + client.getNotifier(), logger, configuration.getApiKey()); notifySessionStartObserver(session); } else { updateState(StateEvent.PauseSession.INSTANCE); @@ -256,9 +259,11 @@ class SessionTracker extends BaseObservable { void flushStoredSession(File storedFile) { logger.d("SessionTracker#flushStoredSession() - attempting delivery"); - Session payload = new Session(storedFile, client.getNotifier(), logger); + Session payload = new Session( + storedFile, client.getNotifier(), logger, configuration.getApiKey() + ); - if (!payload.isV2Payload()) { // collect data here + if (payload.isLegacyPayload()) { // collect data here payload.setApp(client.getAppDataCollector().generateApp()); payload.setDevice(client.getDeviceDataCollector().generateDevice()); } @@ -330,7 +335,7 @@ class SessionTracker extends BaseObservable { } DeliveryStatus deliverSessionPayload(Session payload) { - DeliveryParams params = configuration.getSessionApiDeliveryParams(); + DeliveryParams params = configuration.getSessionApiDeliveryParams(payload); Delivery delivery = configuration.getDelivery(); return delivery.deliver(payload, params); } diff --git a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt index d664576791..2f7ccc238f 100644 --- a/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt +++ b/app/src/main/java/com/bugsnag/android/SharedPrefMigrator.kt @@ -2,32 +2,38 @@ package com.bugsnag.android import android.annotation.SuppressLint import android.content.Context +import android.content.SharedPreferences /** * Reads legacy information left in SharedPreferences and migrates it to the new location. */ internal class SharedPrefMigrator(context: Context) : DeviceIdPersistence { - private val prefs = context - .getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE) + private val prefs: SharedPreferences? = + try { + context.getSharedPreferences("com.bugsnag.android", Context.MODE_PRIVATE) + } catch (e: RuntimeException) { + null + } /** * This implementation will never create an ID; it will only fetch one if present. */ - override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = prefs.getString(INSTALL_ID_KEY, null) + override fun loadDeviceId(requestCreateIfDoesNotExist: Boolean) = + prefs?.getString(INSTALL_ID_KEY, null) fun loadUser(deviceId: String?) = User( - prefs.getString(USER_ID_KEY, deviceId), - prefs.getString(USER_EMAIL_KEY, null), - prefs.getString(USER_NAME_KEY, null) + prefs?.getString(USER_ID_KEY, deviceId), + prefs?.getString(USER_EMAIL_KEY, null), + prefs?.getString(USER_NAME_KEY, null) ) - fun hasPrefs() = prefs.contains(INSTALL_ID_KEY) + fun hasPrefs() = prefs?.contains(INSTALL_ID_KEY) == true @SuppressLint("ApplySharedPref") fun deleteLegacyPrefs() { if (hasPrefs()) { - prefs.edit().clear().commit() + prefs?.edit()?.clear()?.commit() } } diff --git a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt b/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt index ac8e309ccb..d6c89583c0 100644 --- a/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt +++ b/app/src/main/java/com/bugsnag/android/SystemBroadcastReceiver.kt @@ -66,6 +66,7 @@ internal class SystemBroadcastReceiver( ) { val extras = intent.extras extras?.keySet()?.forEach { key -> + @Suppress("DEPRECATION") val valObj = extras[key] ?: return@forEach val strVal = valObj.toString() if (isAndroidKey(key)) { // shorten the Intent action diff --git a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt index 07f2dcb835..8ff97eab96 100644 --- a/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt +++ b/app/src/main/java/com/bugsnag/android/internal/ImmutableConfig.kt @@ -18,6 +18,7 @@ import com.bugsnag.android.EventPayload import com.bugsnag.android.Logger import com.bugsnag.android.ManifestConfigLoader.Companion.BUILD_UUID import com.bugsnag.android.NoopLogger +import com.bugsnag.android.Session import com.bugsnag.android.Telemetry import com.bugsnag.android.ThreadSendPolicy import com.bugsnag.android.errorApiHeaders @@ -65,8 +66,8 @@ data class ImmutableConfig( DeliveryParams(endpoints.notify, errorApiHeaders(payload)) @JvmName("getSessionApiDeliveryParams") - internal fun getSessionApiDeliveryParams() = - DeliveryParams(endpoints.sessions, sessionApiHeaders(apiKey)) + internal fun getSessionApiDeliveryParams(session: Session) = + DeliveryParams(endpoints.sessions, sessionApiHeaders(session.apiKey)) /** * Returns whether the given throwable should be discarded diff --git a/build.gradle b/build.gradle index 49d2648aeb..dbcb7f4966 100644 --- a/build.gradle +++ b/build.gradle @@ -10,7 +10,7 @@ buildscript { classpath 'com.android.tools.build:gradle:8.1.3' // https://github.com/bugsnag/bugsnag-android-gradle-plugin // https://mvnrepository.com/artifact/com.bugsnag/bugsnag-android-gradle-plugin - classpath "com.bugsnag:bugsnag-android-gradle-plugin:8.0.1" + classpath "com.bugsnag:bugsnag-android-gradle-plugin:8.1.0" // https://mvnrepository.com/artifact/org.jetbrains.kotlin/kotlin-gradle-plugin classpath "org.jetbrains.kotlin:kotlin-gradle-plugin:1.8.20" classpath "org.jetbrains.kotlin:kotlin-android-extensions:1.8.20"