diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java index 65dedc6eaf4b8..21adb3ff13c3d 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/QuickStepContract.java @@ -115,6 +115,26 @@ public class QuickStepContract { public static final int SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE = 1 << 26; // Device dreaming state public static final int SYSUI_STATE_DEVICE_DREAMING = 1 << 27; + // Whether the screen is currently on. Note that the screen is considered on while turning on, + // but not while turning off. + public static final int SYSUI_STATE_SCREEN_ON = 1 << 28; + // Whether the screen is currently transitioning into the state indicated by + // SYSUI_STATE_SCREEN_ON. + public static final int SYSUI_STATE_SCREEN_TRANSITION = 1 << 29; + + // Mask for SystemUiStateFlags to isolate SYSUI_STATE_SCREEN_ON and + // SYSUI_STATE_SCREEN_TRANSITION, to match SCREEN_STATE_* + public static final int SYSUI_STATE_SCREEN_STATE_MASK = + SYSUI_STATE_SCREEN_ON | SYSUI_STATE_SCREEN_TRANSITION; + // Screen is off. + public static final int SCREEN_STATE_OFF = 0; + // Screen is on. + public static final int SCREEN_STATE_ON = SYSUI_STATE_SCREEN_ON; + // Screen is still on, but transitioning to turn off. + public static final int SCREEN_STATE_TURNING_OFF = SYSUI_STATE_SCREEN_TRANSITION; + // Screen was off and is now turning on. + public static final int SCREEN_STATE_TURNING_ON = + SYSUI_STATE_SCREEN_TRANSITION | SYSUI_STATE_SCREEN_ON; @Retention(RetentionPolicy.SOURCE) @IntDef({SYSUI_STATE_SCREEN_PINNING, @@ -144,7 +164,9 @@ public class QuickStepContract { SYSUI_STATE_IMMERSIVE_MODE, SYSUI_STATE_VOICE_INTERACTION_WINDOW_SHOWING, SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE, - SYSUI_STATE_DEVICE_DREAMING + SYSUI_STATE_DEVICE_DREAMING, + SYSUI_STATE_SCREEN_ON, + SYSUI_STATE_SCREEN_TRANSITION, }) public @interface SystemUiStateFlags {} @@ -183,6 +205,10 @@ public class QuickStepContract { str.add((flags & SYSUI_STATE_FREEFORM_ACTIVE_IN_DESKTOP_MODE) != 0 ? "freeform_active_in_desktop_mode" : ""); str.add((flags & SYSUI_STATE_DEVICE_DREAMING) != 0 ? "device_dreaming" : ""); + str.add("screen_" + + ((flags & SYSUI_STATE_SCREEN_TRANSITION) != 0 ? "turning_" : "") + + ((flags & SYSUI_STATE_SCREEN_ON) != 0 ? "on" : "off")); + return str.toString(); } diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java index 019ca52107ddb..8a3ecc6afaacb 100644 --- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java +++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java @@ -31,6 +31,8 @@ import static com.android.systemui.shared.system.QuickStepContract.KEY_EXTRA_WIN import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_BOUNCER_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DOZING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_DEVICE_DREAMING; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_ON; +import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_TRANSITION; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING_OCCLUDED; import static com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_TRACING_ENABLED; @@ -121,7 +123,8 @@ import dagger.Lazy; public class OverviewProxyService implements CallbackController, NavigationModeController.ModeChangedListener, Dumpable { - private static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; + @VisibleForTesting + static final String ACTION_QUICKSTEP = "android.intent.action.QUICKSTEP_SERVICE"; public static final String TAG_OPS = "OverviewProxyService"; private static final long BACKOFF_MILLIS = 1000; @@ -548,6 +551,7 @@ public class OverviewProxyService implements CallbackController + * The in-production service is never shuts down, and it was not designed with testing in mind. + * This unregisters the mechanisms by which the service will be revived after a testcase. + *

+ * NOTE: This is a stop-gap introduced when first added some tests to this class. It should + * probably be replaced by proper lifecycle management on this class. + */ + @VisibleForTesting() + void shutdownForTest() { + mContext.unregisterReceiver(mLauncherStateChangedReceiver); + mIsEnabled = false; + mHandler.removeCallbacks(mConnectionRunnable); + disconnectFromLauncherService(); + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt new file mode 100644 index 0000000000000..eb7b481e52441 --- /dev/null +++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt @@ -0,0 +1,186 @@ +/* + * 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.recents + +import android.content.ComponentName +import android.content.pm.PackageManager +import android.content.pm.ResolveInfo +import android.testing.AndroidTestingRunner +import android.testing.TestableContext +import android.testing.TestableLooper +import androidx.test.filters.SmallTest +import com.android.internal.app.AssistUtils +import com.android.internal.logging.UiEventLogger +import com.android.systemui.SysuiTestCase +import com.android.systemui.dagger.qualifiers.Main +import com.android.systemui.dump.DumpManager +import com.android.systemui.keyguard.KeyguardUnlockAnimationController +import com.android.systemui.keyguard.ScreenLifecycle +import com.android.systemui.model.SysUiState +import com.android.systemui.navigationbar.NavigationBarController +import com.android.systemui.navigationbar.NavigationModeController +import com.android.systemui.recents.OverviewProxyService.ACTION_QUICKSTEP +import com.android.systemui.settings.FakeDisplayTracker +import com.android.systemui.settings.UserTracker +import com.android.systemui.shared.recents.IOverviewProxy +import com.android.systemui.shared.system.QuickStepContract.SCREEN_STATE_OFF +import com.android.systemui.shared.system.QuickStepContract.SCREEN_STATE_ON +import com.android.systemui.shared.system.QuickStepContract.SCREEN_STATE_TURNING_OFF +import com.android.systemui.shared.system.QuickStepContract.SCREEN_STATE_TURNING_ON +import com.android.systemui.shared.system.QuickStepContract.SYSUI_STATE_SCREEN_STATE_MASK +import com.android.systemui.statusbar.CommandQueue +import com.android.systemui.statusbar.NotificationShadeWindowController +import com.android.systemui.statusbar.phone.CentralSurfaces +import com.android.systemui.unfold.progress.UnfoldTransitionProgressForwarder +import com.android.systemui.util.mockito.whenever +import com.android.wm.shell.sysui.ShellInterface +import com.google.common.util.concurrent.MoreExecutors +import dagger.Lazy +import java.util.Optional +import java.util.concurrent.Executor +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers +import org.mockito.Mock +import org.mockito.Mockito.any +import org.mockito.Mockito.anyInt +import org.mockito.Mockito.intThat +import org.mockito.Mockito.mock +import org.mockito.Mockito.verify +import org.mockito.MockitoAnnotations + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class OverviewProxyServiceTest : SysuiTestCase() { + + @Main private val executor: Executor = MoreExecutors.directExecutor() + + private lateinit var subject: OverviewProxyService + private val dumpManager = DumpManager() + private val displayTracker = FakeDisplayTracker(mContext) + private val sysUiState = SysUiState(displayTracker) + private val screenLifecycle = ScreenLifecycle(dumpManager) + + @Mock private lateinit var overviewProxy: IOverviewProxy.Stub + @Mock private lateinit var packageManager: PackageManager + + // The following mocks belong to not-yet-tested parts of OverviewProxyService. + @Mock private lateinit var commandQueue: CommandQueue + @Mock private lateinit var shellInterface: ShellInterface + @Mock private lateinit var navBarController: NavigationBarController + @Mock private lateinit var centralSurfaces: CentralSurfaces + @Mock private lateinit var navModeController: NavigationModeController + @Mock private lateinit var statusBarWinController: NotificationShadeWindowController + @Mock private lateinit var userTracker: UserTracker + @Mock private lateinit var uiEventLogger: UiEventLogger + @Mock private lateinit var sysuiUnlockAnimationController: KeyguardUnlockAnimationController + @Mock private lateinit var assistUtils: AssistUtils + @Mock + private lateinit var unfoldTransitionProgressForwarder: + Optional + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + + val serviceComponent = ComponentName("test_package", "service_provider") + context.addMockService(serviceComponent, overviewProxy) + context.addMockServiceResolver( + TestableContext.MockServiceResolver { + if (it.action == ACTION_QUICKSTEP) serviceComponent else null + } + ) + whenever(overviewProxy.queryLocalInterface(ArgumentMatchers.anyString())) + .thenReturn(overviewProxy) + whenever(overviewProxy.asBinder()).thenReturn(overviewProxy) + + // packageManager.resolveServiceAsUser has to return non-null for + // OverviewProxyService#isEnabled to become true. + context.setMockPackageManager(packageManager) + whenever(packageManager.resolveServiceAsUser(any(), anyInt(), anyInt())) + .thenReturn(mock(ResolveInfo::class.java)) + + subject = + OverviewProxyService( + context, + executor, + commandQueue, + shellInterface, + Lazy { navBarController }, + Lazy { Optional.of(centralSurfaces) }, + navModeController, + statusBarWinController, + sysUiState, + userTracker, + screenLifecycle, + uiEventLogger, + displayTracker, + sysuiUnlockAnimationController, + assistUtils, + dumpManager, + unfoldTransitionProgressForwarder + ) + } + + @After + fun tearDown() { + subject.shutdownForTest() + } + + @Test + fun `ScreenLifecycle - screenTurnedOn triggers SysUI state flag changes `() { + screenLifecycle.dispatchScreenTurnedOn() + + verify(overviewProxy) + .onSystemUiStateChanged( + intThat { it and SYSUI_STATE_SCREEN_STATE_MASK == SCREEN_STATE_ON } + ) + } + + @Test + fun `ScreenLifecycle - screenTurningOn triggers SysUI state flag changes `() { + screenLifecycle.dispatchScreenTurningOn() + + verify(overviewProxy) + .onSystemUiStateChanged( + intThat { it and SYSUI_STATE_SCREEN_STATE_MASK == SCREEN_STATE_TURNING_ON } + ) + } + + @Test + fun `ScreenLifecycle - screenTurnedOff triggers SysUI state flag changes `() { + screenLifecycle.dispatchScreenTurnedOff() + + verify(overviewProxy) + .onSystemUiStateChanged( + intThat { it and SYSUI_STATE_SCREEN_STATE_MASK == SCREEN_STATE_OFF } + ) + } + + @Test + fun `ScreenLifecycle - screenTurningOff triggers SysUI state flag changes `() { + screenLifecycle.dispatchScreenTurningOff() + + verify(overviewProxy) + .onSystemUiStateChanged( + intThat { it and SYSUI_STATE_SCREEN_STATE_MASK == SCREEN_STATE_TURNING_OFF } + ) + } +} diff --git a/tests/testables/src/android/testing/TestableContext.java b/tests/testables/src/android/testing/TestableContext.java index e2668bc4281fa..0f04d6ae17210 100644 --- a/tests/testables/src/android/testing/TestableContext.java +++ b/tests/testables/src/android/testing/TestableContext.java @@ -33,11 +33,15 @@ import android.provider.Settings; import android.util.ArrayMap; import android.view.LayoutInflater; +import androidx.annotation.Nullable; + import org.junit.rules.TestRule; import org.junit.rules.TestWatcher; import org.junit.runner.Description; import org.junit.runners.model.Statement; +import java.util.ArrayList; + /** * A ContextWrapper with utilities specifically designed to make Testing easier. * @@ -61,6 +65,7 @@ public class TestableContext extends ContextWrapper implements TestRule { private final TestableContentResolver mTestableContentResolver; private final TestableSettingsProvider mSettingsProvider; + private ArrayList mMockServiceResolvers; private ArrayMap mMockSystemServices; private ArrayMap mMockServices; private ArrayMap mActiveServices; @@ -214,25 +219,54 @@ public class TestableContext extends ContextWrapper implements TestRule { /** * Adds a mock service to be connected to by a bindService call. *

- * Normally a TestableContext will pass through all bind requests to the base context - * but when addMockService has been called for a ComponentName being bound, then - * TestableContext will immediately trigger a {@link ServiceConnection#onServiceConnected} - * with the specified service, and will call {@link ServiceConnection#onServiceDisconnected} - * when the service is unbound. + * Normally a TestableContext will pass through all bind requests to the base context + * but when addMockService has been called for a ComponentName being bound, then + * TestableContext will immediately trigger a {@link ServiceConnection#onServiceConnected} + * with the specified service, and will call {@link ServiceConnection#onServiceDisconnected} + * when the service is unbound. *

+ * + * @see #addMockServiceResolver(MockServiceResolver) for custom resolution of service Intents to + * ComponentNames */ public void addMockService(ComponentName component, IBinder service) { if (mMockServices == null) mMockServices = new ArrayMap<>(); mMockServices.put(component, service); } + /** + * Strategy to resolve a service {@link Intent} to a mock service {@link ComponentName}. + */ + public interface MockServiceResolver { + @Nullable + ComponentName resolve(Intent service); + } + + /** + * Registers a strategy to resolve service intents to registered mock services. + *

+ * The result of the first {@link MockServiceResolver} to return a non-null + * {@link ComponentName} is used to look up a mock service. The mock service must be registered + * via {@link #addMockService(ComponentName, IBinder)} separately, using the same component + * name. + * + * If none of the resolvers return a non-null value, or the first returned component name + * does not link to a registered mock service, the bind requests are passed to the base context + * + * The resolvers are queried in order of registration. + */ + public void addMockServiceResolver(MockServiceResolver resolver) { + if (mMockServiceResolvers == null) mMockServiceResolvers = new ArrayList<>(); + mMockServiceResolvers.add(resolver); + } + /** * @see #addMockService(ComponentName, IBinder) */ @Override public boolean bindService(Intent service, ServiceConnection conn, int flags) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); - if (checkMocks(service.getComponent(), conn)) return true; + if (checkMocks(service, conn)) return true; return super.bindService(service, conn, flags); } @@ -243,7 +277,7 @@ public class TestableContext extends ContextWrapper implements TestRule { public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, Handler handler, UserHandle user) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); - if (checkMocks(service.getComponent(), conn)) return true; + if (checkMocks(service, conn)) return true; return super.bindServiceAsUser(service, conn, flags, handler, user); } @@ -254,18 +288,36 @@ public class TestableContext extends ContextWrapper implements TestRule { public boolean bindServiceAsUser(Intent service, ServiceConnection conn, int flags, UserHandle user) { if (mService != null) mService.getLeakInfo(conn).addAllocation(new Throwable()); - if (checkMocks(service.getComponent(), conn)) return true; + if (checkMocks(service, conn)) return true; return super.bindServiceAsUser(service, conn, flags, user); } - private boolean checkMocks(ComponentName component, ServiceConnection conn) { - if (mMockServices != null && component != null && mMockServices.containsKey(component)) { - if (mActiveServices == null) mActiveServices = new ArrayMap<>(); - mActiveServices.put(conn, component); - conn.onServiceConnected(component, mMockServices.get(component)); - return true; + private boolean checkMocks(Intent service, ServiceConnection conn) { + if (mMockServices == null) return false; + + ComponentName serviceComponent = resolveMockServiceComponent(service); + if (serviceComponent == null) return false; + + IBinder serviceImpl = mMockServices.get(serviceComponent); + if (serviceImpl == null) return false; + + if (mActiveServices == null) mActiveServices = new ArrayMap<>(); + mActiveServices.put(conn, serviceComponent); + conn.onServiceConnected(serviceComponent, serviceImpl); + return true; + } + + private ComponentName resolveMockServiceComponent(Intent service) { + ComponentName specifiedComponentName = service.getComponent(); + if (specifiedComponentName != null) return specifiedComponentName; + + if (mMockServiceResolvers == null) return null; + + for (MockServiceResolver resolver : mMockServiceResolvers) { + ComponentName resolvedComponent = resolver.resolve(service); + if (resolvedComponent != null) return resolvedComponent; } - return false; + return null; } /**