diff --git a/CHANGELOG.md b/CHANGELOG.md
index c0c4ebf583..18b283d84b 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -7,6 +7,9 @@
* Prevent rare app crash while migrating old `SharedPreferences` data from older versions of `bugsnag-android`
[#1860](https://github.com/bugsnag/bugsnag-android/pull/1860)
+* Prevent free memory calculation from potentially crashing the app when `ActivityManager` cannot be reached.
+ [#1861](https://github.com/bugsnag/bugsnag-android/pull/1861)
+
## 5.30.0 (2023-05-11)
### Enhancements
diff --git a/bugsnag-android-core/detekt-baseline.xml b/bugsnag-android-core/detekt-baseline.xml
index 0342851daf..fb7824ddb0 100644
--- a/bugsnag-android-core/detekt-baseline.xml
+++ b/bugsnag-android-core/detekt-baseline.xml
@@ -45,6 +45,8 @@
SwallowedException:BugsnagEventMapper.kt$BugsnagEventMapper$catch (pe: IllegalArgumentException) { ndkDateFormatHolder.get()!!.parse(this) ?: throw IllegalArgumentException("cannot parse date $this") }
SwallowedException:ConnectivityCompat.kt$ConnectivityLegacy$catch (e: NullPointerException) { // in some rare cases we get a remote NullPointerException via Parcel.readException null }
SwallowedException:ContextExtensions.kt$catch (exc: RuntimeException) { null }
+ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (e: Throwable) { null }
+ SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (e: Throwable) { return null }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exc: Exception) { false }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get battery status") }
SwallowedException:DeviceDataCollector.kt$DeviceDataCollector$catch (exception: Exception) { logger.w("Could not get locationStatus") }
diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
index a7755a2edc..493763a493 100644
--- a/bugsnag-android-core/src/main/java/com/bugsnag/android/DeviceDataCollector.kt
+++ b/bugsnag-android-core/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/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt b/bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
similarity index 64%
rename from bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt
rename to bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
index 443bf353c6..307151bc20 100644
--- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/DataCollectorTest.kt
+++ b/bugsnag-android-core/src/test/java/com/bugsnag/android/DataCollectorTest.kt
@@ -4,24 +4,34 @@ import android.content.Context
import android.content.res.Configuration
import android.content.res.Resources
import com.bugsnag.android.internal.BackgroundTaskService
-import org.junit.Ignore
+import org.junit.Assert
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.`when`
import org.mockito.junit.MockitoJUnitRunner
import java.io.File
import kotlin.concurrent.thread
-@RunWith(MockitoJUnitRunner::class)
+@RunWith(MockitoJUnitRunner.Silent::class)
class DataCollectorTest {
- @Ignore("Disabled until we're able to mock final classes or auto-open classes")
- @Test
- fun testConcurretAccess() {
+ private lateinit var collector: DeviceDataCollector
+
+ @Mock
+ lateinit var context: Context
+
+ @Mock
+ lateinit var logger: Logger
+
+ @Before
+ fun setUp() {
val res = Mockito.mock(Resources::class.java)
- Mockito.`when`(res.configuration).thenReturn(Configuration())
+ `when`(res.configuration).thenReturn(Configuration())
- val collector = DeviceDataCollector(
+ collector = DeviceDataCollector(
Mockito.mock(Connectivity::class.java),
Mockito.mock(Context::class.java),
res,
@@ -33,7 +43,17 @@ class DataCollectorTest {
Mockito.mock(BackgroundTaskService::class.java),
Mockito.mock(Logger::class.java)
)
+ }
+ @Test
+ fun testCalculateFreeMemoryWithException() {
+ `when`(collector.calculateFreeMemory()).thenThrow(RuntimeException())
+ collector.generateDeviceWithState(0)
+ Assert.assertNull(collector.calculateFreeMemory())
+ }
+
+ @Test
+ fun testConcurrentAccess() {
repeat(10) { index ->
collector.addRuntimeVersionInfo("key" + index, "value" + index)
}