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

Commit 9eaae4b6 authored by Brian Isganitis's avatar Brian Isganitis
Browse files

Initial TaskbarUnitTestRule with example overlay controller tests.

Flag: TEST_ONLY
Bug: 230027385
Test: TaskbarOverlayControllerTest
Change-Id: I858906ece7e67677962ec8b4432bfcca5ec30283
parent d141b309
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -1605,4 +1605,9 @@ public class TaskbarActivityContext extends BaseTaskbarContext {
    boolean canToggleHomeAllApps() {
        return mControllers.uiController.canToggleHomeAllApps();
    }

    @VisibleForTesting
    public TaskbarControllers getControllers() {
        return mControllers;
    }
}
+4 −2
Original line number Diff line number Diff line
@@ -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;
+19 −9
Original line number Diff line number Diff line
@@ -133,16 +133,19 @@ public final class TaskbarOverlayController {
     * <p>
     * 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) {
            if (!mIsOpen) return;
            mTaskbarContext.getDragLayer().removeView(this);
                Optional.ofNullable(mOverlayContext).ifPresent(c -> closeAllOpenViews(c, animate));
            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
+144 −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.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
}
+215 −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.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() }
            }
        }
    }
}