diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java new file mode 100644 index 0000000000..3d0804b030 --- /dev/null +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/MemoryTrimTest.java @@ -0,0 +1,62 @@ +package com.bugsnag.android; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.bugsnag.android.ObserverInterfaceTest.BugsnagTestObserver; + +import android.content.ComponentCallbacks; +import android.content.Context; +import androidx.test.core.app.ApplicationProvider; + +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Captor; +import org.mockito.Spy; +import org.mockito.junit.MockitoJUnitRunner; + +/** + * Heavily mocked test to ensure that onLowMemory events are distributed to Client Observers + */ +@RunWith(MockitoJUnitRunner.class) +public class MemoryTrimTest { + + @Spy + Context context = ApplicationProvider.getApplicationContext(); + + @Captor + ArgumentCaptor componentCallbacksCaptor; + + @Test + public void onLowMemoryEvent() { + when(context.getApplicationContext()).thenReturn(context); + Client client = new Client(context, BugsnagTestUtils.generateConfiguration()); + + // capture the registered ComponentCallbacks + verify(context, times(1)).registerComponentCallbacks(componentCallbacksCaptor.capture()); + + BugsnagTestObserver observer = new BugsnagTestObserver(); + client.registerObserver(observer); + + ComponentCallbacks callbacks = componentCallbacksCaptor.getValue(); + callbacks.onLowMemory(); + + assertEquals(1, observer.observed.size()); + Object observedEvent = observer.observed.get(0); + + assertTrue( + "observed event should be UpdateMemoryTrimEvent", + observedEvent instanceof StateEvent.UpdateMemoryTrimEvent + ); + + assertTrue( + "observed event should be marked isLowMemory", + ((StateEvent.UpdateMemoryTrimEvent) observedEvent).isLowMemory() + ); + } + +} diff --git a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java index 4b1315f565..1adc99f61f 100644 --- a/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java +++ b/bugsnag-android-core/src/androidTest/java/com/bugsnag/android/ObserverInterfaceTest.java @@ -207,7 +207,7 @@ private T findMessageInQueue(Class argClass) { } static class BugsnagTestObserver implements Observer { - private final ArrayList observed; + final ArrayList observed; BugsnagTestObserver() { observed = new ArrayList<>(4); diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java index 5cbf28debc..6ed0a6f7b6 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/Client.java @@ -19,6 +19,7 @@ import androidx.annotation.VisibleForTesting; import kotlin.Unit; +import kotlin.jvm.functions.Function1; import kotlin.jvm.functions.Function2; import java.util.ArrayList; @@ -224,6 +225,7 @@ public Unit invoke(String activity, Map metadata) { systemBroadcastReceiver = SystemBroadcastReceiver.register(this, logger, bgTaskService); registerOrientationChangeListener(); + registerMemoryTrimListener(); // load last run info lastRunInfoStore = new LastRunInfoStore(immutableConfig); @@ -357,6 +359,18 @@ public Unit invoke(String oldOrientation, String newOrientation) { ContextExtensionsKt.registerReceiverSafe(appContext, receiver, configFilter, logger); } + private void registerMemoryTrimListener() { + appContext.registerComponentCallbacks(new ClientComponentCallbacks( + new Function1() { + @Override + public Unit invoke(Boolean isLowMemory) { + clientObservable.postMemoryTrimEvent(isLowMemory); + return null; + } + } + )); + } + void setupNdkPlugin() { String lastRunInfoPath = lastRunInfoStore.getFile().getAbsolutePath(); int crashes = (lastRunInfo != null) ? lastRunInfo.getConsecutiveLaunchCrashes() : 0; diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt new file mode 100644 index 0000000000..74c2435598 --- /dev/null +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientComponentCallbacks.kt @@ -0,0 +1,14 @@ +package com.bugsnag.android + +import android.content.ComponentCallbacks +import android.content.res.Configuration + +internal class ClientComponentCallbacks( + val callback: (Boolean) -> Unit +) : ComponentCallbacks { + override fun onConfigurationChanged(newConfig: Configuration) {} + + override fun onLowMemory() { + callback(true) + } +} diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt index a654a54e24..88e97d745e 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/ClientObservable.kt @@ -6,6 +6,10 @@ internal class ClientObservable : BaseObservable() { notifyObservers(StateEvent.UpdateOrientation(orientation)) } + fun postMemoryTrimEvent(isLowMemory: Boolean) { + notifyObservers(StateEvent.UpdateMemoryTrimEvent(isLowMemory)) + } + fun postNdkInstall(conf: ImmutableConfig, lastRunInfoPath: String, consecutiveLaunchCrashes: Int) { notifyObservers( StateEvent.Install( diff --git a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt index 8e53a58bfb..bc3c83e0ea 100644 --- a/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt +++ b/bugsnag-android-core/src/main/java/com/bugsnag/android/StateEvent.kt @@ -42,4 +42,6 @@ sealed class StateEvent { class UpdateOrientation(val orientation: String?) : StateEvent() class UpdateUser(val user: User) : StateEvent() + + class UpdateMemoryTrimEvent(val isLowMemory: Boolean) : StateEvent() } diff --git a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt index 7b66d68eb9..1e908c5386 100644 --- a/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt +++ b/bugsnag-plugin-android-ndk/src/main/java/com/bugsnag/android/ndk/NativeBridge.kt @@ -80,6 +80,7 @@ class NativeBridge : Observer { external fun updateUserEmail(newValue: String) external fun updateUserName(newValue: String) external fun getUnwindStackFunction(): Long + external fun updateLowMemory(newValue: Boolean) /** * Creates a new native bridge for interacting with native components. @@ -134,6 +135,7 @@ class NativeBridge : Observer { updateUserName(makeSafe(msg.user.name ?: "")) updateUserEmail(makeSafe(msg.user.email ?: "")) } + is StateEvent.UpdateMemoryTrimEvent -> updateLowMemory(msg.isLowMemory) } }