Merge "Address memory leak in EdgeBackGestureHandler." into main
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
index bc142e6..6395448 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt
@@ -16,120 +16,108 @@
package com.android.systemui.gesture.domain
+import android.app.ActivityManager
import android.content.ComponentName
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.Dispatchers
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.StandardTestDispatcher
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.resetMain
-import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import kotlinx.coroutines.test.setMain
-import org.junit.After
-import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.Mock
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
-import org.mockito.kotlin.any
-import org.mockito.kotlin.argumentCaptor
import org.mockito.kotlin.mock
-import org.mockito.kotlin.never
-import org.mockito.kotlin.verify
+import org.mockito.kotlin.spy
import org.mockito.kotlin.whenever
@RunWith(AndroidJUnit4::class)
@SmallTest
class GestureInteractorTest : SysuiTestCase() {
@Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
- val dispatcher = StandardTestDispatcher()
+ val dispatcher = kosmos.testDispatcher
+ val repository = spy(kosmos.gestureRepository)
val testScope = TestScope(dispatcher)
- @Mock private lateinit var gestureRepository: GestureRepository
+ private val underTest by lazy { createInteractor() }
- private val underTest by lazy {
- GestureInteractor(gestureRepository, testScope.backgroundScope)
+ private fun createInteractor(): GestureInteractor {
+ return GestureInteractor(
+ repository,
+ dispatcher,
+ kosmos.backgroundCoroutineContext,
+ testScope,
+ kosmos.activityManagerWrapper,
+ kosmos.taskStackChangeListeners
+ )
}
- @Before
- fun setup() {
- Dispatchers.setMain(dispatcher)
- whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf()))
- }
+ private fun setTopActivity(componentName: ComponentName) {
+ val task = mock<ActivityManager.RunningTaskInfo>()
+ task.topActivity = componentName
+ whenever(kosmos.activityManagerWrapper.runningTask).thenReturn(task)
- @After
- fun tearDown() {
- Dispatchers.resetMain()
+ kosmos.taskStackChangeListeners.listenerImpl.onTaskStackChanged()
}
@Test
fun addBlockedActivity_testCombination() =
testScope.runTest {
val globalComponent = mock<ComponentName>()
- whenever(gestureRepository.gestureBlockedActivities)
- .thenReturn(MutableStateFlow(setOf(globalComponent)))
+ repository.addGestureBlockedActivity(globalComponent)
+
val localComponent = mock<ComponentName>()
+
+ val blocked by collectLastValue(underTest.topActivityBlocked)
+
underTest.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- verify(gestureRepository, never()).addGestureBlockedActivity(any())
- assertThat(lastSeen).hasSize(2)
- assertThat(lastSeen).containsExactly(globalComponent, localComponent)
+
+ assertThat(blocked).isFalse()
+
+ setTopActivity(localComponent)
+
+ assertThat(blocked).isTrue()
+ }
+
+ @Test
+ fun initialization_testEmit() =
+ testScope.runTest {
+ val globalComponent = mock<ComponentName>()
+ repository.addGestureBlockedActivity(globalComponent)
+ setTopActivity(globalComponent)
+
+ val interactor = createInteractor()
+
+ val blocked by collectLastValue(interactor.topActivityBlocked)
+ assertThat(blocked).isTrue()
}
@Test
fun addBlockedActivityLocally_onlyAffectsLocalInteractor() =
testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- verify(gestureRepository, never()).addGestureBlockedActivity(any())
- assertThat(lastSeen).contains(component)
- }
+ val interactor1 = createInteractor()
+ val interactor1Blocked by collectLastValue(interactor1.topActivityBlocked)
+ val interactor2 = createInteractor()
+ val interactor2Blocked by collectLastValue(interactor2.topActivityBlocked)
- @Test
- fun removeBlockedActivityLocally_onlyAffectsLocalInteractor() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- val lastSeen by collectLastValue(underTest.gestureBlockedActivities)
- testScope.runCurrent()
- underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Local)
- testScope.runCurrent()
- verify(gestureRepository, never()).removeGestureBlockedActivity(any())
- assertThat(lastSeen).isEmpty()
- }
+ val localComponent = mock<ComponentName>()
- @Test
- fun addBlockedActivity_invokesRepository() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.addGestureBlockedActivity(component, GestureInteractor.Scope.Global)
- runCurrent()
- val captor = argumentCaptor<ComponentName>()
- verify(gestureRepository).addGestureBlockedActivity(captor.capture())
- assertThat(captor.firstValue).isEqualTo(component)
- }
+ interactor1.addGestureBlockedActivity(localComponent, GestureInteractor.Scope.Local)
+ setTopActivity(localComponent)
- @Test
- fun removeBlockedActivity_invokesRepository() =
- testScope.runTest {
- val component = mock<ComponentName>()
- underTest.removeGestureBlockedActivity(component, GestureInteractor.Scope.Global)
- runCurrent()
- val captor = argumentCaptor<ComponentName>()
- verify(gestureRepository).removeGestureBlockedActivity(captor.capture())
- assertThat(captor.firstValue).isEqualTo(component)
+ assertThat(interactor1Blocked).isTrue()
+ assertThat(interactor2Blocked).isFalse()
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
index 388272f..0f82e02 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java
@@ -73,8 +73,8 @@
import com.android.internal.config.sysui.SystemUiDeviceConfigFlags;
import com.android.internal.policy.GestureNavigationSettingsObserver;
-import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.contextualeducation.GestureType;
+import com.android.systemui.dagger.qualifiers.Background;
import com.android.systemui.model.SysUiState;
import com.android.systemui.navigationbar.NavigationModeController;
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor;
@@ -102,6 +102,8 @@
import com.android.wm.shell.desktopmode.DesktopMode;
import com.android.wm.shell.pip.Pip;
+import kotlinx.coroutines.Job;
+
import java.io.PrintWriter;
import java.util.ArrayDeque;
import java.util.Date;
@@ -109,6 +111,7 @@
import java.util.Map;
import java.util.Optional;
import java.util.Set;
+import java.util.concurrent.CancellationException;
import java.util.concurrent.Executor;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.function.Consumer;
@@ -158,7 +161,7 @@
private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() {
@Override
public void onTaskStackChanged() {
- updateRunningActivityGesturesBlocked();
+ updateTopActivity();
}
@Override
public void onTaskCreated(int taskId, ComponentName componentName) {
@@ -222,6 +225,8 @@
private final Provider<LightBarController> mLightBarControllerProvider;
private final GestureInteractor mGestureInteractor;
+ private final ArraySet<ComponentName> mBlockedActivities = new ArraySet<>();
+ private Job mBlockedActivitiesJob = null;
private final JavaAdapter mJavaAdapter;
@@ -450,9 +455,6 @@
mJavaAdapter = javaAdapter;
mLastReportedConfig.setTo(mContext.getResources().getConfiguration());
- mJavaAdapter.alwaysCollectFlow(mGestureInteractor.getGestureBlockedActivities(),
- componentNames -> updateRunningActivityGesturesBlocked());
-
ComponentName recentsComponentName = ComponentName.unflattenFromString(
context.getString(com.android.internal.R.string.config_recentsComponentName));
if (recentsComponentName != null) {
@@ -568,12 +570,11 @@
}
}
- private void updateRunningActivityGesturesBlocked() {
+ private void updateTopActivity() {
if (edgebackGestureHandlerGetRunningTasksBackground()) {
- mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set(
- isGestureBlockingActivityRunning()));
+ mBackgroundExecutor.execute(() -> updateTopActivityPackageName());
} else {
- mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning());
+ updateTopActivityPackageName();
}
}
@@ -678,6 +679,11 @@
Log.e(TAG, "Failed to unregister window manager callbacks", e);
}
+ if (mBlockedActivitiesJob != null) {
+ mBlockedActivitiesJob.cancel(new CancellationException());
+ mBlockedActivitiesJob = null;
+ }
+ mBlockedActivities.clear();
} else {
mBackgroundExecutor.execute(mGestureNavigationSettingsObserver::register);
updateDisplaySize();
@@ -710,6 +716,12 @@
resetEdgeBackPlugin();
mPluginManager.addPluginListener(
this, NavigationEdgeBackPlugin.class, /*allowMultiple=*/ false);
+
+ // Begin listening to changes in blocked activities list
+ mBlockedActivitiesJob = mJavaAdapter.alwaysCollectFlow(
+ mGestureInteractor.getTopActivityBlocked(),
+ blocked -> mGestureBlockingActivityRunning.set(blocked));
+
}
// Update the ML model resources.
updateMLModelState();
@@ -1302,7 +1314,7 @@
}
}
- private boolean isGestureBlockingActivityRunning() {
+ private void updateTopActivityPackageName() {
ActivityManager.RunningTaskInfo runningTask =
ActivityManagerWrapper.getInstance().getRunningTask();
ComponentName topActivity = runningTask == null ? null : runningTask.topActivity;
@@ -1311,8 +1323,6 @@
} else {
mPackageName = "_UNKNOWN";
}
-
- return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity);
}
public void setBackAnimation(BackAnimation backAnimation) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
index 6dc5939..6182878 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt
@@ -17,17 +17,29 @@
package com.android.systemui.navigationbar.gestural.domain
import android.content.ComponentName
+import com.android.app.tracing.coroutines.flow.flowOn
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository
+import com.android.systemui.shared.system.ActivityManagerWrapper
+import com.android.systemui.shared.system.TaskStackChangeListener
+import com.android.systemui.shared.system.TaskStackChangeListeners
+import com.android.systemui.util.kotlin.combine
+import com.android.systemui.util.kotlin.emitOnStart
+import com.android.systemui.utils.coroutines.flow.conflatedCallbackFlow
import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.mapLatest
import kotlinx.coroutines.launch
+import kotlinx.coroutines.withContext
/**
* {@link GestureInteractor} helps interact with gesture-related logic, including accessing the
@@ -37,7 +49,11 @@
@Inject
constructor(
private val gestureRepository: GestureRepository,
- @Application private val scope: CoroutineScope
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Application private val scope: CoroutineScope,
+ private val activityManagerWrapper: ActivityManagerWrapper,
+ private val taskStackChangeListeners: TaskStackChangeListeners,
) {
enum class Scope {
Local,
@@ -45,16 +61,38 @@
}
private val _localGestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(setOf())
- /** A {@link StateFlow} for listening to changes in Activities where gestures are blocked */
- val gestureBlockedActivities: StateFlow<Set<ComponentName>>
- get() =
- combine(
- gestureRepository.gestureBlockedActivities,
- _localGestureBlockedActivities.asStateFlow()
- ) { global, local ->
- global + local
- }
- .stateIn(scope, SharingStarted.WhileSubscribed(), setOf())
+
+ private val _topActivity =
+ conflatedCallbackFlow {
+ val taskListener =
+ object : TaskStackChangeListener {
+ override fun onTaskStackChanged() {
+ trySend(Unit)
+ }
+ }
+
+ taskStackChangeListeners.registerTaskStackListener(taskListener)
+ awaitClose { taskStackChangeListeners.unregisterTaskStackListener(taskListener) }
+ }
+ .flowOn(mainDispatcher)
+ .emitOnStart()
+ .mapLatest { getTopActivity() }
+ .distinctUntilChanged()
+
+ private suspend fun getTopActivity(): ComponentName? =
+ withContext(backgroundCoroutineContext) {
+ val runningTask = activityManagerWrapper.runningTask
+ runningTask?.topActivity
+ }
+
+ val topActivityBlocked =
+ combine(
+ _topActivity,
+ gestureRepository.gestureBlockedActivities,
+ _localGestureBlockedActivities.asStateFlow()
+ ) { activity, global, local ->
+ activity != null && (global + local).contains(activity)
+ }
/**
* Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link
@@ -92,12 +130,4 @@
}
}
}
-
- /**
- * Checks whether the specified {@link Activity} {@link ComponentName} is being blocked from
- * gestures.
- */
- fun areGesturesBlocked(activity: ComponentName): Boolean {
- return gestureBlockedActivities.value.contains(activity)
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
index 658aaa6..1d2439c 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt
@@ -16,12 +16,23 @@
package com.android.systemui.keyguard.gesture.domain
-import com.android.systemui.keyguard.gesture.data.gestureRepository
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.backgroundCoroutineContext
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.navigationbar.gestural.data.gestureRepository
import com.android.systemui.navigationbar.gestural.domain.GestureInteractor
+import com.android.systemui.shared.system.activityManagerWrapper
+import com.android.systemui.shared.system.taskStackChangeListeners
val Kosmos.gestureInteractor: GestureInteractor by
Kosmos.Fixture {
- GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope)
+ GestureInteractor(
+ gestureRepository = gestureRepository,
+ mainDispatcher = testDispatcher,
+ backgroundCoroutineContext = backgroundCoroutineContext,
+ scope = applicationCoroutineScope,
+ activityManagerWrapper = activityManagerWrapper,
+ taskStackChangeListeners = taskStackChangeListeners
+ )
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
similarity index 94%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
index 9bd346e..55ce43a 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/navigationbar/gestural/data/GestureRepositoryKosmos.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.keyguard.gesture.data
+package com.android.systemui.navigationbar.gestural.data
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher