/* * Copyright (C) 2023 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 import android.content.ComponentName import android.content.Context import android.content.pm.PackageManager import android.content.pm.ResolveInfo import android.os.Binder import android.os.PowerManager import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.testing.TestableLooper import android.view.Display import android.view.MotionEvent import android.view.ViewGroup import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.app.AssistUtils import com.android.internal.logging.UiEventLogger import com.android.systemui.LauncherProxyService.ACTION_QUICKSTEP import com.android.systemui.broadcast.BroadcastDispatcher import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.dump.DumpManager import com.android.systemui.keyguard.KeyguardUnlockAnimationController import com.android.systemui.keyguard.WakefulnessLifecycle import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager import com.android.systemui.kosmos.testScope import com.android.systemui.log.assertLogsWtf import com.android.systemui.model.fakeSysUIStatePerDisplayRepository import com.android.systemui.model.sysUiState import com.android.systemui.model.sysUiStateFactory import com.android.systemui.navigationbar.NavigationBarController import com.android.systemui.navigationbar.NavigationModeController import com.android.systemui.navigationbar.views.NavigationBar import com.android.systemui.process.ProcessWrapper import com.android.systemui.recents.ScreenPinningRequest import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.settings.FakeDisplayTracker import com.android.systemui.settings.UserTracker import com.android.systemui.shade.ShadeViewController import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy import com.android.systemui.shade.domain.interactor.shadeInteractor import com.android.systemui.shade.domain.interactor.shadeModeInteractor import com.android.systemui.shared.recents.ILauncherProxy import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_NAVIGATION_BAR_DISABLED import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_WAKEFULNESS_MASK import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_ASLEEP import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_AWAKE import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_GOING_TO_SLEEP import com.android.systemui.shared.system.QuickStepContract.WAKEFULNESS_WAKING import com.android.systemui.statusbar.CommandQueue import com.android.systemui.statusbar.NotificationShadeWindowController import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder import com.android.systemui.user.domain.interactor.HeadlessSystemUserModeFake import com.android.systemui.util.mockito.mock import com.android.systemui.util.time.FakeSystemClock import com.android.wm.shell.back.BackAnimation import com.android.wm.shell.shared.desktopmode.FakeDesktopState import com.android.wm.shell.sysui.ShellInterface import com.google.common.util.concurrent.MoreExecutors import java.util.Optional import java.util.concurrent.Executor import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.runTest import org.junit.After import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers import org.mockito.ArgumentMatchers.anyLong import org.mockito.ArgumentMatchers.anyString import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.Mockito.any import org.mockito.Mockito.anyInt import org.mockito.Mockito.atLeast import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.longThat import org.mockito.Mockito.mock import org.mockito.Mockito.spy import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.Mockito.`when` import org.mockito.MockitoAnnotations import org.mockito.kotlin.argThat import org.mockito.kotlin.doNothing import org.mockito.kotlin.never import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidJUnit4::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class LauncherProxyServiceTest : SysuiTestCase() { @Main private val executor: Executor = MoreExecutors.directExecutor() private val kosmos = testKosmos() private lateinit var subject: LauncherProxyService @Mock private val dumpManager = DumpManager() @Mock private lateinit var processWrapper: ProcessWrapper private val displayTracker = FakeDisplayTracker(mContext) private val fakeSystemClock = FakeSystemClock() private val sysUiState = kosmos.sysUiState private val sysUiStateFactory = kosmos.sysUiStateFactory private val wakefulnessLifecycle = WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager) private val sysuiStatePerDisplayRepository = kosmos.fakeSysUIStatePerDisplayRepository @Mock private lateinit var launcherProxy: ILauncherProxy.Stub @Mock private lateinit var packageManager: PackageManager // The following mocks belong to not-yet-tested parts of LauncherProxyService. @Mock private lateinit var commandQueue: CommandQueue @Mock private lateinit var shellInterface: ShellInterface @Mock private lateinit var navBarController: NavigationBarController @Mock private lateinit var shadeViewController: ShadeViewController @Mock private lateinit var sceneInteractor: SceneInteractor @Mock private lateinit var screenPinningRequest: ScreenPinningRequest @Mock private lateinit var navModeController: NavigationModeController @Mock private lateinit var statusBarWinController: NotificationShadeWindowController @Mock private lateinit var userTracker: UserTracker @Mock private lateinit var userManager: UserManager @Mock private lateinit var uiEventLogger: UiEventLogger @Mock private lateinit var sysuiUnlockAnimationController: KeyguardUnlockAnimationController @Mock private lateinit var inWindowLauncherUnlockAnimationManager: InWindowLauncherUnlockAnimationManager @Mock private lateinit var assistUtils: AssistUtils @Mock private lateinit var unfoldTransitionProgressForwarder: Optional @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher @Mock private lateinit var statusBarShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy @Mock private lateinit var backAnimation: Optional private lateinit var desktopState: FakeDesktopState private val fakeHeadlessSystemUserMode = HeadlessSystemUserModeFake() @Before fun setUp() { MockitoAnnotations.initMocks(this) desktopState = FakeDesktopState() val serviceComponent = ComponentName("test_package", "service_provider") context.addMockService(serviceComponent, launcherProxy) context.addMockServiceResolver { if (it.action == ACTION_QUICKSTEP) serviceComponent else null } whenever(launcherProxy.queryLocalInterface(ArgumentMatchers.anyString())) .thenReturn(launcherProxy) whenever(launcherProxy.asBinder()).thenReturn(launcherProxy) doNothing().whenever(sceneInteractor).onRemoteUserInputStarted(anyString()) doNothing().whenever(shadeViewController).startInputFocusTransfer() // packageManager.resolveServiceAsUser has to return non-null for // LauncherProxyService#isEnabled to become true. context.setMockPackageManager(packageManager) whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt())) .thenReturn(mock(ResolveInfo::class.java)) // return isSystemUser as true by default. `when`(processWrapper.isSystemUser).thenReturn(true) sysuiStatePerDisplayRepository.add(Display.DEFAULT_DISPLAY, sysUiState) runBlocking { kosmos.displayRepository.apply { addDisplay(0) } } fakeHeadlessSystemUserMode.setIsHeadlessSystemUser(false) whenever(userTracker.userId).then { Binder.getCallingUserHandle().identifier } subject = createLauncherProxyService(context) } @After fun tearDown() { subject.shutdownForTest() } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun wakefulnessLifecycle_dispatchFinishedWakingUpSetsSysUIflagToAWAKE() { // WakefulnessLifecycle is initialized to AWAKE initially, and won't emit a noop. wakefulnessLifecycle.dispatchFinishedGoingToSleep() clearInvocations(launcherProxy) wakefulnessLifecycle.dispatchFinishedWakingUp() verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_AWAKE }, eq(Display.DEFAULT_DISPLAY), ) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun wakefulnessLifecycle_dispatchStartedWakingUpSetsSysUIflagToWAKING() { wakefulnessLifecycle.dispatchStartedWakingUp(PowerManager.WAKE_REASON_UNKNOWN) verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_WAKING }, eq(Display.DEFAULT_DISPLAY), ) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun wakefulnessLifecycle_dispatchFinishedGoingToSleepSetsSysUIflagToASLEEP() { wakefulnessLifecycle.dispatchFinishedGoingToSleep() verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_ASLEEP }, eq(Display.DEFAULT_DISPLAY), ) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun wakefulnessLifecycle_dispatchStartedGoingToSleepSetsSysUIflagToGOING_TO_SLEEP() { wakefulnessLifecycle.dispatchStartedGoingToSleep( PowerManager.GO_TO_SLEEP_REASON_POWER_BUTTON ) verify(launcherProxy) .onSystemUiStateChanged( longThat { it and SYSUI_STATE_WAKEFULNESS_MASK == WAKEFULNESS_GOING_TO_SLEEP }, eq(Display.DEFAULT_DISPLAY), ) } @Test fun navigationBarController_cannotCreateNavBarOrTaskbarSetsSysUIFlagToNAVIGATION_BAR_DISABLED() { whenever(navBarController.canCreateNavBarOrTaskBar(anyInt())).thenReturn(false) subject.updateSystemUiStateFlags() verify(launcherProxy) .onSystemUiStateChanged( longThat { (it and SYSUI_STATE_NAVIGATION_BAR_DISABLED) != 0L }, eq(Display.DEFAULT_DISPLAY), ) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun connectToLauncherService_primaryUserNoVisibleBgUsersSupported_expectBindService() { `when`(processWrapper.isSystemUser).thenReturn(true) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false) val spyContext = spy(context) val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun connectToLauncherService_nonPrimaryUserNoVisibleBgUsersSupported_expectNoBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(false) val spyContext = spy(context) val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun connectToLauncherService_nonPrimaryBgUserVisibleBgUsersSupported_expectBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(false) val spyContext = spy(context) val ops = createLauncherProxyService(spyContext) ops.startConnectionToCurrentUser() verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun connectToLauncherService_nonPrimaryFgUserVisibleBgUsersSupported_expectNoBindService() { `when`(processWrapper.isSystemUser).thenReturn(false) `when`(userManager.isVisibleBackgroundUsersSupported()).thenReturn(true) `when`(userManager.isUserForeground()).thenReturn(true) val spyContext = spy(context) val ops = assertLogsWtf { createLauncherProxyService(spyContext) }.result ops.startConnectionToCurrentUser() verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any()) } @Test @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun notifySysUiStateFlagsForAllDisplays_triggersUpdateInAllDisplays() = kosmos.testScope.runTest { kosmos.displayRepository.apply { addDisplay(0) addDisplay(1) addDisplay(2) } kosmos.fakeSysUIStatePerDisplayRepository.apply { add(1, sysUiStateFactory.create(1)) add(2, sysUiStateFactory.create(2)) } clearInvocations(launcherProxy) subject.notifySysUiStateFlagsForAllDisplays() verify(launcherProxy).onSystemUiStateChanged(anyLong(), eq(0)) verify(launcherProxy).onSystemUiStateChanged(anyLong(), eq(1)) verify(launcherProxy).onSystemUiStateChanged(anyLong(), eq(2)) } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun updateSystemUiStateFlags_updatesAllNavBars() = kosmos.testScope.runTest { kosmos.displayRepository.apply { addDisplay(0) addDisplay(1) } kosmos.fakeSysUIStatePerDisplayRepository.apply { add(1, sysUiStateFactory.create(1)) } val navBar0 = mock() val navBar1 = mock() whenever(navBarController.getNavigationBar(eq(0))).thenReturn(navBar0) whenever(navBarController.getNavigationBar(eq(1))).thenReturn(navBar1) subject.updateSystemUiStateFlags() verify(navBar0).updateSystemUiStateFlags() verify(navBar1).updateSystemUiStateFlags() } @Test @EnableFlags( Flags.FLAG_SHADE_WINDOW_GOES_AROUND, Flags.FLAG_SCENE_CONTAINER, Flags.FLAG_MEDIA_CONTROLS_IN_COMPOSE, Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, ) fun onStatusBarTrackpadEvent_callsOnStatusBarOrLauncherTouched() = kosmos.testScope.runTest { val shadeDisplayId = 1 whenever(statusBarShadeDisplayPolicy.displayId) .thenReturn(MutableStateFlow(shadeDisplayId)) val event = MotionEvent.obtain(500, 500, MotionEvent.ACTION_MOVE, 500f, 500f, 0).apply { displayId = 0 } whenever(statusBarWinController.windowRootView).thenReturn(mock(ViewGroup::class.java)) whenever( shadeViewController.handleExternalTouch( argThat { displayId == event.displayId } ) ) .thenReturn(true) subject.mSysUiProxy.onStatusBarTrackpadEvent(event) verify(statusBarShadeDisplayPolicy) .onStatusBarOrLauncherTouched( argThat { displayId == event.displayId }, anyInt(), ) } @Test @EnableFlags( Flags.FLAG_SHADE_WINDOW_GOES_AROUND, Flags.FLAG_SCENE_CONTAINER, Flags.FLAG_MEDIA_CONTROLS_IN_COMPOSE, Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR, ) fun onStatusBarTouchEvent_withSceneFlag_callsOnStatusBarOrLauncherTouched() = kosmos.testScope.runTest { val shadeDisplayId = 1 whenever(statusBarShadeDisplayPolicy.displayId) .thenReturn(MutableStateFlow(shadeDisplayId)) val event = MotionEvent.obtain(500, 500, MotionEvent.ACTION_MOVE, 500f, 500f, 0).apply { displayId = 0 } subject.mSysUiProxy.onStatusBarTouchEvent(event) verify(statusBarShadeDisplayPolicy) .onStatusBarOrLauncherTouched( argThat { displayId == event.displayId }, anyInt(), ) } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableFlags(Flags.FLAG_SCENE_CONTAINER, Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun onStatusBarTouchEvent_withoutSceneFlag_onDifferentDisplayTouch_ignoresInput() = kosmos.testScope.runTest { val shadeDisplayId = 1 whenever(statusBarShadeDisplayPolicy.displayId) .thenReturn(MutableStateFlow(shadeDisplayId)) val event = MotionEvent.obtain(500, 500, MotionEvent.ACTION_DOWN, 500f, 500f, 0).apply { displayId = 0 } subject.mSysUiProxy.onStatusBarTouchEvent(event) verify(shadeViewController, never()).startExpandLatencyTracking() } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableFlags(Flags.FLAG_SCENE_CONTAINER, Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun onStatusBarTouchEvent_withoutSceneFlag_dispatchesToShadeDisplayPolicy() = kosmos.testScope.runTest { val shadeDisplayId = 1 whenever(statusBarShadeDisplayPolicy.displayId) .thenReturn(MutableStateFlow(shadeDisplayId)) val event = MotionEvent.obtain(500, 500, MotionEvent.ACTION_DOWN, 500f, 500f, 0).apply { displayId = 0 } subject.mSysUiProxy.onStatusBarTouchEvent(event) verify(statusBarShadeDisplayPolicy) .onStatusBarOrLauncherTouched(argThat { displayId == 0 }, anyInt()) } @Test @EnableFlags(Flags.FLAG_SHADE_WINDOW_GOES_AROUND) @DisableFlags(Flags.FLAG_SCENE_CONTAINER, Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR) fun onStatusBarTouchEvent_withoutSceneFlag_onSameDisplayTouch_handlesInput() = kosmos.testScope.runTest { val shadeDisplayId = 0 whenever(statusBarShadeDisplayPolicy.displayId) .thenReturn(MutableStateFlow(shadeDisplayId)) val event = MotionEvent.obtain(500, 500, MotionEvent.ACTION_DOWN, 500f, 500f, 0).apply { displayId = shadeDisplayId } subject.mSysUiProxy.onStatusBarTouchEvent(event) verify(statusBarShadeDisplayPolicy) .onStatusBarOrLauncherTouched( argThat { displayId == shadeDisplayId }, anyInt(), ) verify(shadeViewController).startExpandLatencyTracking() } private fun createLauncherProxyService(ctx: Context): LauncherProxyService { whenever(navBarController.canCreateNavBarOrTaskBar(anyInt())).thenReturn(true) return LauncherProxyService( ctx, executor, commandQueue, shellInterface, { navBarController }, { shadeViewController }, screenPinningRequest, navModeController, statusBarWinController, kosmos.fakeSysUIStatePerDisplayRepository, { sceneInteractor }, { kosmos.shadeInteractor }, { kosmos.shadeModeInteractor }, statusBarShadeDisplayPolicy, userTracker, userManager, wakefulnessLifecycle, uiEventLogger, displayTracker, sysuiUnlockAnimationController, inWindowLauncherUnlockAnimationManager, assistUtils, dumpManager, unfoldTransitionProgressForwarder, broadcastDispatcher, backAnimation, processWrapper, kosmos.displayRepository, desktopState, fakeHeadlessSystemUserMode, ) } }