summaryrefslogtreecommitdiff
path: root/quickstep
diff options
context:
space:
mode:
Diffstat (limited to 'quickstep')
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt18
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt14
-rw-r--r--quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java8
-rw-r--r--quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt100
-rw-r--r--quickstep/src/com/android/quickstep/LauncherBackAnimationController.java91
-rw-r--r--quickstep/src/com/android/quickstep/TaskOverlayFactory.java1
-rw-r--r--quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt285
-rw-r--r--quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt90
8 files changed, 575 insertions, 32 deletions
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
index eac5235dda..06e6734a37 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewDismissTouchController.kt
@@ -25,6 +25,7 @@ import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.R
import com.android.launcher3.Utilities.EDGE_NAV_BAR
import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.Utilities.debugLog
import com.android.launcher3.Utilities.isRtl
import com.android.launcher3.Utilities.mapToRange
import com.android.launcher3.touch.SingleAxisSwipeDetector
@@ -70,6 +71,7 @@ CONTAINER : RecentsViewContainer {
// Don't intercept swipes on the nav bar, as user might be trying to go home during a
// task dismiss animation.
(ev.edgeFlags and EDGE_NAV_BAR) != 0 -> {
+ debugLog(TAG, "Not intercepting edge swipe on nav bar.")
false
}
@@ -77,14 +79,23 @@ CONTAINER : RecentsViewContainer {
AbstractFloatingView.getTopOpenViewWithType(
container,
AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT,
- ) != null -> false
+ ) != null -> {
+ debugLog(TAG, "Not intercepting, open floating view blocking touch.")
+ false
+ }
// Disable swiping if the task overlay is modal.
taskViewRecentsTouchContext.isRecentsModal -> {
+ debugLog(TAG, "Not intercepting touch in modal overlay.")
false
}
- else -> taskViewRecentsTouchContext.isRecentsInteractive
+ else ->
+ taskViewRecentsTouchContext.isRecentsInteractive.also { isRecentsInteractive ->
+ if (!isRecentsInteractive) {
+ debugLog(TAG, "Not intercepting touch, recents not interactive.")
+ }
+ }
}
override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
@@ -140,6 +151,7 @@ CONTAINER : RecentsViewContainer {
override fun onDragStart(start: Boolean, startDisplacement: Float) {
if (isBlockedDuringDismissal) return
val taskBeingDragged = taskBeingDragged ?: return
+ debugLog(TAG, "Handling touch event.")
initialDisplacement =
taskBeingDragged.secondaryDismissTranslationProperty.get(taskBeingDragged)
@@ -289,6 +301,8 @@ CONTAINER : RecentsViewContainer {
}
companion object {
+ private const val TAG = "TaskViewDismissTouchController"
+
private const val DISMISS_THRESHOLD_FRACTION = 0.5f
private const val DISMISS_THRESHOLD_HAPTIC_RANGE = 10f
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
index 8ee552d046..fe9cae55f9 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewLaunchTouchController.kt
@@ -24,6 +24,7 @@ import com.android.launcher3.AbstractFloatingView
import com.android.launcher3.LauncherAnimUtils
import com.android.launcher3.Utilities.EDGE_NAV_BAR
import com.android.launcher3.Utilities.boundToRange
+import com.android.launcher3.Utilities.debugLog
import com.android.launcher3.Utilities.isRtl
import com.android.launcher3.anim.AnimatorPlaybackController
import com.android.launcher3.touch.BaseSwipeDetector
@@ -72,6 +73,7 @@ CONTAINER : RecentsViewContainer {
// Don't intercept swipes on the nav bar, as user might be trying to go home during a
// task dismiss animation.
(ev.edgeFlags and EDGE_NAV_BAR) != 0 -> {
+ debugLog(TAG, "Not intercepting edge swipe on nav bar.")
false
}
@@ -80,15 +82,22 @@ CONTAINER : RecentsViewContainer {
container,
AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT,
) != null -> {
+ debugLog(TAG, "Not intercepting, open floating view blocking touch.")
false
}
// Disable swiping if the task overlay is modal.
taskViewRecentsTouchContext.isRecentsModal -> {
+ debugLog(TAG, "Not intercepting touch in modal overlay.")
false
}
- else -> taskViewRecentsTouchContext.isRecentsInteractive
+ else ->
+ taskViewRecentsTouchContext.isRecentsInteractive.also { isRecentsInteractive ->
+ if (!isRecentsInteractive) {
+ debugLog(TAG, "Not intercepting touch, recents not interactive.")
+ }
+ }
}
override fun onControllerInterceptTouchEvent(ev: MotionEvent): Boolean {
@@ -128,6 +137,7 @@ CONTAINER : RecentsViewContainer {
recentsView.pagedOrientationHandler.getTaskDragDisplacementFactor(isRtl)
}
if (!canTaskLaunchTaskView(taskBeingDragged)) {
+ debugLog(TAG, "Not intercepting touch, task cannot be launched.")
return false
}
detector.setDetectableScrollConditions(downDirection, /* ignoreSlop= */ false)
@@ -136,6 +146,7 @@ CONTAINER : RecentsViewContainer {
override fun onDragStart(start: Boolean, startDisplacement: Float) {
val taskBeingDragged = taskBeingDragged ?: return
+ debugLog(TAG, "Handling touch event.")
val secondaryLayerDimension: Int =
recentsView.pagedOrientationHandler.getSecondaryDimension(container.getDragLayer())
@@ -202,6 +213,7 @@ CONTAINER : RecentsViewContainer {
}
companion object {
+ private const val TAG = "TaskViewLaunchTouchController"
private const val LAUNCH_THRESHOLD_FRACTION: Float = 0.5f
}
}
diff --git a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
index f26bd13586..57ffd9538d 100644
--- a/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
+++ b/quickstep/src/com/android/launcher3/uioverrides/touchcontrollers/TaskViewTouchControllerDeprecated.java
@@ -17,6 +17,7 @@ package com.android.launcher3.uioverrides.touchcontrollers;
import static com.android.launcher3.AbstractFloatingView.TYPE_TOUCH_CONTROLLER_NO_INTERCEPT;
import static com.android.launcher3.LauncherAnimUtils.SUCCESS_TRANSITION_PROGRESS;
+import static com.android.launcher3.Utilities.debugLog;
import static com.android.launcher3.touch.SingleAxisSwipeDetector.DIRECTION_BOTH;
import android.animation.Animator;
@@ -56,6 +57,7 @@ import com.android.quickstep.views.TaskView;
public class TaskViewTouchControllerDeprecated<
CONTAINER extends Context & RecentsViewContainer> extends AnimatorListenerAdapter
implements TouchController, SingleAxisSwipeDetector.Listener {
+ private static final String TAG = "TaskViewTouchControllerDeprecated";
private static final float ANIMATION_PROGRESS_FRACTION_MIDPOINT = 0.5f;
private static final long MIN_TASK_DISMISS_ANIMATION_DURATION = 300;
@@ -110,6 +112,7 @@ public class TaskViewTouchControllerDeprecated<
if (mCurrentAnimation != null) {
mCurrentAnimation.getAnimationPlayer().end();
}
+ debugLog(TAG, "Not intercepting edge swipe on nav bar.");
return false;
}
if (mCurrentAnimation != null) {
@@ -121,6 +124,7 @@ public class TaskViewTouchControllerDeprecated<
}
if (AbstractFloatingView.getTopOpenViewWithType(
mContainer, TYPE_TOUCH_CONTROLLER_NO_INTERCEPT) != null) {
+ debugLog(TAG, "Not intercepting, open floating view blocking touch.");
return false;
}
return mTaskViewRecentsTouchContext.isRecentsInteractive();
@@ -142,6 +146,7 @@ public class TaskViewTouchControllerDeprecated<
if (ev.getAction() == MotionEvent.ACTION_DOWN) {
mNoIntercept = !canInterceptTouch(ev);
if (mNoIntercept) {
+ debugLog(TAG, "Not intercepting touch.");
return false;
}
@@ -186,6 +191,7 @@ public class TaskViewTouchControllerDeprecated<
}
if (mTaskBeingDragged == null) {
mNoIntercept = true;
+ debugLog(TAG, "Not intercepting touch, no task to drag.");
return false;
}
}
@@ -195,6 +201,7 @@ public class TaskViewTouchControllerDeprecated<
}
if (mNoIntercept) {
+ debugLog(TAG, "Not intercepting touch.");
return false;
}
@@ -266,6 +273,7 @@ public class TaskViewTouchControllerDeprecated<
@Override
public void onDragStart(boolean start, float startDisplacement) {
if (!mDraggingEnabled) return;
+ debugLog(TAG, "Handling touch.");
RecentsPagedOrientationHandler orientationHandler =
mRecentsView.getPagedOrientationHandler();
diff --git a/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt b/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt
new file mode 100644
index 0000000000..68860ac91b
--- /dev/null
+++ b/quickstep/src/com/android/quickstep/AspectRatioSystemShortcut.kt
@@ -0,0 +1,100 @@
+/*
+ * Copyright (C) 2025 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.quickstep
+
+import android.content.Intent
+import android.provider.Settings
+import android.view.View
+import androidx.core.net.toUri
+import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.popup.SystemShortcut
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskContainer
+import com.android.window.flags.Flags.universalResizableByDefault
+
+/**
+ * System shortcut to change the application's aspect ratio compatibility mode.
+ *
+ * This shows up only on screens that are not compact, ie. shortest-width greater than {@link
+ * com.android.launcher3.util.window.WindowManagerProxy#MIN_TABLET_WIDTH}.
+ */
+class AspectRatioSystemShortcut(
+ viewContainer: RecentsViewContainer,
+ taskContainer: TaskContainer,
+ abstractFloatingViewHelper: AbstractFloatingViewHelper,
+) :
+ SystemShortcut<RecentsViewContainer>(
+ R.drawable.ic_aspect_ratio,
+ R.string.recent_task_option_aspect_ratio,
+ viewContainer,
+ taskContainer.itemInfo,
+ taskContainer.taskView,
+ abstractFloatingViewHelper,
+ ) {
+ override fun onClick(view: View) {
+ dismissTaskMenuView()
+
+ val intent =
+ Intent(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS)
+ .addFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_CLEAR_TASK)
+ if (mItemInfo.targetPackage != null) {
+ intent.setData(("package:" + mItemInfo.targetPackage).toUri())
+ }
+
+ mTarget.startActivitySafely(view, intent, mItemInfo)
+ mTarget
+ .statsLogManager
+ .logger()
+ .withItemInfo(mItemInfo)
+ .log(LauncherEvent.LAUNCHER_ASPECT_RATIO_SETTINGS_SYSTEM_SHORTCUT_TAP)
+ }
+
+ companion object {
+ /** Optionally create a factory for the aspect ratio system shortcut. */
+ @JvmOverloads
+ fun createFactory(
+ abstractFloatingViewHelper: AbstractFloatingViewHelper = AbstractFloatingViewHelper()
+ ): TaskShortcutFactory {
+ return object : TaskShortcutFactory {
+ override fun getShortcuts(
+ viewContainer: RecentsViewContainer,
+ taskContainer: TaskContainer,
+ ): List<AspectRatioSystemShortcut>? {
+ return when {
+ // Only available when the feature flag is on.
+ !universalResizableByDefault() -> null
+
+ // The option is only shown on sw600dp+ screens (checked by isTablet)
+ !viewContainer.deviceProfile.isTablet -> null
+
+ else -> {
+ listOf(
+ AspectRatioSystemShortcut(
+ viewContainer,
+ taskContainer,
+ abstractFloatingViewHelper,
+ )
+ )
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
index 783ec2c7da..c6785629b0 100644
--- a/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
+++ b/quickstep/src/com/android/quickstep/LauncherBackAnimationController.java
@@ -26,6 +26,7 @@ import static com.android.launcher3.BaseActivity.INVISIBLE_ALL;
import static com.android.launcher3.BaseActivity.INVISIBLE_BY_PENDING_FLAGS;
import static com.android.launcher3.BaseActivity.PENDING_INVISIBLE_BY_WALLPAPER_ANIMATION;
import static com.android.window.flags.Flags.predictiveBackThreeButtonNav;
+import static com.android.window.flags.Flags.removeDepartTargetFromMotion;
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
@@ -121,6 +122,7 @@ public class LauncherBackAnimationController {
private final SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
private float mBackProgress = 0;
private boolean mBackInProgress = false;
+ private boolean mWaitStartTransition = false;
private OnBackInvokedCallbackStub mBackCallback;
private IRemoteAnimationFinishedCallback mAnimationFinishedCallback;
private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
@@ -158,7 +160,8 @@ public class LauncherBackAnimationController {
mBackCallback = new OnBackInvokedCallbackStub(handler, mProgressAnimator,
mProgressInterpolator, this);
SystemUiProxy.INSTANCE.get(mLauncher).setBackToLauncherCallback(mBackCallback,
- new RemoteAnimationRunnerStub(this));
+ new RemoteAnimationRunnerStub(this,
+ removeDepartTargetFromMotion() ? handler : null));
}
private static class OnBackInvokedCallbackStub extends IOnBackInvokedCallback.Stub {
@@ -195,7 +198,14 @@ public class LauncherBackAnimationController {
mHandler.post(() -> {
LauncherBackAnimationController controller = mControllerRef.get();
if (controller != null) {
- controller.startTransition();
+ if (!removeDepartTargetFromMotion()) {
+ controller.startTransition();
+ } else {
+ controller.mWaitStartTransition = true;
+ if (controller.mBackTarget != null && controller.mBackInProgress) {
+ controller.startTransition();
+ }
+ }
}
mProgressAnimator.reset();
});
@@ -220,7 +230,8 @@ public class LauncherBackAnimationController {
mHandler.post(() -> {
LauncherBackAnimationController controller = mControllerRef.get();
if (controller != null) {
- controller.startBack(backEvent);
+ controller.initBackMotion(backEvent);
+ controller.tryStartBackAnimation();
mProgressAnimator.onBackStarted(backEvent, event -> {
float backProgress = event.getProgress();
controller.mBackProgress =
@@ -248,9 +259,12 @@ public class LauncherBackAnimationController {
// LauncherBackAnimationController has strong reference to Launcher activity, the binder
// callback should not hold strong reference to it to avoid memory leak.
private WeakReference<LauncherBackAnimationController> mControllerRef;
+ private final Handler mHandler;
- private RemoteAnimationRunnerStub(LauncherBackAnimationController controller) {
+ private RemoteAnimationRunnerStub(LauncherBackAnimationController controller,
+ Handler handler) {
mControllerRef = new WeakReference<>(controller);
+ mHandler = handler;
}
@Override
@@ -261,15 +275,29 @@ public class LauncherBackAnimationController {
if (controller == null) {
return;
}
- for (final RemoteAnimationTarget target : apps) {
- if (MODE_CLOSING == target.mode) {
- controller.mBackTarget = target;
+ final Runnable r = () -> {
+ for (final RemoteAnimationTarget target : apps) {
+ if (MODE_CLOSING == target.mode) {
+ controller.mBackTarget = target;
+ }
+ if (MODE_OPENING == target.mode) {
+ controller.mLauncherTarget = target;
+ }
}
- if (MODE_OPENING == target.mode) {
- controller.mLauncherTarget = target;
+ controller.mAnimationFinishedCallback = finishedCallback;
+ if (!removeDepartTargetFromMotion()) {
+ return;
+ }
+ controller.tryStartBackAnimation();
+ if (controller.mWaitStartTransition) {
+ controller.startTransition();
}
+ };
+ if (mHandler != null) {
+ mHandler.post(r);
+ } else {
+ r.run();
}
- controller.mAnimationFinishedCallback = finishedCallback;
}
@Override
@@ -294,34 +322,39 @@ public class LauncherBackAnimationController {
mBackCallback = null;
}
- private void startBack(BackMotionEvent backEvent) {
+ private void initBackMotion(BackMotionEvent backEvent) {
// in case we're still animating an onBackCancelled event, let's remove the finish-
// callback from the progress animator to prevent calling finishAnimation() before
// restarting a new animation
- // Side note: startBack is never called during the post-commit phase if the back gesture
- // was committed (not cancelled). BackAnimationController prevents that. Therefore we
- // don't have to handle that case.
+ // Side note: initBackMotion is never called during the post-commit phase if the back
+ // gesture was committed (not cancelled). BackAnimationController prevents that. Therefore
+ // we don't have to handle that case.
mProgressAnimator.removeOnBackCancelledFinishCallback();
+ if (!removeDepartTargetFromMotion()) {
+ RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
+ if (appTarget == null || appTarget.leash == null || !appTarget.leash.isValid()) {
+ return;
+ }
+ mBackTarget = appTarget;
+ }
mBackInProgress = true;
- RemoteAnimationTarget appTarget = backEvent.getDepartingAnimationTarget();
-
- if (appTarget == null || appTarget.leash == null || !appTarget.leash.isValid()) {
+ mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
+ }
+ private void tryStartBackAnimation() {
+ if (mBackTarget == null || (removeDepartTargetFromMotion() && !mBackInProgress)) {
return;
}
mTransaction
- .show(appTarget.leash)
+ .show(mBackTarget.leash)
.setAnimationTransaction();
- mBackTarget = appTarget;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- mStartRect.set(appTarget.windowConfiguration.getMaxBounds());
+ mStartRect.set(mBackTarget.windowConfiguration.getMaxBounds());
// inset bottom in case of taskbar being present
if (!predictiveBackThreeButtonNav() || mLauncher.getDeviceProfile().isTaskbarPresent
|| DisplayController.getNavigationMode(mLauncher) == NavigationMode.NO_BUTTON) {
- mStartRect.inset(0, 0, 0, appTarget.contentInsets.bottom);
+ mStartRect.inset(0, 0, 0, mBackTarget.contentInsets.bottom);
}
mLauncherTargetView = mQuickstepTransitionManager.findLauncherView(
@@ -466,10 +499,14 @@ public class LauncherBackAnimationController {
}
private void startTransition() {
- if (mBackTarget == null) {
- // Trigger transition system instead of custom transition animation.
- finishAnimation();
- return;
+ if (!removeDepartTargetFromMotion()) {
+ if (mBackTarget == null) {
+ // Trigger transition system instead of custom transition animation.
+ finishAnimation();
+ return;
+ }
+ } else {
+ mWaitStartTransition = false;
}
if (mLauncher.isDestroyed()) {
return;
diff --git a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
index ae6bfbc517..5bf4451fad 100644
--- a/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
+++ b/quickstep/src/com/android/quickstep/TaskOverlayFactory.java
@@ -118,6 +118,7 @@ public class TaskOverlayFactory implements ResourceBasedOverride {
TaskShortcutFactory.FREE_FORM,
DesktopSystemShortcut.Companion.createFactory(),
ExternalDisplaySystemShortcut.Companion.createFactory(),
+ AspectRatioSystemShortcut.Companion.createFactory(),
TaskShortcutFactory.WELLBEING,
TaskShortcutFactory.SAVE_APP_PAIR,
TaskShortcutFactory.SCREENSHOT,
diff --git a/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt
new file mode 100644
index 0000000000..10e85e6a1b
--- /dev/null
+++ b/quickstep/tests/src/com/android/quickstep/AspectRatioSystemShortcutTests.kt
@@ -0,0 +1,285 @@
+/*
+ * Copyright (C) 2025 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.quickstep
+
+import android.content.ComponentName
+import android.content.Context
+import android.content.ContextWrapper
+import android.content.Intent
+import android.platform.test.annotations.DisableFlags
+import android.platform.test.annotations.EnableFlags
+import android.platform.test.flag.junit.SetFlagsRule
+import android.provider.Settings
+import android.view.Display.DEFAULT_DISPLAY
+import android.view.LayoutInflater
+import android.view.Surface
+import android.view.View
+import android.view.ViewGroup
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.launcher3.AbstractFloatingView
+import com.android.launcher3.AbstractFloatingViewHelper
+import com.android.launcher3.Flags.enableRefactorTaskThumbnail
+import com.android.launcher3.InvariantDeviceProfile
+import com.android.launcher3.R
+import com.android.launcher3.logging.StatsLogManager
+import com.android.launcher3.logging.StatsLogManager.LauncherEvent
+import com.android.launcher3.logging.StatsLogManager.StatsLogger
+import com.android.launcher3.model.data.ItemInfo
+import com.android.launcher3.model.data.TaskViewItemInfo
+import com.android.launcher3.util.RunnableList
+import com.android.launcher3.util.SplitConfigurationOptions
+import com.android.launcher3.util.TransformingTouchDelegate
+import com.android.launcher3.util.WindowBounds
+import com.android.quickstep.orientation.LandscapePagedViewHandler
+import com.android.quickstep.recents.data.RecentsDeviceProfileRepository
+import com.android.quickstep.recents.data.RecentsRotationStateRepository
+import com.android.quickstep.recents.di.RecentsDependencies
+import com.android.quickstep.task.thumbnail.TaskThumbnailView
+import com.android.quickstep.util.RecentsOrientedState
+import com.android.quickstep.views.LauncherRecentsView
+import com.android.quickstep.views.RecentsViewContainer
+import com.android.quickstep.views.TaskContainer
+import com.android.quickstep.views.TaskThumbnailViewDeprecated
+import com.android.quickstep.views.TaskView
+import com.android.quickstep.views.TaskViewIcon
+import com.android.systemui.shared.recents.model.Task
+import com.android.systemui.shared.recents.model.Task.TaskKey
+import com.android.window.flags.Flags
+import com.google.common.truth.Truth.assertThat
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.mockito.Mockito
+import org.mockito.Mockito.eq
+import org.mockito.Mockito.isNull
+import org.mockito.kotlin.any
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+/** Test for [AspectRatioSystemShortcut] */
+class AspectRatioSystemShortcutTests {
+
+ @get:Rule val setFlagsRule = SetFlagsRule(SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT)
+
+ /** Spy on a concrete Context so we can reference real View, Layout, and Display properties. */
+ private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
+
+ /**
+ * RecentsViewContainer and its super-interface ActivityContext contain methods to convert
+ * themselves to a Context at runtime, and static methods to convert a Context back to
+ * themselves by traversing ContextWrapper layers.
+ *
+ * Thus there is an undocumented assumption that a RecentsViewContainer always extends Context.
+ * We need to mock all of the RecentsViewContainer methods but leave the Context-under-test
+ * intact.
+ *
+ * The simplest way is to extend ContextWrapper and delegate the RecentsViewContainer interface
+ * to a mock.
+ */
+ class RecentsViewContainerContextWrapper(base: Context) :
+ ContextWrapper(base), RecentsViewContainer by mock() {
+
+ private val statsLogManager: StatsLogManager = mock()
+
+ override fun getStatsLogManager(): StatsLogManager = statsLogManager
+
+ override fun startActivitySafely(v: View, intent: Intent, item: ItemInfo?): RunnableList? =
+ null
+ }
+
+ /**
+ * This <RecentsViewContainer & Context> is implicitly required in many parts of Launcher that
+ * require a Context. See RecentsViewContainerContextWrapper.
+ */
+ private val launcher: RecentsViewContainerContextWrapper =
+ spy(RecentsViewContainerContextWrapper(context))
+
+ private val recentsView: LauncherRecentsView = mock()
+ private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
+ private val taskOverlayFactory: TaskOverlayFactory =
+ mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
+ private val factory: TaskShortcutFactory =
+ AspectRatioSystemShortcut.createFactory(abstractFloatingViewHelper)
+ private val statsLogger = mock<StatsLogger>()
+ private val orientedState: RecentsOrientedState =
+ mock(defaultAnswer = Mockito.RETURNS_DEEP_STUBS)
+ private val taskView: TaskView =
+ LayoutInflater.from(context).cloneInContext(launcher).inflate(R.layout.task, null) as
+ TaskView
+
+ @Before
+ fun setUp() {
+ whenever(launcher.getOverviewPanel<LauncherRecentsView>()).thenReturn(recentsView)
+
+ val statsLogManager = launcher.getStatsLogManager()
+ whenever(statsLogManager.logger()).thenReturn(statsLogger)
+ whenever(statsLogger.withItemInfo(any())).thenReturn(statsLogger)
+
+ whenever(orientedState.orientationHandler).thenReturn(LandscapePagedViewHandler())
+ taskView.setLayoutParams(ViewGroup.LayoutParams(MATCH_PARENT, MATCH_PARENT))
+
+ if (enableRefactorTaskThumbnail()) {
+ val recentsDependencies = RecentsDependencies.maybeInitialize(launcher)
+ val scopeId = recentsDependencies.createRecentsViewScope(launcher)
+ recentsDependencies.provide(
+ RecentsRotationStateRepository::class.java,
+ scopeId,
+ { mock<RecentsRotationStateRepository>() }
+ )
+ recentsDependencies.provide(
+ RecentsDeviceProfileRepository::class.java,
+ scopeId,
+ { mock<RecentsDeviceProfileRepository>() }
+ )
+ }
+ }
+
+ @After
+ fun tearDown() {
+ if (enableRefactorTaskThumbnail()) {
+ RecentsDependencies.destroy(launcher)
+ }
+ }
+
+ /**
+ * When the corresponding feature flag is off, there will not be an option to open aspect ratio
+ * settings.
+ */
+ @DisableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT)
+ @Test
+ fun createShortcut_flaggedOff_notCreated() {
+ val task = createTask()
+ val taskContainer = createTaskContainer(task)
+
+ setScreenSizeDp(widthDp = 1200, heightDp = 800)
+ taskView.bind(task, orientedState, taskOverlayFactory)
+
+ assertThat(factory.getShortcuts(launcher, taskContainer)).isNull()
+ }
+
+ /**
+ * When the screen doesn't meet or exceed sw600dp (eg. phone, watch), there will not be an
+ * option to open aspect ratio settings.
+ */
+ @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT)
+ @Test
+ fun createShortcut_sw599dp_notCreated() {
+ val task = createTask()
+ val taskContainer = createTaskContainer(task)
+
+ setScreenSizeDp(widthDp = 599, heightDp = 599)
+ taskView.bind(task, orientedState, taskOverlayFactory)
+
+ assertThat(factory.getShortcuts(launcher, taskContainer)).isNull()
+ }
+
+ /**
+ * When the screen does meet or exceed sw600dp (eg. tablet, inner foldable screen, home cinema)
+ * there will be an option to open aspect ratio settings.
+ */
+ @EnableFlags(com.android.window.flags.Flags.FLAG_UNIVERSAL_RESIZABLE_BY_DEFAULT)
+ @Test
+ fun createShortcut_sw800dp_created_andOpensSettings() {
+ val task = createTask()
+ val taskContainer = spy(createTaskContainer(task))
+ val taskViewItemInfo = mock<TaskViewItemInfo>()
+ doReturn(taskViewItemInfo).whenever(taskContainer).itemInfo
+
+ setScreenSizeDp(widthDp = 1200, heightDp = 800)
+ taskView.bind(task, orientedState, taskOverlayFactory)
+
+ val shortcuts = factory.getShortcuts(launcher, taskContainer)
+ assertThat(shortcuts).hasSize(1)
+
+ // On clicking the shortcut:
+ val shortcut = shortcuts!!.first() as AspectRatioSystemShortcut
+ shortcut.onClick(taskView)
+
+ // 1) Panel should be closed
+ val allTypesExceptRebindSafe =
+ AbstractFloatingView.TYPE_ALL and AbstractFloatingView.TYPE_REBIND_SAFE.inv()
+ verify(abstractFloatingViewHelper).closeOpenViews(launcher, true, allTypesExceptRebindSafe)
+
+ // 2) Compat mode settings activity should be launched
+ val intentCaptor = argumentCaptor<Intent>()
+ verify(launcher)
+ .startActivitySafely(any<View>(), intentCaptor.capture(), eq(taskViewItemInfo))
+ val intent = intentCaptor.firstValue!!
+ assertThat(intent.action).isEqualTo(Settings.ACTION_MANAGE_USER_ASPECT_RATIO_SETTINGS)
+
+ // 3) Shortcut tap event should be reported
+ verify(statsLogger).withItemInfo(taskViewItemInfo)
+ verify(statsLogger).log(LauncherEvent.LAUNCHER_ASPECT_RATIO_SETTINGS_SYSTEM_SHORTCUT_TAP)
+ }
+
+ /**
+ * Overrides the screen size reported in the DeviceProfile, keeping the same pixel density as
+ * the underlying device and adjusting the pixel width/height to match what is required.
+ */
+ private fun setScreenSizeDp(widthDp: Int, heightDp: Int) {
+ val density = context.resources.configuration.densityDpi
+ val widthPx = widthDp * density / 160
+ val heightPx = heightDp * density / 160
+
+ val screenBounds = WindowBounds(widthPx, heightPx, widthPx, heightPx, Surface.ROTATION_0)
+ val deviceProfile =
+ InvariantDeviceProfile.INSTANCE[context].getDeviceProfile(context)
+ .toBuilder(context)
+ .setWindowBounds(screenBounds)
+ .build()
+ whenever(launcher.getDeviceProfile()).thenReturn(deviceProfile)
+ }
+
+ /** Create a (very) fake task for testing. */
+ private fun createTask() =
+ Task(
+ TaskKey(
+ /* id */ 1,
+ /* windowingMode */ 0,
+ Intent(),
+ ComponentName("", ""),
+ /* userId */ 0,
+ /* lastActiveTime */ 2000,
+ DEFAULT_DISPLAY,
+ ComponentName("", ""),
+ /* numActivities */ 1,
+ /* isTopActivityNoDisplay */ false,
+ /* isActivityStackTransparent */ false,
+ )
+ )
+
+ /** Create TaskContainer out of a given Task and fill in the rest with mocks. */
+ private fun createTaskContainer(task: Task) =
+ TaskContainer(
+ taskView,
+ task,
+ if (enableRefactorTaskThumbnail()) mock<TaskThumbnailView>()
+ else mock<TaskThumbnailViewDeprecated>(),
+ mock<TaskViewIcon>(),
+ mock<TransformingTouchDelegate>(),
+ SplitConfigurationOptions.STAGE_POSITION_UNDEFINED,
+ digitalWellBeingToast = null,
+ showWindowsView = null,
+ taskOverlayFactory,
+ )
+}
diff --git a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
index 5f61ba2e07..8a2393d539 100644
--- a/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
+++ b/quickstep/tests/src/com/android/quickstep/DesktopSystemShortcutTest.kt
@@ -16,9 +16,11 @@
package com.android.quickstep
+import android.Manifest.permission.SYSTEM_ALERT_WINDOW
import android.content.ComponentName
import android.content.Context
import android.content.Intent
+import android.content.pm.PackageInfo
import android.content.pm.PackageManager
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
@@ -44,6 +46,7 @@ import com.android.quickstep.views.TaskContainer
import com.android.quickstep.views.TaskThumbnailViewDeprecated
import com.android.quickstep.views.TaskView
import com.android.quickstep.views.TaskViewIcon
+import com.android.quickstep.views.TaskViewType
import com.android.systemui.shared.recents.model.Task
import com.android.systemui.shared.recents.model.Task.TaskKey
import com.android.window.flags.Flags
@@ -54,6 +57,8 @@ import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
+import org.mockito.ArgumentMatchers.anyInt
+import org.mockito.ArgumentMatchers.anyString
import org.mockito.Mockito.`when`
import org.mockito.kotlin.any
import org.mockito.kotlin.doReturn
@@ -74,12 +79,12 @@ class DesktopSystemShortcutTest {
private val statsLogManager: StatsLogManager = mock()
private val statsLogger: StatsLogManager.StatsLogger = mock()
private val recentsView: LauncherRecentsView = mock()
- private val taskView: TaskView = mock()
private val abstractFloatingViewHelper: AbstractFloatingViewHelper = mock()
private val overlayFactory: TaskOverlayFactory = mock()
private val factory: TaskShortcutFactory =
DesktopSystemShortcut.createFactory(abstractFloatingViewHelper)
private val context: Context = spy(InstrumentationRegistry.getInstrumentation().targetContext)
+ private val taskView: TaskView = createTaskViewMock()
private lateinit var mockitoSession: StaticMockitoSession
@@ -135,6 +140,64 @@ class DesktopSystemShortcutTest {
}
@Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION,
+ )
+ fun createDesktopTaskShortcutFactoryPermissionEnabledAllowed_transparentTask() {
+ val packageManager: PackageManager = mock()
+ setUpTransparentPermission(packageManager, isAllowed = true)
+ val baseComponent = ComponentName("", /* class */ "")
+ val taskKey =
+ TaskKey(
+ /* id */ 1,
+ /* windowingMode */ 0,
+ Intent(),
+ baseComponent,
+ /* userId */ 0,
+ /* lastActiveTime */ 2000,
+ DEFAULT_DISPLAY,
+ baseComponent,
+ /* numActivities */ 1,
+ /* isTopActivityNoDisplay */ false,
+ /* isActivityStackTransparent */ true,
+ )
+ val taskContainer = createTaskContainer(Task(taskKey))
+ val shortcuts = factory.getShortcuts(launcher, taskContainer)
+ assertThat(shortcuts).isNull()
+ }
+
+ @Test
+ @EnableFlags(
+ Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY,
+ Flags.FLAG_ENABLE_MODALS_FULLSCREEN_WITH_PERMISSION,
+ )
+ fun createDesktopTaskShortcutFactoryPermissionEnabledNotAllowed_transparentTask() {
+ val packageManager: PackageManager = mock()
+ setUpTransparentPermission(packageManager, isAllowed = false)
+ val baseComponent = ComponentName("", /* class */ "")
+ val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
+ whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
+ val taskKey =
+ TaskKey(
+ /* id */ 1,
+ /* windowingMode */ 0,
+ Intent(),
+ baseComponent,
+ /* userId */ 0,
+ /* lastActiveTime */ 2000,
+ DEFAULT_DISPLAY,
+ baseComponent,
+ /* numActivities */ 1,
+ /* isTopActivityNoDisplay */ false,
+ /* isActivityStackTransparent */ true,
+ )
+ val taskContainer = createTaskContainer(Task(taskKey).apply { isDockable = true })
+ val shortcuts = factory.getShortcuts(launcher, taskContainer)
+ assertThat(shortcuts).isNotEmpty()
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun createDesktopTaskShortcutFactory_systemUiTask() {
val sysUiPackageName: String = context.resources.getString(R.string.config_systemUi)
@@ -162,8 +225,8 @@ class DesktopSystemShortcutTest {
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun createDesktopTaskShortcutFactory_defaultHomeTask() {
val packageManager: PackageManager = mock()
- val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
whenever(context.packageManager).thenReturn(packageManager)
+ val homeActivities = ComponentName("defaultHomePackage", /* class */ "")
whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities)
val taskKey =
TaskKey(
@@ -262,4 +325,27 @@ class DesktopSystemShortcutTest {
showWindowsView = null,
overlayFactory,
)
+
+ private fun setUpTransparentPermission(packageManager: PackageManager, isAllowed: Boolean) {
+ val packageInfo: PackageInfo = mock()
+ if (isAllowed) {
+ packageInfo.requestedPermissions = arrayOf(SYSTEM_ALERT_WINDOW)
+ }
+ whenever(context.packageManager).thenReturn(packageManager)
+ whenever(
+ packageManager.getPackageInfoAsUser(
+ anyString(),
+ eq(PackageManager.GET_PERMISSIONS),
+ anyInt(),
+ )
+ )
+ .thenReturn(packageInfo)
+ }
+
+ private fun createTaskViewMock(): TaskView {
+ val taskView: TaskView = mock()
+ whenever(taskView.type).thenReturn(TaskViewType.SINGLE)
+ whenever(taskView.context).thenReturn(context)
+ return taskView
+ }
}