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