From 9eaae4b6a418ebef7db49fc6be4ffae02b9d1f6a Mon Sep 17 00:00:00 2001 From: Brian Isganitis Date: Mon, 3 Jun 2024 23:20:07 +0000 Subject: [PATCH] Initial TaskbarUnitTestRule with example overlay controller tests. Flag: TEST_ONLY Bug: 230027385 Test: TaskbarOverlayControllerTest Change-Id: I858906ece7e67677962ec8b4432bfcca5ec30283 --- .../taskbar/TaskbarActivityContext.java | 5 + .../launcher3/taskbar/TaskbarManager.java | 6 +- .../overlay/TaskbarOverlayController.java | 28 ++- .../launcher3/taskbar/TaskbarUnitTestRule.kt | 144 ++++++++++++ .../overlay/TaskbarOverlayControllerTest.kt | 215 ++++++++++++++++++ 5 files changed, 387 insertions(+), 11 deletions(-) create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt create mode 100644 quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java index 0de0550016..d536e8414f 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarActivityContext.java @@ -1605,4 +1605,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext { boolean canToggleHomeAllApps() { return mControllers.uiController.canToggleHomeAllApps(); } + + @VisibleForTesting + public TaskbarControllers getControllers() { + return mControllers; + } } diff --git a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java index ec2cee2bd9..2a58db25df 100644 --- a/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java +++ b/quickstep/src/com/android/launcher3/taskbar/TaskbarManager.java @@ -611,7 +611,8 @@ public class TaskbarManager { } } - private void addTaskbarRootViewToWindow() { + @VisibleForTesting + void addTaskbarRootViewToWindow() { if (enableTaskbarNoRecreate() && !mAddedWindow && mTaskbarActivityContext != null) { mWindowManager.addView(mTaskbarRootLayout, mTaskbarActivityContext.getWindowLayoutParams()); @@ -619,7 +620,8 @@ public class TaskbarManager { } } - private void removeTaskbarRootViewFromWindow() { + @VisibleForTesting + void removeTaskbarRootViewFromWindow() { if (enableTaskbarNoRecreate() && mAddedWindow) { mWindowManager.removeViewImmediate(mTaskbarRootLayout); mAddedWindow = false; diff --git a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java index adbec65ad3..7eb34a51c8 100644 --- a/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java +++ b/quickstep/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayController.java @@ -133,16 +133,19 @@ public final class TaskbarOverlayController { *

* This method should be called after an exit animation finishes, if applicable. */ - @SuppressLint("WrongConstant") void maybeCloseWindow() { - if (mOverlayContext != null && (AbstractFloatingView.hasOpenView(mOverlayContext, TYPE_ALL) - || mOverlayContext.getDragController().isSystemDragInProgress())) { - return; - } + if (!canCloseWindow()) return; mProxyView.close(false); onDestroy(); } + @SuppressLint("WrongConstant") + private boolean canCloseWindow() { + if (mOverlayContext == null) return true; + if (AbstractFloatingView.hasOpenView(mOverlayContext, TYPE_ALL)) return false; + return !mOverlayContext.getDragController().isSystemDragInProgress(); + } + /** Destroys the controller and any overlay window if present. */ public void onDestroy() { TaskStackChangeListeners.getInstance().unregisterTaskStackListener(mTaskStackListener); @@ -212,10 +215,17 @@ public final class TaskbarOverlayController { @Override protected void handleClose(boolean animate) { - if (mIsOpen) { - mTaskbarContext.getDragLayer().removeView(this); - Optional.ofNullable(mOverlayContext).ifPresent(c -> closeAllOpenViews(c, animate)); - } + if (!mIsOpen) return; + mTaskbarContext.getDragLayer().removeView(this); + Optional.ofNullable(mOverlayContext).ifPresent(c -> { + if (canCloseWindow()) { + onDestroy(); // Window is already ready to be destroyed. + } else { + // Close window's AFVs before destroying it. Its drag layer will attempt to + // close the proxy view again once its children are removed. + closeAllOpenViews(c, animate); + } + }); } @Override diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt new file mode 100644 index 0000000000..77cd1fe67a --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/TaskbarUnitTestRule.kt @@ -0,0 +1,144 @@ +/* + * 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.launcher3.taskbar + +import android.app.PendingIntent +import android.content.IIntentSender +import android.content.Intent +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.rule.ServiceTestRule +import com.android.launcher3.LauncherAppState +import com.android.launcher3.taskbar.TaskbarNavButtonController.TaskbarNavButtonCallbacks +import com.android.launcher3.util.Executors.UI_HELPER_EXECUTOR +import com.android.launcher3.util.LauncherMultivalentJUnit.Companion.isRunningInRobolectric +import com.android.quickstep.AllAppsActionManager +import com.android.quickstep.TouchInteractionService +import com.android.quickstep.TouchInteractionService.TISBinder +import org.junit.Assume.assumeTrue +import org.junit.rules.MethodRule +import org.junit.runners.model.FrameworkMethod +import org.junit.runners.model.Statement + +/** + * Manages the Taskbar lifecycle for unit tests. + * + * See [InjectController] for grabbing controller(s) under test with minimal boilerplate. + */ +class TaskbarUnitTestRule : MethodRule { + private val instrumentation = InstrumentationRegistry.getInstrumentation() + private val serviceTestRule = ServiceTestRule() + + private lateinit var taskbarManager: TaskbarManager + private lateinit var target: Any + + val activityContext: TaskbarActivityContext + get() { + return taskbarManager.currentActivityContext + ?: throw RuntimeException("Failed to obtain TaskbarActivityContext.") + } + + override fun apply(base: Statement, method: FrameworkMethod, target: Any): Statement { + return object : Statement() { + override fun evaluate() { + this@TaskbarUnitTestRule.target = target + + val context = instrumentation.targetContext + instrumentation.runOnMainSync { + assumeTrue( + LauncherAppState.getIDP(context).getDeviceProfile(context).isTaskbarPresent + ) + } + + // Check for existing Taskbar instance from Launcher process. + val launcherTaskbarManager: TaskbarManager? = + if (!isRunningInRobolectric) { + try { + val tisBinder = + serviceTestRule.bindService( + Intent(context, TouchInteractionService::class.java) + ) as? TISBinder + tisBinder?.taskbarManager + } catch (_: Exception) { + null + } + } else { + null + } + + instrumentation.runOnMainSync { + taskbarManager = + TaskbarManager( + context, + AllAppsActionManager(context, UI_HELPER_EXECUTOR) { + PendingIntent(IIntentSender.Default()) + }, + object : TaskbarNavButtonCallbacks {}, + ) + } + + try { + // Replace Launcher Taskbar window with test instance. + instrumentation.runOnMainSync { + launcherTaskbarManager?.removeTaskbarRootViewFromWindow() + taskbarManager.onUserUnlocked() // Required to complete initialization. + } + + injectControllers() + base.evaluate() + } finally { + // Revert Taskbar window. + instrumentation.runOnMainSync { + taskbarManager.destroy() + launcherTaskbarManager?.addTaskbarRootViewToWindow() + } + } + } + } + } + + /** Simulates Taskbar recreation lifecycle. */ + fun recreateTaskbar() { + taskbarManager.recreateTaskbar() + injectControllers() + } + + private fun injectControllers() { + val controllers = activityContext.controllers + val controllerFieldsByType = controllers.javaClass.fields.associateBy { it.type } + target.javaClass.fields + .filter { it.isAnnotationPresent(InjectController::class.java) } + .forEach { + it.set( + target, + controllerFieldsByType[it.type]?.get(controllers) + ?: throw NoSuchElementException("Failed to find controller for ${it.type}"), + ) + } + } + + /** + * Annotates test controller fields to inject the corresponding controllers from the current + * [TaskbarControllers] instance. + * + * Controllers are injected during test setup and upon calling [recreateTaskbar]. + * + * Multiple controllers can be injected if needed. + */ + @Retention(AnnotationRetention.RUNTIME) + @Target(AnnotationTarget.FIELD) + annotation class InjectController +} diff --git a/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt new file mode 100644 index 0000000000..8768cb974b --- /dev/null +++ b/quickstep/tests/multivalentTests/src/com/android/launcher3/taskbar/overlay/TaskbarOverlayControllerTest.kt @@ -0,0 +1,215 @@ +/* + * 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.launcher3.taskbar.overlay + +import android.app.ActivityManager.RunningTaskInfo +import android.view.MotionEvent +import androidx.test.annotation.UiThreadTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.launcher3.AbstractFloatingView +import com.android.launcher3.AbstractFloatingView.TYPE_OPTIONS_POPUP +import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_ALL_APPS +import com.android.launcher3.AbstractFloatingView.TYPE_TASKBAR_OVERLAY_PROXY +import com.android.launcher3.AbstractFloatingView.hasOpenView +import com.android.launcher3.taskbar.TaskbarActivityContext +import com.android.launcher3.taskbar.TaskbarUnitTestRule +import com.android.launcher3.taskbar.TaskbarUnitTestRule.InjectController +import com.android.launcher3.util.LauncherMultivalentJUnit +import com.android.launcher3.util.LauncherMultivalentJUnit.EmulatedDevices +import com.android.systemui.shared.system.TaskStackChangeListeners +import com.google.common.truth.Truth.assertThat +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(LauncherMultivalentJUnit::class) +@EmulatedDevices(["pixelFoldable2023"]) +class TaskbarOverlayControllerTest { + + @get:Rule val taskbarUnitTestRule = TaskbarUnitTestRule() + @InjectController lateinit var overlayController: TaskbarOverlayController + + private val taskbarContext: TaskbarActivityContext + get() = taskbarUnitTestRule.activityContext + + @Test + @UiThreadTest + fun testRequestWindow_twice_reusesWindow() { + val context1 = overlayController.requestWindow() + val context2 = overlayController.requestWindow() + assertThat(context1).isSameInstanceAs(context2) + } + + @Test + @UiThreadTest + fun testRequestWindow_afterHidingExistingWindow_createsNewWindow() { + val context1 = overlayController.requestWindow() + overlayController.hideWindow() + + val context2 = overlayController.requestWindow() + assertThat(context1).isNotSameInstanceAs(context2) + } + + @Test + @UiThreadTest + fun testRequestWindow_addsProxyView() { + TestOverlayView.show(overlayController.requestWindow()) + assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() + } + + @Test + @UiThreadTest + fun testRequestWindow_closeProxyView_closesOverlay() { + val overlay = TestOverlayView.show(overlayController.requestWindow()) + AbstractFloatingView.closeOpenContainer(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY) + assertThat(overlay.isOpen).isFalse() + } + + @Test + @UiThreadTest + fun testHideWindow_closesOverlay() { + val overlay = TestOverlayView.show(overlayController.requestWindow()) + overlayController.hideWindow() + assertThat(overlay.isOpen).isFalse() + } + + @Test + @UiThreadTest + fun testTwoOverlays_closeOne_windowStaysOpen() { + val context = overlayController.requestWindow() + val overlay1 = TestOverlayView.show(context) + val overlay2 = TestOverlayView.show(context) + + overlay1.close(false) + assertThat(overlay2.isOpen).isTrue() + assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isTrue() + } + + @Test + @UiThreadTest + fun testTwoOverlays_closeAll_closesWindow() { + val context = overlayController.requestWindow() + val overlay1 = TestOverlayView.show(context) + val overlay2 = TestOverlayView.show(context) + + overlay1.close(false) + overlay2.close(false) + assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() + } + + @Test + @UiThreadTest + fun testRecreateTaskbar_closesWindow() { + TestOverlayView.show(overlayController.requestWindow()) + taskbarUnitTestRule.recreateTaskbar() + assertThat(hasOpenView(taskbarContext, TYPE_TASKBAR_OVERLAY_PROXY)).isFalse() + } + + @Test + fun testTaskMovedToFront_closesOverlay() { + lateinit var overlay: TestOverlayView + getInstrumentation().runOnMainSync { + overlay = TestOverlayView.show(overlayController.requestWindow()) + } + + TaskStackChangeListeners.getInstance().listenerImpl.onTaskMovedToFront(RunningTaskInfo()) + // Make sure TaskStackChangeListeners' Handler posts the callback before checking state. + getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + } + + @Test + fun testTaskStackChanged_allAppsClosed_overlayStaysOpen() { + lateinit var overlay: TestOverlayView + getInstrumentation().runOnMainSync { + overlay = TestOverlayView.show(overlayController.requestWindow()) + taskbarContext.controllers.sharedState?.allAppsVisible = false + } + + TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() + getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isTrue() } + } + + @Test + fun testTaskStackChanged_allAppsOpen_closesOverlay() { + lateinit var overlay: TestOverlayView + getInstrumentation().runOnMainSync { + overlay = TestOverlayView.show(overlayController.requestWindow()) + taskbarContext.controllers.sharedState?.allAppsVisible = true + } + + TaskStackChangeListeners.getInstance().listenerImpl.onTaskStackChanged() + getInstrumentation().runOnMainSync { assertThat(overlay.isOpen).isFalse() } + } + + @Test + @UiThreadTest + fun testUpdateLauncherDeviceProfile_overlayNotRebindSafe_closesOverlay() { + val overlayContext = overlayController.requestWindow() + val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_OPTIONS_POPUP } + + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(overlayContext) + .setGestureMode(false) + .build() + ) + + assertThat(overlay.isOpen).isFalse() + } + + @Test + @UiThreadTest + fun testUpdateLauncherDeviceProfile_overlayRebindSafe_overlayStaysOpen() { + val overlayContext = overlayController.requestWindow() + val overlay = TestOverlayView.show(overlayContext).apply { type = TYPE_TASKBAR_ALL_APPS } + + overlayController.updateLauncherDeviceProfile( + overlayController.launcherDeviceProfile + .toBuilder(overlayContext) + .setGestureMode(false) + .build() + ) + + assertThat(overlay.isOpen).isTrue() + } + + private class TestOverlayView + private constructor( + private val overlayContext: TaskbarOverlayContext, + ) : AbstractFloatingView(overlayContext, null) { + + var type = TYPE_OPTIONS_POPUP + + private fun show() { + mIsOpen = true + overlayContext.dragLayer.addView(this) + } + + override fun onControllerInterceptTouchEvent(ev: MotionEvent?): Boolean = false + + override fun handleClose(animate: Boolean) = overlayContext.dragLayer.removeView(this) + + override fun isOfType(type: Int): Boolean = (type and this.type) != 0 + + companion object { + /** Adds a generic View to the Overlay window for testing. */ + fun show(context: TaskbarOverlayContext): TestOverlayView { + return TestOverlayView(context).apply { show() } + } + } + } +}