diff options
11 files changed, 560 insertions, 19 deletions
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt index 4c24ce2ad8c5..5c09777ddde5 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt @@ -62,12 +62,13 @@ import com.android.systemui.complication.dagger.ComplicationComponent import com.android.systemui.dreams.complication.HideComplicationTouchHandler import com.android.systemui.dreams.dagger.DreamOverlayComponent import com.android.systemui.keyguard.domain.interactor.keyguardInteractor +import com.android.systemui.keyguard.gesture.domain.gestureInteractor import com.android.systemui.kosmos.testScope +import com.android.systemui.navigationbar.gestural.domain.GestureInteractor import com.android.systemui.testKosmos import com.android.systemui.touch.TouchInsetManager import com.android.systemui.util.concurrency.FakeExecutor import com.android.systemui.util.mockito.any -import com.android.systemui.util.mockito.eq import com.android.systemui.util.mockito.whenever import com.android.systemui.util.time.FakeSystemClock import com.google.common.truth.Truth.assertThat @@ -84,9 +85,11 @@ import org.mockito.Mock import org.mockito.Mockito import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.isNull -import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.MockitoAnnotations +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.eq +import org.mockito.kotlin.spy @OptIn(ExperimentalCoroutinesApi::class) @SmallTest @@ -166,6 +169,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { private lateinit var bouncerRepository: FakeKeyguardBouncerRepository private lateinit var communalRepository: FakeCommunalSceneRepository private var viewCaptureSpy = spy(ViewCaptureFactory.getInstance(context)) + private lateinit var gestureInteractor: GestureInteractor @Captor var mViewCaptor: ArgumentCaptor<View>? = null private lateinit var mService: DreamOverlayService @@ -177,6 +181,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { lifecycleRegistry = FakeLifecycleRegistry(mLifecycleOwner) bouncerRepository = kosmos.fakeKeyguardBouncerRepository communalRepository = kosmos.fakeCommunalSceneRepository + gestureInteractor = spy(kosmos.gestureInteractor) whenever(mDreamOverlayComponent.getDreamOverlayContainerViewController()) .thenReturn(mDreamOverlayContainerViewController) @@ -231,6 +236,7 @@ class DreamOverlayServiceTest : SysuiTestCase() { HOME_CONTROL_PANEL_DREAM_COMPONENT, mDreamOverlayCallbackController, kosmos.keyguardInteractor, + gestureInteractor, WINDOW_NAME ) } @@ -955,6 +961,47 @@ class DreamOverlayServiceTest : SysuiTestCase() { assertThat(lifecycleRegistry.currentState).isEqualTo(Lifecycle.State.RESUMED) } + @Test + fun testDreamActivityGesturesBlockedOnStart() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + val captor = argumentCaptor<ComponentName>() + verify(gestureInteractor) + .addGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global)) + assertThat(captor.firstValue.packageName) + .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName) + } + + @Test + fun testDreamActivityGesturesUnblockedOnEnd() { + val client = client + + // Inform the overlay service of dream starting. + client.startDream( + mWindowParams, + mDreamOverlayCallback, + DREAM_COMPONENT, + false /*shouldShowComplication*/ + ) + mMainExecutor.runAllReady() + + client.endDream() + mMainExecutor.runAllReady() + val captor = argumentCaptor<ComponentName>() + verify(gestureInteractor) + .removeGestureBlockedActivity(captor.capture(), eq(GestureInteractor.Scope.Global)) + assertThat(captor.firstValue.packageName) + .isEqualTo(ComponentName.unflattenFromString(DREAM_COMPONENT)?.packageName) + } + internal class FakeLifecycleRegistry(provider: LifecycleOwner) : LifecycleRegistry(provider) { val mLifecycles: MutableList<State> = ArrayList() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt new file mode 100644 index 000000000000..91d37cf1816a --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/data/GestureRepositoryTest.kt @@ -0,0 +1,53 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.gesture.data + +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.GestureRepositoryImpl +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock + +@RunWith(AndroidJUnit4::class) +@SmallTest +class GestureRepositoryTest : SysuiTestCase() { + private val testDispatcher = StandardTestDispatcher() + private val testScope = TestScope(testDispatcher) + private val underTest by lazy { GestureRepositoryImpl(testDispatcher) } + + @Test + fun addRemoveComponentToBlock_updatesBlockedComponentSet() = + testScope.runTest { + val component = mock<ComponentName>() + + underTest.addGestureBlockedActivity(component) + val addedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities) + assertThat(addedBlockedComponents).contains(component) + + underTest.removeGestureBlockedActivity(component) + val removedBlockedComponents by collectLastValue(underTest.gestureBlockedActivities) + assertThat(removedBlockedComponents).isEmpty() + } +} 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 new file mode 100644 index 000000000000..bc142e62a0aa --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/gesture/domain/GestureInteractorTest.kt @@ -0,0 +1,135 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.gesture.domain + +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.navigationbar.gestural.domain.GestureInteractor +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.whenever + +@RunWith(AndroidJUnit4::class) +@SmallTest +class GestureInteractorTest : SysuiTestCase() { + @Rule @JvmField val mockitoRule: MockitoRule = MockitoJUnit.rule() + + val dispatcher = StandardTestDispatcher() + val testScope = TestScope(dispatcher) + + @Mock private lateinit var gestureRepository: GestureRepository + + private val underTest by lazy { + GestureInteractor(gestureRepository, testScope.backgroundScope) + } + + @Before + fun setup() { + Dispatchers.setMain(dispatcher) + whenever(gestureRepository.gestureBlockedActivities).thenReturn(MutableStateFlow(setOf())) + } + + @After + fun tearDown() { + Dispatchers.resetMain() + } + + @Test + fun addBlockedActivity_testCombination() = + testScope.runTest { + val globalComponent = mock<ComponentName>() + whenever(gestureRepository.gestureBlockedActivities) + .thenReturn(MutableStateFlow(setOf(globalComponent))) + val localComponent = mock<ComponentName>() + 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) + } + + @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) + } + + @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() + } + + @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) + } + + @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) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java index 1771f4dc8572..15ddf5bfd281 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/SystemUIModule.java @@ -80,6 +80,7 @@ import com.android.systemui.model.SceneContainerPlugin; import com.android.systemui.model.SysUiState; import com.android.systemui.motiontool.MotionToolModule; import com.android.systemui.navigationbar.NavigationBarComponent; +import com.android.systemui.navigationbar.gestural.dagger.GestureModule; import com.android.systemui.notetask.NoteTaskModule; import com.android.systemui.people.PeopleModule; import com.android.systemui.plugins.BcSmartspaceConfigPlugin; @@ -215,6 +216,7 @@ import javax.inject.Named; FlagsModule.class, FlagDependenciesModule.class, FooterActionsModule.class, + GestureModule.class, InputMethodModule.class, KeyEventRepositoryModule.class, KeyboardModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java index 982398579477..931066d5c582 100644 --- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java +++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java @@ -27,7 +27,9 @@ import static com.android.systemui.util.kotlin.JavaAdapterKt.collectFlow; import android.content.ComponentName; import android.content.Context; import android.content.Intent; +import android.content.pm.ActivityInfo; import android.graphics.drawable.ColorDrawable; +import android.service.dreams.DreamActivity; import android.util.Log; import android.view.View; import android.view.ViewGroup; @@ -63,6 +65,7 @@ import com.android.systemui.complication.dagger.ComplicationComponent; import com.android.systemui.dagger.qualifiers.Main; import com.android.systemui.dreams.dagger.DreamOverlayComponent; import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor; +import com.android.systemui.navigationbar.gestural.domain.GestureInteractor; import com.android.systemui.shade.ShadeExpansionChangeEvent; import com.android.systemui.touch.TouchInsetManager; import com.android.systemui.util.concurrency.DelayableExecutor; @@ -135,6 +138,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final DreamOverlayComponent mDreamOverlayComponent; + private ComponentName mCurrentBlockedGestureDreamActivityComponent; + /** * This {@link LifecycleRegistry} controls when dream overlay functionality, like touch * handling, should be active. It will automatically be paused when the dream overlay is hidden @@ -222,6 +227,8 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ private final DreamOverlayStateController mStateController; + private final GestureInteractor mGestureInteractor; + @VisibleForTesting public enum DreamOverlayEvent implements UiEventLogger.UiEventEnum { @UiEvent(doc = "The dream overlay has entered start.") @@ -265,6 +272,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ ComponentName homeControlPanelDreamComponent, DreamOverlayCallbackController dreamOverlayCallbackController, KeyguardInteractor keyguardInteractor, + GestureInteractor gestureInteractor, @Named(DREAM_OVERLAY_WINDOW_TITLE) String windowTitle) { super(executor); mContext = context; @@ -281,6 +289,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mWindowTitle = windowTitle; mCommunalInteractor = communalInteractor; mSystemDialogsCloser = systemDialogsCloser; + mGestureInteractor = gestureInteractor; final ViewModelStore viewModelStore = new ViewModelStore(); final Complication.Host host = @@ -391,6 +400,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ mStarted = true; updateRedirectWakeup(); + updateBlockedGestureDreamActivityComponent(); } private void updateRedirectWakeup() { @@ -401,6 +411,18 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ redirectWake(mCommunalAvailable && !glanceableHubAllowKeyguardWhenDreaming()); } + private void updateBlockedGestureDreamActivityComponent() { + // TODO(b/343815446): We should not be crafting this ActivityInfo ourselves. It should be + // in a common place, Such as DreamActivity itself. + final ActivityInfo info = new ActivityInfo(); + info.name = DreamActivity.class.getName(); + info.packageName = getDreamComponent().getPackageName(); + mCurrentBlockedGestureDreamActivityComponent = info.getComponentName(); + + mGestureInteractor.addGestureBlockedActivity(mCurrentBlockedGestureDreamActivityComponent, + GestureInteractor.Scope.Global); + } + @Override public void onEndDream() { resetCurrentDreamOverlayLocked(); @@ -472,6 +494,7 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ * into the dream window. */ private boolean addOverlayWindowLocked(WindowManager.LayoutParams layoutParams) { + mWindow = new PhoneWindow(mContext); // Default to SystemUI name for TalkBack. mWindow.setTitle(mWindowTitle); @@ -554,6 +577,14 @@ public class DreamOverlayService extends android.service.dreams.DreamOverlayServ } mWindow = null; + + // Always unregister the any set DreamActivity from being blocked from gestures. + if (mCurrentBlockedGestureDreamActivityComponent != null) { + mGestureInteractor.removeGestureBlockedActivity( + mCurrentBlockedGestureDreamActivityComponent, GestureInteractor.Scope.Global); + mCurrentBlockedGestureDreamActivityComponent = null; + } + mStarted = false; } } 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 947336d41590..9eca34f88a78 100644 --- a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/EdgeBackGestureHandler.java @@ -76,6 +76,7 @@ import com.android.internal.policy.GestureNavigationSettingsObserver; 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; import com.android.systemui.plugins.FalsingManager; import com.android.systemui.plugins.NavigationEdgeBackPlugin; import com.android.systemui.plugins.PluginListener; @@ -95,15 +96,14 @@ import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.phone.LightBarController; import com.android.systemui.util.concurrency.BackPanelUiThread; import com.android.systemui.util.concurrency.UiThreadContext; +import com.android.systemui.util.kotlin.JavaAdapter; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.pip.Pip; import java.io.PrintWriter; import java.util.ArrayDeque; -import java.util.ArrayList; import java.util.Date; -import java.util.List; import java.util.Locale; import java.util.Map; import java.util.Optional; @@ -157,12 +157,7 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private TaskStackChangeListener mTaskStackListener = new TaskStackChangeListener() { @Override public void onTaskStackChanged() { - if (edgebackGestureHandlerGetRunningTasksBackground()) { - mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set( - isGestureBlockingActivityRunning())); - } else { - mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning()); - } + updateRunningActivityGesturesBlocked(); } @Override public void onTaskCreated(int taskId, ComponentName componentName) { @@ -209,8 +204,6 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Optional<DesktopMode> mDesktopModeOptional; private final FalsingManager mFalsingManager; private final Configuration mLastReportedConfig = new Configuration(); - // Activities which should not trigger Back gesture. - private final List<ComponentName> mGestureBlockingActivities = new ArrayList<>(); private final Point mDisplaySize = new Point(); private final int mDisplayId; @@ -227,6 +220,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mBackGestureTfClassifierProviderProvider; private final Provider<LightBarController> mLightBarControllerProvider; + private final GestureInteractor mGestureInteractor; + + private final JavaAdapter mJavaAdapter; + // The left side edge width where touch down is allowed private int mEdgeWidthLeft; // The right side edge width where touch down is allowed @@ -426,7 +423,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack FalsingManager falsingManager, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, Provider<LightBarController> lightBarControllerProvider, - NotificationShadeWindowController notificationShadeWindowController) { + NotificationShadeWindowController notificationShadeWindowController, + GestureInteractor gestureInteractor, + JavaAdapter javaAdapter) { mContext = context; mDisplayId = context.getDisplayId(); mUiThreadContext = uiThreadContext; @@ -446,7 +445,13 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mFalsingManager = falsingManager; mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mLightBarControllerProvider = lightBarControllerProvider; + mGestureInteractor = gestureInteractor; + 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) { @@ -466,8 +471,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } else { String[] gestureBlockingActivities = resources.getStringArray(resId); for (String gestureBlockingActivity : gestureBlockingActivities) { - mGestureBlockingActivities.add( - ComponentName.unflattenFromString(gestureBlockingActivity)); + mGestureInteractor.addGestureBlockedActivity( + ComponentName.unflattenFromString(gestureBlockingActivity), + GestureInteractor.Scope.Local); } } } catch (NameNotFoundException e) { @@ -561,6 +567,15 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } } + private void updateRunningActivityGesturesBlocked() { + if (edgebackGestureHandlerGetRunningTasksBackground()) { + mBackgroundExecutor.execute(() -> mGestureBlockingActivityRunning.set( + isGestureBlockingActivityRunning())); + } else { + mGestureBlockingActivityRunning.set(isGestureBlockingActivityRunning()); + } + } + /** * Called when the nav/task bar is attached. */ @@ -1293,7 +1308,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack } else { mPackageName = "_UNKNOWN"; } - return topActivity != null && mGestureBlockingActivities.contains(topActivity); + + return topActivity != null && mGestureInteractor.areGesturesBlocked(topActivity); } public void setBackAnimation(BackAnimation backAnimation) { @@ -1342,6 +1358,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack private final Provider<LightBarController> mLightBarControllerProvider; private final NotificationShadeWindowController mNotificationShadeWindowController; + private final GestureInteractor mGestureInteractor; + + private final JavaAdapter mJavaAdapter; + @Inject public Factory(OverviewProxyService overviewProxyService, SysUiState sysUiState, @@ -1361,8 +1381,10 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack FalsingManager falsingManager, Provider<BackGestureTfClassifierProvider> backGestureTfClassifierProviderProvider, - Provider<LightBarController> lightBarControllerProvider, - NotificationShadeWindowController notificationShadeWindowController) { + Provider<LightBarController> lightBarControllerProvider, + NotificationShadeWindowController notificationShadeWindowController, + GestureInteractor gestureInteractor, + JavaAdapter javaAdapter) { mOverviewProxyService = overviewProxyService; mSysUiState = sysUiState; mPluginManager = pluginManager; @@ -1382,6 +1404,8 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mBackGestureTfClassifierProviderProvider = backGestureTfClassifierProviderProvider; mLightBarControllerProvider = lightBarControllerProvider; mNotificationShadeWindowController = notificationShadeWindowController; + mGestureInteractor = gestureInteractor; + mJavaAdapter = javaAdapter; } /** Construct a {@link EdgeBackGestureHandler}. */ @@ -1407,7 +1431,9 @@ public class EdgeBackGestureHandler implements PluginListener<NavigationEdgeBack mFalsingManager, mBackGestureTfClassifierProviderProvider, mLightBarControllerProvider, - mNotificationShadeWindowController)); + mNotificationShadeWindowController, + mGestureInteractor, + mJavaAdapter)); } } diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt new file mode 100644 index 000000000000..72a84f54ab9f --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/dagger/GestureModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.navigationbar.gestural.dagger + +import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository +import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl +import dagger.Binds +import dagger.Module + +/** {@link Module} for gesture related dependencies */ +@Module +interface GestureModule { + /** */ + @Binds fun gestureRespoitory(impl: GestureRepositoryImpl): GestureRepository +} diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt new file mode 100644 index 000000000000..8f35343626e8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/data/respository/GestureRepository.kt @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.navigationbar.gestural.data.respository + +import android.content.ComponentName +import android.util.ArraySet +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Main +import javax.inject.Inject +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.withContext + +/** A repository for storing gesture related information */ +interface GestureRepository { + /** A {@link StateFlow} tracking activities currently blocked from gestures. */ + val gestureBlockedActivities: StateFlow<Set<ComponentName>> + + /** Adds an activity to be blocked from gestures. */ + suspend fun addGestureBlockedActivity(activity: ComponentName) + + /** Removes an activity from being blocked from gestures. */ + suspend fun removeGestureBlockedActivity(activity: ComponentName) +} + +@SysUISingleton +class GestureRepositoryImpl +@Inject +constructor(@Main private val mainDispatcher: CoroutineDispatcher) : GestureRepository { + private val _gestureBlockedActivities = MutableStateFlow<Set<ComponentName>>(ArraySet()) + + override val gestureBlockedActivities: StateFlow<Set<ComponentName>> + get() = _gestureBlockedActivities + + override suspend fun addGestureBlockedActivity(activity: ComponentName) = + withContext(mainDispatcher) { + _gestureBlockedActivities.emit( + _gestureBlockedActivities.value.toMutableSet().apply { add(activity) } + ) + } + + override suspend fun removeGestureBlockedActivity(activity: ComponentName) = + withContext(mainDispatcher) { + _gestureBlockedActivities.emit( + _gestureBlockedActivities.value.toMutableSet().apply { remove(activity) } + ) + } +} 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 new file mode 100644 index 000000000000..6dc5939b7c1a --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/navigationbar/gestural/domain/GestureInteractor.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.navigationbar.gestural.domain + +import android.content.ComponentName +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +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.launch + +/** + * {@link GestureInteractor} helps interact with gesture-related logic, including accessing the + * underlying {@link GestureRepository}. + */ +class GestureInteractor +@Inject +constructor( + private val gestureRepository: GestureRepository, + @Application private val scope: CoroutineScope +) { + enum class Scope { + Local, + Global + } + + 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()) + + /** + * Adds an {@link Activity} to be blocked based on component when the topmost, focused {@link + * Activity}. + */ + fun addGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) { + scope.launch { + when (gestureScope) { + Scope.Local -> { + _localGestureBlockedActivities.emit( + _localGestureBlockedActivities.value.toMutableSet().apply { add(activity) } + ) + } + Scope.Global -> { + gestureRepository.addGestureBlockedActivity(activity) + } + } + } + } + + /** Removes an {@link Activity} from being blocked from gestures. */ + fun removeGestureBlockedActivity(activity: ComponentName, gestureScope: Scope) { + scope.launch { + when (gestureScope) { + Scope.Local -> { + _localGestureBlockedActivities.emit( + _localGestureBlockedActivities.value.toMutableSet().apply { + remove(activity) + } + ) + } + Scope.Global -> { + gestureRepository.removeGestureBlockedActivity(activity) + } + } + } + } + + /** + * 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/data/GestureRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt new file mode 100644 index 000000000000..9bd346effdc3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/data/GestureRepositoryKosmos.kt @@ -0,0 +1,25 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.keyguard.gesture.data + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testDispatcher +import com.android.systemui.navigationbar.gestural.data.respository.GestureRepository +import com.android.systemui.navigationbar.gestural.data.respository.GestureRepositoryImpl + +val Kosmos.gestureRepository: GestureRepository by + Kosmos.Fixture { GestureRepositoryImpl(testDispatcher) } 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 new file mode 100644 index 000000000000..658aaa6c1a62 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/gesture/domain/GestureInteractorKosmos.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +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.navigationbar.gestural.domain.GestureInteractor + +val Kosmos.gestureInteractor: GestureInteractor by + Kosmos.Fixture { + GestureInteractor(gestureRepository = gestureRepository, scope = applicationCoroutineScope) + } |