Initial TaskbarUnitTestRule with example overlay controller tests.

Flag: TEST_ONLY
Bug: 230027385
Test: TaskbarOverlayControllerTest
Change-Id: I858906ece7e67677962ec8b4432bfcca5ec30283
This commit is contained in:
Brian Isganitis
2024-06-03 23:20:07 +00:00
parent d141b3094d
commit 9eaae4b6a4
5 changed files with 387 additions and 11 deletions

View File

@@ -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
}

View File

@@ -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() }
}
}
}
}