Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f45a5038 authored by Bryce Lee's avatar Bryce Lee
Browse files

Disable edge back gesture handling for dreams when overlay is active.

This changelist adds new functionality for disabling gesture handling on
specific activity components at any given time. DreamOverlayService
uses this new functionality to disable gestures on DreamActivities when
they start.

Fixes: 333734282
Test: atest DreamOverlayServiceTest#testDreamActivityGesturesBlockedOnStart
Test: atest DreamOverlayServiceTest#testDreamActivityGesturesUnblockedOnEnd
Test: atest GestureInteractorTest
Test: atest GestureRepositoryTest
Flag: EXEMPT bugfix
Change-Id: If67a2ba9e069455a9c21e8d934872459e7d1b9f1
parent 40477f9b
Loading
Loading
Loading
Loading
+49 −2
Original line number Diff line number Diff line
@@ -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()

+53 −0
Original line number Diff line number Diff line
/*
 * 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()
        }
}
+135 −0
Original line number Diff line number Diff line
/*
 * 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)
        }
}
+2 −0
Original line number Diff line number Diff line
@@ -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,
+31 −0
Original line number Diff line number Diff line
@@ -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;
    }
}
Loading