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

Commit e59b640f authored by Brian Isganitis's avatar Brian Isganitis Committed by Android (Google) Code Review
Browse files

Merge "Initial TaskbarUnitTestRule with example overlay controller tests." into main

parents ec942d57 9eaae4b6
Loading
Loading
Loading
Loading
+5 −0
Original line number Diff line number Diff line
@@ -1602,4 +1602,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() }
            }
        }
    }
}