diff --git a/app/build.gradle b/app/build.gradle index 40e043ff5d..9fd8f34de6 100644 --- a/app/build.gradle +++ b/app/build.gradle @@ -299,7 +299,7 @@ dependencies { def dnsjava_version = "2.1.9" def openpgp_version = "12.0" def badge_version = "1.1.22" - def bugsnag_version = "5.11.0" + def bugsnag_version = "5.12.0" def biweekly_version = "0.6.6" def relinker_version = "1.4.3" def markwon_version = "4.6.2" diff --git a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt index e3f3a50762..07fe06cbd9 100644 --- a/app/src/main/java/com/bugsnag/android/AppDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/AppDataCollector.kt @@ -20,8 +20,9 @@ internal class AppDataCollector( private val sessionTracker: SessionTracker, private val activityManager: ActivityManager?, private val launchCrashTracker: LaunchCrashTracker, - private val logger: Logger + private val memoryTrimState: MemoryTrimState ) { + var codeBundleId: String? = null private val packageName: String = appContext.packageName @@ -51,8 +52,10 @@ internal class AppDataCollector( val map = HashMap() map["name"] = appName map["activeScreen"] = sessionTracker.contextActivity - map["memoryUsage"] = getMemoryUsage() - map["lowMemory"] = isLowMemory() + map["lowMemory"] = memoryTrimState.isLowMemory + map["memoryTrimLevel"] = memoryTrimState.trimLevelDescription + + populateRuntimeMemoryMetadata(map) bgWorkRestricted?.let { map["backgroundWorkRestricted"] = bgWorkRestricted @@ -63,13 +66,14 @@ internal class AppDataCollector( return map } - /** - * Get the actual memory used by the VM (which may not be the total used - * by the app in the case of NDK usage). - */ - private fun getMemoryUsage(): Long { + private fun populateRuntimeMemoryMetadata(map: MutableMap) { val runtime = Runtime.getRuntime() - return runtime.totalMemory() - runtime.freeMemory() + val totalMemory = runtime.totalMemory() + val freeMemory = runtime.freeMemory() + map["memoryUsage"] = totalMemory - freeMemory + map["totalMemory"] = totalMemory + map["freeMemory"] = freeMemory + map["memoryLimit"] = runtime.maxMemory() } /** @@ -86,22 +90,6 @@ internal class AppDataCollector( } } - /** - * Check if the device is currently running low on memory. - */ - private fun isLowMemory(): Boolean? { - try { - if (activityManager != null) { - val memInfo = ActivityManager.MemoryInfo() - activityManager.getMemoryInfo(memInfo) - return memInfo.lowMemory - } - } catch (exception: Exception) { - logger.w("Could not check lowMemory status") - } - return null - } - fun setBinaryArch(binaryArch: String) { this.binaryArch = binaryArch } diff --git a/app/src/main/java/com/bugsnag/android/Client.java b/app/src/main/java/com/bugsnag/android/Client.java index bdc21e2e38..205ecdfc44 100644 --- a/app/src/main/java/com/bugsnag/android/Client.java +++ b/app/src/main/java/com/bugsnag/android/Client.java @@ -62,6 +62,9 @@ public class Client implements MetadataAware, CallbackAware, UserAware { @NonNull final BreadcrumbState breadcrumbState; + @NonNull + final MemoryTrimState memoryTrimState = new MemoryTrimState(); + @NonNull protected final EventStore eventStore; @@ -162,7 +165,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { DataCollectionModule dataCollectionModule = new DataCollectionModule(contextModule, configModule, systemServiceModule, trackerModule, - bgTaskService, connectivity, storageModule.getDeviceId()); + bgTaskService, connectivity, storageModule.getDeviceId(), memoryTrimState); dataCollectionModule.resolveDependencies(bgTaskService, TaskType.IO); appDataCollector = dataCollectionModule.getAppDataCollector(); deviceDataCollector = dataCollectionModule.getDeviceDataCollector(); @@ -337,10 +340,21 @@ public class Client implements MetadataAware, CallbackAware, UserAware { clientObservable.postOrientationChange(newOrientation); return null; } - }, new Function1() { + }, new Function2() { @Override - public Unit invoke(Boolean isLowMemory) { - clientObservable.postMemoryTrimEvent(isLowMemory); + public Unit invoke(Boolean isLowMemory, Integer memoryTrimLevel) { + memoryTrimState.setLowMemory(Boolean.TRUE.equals(isLowMemory)); + if (memoryTrimState.updateMemoryTrimLevel(memoryTrimLevel)) { + leaveAutoBreadcrumb( + "Trim Memory", + BreadcrumbType.STATE, + Collections.singletonMap( + "trimLevel", memoryTrimState.getTrimLevelDescription() + ) + ); + } + + memoryTrimState.emitObservableEvent(); return null; } } @@ -383,6 +397,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { contextState.addObserver(observer); deliveryDelegate.addObserver(observer); launchCrashTracker.addObserver(observer); + memoryTrimState.addObserver(observer); } void removeObserver(StateObserver observer) { @@ -394,6 +409,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { contextState.removeObserver(observer); deliveryDelegate.removeObserver(observer); launchCrashTracker.removeObserver(observer); + memoryTrimState.removeObserver(observer); } /** @@ -403,6 +419,7 @@ public class Client implements MetadataAware, CallbackAware, UserAware { metadataState.emitObservableEvent(); contextState.emitObservableEvent(); userState.emitObservableEvent(); + memoryTrimState.emitObservableEvent(); } /** diff --git a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt index 7095b50029..d4d97d1df3 100644 --- a/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt +++ b/app/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt @@ -1,13 +1,13 @@ package com.bugsnag.android -import android.content.ComponentCallbacks +import android.content.ComponentCallbacks2 import android.content.res.Configuration internal class ClientComponentCallbacks( private val deviceDataCollector: DeviceDataCollector, private val cb: (oldOrientation: String?, newOrientation: String?) -> Unit, - val callback: (Boolean) -> Unit -) : ComponentCallbacks { + val memoryCallback: (Boolean, Int?) -> Unit +) : ComponentCallbacks2 { override fun onConfigurationChanged(newConfig: Configuration) { val oldOrientation = deviceDataCollector.getOrientationAsString() @@ -18,7 +18,11 @@ internal class ClientComponentCallbacks( } } + override fun onTrimMemory(level: Int) { + memoryCallback(level >= ComponentCallbacks2.TRIM_MEMORY_COMPLETE, level) + } + override fun onLowMemory() { - callback(true) + memoryCallback(true, null) } } diff --git a/app/src/main/java/com/bugsnag/android/ClientObservable.kt b/app/src/main/java/com/bugsnag/android/ClientObservable.kt index 8bd8e47522..d96724c100 100644 --- a/app/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/app/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -8,10 +8,6 @@ internal class ClientObservable : BaseObservable() { updateState { StateEvent.UpdateOrientation(orientation) } } - fun postMemoryTrimEvent(isLowMemory: Boolean) { - updateState { StateEvent.UpdateMemoryTrimEvent(isLowMemory) } - } - fun postNdkInstall( conf: ImmutableConfig, lastRunInfoPath: String, diff --git a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt index 89b1b4f234..577b9bf7bc 100644 --- a/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt +++ b/app/src/main/java/com/bugsnag/android/DataCollectionModule.kt @@ -17,7 +17,8 @@ internal class DataCollectionModule( trackerModule: TrackerModule, bgTaskService: BackgroundTaskService, connectivity: Connectivity, - deviceId: String? + deviceId: String?, + memoryTrimState: MemoryTrimState ) : DependencyModule() { private val ctx = contextModule.ctx @@ -34,7 +35,7 @@ internal class DataCollectionModule( trackerModule.sessionTracker, systemServiceModule.activityManager, trackerModule.launchCrashTracker, - logger + memoryTrimState ) } diff --git a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt index 5587ba7491..2e2f2d2fd4 100644 --- a/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt +++ b/app/src/main/java/com/bugsnag/android/DefaultDelivery.kt @@ -66,7 +66,7 @@ internal class DefaultDelivery( return DeliveryStatus.UNDELIVERED } catch (exception: IOException) { logger.w("IOException encountered in request", exception) - return DeliveryStatus.UNDELIVERED + return DeliveryStatus.FAILURE } catch (exception: Exception) { logger.w("Unexpected error delivering payload", exception) return DeliveryStatus.FAILURE diff --git a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt index 44c4aa2900..69c4063e97 100644 --- a/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt +++ b/app/src/main/java/com/bugsnag/android/DeviceDataCollector.kt @@ -1,6 +1,7 @@ package com.bugsnag.android import android.annotation.SuppressLint +import android.app.ActivityManager import android.content.Context import android.content.Intent import android.content.IntentFilter @@ -8,6 +9,7 @@ import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.content.res.Resources import android.os.BatteryManager +import android.os.Build import android.provider.Settings import java.io.File import java.util.Date @@ -19,6 +21,7 @@ import java.util.concurrent.RejectedExecutionException import java.util.concurrent.atomic.AtomicInteger import kotlin.math.max import kotlin.math.min +import android.os.Process as AndroidProcess internal class DeviceDataCollector( private val connectivity: Connectivity, @@ -41,6 +44,7 @@ internal class DeviceDataCollector( private val cpuAbi = getCpuAbi() private val runtimeVersions: MutableMap private val rootedFuture: Future? + private val totalMemoryFuture: Future? = retrieveTotalDeviceMemory() private var orientation = AtomicInteger(resources.configuration.orientation) init { @@ -68,7 +72,7 @@ internal class DeviceDataCollector( checkIsRooted(), deviceId, locale, - calculateTotalMemory(), + totalMemoryFuture.runCatching { this?.get() }.getOrNull(), runtimeVersions.toMutableMap() ) @@ -77,7 +81,7 @@ internal class DeviceDataCollector( checkIsRooted(), deviceId, locale, - calculateTotalMemory(), + totalMemoryFuture.runCatching { this?.get() }.getOrNull(), runtimeVersions.toMutableMap(), calculateFreeDisk(), calculateFreeMemory(), @@ -216,31 +220,60 @@ internal class DeviceDataCollector( } /** - * Get the amount of memory remaining that the VM can allocate + * Get the amount of memory remaining on the device */ - private fun calculateFreeMemory(): Long { - val runtime = Runtime.getRuntime() - val maxMemory = runtime.maxMemory() + private 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 - return if (maxMemory != Long.MAX_VALUE) { - maxMemory - runtime.totalMemory() + runtime.freeMemory() - } else { - runtime.freeMemory() + if (freeMemory != null) { + return freeMemory + } } + + return runCatching { + @Suppress("PrivateApi") + AndroidProcess::class.java.getDeclaredMethod("getFreeMemory").invoke(null) as Long? + }.getOrNull() } /** - * Get the total memory available on the current Android device, in bytes + * Attempt to retrieve the total amount of memory available on the device */ - private fun calculateTotalMemory(): Long { - val runtime = Runtime.getRuntime() - val maxMemory = runtime.maxMemory() - return when { - maxMemory != Long.MAX_VALUE -> maxMemory - else -> runtime.totalMemory() + private fun retrieveTotalDeviceMemory(): Future? { + return try { + bgTaskService.submitTask( + TaskType.DEFAULT, + Callable { + calculateTotalMemory() + } + ) + } catch (exc: RejectedExecutionException) { + logger.w("Failed to lookup available device memory", exc) + null } } + private fun calculateTotalMemory(): Long? { + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.JELLY_BEAN) { + val totalMemory = appContext.getActivityManager() + ?.let { am -> ActivityManager.MemoryInfo().also { am.getMemoryInfo(it) } } + ?.totalMem + + if (totalMemory != null) { + return totalMemory + } + } + + // we try falling back to a reflective API + return runCatching { + @Suppress("PrivateApi") + AndroidProcess::class.java.getDeclaredMethod("getTotalMemory").invoke(null) as Long? + }.getOrNull() + } + /** * Get the current device orientation, eg. "landscape" */ diff --git a/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt b/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt new file mode 100644 index 0000000000..8168e4881e --- /dev/null +++ b/app/src/main/java/com/bugsnag/android/MemoryTrimState.kt @@ -0,0 +1,41 @@ +package com.bugsnag.android + +import android.content.ComponentCallbacks2 + +internal class MemoryTrimState : BaseObservable() { + var isLowMemory: Boolean = false + var memoryTrimLevel: Int? = null + + val trimLevelDescription: String get() = descriptionFor(memoryTrimLevel) + + fun updateMemoryTrimLevel(newTrimLevel: Int?): Boolean { + if (memoryTrimLevel == newTrimLevel) { + return false + } + + memoryTrimLevel = newTrimLevel + return true + } + + fun emitObservableEvent() { + updateState { + StateEvent.UpdateMemoryTrimEvent( + isLowMemory, + memoryTrimLevel, + trimLevelDescription + ) + } + } + + private fun descriptionFor(memoryTrimLevel: Int?) = when (memoryTrimLevel) { + null -> "None" + ComponentCallbacks2.TRIM_MEMORY_COMPLETE -> "Complete" + ComponentCallbacks2.TRIM_MEMORY_MODERATE -> "Moderate" + ComponentCallbacks2.TRIM_MEMORY_BACKGROUND -> "Background" + ComponentCallbacks2.TRIM_MEMORY_UI_HIDDEN -> "UI hidden" + ComponentCallbacks2.TRIM_MEMORY_RUNNING_CRITICAL -> "Running critical" + ComponentCallbacks2.TRIM_MEMORY_RUNNING_LOW -> "Running low" + ComponentCallbacks2.TRIM_MEMORY_RUNNING_MODERATE -> "Running moderate" + else -> "Unknown ($memoryTrimLevel)" + } +} diff --git a/app/src/main/java/com/bugsnag/android/NativeInterface.java b/app/src/main/java/com/bugsnag/android/NativeInterface.java index cb845ae389..45802b55a3 100644 --- a/app/src/main/java/com/bugsnag/android/NativeInterface.java +++ b/app/src/main/java/com/bugsnag/android/NativeInterface.java @@ -429,4 +429,11 @@ public class NativeInterface { public static void setAutoDetectAnrs(boolean autoDetectAnrs) { getClient().setAutoDetectAnrs(autoDetectAnrs); } + + /** + * Marks the launch period as complete + */ + public static void markLaunchCompleted() { + getClient().markLaunchCompleted(); + } } diff --git a/app/src/main/java/com/bugsnag/android/Notifier.kt b/app/src/main/java/com/bugsnag/android/Notifier.kt index 946d6df62d..578ec134d9 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.11.0", + var version: String = "5.12.0", var url: String = "https://bugsnag.com" ) : JsonStream.Streamable { diff --git a/app/src/main/java/com/bugsnag/android/StateEvent.kt b/app/src/main/java/com/bugsnag/android/StateEvent.kt index a94a0301fb..de6bb3963b 100644 --- a/app/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/app/src/main/java/com/bugsnag/android/StateEvent.kt @@ -62,5 +62,9 @@ sealed class StateEvent { // JvmField allows direct field access optimizations class UpdateUser(@JvmField val user: User) : StateEvent() - class UpdateMemoryTrimEvent(@JvmField val isLowMemory: Boolean) : StateEvent() + class UpdateMemoryTrimEvent( + @JvmField val isLowMemory: Boolean, + @JvmField val memoryTrimLevel: Int? = null, + @JvmField val memoryTrimLevelDescription: String = "None" + ) : StateEvent() }