diff options
108 files changed, 9117 insertions, 7494 deletions
diff --git a/PREUPLOAD.cfg b/PREUPLOAD.cfg index d83109a1a986..5e0428bab467 100644 --- a/PREUPLOAD.cfg +++ b/PREUPLOAD.cfg @@ -18,7 +18,7 @@ clang_format = --commit ${PREUPLOAD_COMMIT} --style file --extensions c,h,cc,cpp tests/ tools/ bpfmt = -d -ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode +ktfmt = --kotlinlang-style --include-dirs=services/permission,packages/SystemUI,libs/WindowManager/Shell/src/com/android/wm/shell/freeform,libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode,libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode [Hook Scripts] checkstyle_hook = ${REPO_ROOT}/prebuilts/checkstyle/checkstyle.py --sha ${PREUPLOAD_COMMIT} diff --git a/core/java/android/hardware/contexthub/HubServiceInfo.java b/core/java/android/hardware/contexthub/HubServiceInfo.java index a1c52fb5864f..2f33e8f8872b 100644 --- a/core/java/android/hardware/contexthub/HubServiceInfo.java +++ b/core/java/android/hardware/contexthub/HubServiceInfo.java @@ -132,6 +132,21 @@ public final class HubServiceInfo implements Parcelable { return 0; } + @Override + public String toString() { + StringBuilder out = new StringBuilder(); + out.append("Service: "); + out.append("descriptor="); + out.append(mServiceDescriptor); + out.append(", format="); + out.append(mFormat); + out.append(", version="); + out.append(Integer.toHexString(mMajorVersion)); + out.append("."); + out.append(Integer.toHexString(mMinorVersion)); + return out.toString(); + } + /** Parcel implementation details */ @Override public void writeToParcel(@NonNull Parcel dest, int flags) { diff --git a/core/java/android/view/Display.java b/core/java/android/view/Display.java index 1ea226b013d6..0c8a0d60a96a 100644 --- a/core/java/android/view/Display.java +++ b/core/java/android/view/Display.java @@ -2056,6 +2056,22 @@ public final class Display { } /** + * Returns whether the display is eligible for hosting tasks. + * + * For example, if the display is used for mirroring, this will return {@code false}. + * + * TODO (b/383666349): Rename this later once there is a better option. + * + * @hide + */ + public boolean canHostTasks() { + synchronized (mLock) { + updateDisplayInfoLocked(); + return mIsValid && mDisplayInfo.canHostTasks; + } + } + + /** * Returns true if the specified UID has access to this display. * @hide */ diff --git a/core/java/android/view/DisplayInfo.java b/core/java/android/view/DisplayInfo.java index 43078847326c..ba098eb53246 100644 --- a/core/java/android/view/DisplayInfo.java +++ b/core/java/android/view/DisplayInfo.java @@ -408,6 +408,15 @@ public final class DisplayInfo implements Parcelable { @Nullable public String thermalBrightnessThrottlingDataId; + /** + * Indicates whether the display is eligible for hosting tasks. + * + * For example, if the display is used for mirroring, this will be {@code false}. + * + * @hide + */ + public boolean canHostTasks; + public static final @android.annotation.NonNull Creator<DisplayInfo> CREATOR = new Creator<DisplayInfo>() { @Override public DisplayInfo createFromParcel(Parcel source) { @@ -493,7 +502,8 @@ public final class DisplayInfo implements Parcelable { && BrightnessSynchronizer.floatEquals(hdrSdrRatio, other.hdrSdrRatio) && thermalRefreshRateThrottling.contentEquals(other.thermalRefreshRateThrottling) && Objects.equals( - thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId); + thermalBrightnessThrottlingDataId, other.thermalBrightnessThrottlingDataId) + && canHostTasks == other.canHostTasks; } @Override @@ -561,6 +571,7 @@ public final class DisplayInfo implements Parcelable { hdrSdrRatio = other.hdrSdrRatio; thermalRefreshRateThrottling = other.thermalRefreshRateThrottling; thermalBrightnessThrottlingDataId = other.thermalBrightnessThrottlingDataId; + canHostTasks = other.canHostTasks; } public void readFromParcel(Parcel source) { @@ -642,6 +653,7 @@ public final class DisplayInfo implements Parcelable { thermalRefreshRateThrottling = source.readSparseArray(null, SurfaceControl.RefreshRateRange.class); thermalBrightnessThrottlingDataId = source.readString8(); + canHostTasks = source.readBoolean(); } @Override @@ -717,6 +729,7 @@ public final class DisplayInfo implements Parcelable { dest.writeFloat(hdrSdrRatio); dest.writeSparseArray(thermalRefreshRateThrottling); dest.writeString8(thermalBrightnessThrottlingDataId); + dest.writeBoolean(canHostTasks); } @Override @@ -1020,6 +1033,8 @@ public final class DisplayInfo implements Parcelable { sb.append(thermalRefreshRateThrottling); sb.append(", thermalBrightnessThrottlingDataId "); sb.append(thermalBrightnessThrottlingDataId); + sb.append(", canHostTasks "); + sb.append(canHostTasks); sb.append("}"); return sb.toString(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java index 5dd49f0b5c0f..39ed2061c675 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenController.java @@ -302,7 +302,8 @@ public class SplitScreenController implements SplitDragPolicy.Starter, mTaskOrganizer, mDisplayController, mDisplayImeController, mDisplayInsetsController, mTransitions, mTransactionPool, mIconProvider, mMainExecutor, mMainHandler, mBgExecutor, mRecentTasksOptional, - mLaunchAdjacentController, mWindowDecorViewModel, mSplitState); + mLaunchAdjacentController, mWindowDecorViewModel, mSplitState, + mDesktopTasksController); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java index d0c21c9ec7c0..164dbe97c1f5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java @@ -142,6 +142,7 @@ import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.common.split.SplitState; import com.android.wm.shell.common.split.SplitWindowManager; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; @@ -222,6 +223,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, private final Optional<RecentTasksController> mRecentTasks; private final LaunchAdjacentController mLaunchAdjacentController; private final Optional<WindowDecorViewModel> mWindowDecorViewModel; + private final Optional<DesktopTasksController> mDesktopTasksController; /** Singleton source of truth for the current state of split screen on this device. */ private final SplitState mSplitState; @@ -358,7 +360,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellExecutor bgExecutor, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, - Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) { + Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, + Optional<DesktopTasksController> desktopTasksController) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -371,6 +374,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; mSplitState = splitState; + mDesktopTasksController = desktopTasksController; taskOrganizer.createRootTask(displayId, WINDOWING_MODE_FULLSCREEN, this /* listener */); @@ -443,7 +447,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, ShellExecutor bgExecutor, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, - Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) { + Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, + Optional<DesktopTasksController> desktopTasksController) { mContext = context; mDisplayId = displayId; mSyncQueue = syncQueue; @@ -465,6 +470,7 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mLaunchAdjacentController = launchAdjacentController; mWindowDecorViewModel = windowDecorViewModel; mSplitState = splitState; + mDesktopTasksController = desktopTasksController; mDisplayController.addDisplayWindowListener(this); transitions.addHandler(this); @@ -2768,11 +2774,17 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final @WindowManager.TransitionType int type = request.getType(); final boolean isOpening = isOpeningType(type); final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN; - final boolean inDesktopMode = DesktopModeStatus.canEnterDesktopMode(mContext) - && triggerTask.getWindowingMode() == WINDOWING_MODE_FREEFORM; + final boolean inDesktopMode = mDesktopTasksController.isPresent() + && mDesktopTasksController.get().isDesktopModeShowing(mDisplayId); + final boolean isLaunchingDesktopTask = isOpening && DesktopModeStatus.canEnterDesktopMode( + mContext) && triggerTask.getWindowingMode() == WINDOWING_MODE_FREEFORM; final StageTaskListener stage = getStageOfTask(triggerTask); - if (isOpening && inFullscreen) { + if (inDesktopMode || isLaunchingDesktopTask) { + // Don't handle request when desktop mode is showing (since they don't coexist), or + // when launching a desktop task (defer to DesktopTasksController) + return null; + } else if (isOpening && inFullscreen) { // One task is opening into fullscreen mode, remove the corresponding split record. mRecentTasks.ifPresent(recentTasks -> recentTasks.removeSplitPair(triggerTask.taskId)); logExit(EXIT_REASON_FULLSCREEN_REQUEST); @@ -2824,12 +2836,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.setDismissTransition(transition, stageType, EXIT_REASON_FULLSCREEN_REQUEST); } - } else if (isOpening && inDesktopMode) { - // If the app being opened is in Desktop mode, set it to full screen and dismiss - // split screen stage. - prepareExitSplitScreen(STAGE_TYPE_UNDEFINED, out); - out.setWindowingMode(triggerTask.token, WINDOWING_MODE_UNDEFINED) - .setBounds(triggerTask.token, null); } else if (isOpening && inFullscreen) { final int activityType = triggerTask.getActivityType(); if (activityType == ACTIVITY_TYPE_HOME || activityType == ACTIVITY_TYPE_RECENTS) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java index a318bcf97e6e..9d85bea421ae 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/tv/TvStageCoordinator.java @@ -59,7 +59,7 @@ public class TvStageCoordinator extends StageCoordinator super(context, displayId, syncQueue, taskOrganizer, displayController, displayImeController, displayInsetsController, transitions, transactionPool, iconProvider, mainExecutor, mainHandler, bgExecutor, recentTasks, launchAdjacentController, - Optional.empty(), splitState); + Optional.empty(), splitState, Optional.empty()); mTvSplitMenuController = new TvSplitMenuController(context, this, systemWindows, mainHandler); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt index d52fd4fdf6c7..3ab7b3418e02 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/AppCompatUtilsTest.kt @@ -20,7 +20,6 @@ import android.content.ComponentName import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.R -import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue @@ -35,7 +34,7 @@ import org.junit.runner.RunWith */ @RunWith(AndroidTestingRunner::class) @SmallTest -class AppCompatUtilsTest : ShellTestCase() { +class AppCompatUtilsTest : CompatUIShellTestCase() { @Test fun testIsTopActivityExemptFromDesktopWindowing_onlyTransparentActivitiesInStack() { assertTrue(isTopActivityExemptFromDesktopWindowing(mContext, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java index 67573dabf2ec..ecf766db0be3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIControllerTest.java @@ -39,9 +39,6 @@ import android.content.res.Configuration; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.view.InsetsSource; import android.view.InsetsState; @@ -52,7 +49,6 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; import com.android.wm.shell.common.DisplayInsetsController; @@ -70,11 +66,8 @@ import com.android.wm.shell.transition.Transitions; import dagger.Lazy; -import java.util.Optional; - import org.junit.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -82,25 +75,20 @@ import org.mockito.Captor; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.Optional; + /** * Tests for {@link CompatUIController}. * * Build/Install/Run: - * atest WMShellUnitTests:CompatUIControllerTest + * atest WMShellUnitTests:CompatUIControllerTest */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIControllerTest extends ShellTestCase { +public class CompatUIControllerTest extends CompatUIShellTestCase { private static final int DISPLAY_ID = 0; private static final int TASK_ID = 12; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private CompatUIController mController; private ShellInit mShellInit; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java index e5d1919decf4..2117b062bf57 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUILayoutTest.java @@ -26,8 +26,6 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.graphics.Rect; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -40,7 +38,6 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; @@ -49,7 +46,6 @@ import com.android.wm.shell.compatui.api.CompatUIEvent; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -66,14 +62,10 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUILayoutTest extends ShellTestCase { +public class CompatUILayoutTest extends CompatUIShellTestCase { private static final int TASK_ID = 1; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock private Consumer<CompatUIEvent> mCallback; @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnRestartButtonClicked; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java new file mode 100644 index 000000000000..5a49d01f2991 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java @@ -0,0 +1,39 @@ +/* + * 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.wm.shell.compatui; + +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; + +import com.android.wm.shell.ShellTestCase; + +import org.junit.Rule; + +/** + * Base class for CompatUI tests. + */ +public class CompatUIShellTestCase extends ShellTestCase { + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java index 8fd7c0ec3099..0b37648faeec 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIStatusManagerTest.java @@ -27,8 +27,6 @@ import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; -import com.android.wm.shell.ShellTestCase; - import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -44,7 +42,7 @@ import java.util.function.IntSupplier; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIStatusManagerTest extends ShellTestCase { +public class CompatUIStatusManagerTest extends CompatUIShellTestCase { private FakeCompatUIStatusManagerTest mTestState; private CompatUIStatusManager mStatusManager; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java index 1c0175603df9..61b6d803c8be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIWindowManagerTest.java @@ -16,7 +16,6 @@ package com.android.wm.shell.compatui; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.WindowInsets.Type.navigationBars; import static android.view.WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; @@ -40,9 +39,6 @@ import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.DisplayInfo; @@ -56,7 +52,6 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; @@ -65,7 +60,6 @@ import com.android.wm.shell.compatui.api.CompatUIEvent; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -82,13 +76,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIWindowManagerTest extends ShellTestCase { - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); +public class CompatUIWindowManagerTest extends CompatUIShellTestCase { private static final int TASK_ID = 1; private static final int TASK_WIDTH = 2000; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java index e8191db13084..e786fef1855c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduDialogLayoutTest.java @@ -25,8 +25,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; import android.view.View; @@ -34,10 +32,8 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; -import com.android.wm.shell.ShellTestCase; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -51,7 +47,7 @@ import org.mockito.MockitoAnnotations; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduDialogLayoutTest extends ShellTestCase { +public class LetterboxEduDialogLayoutTest extends CompatUIShellTestCase { @Mock private Runnable mDismissCallback; @@ -60,10 +56,6 @@ public class LetterboxEduDialogLayoutTest extends ShellTestCase { private View mDismissButton; private View mDialogContainer; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java index 4c97c76ae122..09fc082a63e3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/LetterboxEduWindowManagerTest.java @@ -46,9 +46,6 @@ import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.DisplayCutout; @@ -65,7 +62,6 @@ import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.DockStateReader; @@ -75,7 +71,6 @@ import com.android.wm.shell.transition.Transitions; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -95,7 +90,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduWindowManagerTest extends ShellTestCase { +public class LetterboxEduWindowManagerTest extends CompatUIShellTestCase { private static final int USER_ID_1 = 1; private static final int USER_ID_2 = 2; @@ -128,18 +123,11 @@ public class LetterboxEduWindowManagerTest extends ShellTestCase { @Mock private Consumer<Pair<TaskInfo, ShellTaskOrganizer.TaskListener>> mOnDismissCallback; @Mock private DockStateReader mDockStateReader; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private CompatUIConfiguration mCompatUIConfiguration; private TestShellExecutor mExecutor; private FakeCompatUIStatusManagerTest mCompatUIStatus; private CompatUIStatusManager mCompatUIStatusManager; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java index 0da14d673732..02c099b3cfb2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduLayoutTest.java @@ -26,8 +26,6 @@ import static org.mockito.Mockito.verify; import android.app.TaskInfo; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.LayoutInflater; @@ -36,10 +34,8 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; -import com.android.wm.shell.ShellTestCase; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -54,7 +50,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class ReachabilityEduLayoutTest extends ShellTestCase { +public class ReachabilityEduLayoutTest extends CompatUIShellTestCase { private ReachabilityEduLayout mLayout; private View mMoveUpButton; @@ -68,10 +64,6 @@ public class ReachabilityEduLayoutTest extends ShellTestCase { @Mock private TaskInfo mTaskInfo; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java index eafb41470cda..fa04e070250e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/ReachabilityEduWindowManagerTest.java @@ -25,14 +25,11 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -40,7 +37,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -56,7 +52,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class ReachabilityEduWindowManagerTest extends ShellTestCase { +public class ReachabilityEduWindowManagerTest extends CompatUIShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock @@ -71,10 +67,6 @@ public class ReachabilityEduWindowManagerTest extends ShellTestCase { private TaskInfo mTaskInfo; private ReachabilityEduWindowManager mWindowManager; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java index 6b0c5dd2e1c7..2cded9d9776c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogLayoutTest.java @@ -26,8 +26,6 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.view.LayoutInflater; import android.view.View; @@ -36,10 +34,8 @@ import android.widget.CheckBox; import androidx.test.filters.SmallTest; import com.android.wm.shell.R; -import com.android.wm.shell.ShellTestCase; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -55,7 +51,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogLayoutTest extends ShellTestCase { +public class RestartDialogLayoutTest extends CompatUIShellTestCase { @Mock private Runnable mDismissCallback; @Mock private Consumer<Boolean> mRestartCallback; @@ -66,10 +62,6 @@ public class RestartDialogLayoutTest extends ShellTestCase { private View mDialogContainer; private CheckBox mDontRepeatCheckBox; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java index cfeef90cb4b6..ebd0f412a0a1 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/RestartDialogWindowManagerTest.java @@ -22,15 +22,12 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; @@ -38,7 +35,6 @@ import com.android.wm.shell.transition.Transitions; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -54,7 +50,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogWindowManagerTest extends ShellTestCase { +public class RestartDialogWindowManagerTest extends CompatUIShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @@ -66,10 +62,6 @@ public class RestartDialogWindowManagerTest extends ShellTestCase { private RestartDialogWindowManager mWindowManager; private TaskInfo mTaskInfo; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java index e8e68bdbd940..c6532e13f3cc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayoutTest.java @@ -27,8 +27,6 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.content.ComponentName; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.util.Pair; import android.view.LayoutInflater; @@ -40,7 +38,6 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -48,7 +45,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -66,7 +62,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { +public class UserAspectRatioSettingsLayoutTest extends CompatUIShellTestCase { private static final int TASK_ID = 1; @@ -88,10 +84,6 @@ public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { private UserAspectRatioSettingsLayout mLayout; private TaskInfo mTaskInfo; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java index 9f86d49b52c4..096e900199ba 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/UserAspectRatioSettingsWindowManagerTest.java @@ -41,8 +41,6 @@ import android.content.Intent; import android.content.res.Configuration; import android.graphics.Rect; import android.platform.test.annotations.RequiresFlagsDisabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper.RunWithLooper; import android.util.Pair; @@ -56,7 +54,6 @@ import android.view.View; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTaskOrganizer; -import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.SyncTransactionQueue; @@ -65,7 +62,6 @@ import com.android.wm.shell.compatui.CompatUIController.CompatUIHintsState; import junit.framework.Assert; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -87,7 +83,7 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest -public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { +public class UserAspectRatioSettingsWindowManagerTest extends CompatUIShellTestCase { private static final int TASK_ID = 1; @@ -112,10 +108,6 @@ public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { private TestShellExecutor mExecutor; - @Rule - public final CheckFlagsRule mCheckFlagsRule = - DeviceFlagsValueProvider.createCheckFlagsRule(); - @Before public void setUp() { MockitoAnnotations.initMocks(this); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt index 9b4cc17e19d9..db00f41f723b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/CloseDesktopTaskTransitionHandlerTest.kt @@ -64,7 +64,7 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { context, testExecutor, testExecutor, - transactionSupplier + transactionSupplier, ) } @@ -81,11 +81,11 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = WindowManager.TRANSIT_OPEN, - task = createTask(WINDOWING_MODE_FREEFORM) + task = createTask(WINDOWING_MODE_FREEFORM), ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate open transition", animates) @@ -99,7 +99,7 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate fullscreen task close transition", animates) @@ -113,11 +113,11 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( changeMode = WindowManager.TRANSIT_OPEN, - task = createTask(WINDOWING_MODE_FREEFORM) + task = createTask(WINDOWING_MODE_FREEFORM), ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate opening freeform task close transition", animates) @@ -131,7 +131,7 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertTrue("Should animate closing freeform task close transition", animates) @@ -140,7 +140,7 @@ class CloseDesktopTaskTransitionHandlerTest : ShellTestCase() { private fun createTransitionInfo( type: Int = WindowManager.TRANSIT_CLOSE, changeMode: Int = WindowManager.TRANSIT_CLOSE, - task: RunningTaskInfo + task: RunningTaskInfo, ): TransitionInfo = TransitionInfo(type, 0 /* flags */).apply { addChange( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt index 4cc641cd1d81..ecad5217b87f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopActivityOrientationChangeHandlerTest.kt @@ -36,7 +36,6 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE -import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor @@ -47,6 +46,7 @@ import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import junit.framework.Assert.assertEquals @@ -129,16 +129,22 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { persistentRepository, repositoryInitializer, testScope, - userManager + userManager, ) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( - Desktop.getDefaultInstance() - ) + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) + .thenReturn(Desktop.getDefaultInstance()) - handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, - taskStackListener, resizeTransitionHandler, userRepositories) + handler = + DesktopActivityOrientationChangeHandler( + context, + shellInit, + shellTaskOrganizer, + taskStackListener, + resizeTransitionHandler, + userRepositories, + ) shellInit.init() } @@ -161,19 +167,28 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) clearInvocations(shellInit) - handler = DesktopActivityOrientationChangeHandler(context, shellInit, shellTaskOrganizer, - taskStackListener, resizeTransitionHandler, userRepositories) + handler = + DesktopActivityOrientationChangeHandler( + context, + shellInit, + shellTaskOrganizer, + taskStackListener, + resizeTransitionHandler, + userRepositories, + ) - verify(shellInit, never()).addInitCallback(any(), - any<DesktopActivityOrientationChangeHandler>()) + verify(shellInit, never()) + .addInitCallback(any(), any<DesktopActivityOrientationChangeHandler>()) } @Test fun handleActivityOrientationChange_resizeable_doNothing() { val task = setUpFreeformTask() - taskStackListener.onActivityRequestedOrientationChanged(task.taskId, - SCREEN_ORIENTATION_LANDSCAPE) + taskStackListener.onActivityRequestedOrientationChanged( + task.taskId, + SCREEN_ORIENTATION_LANDSCAPE, + ) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @@ -189,8 +204,10 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { userRepositories.current.addTask(DEFAULT_DISPLAY, task.taskId, isVisible = true) runningTasks.add(task) - taskStackListener.onActivityRequestedOrientationChanged(task.taskId, - SCREEN_ORIENTATION_LANDSCAPE) + taskStackListener.onActivityRequestedOrientationChanged( + task.taskId, + SCREEN_ORIENTATION_LANDSCAPE, + ) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @@ -198,8 +215,11 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeablePortrait_requestSameOrientation_doNothing() { val task = setUpFreeformTask(isResizeable = false) - val newTask = setUpFreeformTask(isResizeable = false, - orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT) + val newTask = + setUpFreeformTask( + isResizeable = false, + orientation = SCREEN_ORIENTATION_SENSOR_PORTRAIT, + ) handler.handleActivityOrientationChange(task, newTask) @@ -211,8 +231,10 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { val task = setUpFreeformTask(isResizeable = false) userRepositories.current.updateTask(task.displayId, task.taskId, isVisible = false) - taskStackListener.onActivityRequestedOrientationChanged(task.taskId, - SCREEN_ORIENTATION_LANDSCAPE) + taskStackListener.onActivityRequestedOrientationChanged( + task.taskId, + SCREEN_ORIENTATION_LANDSCAPE, + ) verify(resizeTransitionHandler, never()).startTransition(any(), any()) } @@ -221,8 +243,8 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { fun handleActivityOrientationChange_nonResizeablePortrait_respectLandscapeRequest() { val task = setUpFreeformTask(isResizeable = false) val oldBounds = task.configuration.windowConfiguration.bounds - val newTask = setUpFreeformTask(isResizeable = false, - orientation = SCREEN_ORIENTATION_LANDSCAPE) + val newTask = + setUpFreeformTask(isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE) handler.handleActivityOrientationChange(task, newTask) @@ -242,9 +264,12 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_nonResizeableLandscape_respectPortraitRequest() { val oldBounds = Rect(0, 0, 500, 200) - val task = setUpFreeformTask( - isResizeable = false, orientation = SCREEN_ORIENTATION_LANDSCAPE, bounds = oldBounds - ) + val task = + setUpFreeformTask( + isResizeable = false, + orientation = SCREEN_ORIENTATION_LANDSCAPE, + bounds = oldBounds, + ) val newTask = setUpFreeformTask(isResizeable = false, bounds = oldBounds) handler.handleActivityOrientationChange(task, newTask) @@ -266,7 +291,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { displayId: Int = DEFAULT_DISPLAY, isResizeable: Boolean = true, orientation: Int = SCREEN_ORIENTATION_PORTRAIT, - bounds: Rect? = Rect(0, 0, 200, 500) + bounds: Rect? = Rect(0, 0, 200, 500), ): RunningTaskInfo { val task = createFreeformTask(displayId, bounds) val activityInfo = ActivityInfo() @@ -291,4 +316,4 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt index 6df8d6fd7717..d14c6402982d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandlerTest.kt @@ -56,11 +56,7 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { @Before fun setUp() { handler = - DesktopBackNavigationTransitionHandler( - testExecutor, - testExecutor, - displayController - ) + DesktopBackNavigationTransitionHandler(testExecutor, testExecutor, displayController) whenever(displayController.getDisplayContext(any())).thenReturn(mContext) } @@ -75,13 +71,13 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { handler.startAnimation( transition = mock(), info = - createTransitionInfo( - type = WindowManager.TRANSIT_OPEN, - task = createTask(WINDOWING_MODE_FREEFORM) - ), + createTransitionInfo( + type = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM), + ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate open transition", animates) @@ -95,7 +91,7 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo(task = createTask(WINDOWING_MODE_FULLSCREEN)), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate fullscreen task to back transition", animates) @@ -107,13 +103,13 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { handler.startAnimation( transition = mock(), info = - createTransitionInfo( - changeMode = WindowManager.TRANSIT_OPEN, - task = createTask(WINDOWING_MODE_FREEFORM) - ), + createTransitionInfo( + changeMode = WindowManager.TRANSIT_OPEN, + task = createTask(WINDOWING_MODE_FREEFORM), + ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertFalse("Should not animate opening freeform task to back transition", animates) @@ -127,7 +123,7 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertTrue("Should animate going to back freeform task close transition", animates) @@ -138,22 +134,24 @@ class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { val animates = handler.startAnimation( transition = mock(), - info = createTransitionInfo( - type = TRANSIT_CLOSE, - changeMode = TRANSIT_CLOSE, - task = createTask(WINDOWING_MODE_FREEFORM) - ), + info = + createTransitionInfo( + type = TRANSIT_CLOSE, + changeMode = TRANSIT_CLOSE, + task = createTask(WINDOWING_MODE_FREEFORM), + ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) assertTrue("Should animate going to back freeform task close transition", animates) } + private fun createTransitionInfo( type: Int = WindowManager.TRANSIT_TO_BACK, changeMode: Int = WindowManager.TRANSIT_TO_BACK, - task: RunningTaskInfo + task: RunningTaskInfo, ): TransitionInfo = TransitionInfo(type, 0 /* flags */).apply { addChange( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt index fea82365c1a0..6a3717427e93 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandlerTest.kt @@ -63,109 +63,115 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { - @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var transitions: Transitions - @Mock lateinit var displayController: DisplayController - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock private lateinit var mockWindowManager: IWindowManager + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var transitions: Transitions + @Mock lateinit var displayController: DisplayController + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var mockWindowManager: IWindowManager - private lateinit var shellInit: ShellInit - private lateinit var handler: DesktopDisplayEventHandler + private lateinit var shellInit: ShellInit + private lateinit var handler: DesktopDisplayEventHandler - @Before - fun setUp() { - shellInit = spy(ShellInit(testExecutor)) - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) - handler = - DesktopDisplayEventHandler( - context, - shellInit, - transitions, - displayController, - rootTaskDisplayAreaOrganizer, - mockWindowManager, - ) - shellInit.init() - } + @Before + fun setUp() { + shellInit = spy(ShellInit(testExecutor)) + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + handler = + DesktopDisplayEventHandler( + context, + shellInit, + transitions, + displayController, + rootTaskDisplayAreaOrganizer, + mockWindowManager, + ) + shellInit.init() + } - private fun testDisplayWindowingModeSwitch( - defaultWindowingMode: Int, - extendedDisplayEnabled: Boolean, - expectTransition: Boolean - ) { - val externalDisplayId = 100 - val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java) - verify(displayController).addDisplayWindowListener(captor.capture()) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode - whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } - val settingsSession = ExtendedDisplaySettingsSession( - context.contentResolver, if (extendedDisplayEnabled) 1 else 0) + private fun testDisplayWindowingModeSwitch( + defaultWindowingMode: Int, + extendedDisplayEnabled: Boolean, + expectTransition: Boolean, + ) { + val externalDisplayId = 100 + val captor = ArgumentCaptor.forClass(OnDisplaysChangedListener::class.java) + verify(displayController).addDisplayWindowListener(captor.capture()) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = defaultWindowingMode + whenever(mockWindowManager.getWindowingMode(anyInt())).thenAnswer { defaultWindowingMode } + val settingsSession = + ExtendedDisplaySettingsSession( + context.contentResolver, + if (extendedDisplayEnabled) 1 else 0, + ) - settingsSession.use { - // The external display connected. - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) - captor.value.onDisplayAdded(externalDisplayId) - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - // The external display disconnected. - whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) - .thenReturn(intArrayOf(DEFAULT_DISPLAY)) - captor.value.onDisplayRemoved(externalDisplayId) + settingsSession.use { + // The external display connected. + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, externalDisplayId)) + captor.value.onDisplayAdded(externalDisplayId) + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + // The external display disconnected. + whenever(rootTaskDisplayAreaOrganizer.getDisplayIds()) + .thenReturn(intArrayOf(DEFAULT_DISPLAY)) + captor.value.onDisplayRemoved(externalDisplayId) - if (expectTransition) { - val arg = argumentCaptor<WindowContainerTransaction>() - verify(transitions, times(2)).startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) - assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode) - .isEqualTo(defaultWindowingMode) - } else { - verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) - } + if (expectTransition) { + val arg = argumentCaptor<WindowContainerTransaction>() + verify(transitions, times(2)) + .startTransition(eq(TRANSIT_CHANGE), arg.capture(), isNull()) + assertThat(arg.firstValue.changes[tda.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(arg.secondValue.changes[tda.token.asBinder()]?.windowingMode) + .isEqualTo(defaultWindowingMode) + } else { + verify(transitions, never()).startTransition(eq(TRANSIT_CHANGE), any(), isNull()) + } + } } - } - @Test - fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = false, - expectTransition = false - ) - } + @Test + fun displayWindowingModeSwitchOnDisplayConnected_extendedDisplayDisabled() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = false, + expectTransition = false, + ) + } - @Test - fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, - extendedDisplayEnabled = true, - expectTransition = true - ) - } + @Test + fun displayWindowingModeSwitchOnDisplayConnected_fullscreenDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FULLSCREEN, + extendedDisplayEnabled = true, + expectTransition = true, + ) + } - @Test - fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { - testDisplayWindowingModeSwitch( - defaultWindowingMode = WINDOWING_MODE_FREEFORM, - extendedDisplayEnabled = true, - expectTransition = false - ) - } + @Test + fun displayWindowingModeSwitchOnDisplayConnected_freeformDisplay() { + testDisplayWindowingModeSwitch( + defaultWindowingMode = WINDOWING_MODE_FREEFORM, + extendedDisplayEnabled = true, + expectTransition = false, + ) + } - private class ExtendedDisplaySettingsSession( - private val contentResolver: ContentResolver, - private val overrideValue: Int - ) : AutoCloseable { - private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS - private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) + private class ExtendedDisplaySettingsSession( + private val contentResolver: ContentResolver, + private val overrideValue: Int, + ) : AutoCloseable { + private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS + private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) - init { Settings.Global.putInt(contentResolver, settingName, overrideValue) } + init { + Settings.Global.putInt(contentResolver, settingName, overrideValue) + } - override fun close() { - Settings.Global.putInt(contentResolver, settingName, initialValue) + override fun close() { + Settings.Global.putInt(contentResolver, settingName, initialValue) + } } - } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt index b87f20023796..47d133b974e6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopImmersiveControllerTest.kt @@ -88,9 +88,16 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Before fun setUp() { - userRepositories = DesktopUserRepositories( - context, ShellInit(TestShellExecutor()), mock(), mock(), mock(), mock(), mock() - ) + userRepositories = + DesktopUserRepositories( + context, + ShellInit(TestShellExecutor()), + mock(), + mock(), + mock(), + mock(), + mock(), + ) whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY)) .thenReturn(mockDisplayLayout) whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { invocation -> @@ -98,15 +105,16 @@ class DesktopImmersiveControllerTest : ShellTestCase() { } whenever(mockDisplayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(mockDisplayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) - controller = DesktopImmersiveController( - shellInit = mock(), - transitions = mockTransitions, - desktopUserRepositories = userRepositories, - displayController = mockDisplayController, - shellTaskOrganizer = mockShellTaskOrganizer, - shellCommandHandler = mock(), - transactionSupplier = transactionSupplier, - ) + controller = + DesktopImmersiveController( + shellInit = mock(), + transitions = mockTransitions, + desktopUserRepositories = userRepositories, + displayController = mockDisplayController, + shellTaskOrganizer = mockShellTaskOrganizer, + shellCommandHandler = mock(), + transactionSupplier = transactionSupplier, + ) desktopRepository = userRepositories.current } @@ -119,15 +127,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = false + immersive = false, ) controller.moveTaskToImmersive(task) controller.onTransitionReady( transition = mockBinder, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -145,16 +151,14 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = false + immersive = false, ) assertThat(desktopRepository.removeBoundsBeforeFullImmersive(task.taskId)).isNull() controller.moveTaskToImmersive(task) controller.onTransitionReady( transition = mockBinder, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -171,15 +175,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = true + immersive = true, ) controller.moveTaskToNonImmersive(task, USER_INTERACTION) controller.onTransitionReady( transition = mockBinder, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -197,16 +199,14 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = true + immersive = true, ) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600)) controller.moveTaskToNonImmersive(task, USER_INTERACTION) controller.onTransitionReady( transition = mockBinder, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -220,16 +220,23 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = true + immersive = true, ) controller.onTransitionReady( transition = mock(IBinder::class.java), - info = createTransitionInfo( - changes = listOf(createChange(task).apply { - setRotation(/* start= */ Surface.ROTATION_0, /* end= */ Surface.ROTATION_90) - }) - ), + info = + createTransitionInfo( + changes = + listOf( + createChange(task).apply { + setRotation( + /* start= */ Surface.ROTATION_0, + /* end= */ Surface.ROTATION_90, + ) + } + ) + ), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -247,8 +254,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.moveTaskToImmersive(task) controller.moveTaskToImmersive(task) - verify(mockTransitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) + verify(mockTransitions, times(1)).startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) } @Test @@ -261,8 +267,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { controller.moveTaskToNonImmersive(task, USER_INTERACTION) controller.moveTaskToNonImmersive(task, USER_INTERACTION) - verify(mockTransitions, times(1)) - .startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) + verify(mockTransitions, times(1)).startTransition(eq(TRANSIT_CHANGE), any(), eq(controller)) } @Test @@ -275,7 +280,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) @@ -284,7 +289,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transition = transition, taskId = task.taskId, direction = Direction.EXIT, - animate = false + animate = false, ) } @@ -298,7 +303,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = false + immersive = false, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) @@ -307,7 +312,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transition = transition, taskId = task.taskId, direction = Direction.EXIT, - animate = false + animate = false, ) } @@ -321,7 +326,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) @@ -339,7 +344,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = false + immersive = false, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) @@ -357,21 +362,25 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) - controller.exitImmersiveIfApplicable( - wct = wct, - displayId = DEFAULT_DISPLAY, - excludeTaskId = task.taskId, - reason = USER_INTERACTION, - ).asExit()?.runOnTransitionStart?.invoke(transition) + controller + .exitImmersiveIfApplicable( + wct = wct, + displayId = DEFAULT_DISPLAY, + excludeTaskId = task.taskId, + reason = USER_INTERACTION, + ) + .asExit() + ?.runOnTransitionStart + ?.invoke(transition) assertTransitionNotPending( transition = transition, taskId = task.taskId, animate = false, - direction = Direction.EXIT + direction = Direction.EXIT, ) } @@ -384,7 +393,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION) @@ -401,7 +410,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = false + immersive = false, ) controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) @@ -419,17 +428,20 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) - controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) - .asExit()?.runOnTransitionStart?.invoke(transition) + controller + .exitImmersiveIfApplicable(wct, task, USER_INTERACTION) + .asExit() + ?.runOnTransitionStart + ?.invoke(transition) assertTransitionPending( transition = transition, taskId = task.taskId, direction = Direction.EXIT, - animate = false + animate = false, ) } @@ -442,7 +454,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = false + immersive = false, ) val result = controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) @@ -459,11 +471,16 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = false + immersive = false, ) - val result = controller.exitImmersiveIfApplicable( - wct, task.displayId, excludeTaskId = null, USER_INTERACTION) + val result = + controller.exitImmersiveIfApplicable( + wct, + task.displayId, + excludeTaskId = null, + USER_INTERACTION, + ) assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit) } @@ -478,15 +495,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) controller.onTransitionReady( transition = transition, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -496,7 +511,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transition = transition, taskId = task.taskId, direction = Direction.EXIT, - animate = false + animate = false, ) } @@ -511,15 +526,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) controller.onTransitionReady( transition = transition, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -530,13 +543,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { transition = transition, taskId = task.taskId, animate = false, - direction = Direction.EXIT + direction = Direction.EXIT, ) assertTransitionNotPending( transition = mergedToTransition, taskId = task.taskId, animate = false, - direction = Direction.EXIT + direction = Direction.EXIT, ) } @@ -550,15 +563,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) controller.onTransitionReady( transition = transition, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -569,7 +580,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, - Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE + Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, ) fun onTransitionReady_pendingExit_removesBoundsBeforeImmersive() { val task = createFreeformTask() @@ -579,16 +590,14 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, Rect(100, 100, 600, 600)) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) controller.onTransitionReady( transition = transition, - info = createTransitionInfo( - changes = listOf(createChange(task)) - ), + info = createTransitionInfo(changes = listOf(createChange(task))), startTransaction = SurfaceControl.Transaction(), finishTransaction = SurfaceControl.Transaction(), ) @@ -606,20 +615,21 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION) assertThat( - wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task)) - ).isTrue() + wct.hasBoundsChange(task.token, calculateMaximizeBounds(mockDisplayLayout, task)) + ) + .isTrue() } @Test @EnableFlags( Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, - Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE + Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, ) fun exitImmersiveIfApplicable_preImmersiveBoundsSaved_changesBoundsToPreImmersiveBounds() { val task = createFreeformTask() @@ -628,23 +638,21 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) val preImmersiveBounds = Rect(100, 100, 500, 500) desktopRepository.saveBoundsBeforeFullImmersive(task.taskId, preImmersiveBounds) controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION) - assertThat( - wct.hasBoundsChange(task.token, preImmersiveBounds) - ).isTrue() + assertThat(wct.hasBoundsChange(task.token, preImmersiveBounds)).isTrue() } @Test @EnableFlags( Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, Flags.FLAG_ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE, - Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS + Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS, ) fun exitImmersiveIfApplicable_preImmersiveBoundsNotSaved_changesBoundsToInitialBounds() { val task = createFreeformTask() @@ -653,14 +661,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(wct = wct, taskInfo = task, reason = USER_INTERACTION) - assertThat( - wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task)) - ).isTrue() + assertThat(wct.hasBoundsChange(task.token, calculateInitialBounds(mockDisplayLayout, task))) + .isTrue() } @Test @@ -672,10 +679,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = true + immersive = true, ) - controller.exitImmersiveIfApplicable(wct, task, USER_INTERACTION) - .asExit()?.runOnTransitionStart?.invoke(Binder()) + controller + .exitImmersiveIfApplicable(wct, task, USER_INTERACTION) + .asExit() + ?.runOnTransitionStart + ?.invoke(Binder()) controller.moveTaskToNonImmersive(task, USER_INTERACTION) @@ -693,7 +703,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = DEFAULT_DISPLAY, taskId = task.taskId, - immersive = true + immersive = true, ) controller.exitImmersiveIfApplicable(transition, wct, DEFAULT_DISPLAY, USER_INTERACTION) @@ -711,25 +721,23 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = true + immersive = true, ) controller.moveTaskToNonImmersive(task, USER_INTERACTION) controller.animateResizeChange( - change = TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - }, + change = TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task }, startTransaction = StubTransaction(), finishTransaction = StubTransaction(), - finishCallback = { } + finishCallback = {}, ) animatorTestRule.advanceTimeBy(DesktopImmersiveController.FULL_IMMERSIVE_ANIM_DURATION_MS) assertTransitionPending( transition = mockBinder, taskId = task.taskId, - direction = Direction.EXIT + direction = Direction.EXIT, ) } @@ -743,7 +751,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { desktopRepository.setTaskInFullImmersiveState( displayId = task.displayId, taskId = task.taskId, - immersive = false + immersive = false, ) controller.moveTaskToImmersive(task) @@ -753,13 +761,13 @@ class DesktopImmersiveControllerTest : ShellTestCase() { info = createTransitionInfo(changes = emptyList()), startTransaction = StubTransaction(), finishTransaction = StubTransaction(), - finishCallback = {} + finishCallback = {}, ) assertTransitionNotPending( transition = mockBinder, taskId = task.taskId, - direction = Direction.ENTER + direction = Direction.ENTER, ) } @@ -768,15 +776,18 @@ class DesktopImmersiveControllerTest : ShellTestCase() { taskId: Int, direction: Direction, animate: Boolean = true, - displayId: Int = DEFAULT_DISPLAY + displayId: Int = DEFAULT_DISPLAY, ) { - assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> - pendingTransition.transition == transition - && pendingTransition.displayId == displayId - && pendingTransition.taskId == taskId - && pendingTransition.animate == animate - && pendingTransition.direction == direction - }).isTrue() + assertThat( + controller.pendingImmersiveTransitions.any { pendingTransition -> + pendingTransition.transition == transition && + pendingTransition.displayId == displayId && + pendingTransition.taskId == taskId && + pendingTransition.animate == animate && + pendingTransition.direction == direction + } + ) + .isTrue() } private fun assertTransitionNotPending( @@ -784,43 +795,44 @@ class DesktopImmersiveControllerTest : ShellTestCase() { taskId: Int, direction: Direction, animate: Boolean = true, - displayId: Int = DEFAULT_DISPLAY + displayId: Int = DEFAULT_DISPLAY, ) { - assertThat(controller.pendingImmersiveTransitions.any { pendingTransition -> - pendingTransition.transition == transition - && pendingTransition.displayId == displayId - && pendingTransition.taskId == taskId - && pendingTransition.direction == direction - }).isFalse() + assertThat( + controller.pendingImmersiveTransitions.any { pendingTransition -> + pendingTransition.transition == transition && + pendingTransition.displayId == displayId && + pendingTransition.taskId == taskId && + pendingTransition.direction == direction + } + ) + .isFalse() } private fun createTransitionInfo( @TransitionType type: Int = TRANSIT_CHANGE, @TransitionFlags flags: Int = 0, - changes: List<TransitionInfo.Change> = emptyList() - ): TransitionInfo = TransitionInfo(type, flags).apply { - changes.forEach { change -> addChange(change) } - } + changes: List<TransitionInfo.Change> = emptyList(), + ): TransitionInfo = + TransitionInfo(type, flags).apply { changes.forEach { change -> addChange(change) } } private fun createChange(task: RunningTaskInfo): TransitionInfo.Change = - TransitionInfo.Change(task.token, SurfaceControl()).apply { - taskInfo = task - } + TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } private fun WindowContainerTransaction.hasBoundsChange(token: WindowContainerToken): Boolean = this.changes.any { change -> - change.key == token.asBinder() - && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0 + change.key == token.asBinder() && + (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0 } private fun WindowContainerTransaction.hasBoundsChange( token: WindowContainerToken, bounds: Rect, - ): Boolean = this.changes.any { change -> - change.key == token.asBinder() - && (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0 - && change.value.configuration.windowConfiguration.bounds == bounds - } + ): Boolean = + this.changes.any { change -> + change.key == token.asBinder() && + (change.value.windowSetMask and WINDOW_CONFIG_BOUNDS) != 0 && + change.value.configuration.windowConfiguration.bounds == bounds + } companion object { private val STABLE_BOUNDS = Rect(0, 100, 2000, 1900) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt index 49a7e2951a7e..3cf84d92a625 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandlerTest.kt @@ -85,34 +85,22 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock - lateinit var transitions: Transitions - @Mock - lateinit var userRepositories: DesktopUserRepositories - @Mock - lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler - @Mock - lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler + @Mock lateinit var transitions: Transitions + @Mock lateinit var userRepositories: DesktopUserRepositories + @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler + @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler @Mock lateinit var desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler - @Mock - lateinit var desktopImmersiveController: DesktopImmersiveController - @Mock - lateinit var interactionJankMonitor: InteractionJankMonitor - @Mock - lateinit var mockHandler: Handler - @Mock - lateinit var closingTaskLeash: SurfaceControl - @Mock - lateinit var shellInit: ShellInit - @Mock - lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock - private lateinit var desktopRepository: DesktopRepository + @Mock lateinit var desktopImmersiveController: DesktopImmersiveController + @Mock lateinit var interactionJankMonitor: InteractionJankMonitor + @Mock lateinit var mockHandler: Handler + @Mock lateinit var closingTaskLeash: SurfaceControl + @Mock lateinit var shellInit: ShellInit + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock private lateinit var desktopRepository: DesktopRepository private lateinit var mixedHandler: DesktopMixedTransitionHandler - @Before fun setUp() { whenever(userRepositories.current).thenReturn(desktopRepository) @@ -157,11 +145,11 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + ) fun startRemoveTransition_callsFreeformTaskTransitionHandler() { val wct = WindowContainerTransaction() - whenever(freeformTaskTransitionHandler.startRemoveTransition(wct)) - .thenReturn(mock()) + whenever(freeformTaskTransitionHandler.startRemoveTransition(wct)).thenReturn(mock()) mixedHandler.startRemoveTransition(wct) @@ -171,7 +159,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + ) fun startRemoveTransition_startsCloseTransition() { val wct = WindowContainerTransaction() whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)) @@ -193,18 +182,19 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val transitionInfo = createCloseTransitionInfo( changeMode = TRANSIT_OPEN, - task = createTask(WINDOWING_MODE_FREEFORM) + task = createTask(WINDOWING_MODE_FREEFORM), ) whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())) .thenReturn(true) - val started = mixedHandler.startAnimation( - transition = transition, - info = transitionInfo, - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} - ) + val started = + mixedHandler.startAnimation( + transition = transition, + info = transitionInfo, + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) assertFalse("Should not start animation without closing desktop task", started) } @@ -212,7 +202,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + ) fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() { val wct = WindowContainerTransaction() val transition = mock<IBinder>() @@ -225,13 +216,14 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { .thenReturn(transition) mixedHandler.startRemoveTransition(wct) - val started = mixedHandler.startAnimation( - transition = transition, - info = transitionInfo, - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} - ) + val started = + mixedHandler.startAnimation( + transition = transition, + info = transitionInfo, + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) assertTrue("Should delegate animation to close transition handler", started) verify(closeDesktopTaskTransitionHandler) @@ -241,12 +233,16 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX, + ) fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() { val wct = WindowContainerTransaction() val transition = mock<IBinder>() - val transitionInfo = createCloseTransitionInfo( - task = createTask(WINDOWING_MODE_FREEFORM), withWallpaper = true) + val transitionInfo = + createCloseTransitionInfo( + task = createTask(WINDOWING_MODE_FREEFORM), + withWallpaper = true, + ) whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any())) .thenReturn(mock()) whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)) @@ -258,7 +254,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { info = transitionInfo, startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) verify(transitions) @@ -268,14 +264,14 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { any(), any(), any(), - eq(mixedHandler) + eq(mixedHandler), ) verify(interactionJankMonitor) .begin( closingTaskLeash, context, mockHandler, - CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE + CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, ) } @@ -283,7 +279,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @DisableFlags( Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun startLaunchTransition_immersiveAndAppLaunchFlagsDisabled_doesNotUseMixedHandler() { val wct = WindowContainerTransaction() val task = createTask(WINDOWING_MODE_FREEFORM) @@ -294,7 +291,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { transitionType = TRANSIT_OPEN, wct = wct, taskId = task.taskId, - exitingImmersiveTask = null + exitingImmersiveTask = null, ) verify(transitions).startTransition(TRANSIT_OPEN, wct, /* handler= */ null) @@ -312,7 +309,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { transitionType = TRANSIT_OPEN, wct = wct, taskId = task.taskId, - exitingImmersiveTask = null + exitingImmersiveTask = null, ) verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler) @@ -321,7 +318,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun startLaunchTransition_desktopAppLaunchEnabled_usesMixedHandler() { val wct = WindowContainerTransaction() val task = createTask(WINDOWING_MODE_FREEFORM) @@ -332,7 +330,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { transitionType = TRANSIT_OPEN, wct = wct, taskId = task.taskId, - exitingImmersiveTask = null + exitingImmersiveTask = null, ) verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler) @@ -357,24 +355,22 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val otherChange = createChange(createTask(WINDOWING_MODE_FREEFORM)) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange, otherChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, otherChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } - - verify(transitions).dispatchTransition( - eq(transition), - argThat { info -> - info.changes.contains(launchTaskChange) && info.changes.contains(otherChange) - }, - any(), - any(), - any(), - eq(mixedHandler), - ) + ) {} + + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> + info.changes.contains(launchTaskChange) && info.changes.contains(otherChange) + }, + any(), + any(), + any(), + eq(mixedHandler), + ) } @Test @@ -397,32 +393,32 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val immersiveChange = createChange(immersiveTask) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange, immersiveChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, immersiveChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} verify(desktopImmersiveController) .animateResizeChange(eq(immersiveChange), any(), any(), any()) - verify(transitions).dispatchTransition( - eq(transition), - argThat { info -> - info.changes.contains(launchTaskChange) && !info.changes.contains(immersiveChange) - }, - any(), - any(), - any(), - eq(mixedHandler), - ) + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> + info.changes.contains(launchTaskChange) && + !info.changes.contains(immersiveChange) + }, + any(), + any(), + any(), + eq(mixedHandler), + ) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun startAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { val wct = WindowContainerTransaction() val launchingTask = createTask(WINDOWING_MODE_FREEFORM) @@ -439,22 +435,19 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} - verify(rootTaskDisplayAreaOrganizer, times(0)) - .reparentToDisplayArea(anyInt(), any(), any()) + verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any()) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun startAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { val wct = WindowContainerTransaction() val launchingTask = createTask(WINDOWING_MODE_FREEFORM) @@ -473,22 +466,20 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange, minimizeChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} - verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( - anyInt(), eq(minimizeChange.leash), any()) + verify(rootTaskDisplayAreaOrganizer) + .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any()) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun startAnimation_pendingTransition_noLaunchChange_returnsFalse() { val wct = WindowContainerTransaction() val launchingTask = createTask(WINDOWING_MODE_FREEFORM) @@ -505,15 +496,13 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) ) - val started = mixedHandler.startAnimation( - transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(nonLaunchTaskChange) - ), - SurfaceControl.Transaction(), - SurfaceControl.Transaction(), - ) { } + val started = + mixedHandler.startAnimation( + transition, + createCloseTransitionInfo(TRANSIT_OPEN, listOf(nonLaunchTaskChange)), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) {} assertFalse("Should not start animation without launching desktop task", started) } @@ -529,21 +518,18 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { whenever(transitions.dispatchTransition(eq(transition), any(), any(), any(), any(), any())) .thenReturn(mock()) - mixedHandler.startLaunchTransition( - transitionType = TRANSIT_OPEN, - wct = wct, - taskId = null, - ) + mixedHandler.startLaunchTransition(transitionType = TRANSIT_OPEN, wct = wct, taskId = null) - val started = mixedHandler.startAnimation( - transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(createChange(task, mode = TRANSIT_OPEN)) - ), - StubTransaction(), - StubTransaction(), - ) { } + val started = + mixedHandler.startAnimation( + transition, + createCloseTransitionInfo( + TRANSIT_OPEN, + listOf(createChange(task, mode = TRANSIT_OPEN)), + ), + StubTransaction(), + StubTransaction(), + ) {} assertThat(started).isEqualTo(true) } @@ -569,15 +555,13 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val immersiveChange = createChange(immersiveTask, mode = TRANSIT_CHANGE) val openingChange = createChange(openingTask, mode = TRANSIT_OPEN) - val started = mixedHandler.startAnimation( - transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(immersiveChange, openingChange) - ), - StubTransaction(), - StubTransaction(), - ) { } + val started = + mixedHandler.startAnimation( + transition, + createCloseTransitionInfo(TRANSIT_OPEN, listOf(immersiveChange, openingChange)), + StubTransaction(), + StubTransaction(), + ) {} assertThat(started).isEqualTo(true) verify(desktopImmersiveController) @@ -587,7 +571,8 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { val wct = WindowContainerTransaction() val launchingTask = createTask(WINDOWING_MODE_FREEFORM) @@ -606,22 +591,19 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} - verify(rootTaskDisplayAreaOrganizer, times(0)) - .reparentToDisplayArea(anyInt(), any(), any()) + verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any()) } @Test @EnableFlags( Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS, - Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX, + ) fun addPendingAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { val wct = WindowContainerTransaction() val launchingTask = createTask(WINDOWING_MODE_FREEFORM) @@ -642,16 +624,13 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange, minimizeChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange, minimizeChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} - verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( - anyInt(), eq(minimizeChange.leash), any()) + verify(rootTaskDisplayAreaOrganizer) + .reparentToDisplayArea(anyInt(), eq(minimizeChange.leash), any()) } @Test @@ -672,13 +651,10 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val launchTaskChange = createChange(launchingTask) mixedHandler.startAnimation( transition, - createCloseTransitionInfo( - TRANSIT_OPEN, - listOf(launchTaskChange) - ), + createCloseTransitionInfo(TRANSIT_OPEN, listOf(launchTaskChange)), SurfaceControl.Transaction(), SurfaceControl.Transaction(), - ) { } + ) {} assertThat(mixedHandler.pendingMixedTransitions).isEmpty() } @@ -701,7 +677,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { mixedHandler.onTransitionConsumed( transition = transition, aborted = true, - finishTransaction = SurfaceControl.Transaction() + finishTransaction = SurfaceControl.Transaction(), ) assertThat(mixedHandler.pendingMixedTransitions).isEmpty() @@ -714,8 +690,14 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val transition = Binder() whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) whenever( - desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) - ) + desktopBackNavigationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) .thenReturn(true) mixedHandler.addPendingMixedTransition( PendingMixedTransition.Minimize( @@ -726,24 +708,24 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) val minimizingTaskChange = createChange(minimizingTask) - val started = mixedHandler.startAnimation( - transition = transition, - info = - createCloseTransitionInfo( - TRANSIT_TO_BACK, - listOf(minimizingTaskChange) - ), - startTransaction = mock(), - finishTransaction = mock(), - finishCallback = {} - ) + val started = + mixedHandler.startAnimation( + transition = transition, + info = createCloseTransitionInfo(TRANSIT_TO_BACK, listOf(minimizingTaskChange)), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) assertTrue("Should delegate animation to back navigation transition handler", started) verify(desktopBackNavigationTransitionHandler) .startAnimation( eq(transition), argThat { info -> info.changes.contains(minimizingTaskChange) }, - any(), any(), any()) + any(), + any(), + any(), + ) } @Test @@ -753,8 +735,14 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val transition = Binder() whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) whenever( - desktopBackNavigationTransitionHandler.startAnimation(any(), any(), any(), any(), any()) - ) + desktopBackNavigationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) .thenReturn(true) mixedHandler.addPendingMixedTransition( PendingMixedTransition.Minimize( @@ -767,14 +755,10 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val minimizingTaskChange = createChange(minimizingTask) mixedHandler.startAnimation( transition = transition, - info = - createCloseTransitionInfo( - TRANSIT_TO_BACK, - listOf(minimizingTaskChange) - ), + info = createCloseTransitionInfo(TRANSIT_TO_BACK, listOf(minimizingTaskChange)), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) verify(transitions) @@ -784,7 +768,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { any(), any(), any(), - eq(mixedHandler) + eq(mixedHandler), ) } @@ -814,14 +798,15 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { private fun createCloseTransitionInfo( @TransitionType type: Int, - changes: List<TransitionInfo.Change> = emptyList() - ): TransitionInfo = TransitionInfo(type, /* flags= */ 0).apply { - changes.forEach { change -> addChange(change) } - } + changes: List<TransitionInfo.Change> = emptyList(), + ): TransitionInfo = + TransitionInfo(type, /* flags= */ 0).apply { + changes.forEach { change -> addChange(change) } + } private fun createChange( task: RunningTaskInfo, - @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE + @TransitionInfo.TransitionMode mode: Int = TRANSIT_NONE, ): TransitionInfo.Change = TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task @@ -838,8 +823,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { RunningTaskInfo().apply { token = WindowContainerToken(mock<IWindowContainerToken>()) baseIntent = - Intent().apply { - component = DesktopWallpaperActivity.wallpaperActivityComponent - } + Intent().apply { component = DesktopWallpaperActivity.wallpaperActivityComponent } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt index 2f225f22cce0..abd707817621 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt @@ -53,9 +53,7 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever -/** - * Tests for [DesktopModeEventLogger]. - */ +/** Tests for [DesktopModeEventLogger]. */ class DesktopModeEventLoggerTest : ShellTestCase() { private val desktopModeEventLogger = DesktopModeEventLogger() @@ -64,13 +62,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @JvmField @Rule(order = 0) - val extendedMockitoRule = ExtendedMockitoRule.Builder(this) - .mockStatic(FrameworkStatsLog::class.java) - .mockStatic(EventLogTags::class.java).build()!! + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(FrameworkStatsLog::class.java) + .mockStatic(EventLogTags::class.java) + .build()!! - @JvmField - @Rule(order = 1) - val setFlagsRule = SetFlagsRule() + @JvmField @Rule(order = 1) val setFlagsRule = SetFlagsRule() @Before fun setUp() { @@ -95,14 +93,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* exit_reason */ eq(0), /* sessionId */ - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -127,14 +125,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* exit_reason */ eq(0), /* sessionId */ - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verify { EventLogTags.writeWmShellEnterDesktopMode( eq(EnterReason.KEYBOARD_SHORTCUT_ENTER.reason), - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -164,14 +162,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* exit_reason */ eq(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__DRAG_TO_EXIT), /* sessionId */ - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verify { EventLogTags.writeWmShellExitDesktopMode( eq(ExitReason.DRAG_TO_EXIT.reason), - eq(sessionId) + eq(sessionId), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -214,16 +212,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), /* visible_task_count */ - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED - ), + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED), eq(TASK_UPDATE.instanceId), eq(TASK_UPDATE.uid), eq(TASK_UPDATE.taskHeight), @@ -233,7 +228,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(sessionId), eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -275,16 +270,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), /* visible_task_count */ - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verify { EventLogTags.writeWmShellDesktopModeTaskUpdate( - eq( - FrameworkStatsLog - .DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED - ), + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED), eq(TASK_UPDATE.instanceId), eq(TASK_UPDATE.uid), eq(TASK_UPDATE.taskHeight), @@ -294,7 +286,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(sessionId), eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -339,7 +331,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), /* visible_task_count */ - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) @@ -358,7 +350,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(sessionId), eq(UNSET_MINIMIZE_REASON), eq(UNSET_UNMINIMIZE_REASON), - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -399,7 +391,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* unminimize_reason */ eq(UNSET_UNMINIMIZE_REASON), /* visible_task_count */ - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) @@ -418,7 +410,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(sessionId), eq(MinimizeReason.TASK_LIMIT.reason), eq(UNSET_UNMINIMIZE_REASON), - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -459,7 +451,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* unminimize_reason */ eq(UnminimizeReason.TASKBAR_TAP.reason), /* visible_task_count */ - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) @@ -478,7 +470,7 @@ class DesktopModeEventLoggerTest : ShellTestCase() { eq(sessionId), eq(UNSET_MINIMIZE_REASON), eq(UnminimizeReason.TASKBAR_TAP.reason), - eq(TASK_COUNT) + eq(TASK_COUNT), ) } verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -486,8 +478,11 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @Test fun logTaskResizingStarted_noOngoingSession_doesNotLog() { - desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER, - InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo()) + desktopModeEventLogger.logTaskResizingStarted( + ResizeTrigger.CORNER, + InputMethod.UNKNOWN_INPUT_METHOD, + createTaskInfo(), + ) verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -498,19 +493,33 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskResizingStarted_logsTaskSizeUpdatedWithStartResizingStage() { val sessionId = startDesktopModeSession() - desktopModeEventLogger.logTaskResizingStarted(ResizeTrigger.CORNER, - InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo(), TASK_SIZE_UPDATE.taskWidth, - TASK_SIZE_UPDATE.taskHeight, displayController) + desktopModeEventLogger.logTaskResizingStarted( + ResizeTrigger.CORNER, + InputMethod.UNKNOWN_INPUT_METHOD, + createTaskInfo(), + TASK_SIZE_UPDATE.taskWidth, + TASK_SIZE_UPDATE.taskHeight, + displayController, + ) verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), /* resize_trigger */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER + ), /* resizing_stage */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__START_RESIZING_STAGE + ), /* input_method */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD + ), /* desktop_mode_session_id */ eq(sessionId), /* instance_id */ @@ -530,8 +539,11 @@ class DesktopModeEventLoggerTest : ShellTestCase() { @Test fun logTaskResizingEnded_noOngoingSession_doesNotLog() { - desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER, - InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo()) + desktopModeEventLogger.logTaskResizingEnded( + ResizeTrigger.CORNER, + InputMethod.UNKNOWN_INPUT_METHOD, + createTaskInfo(), + ) verifyZeroInteractions(staticMockMarker(FrameworkStatsLog::class.java)) verifyZeroInteractions(staticMockMarker(EventLogTags::class.java)) @@ -542,18 +554,31 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskResizingEnded_logsTaskSizeUpdatedWithEndResizingStage() { val sessionId = startDesktopModeSession() - desktopModeEventLogger.logTaskResizingEnded(ResizeTrigger.CORNER, - InputMethod.UNKNOWN_INPUT_METHOD, createTaskInfo(), displayController = displayController) + desktopModeEventLogger.logTaskResizingEnded( + ResizeTrigger.CORNER, + InputMethod.UNKNOWN_INPUT_METHOD, + createTaskInfo(), + displayController = displayController, + ) verify { FrameworkStatsLog.write( eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED), /* resize_trigger */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZE_TRIGGER__CORNER_RESIZE_TRIGGER + ), /* resizing_stage */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__RESIZING_STAGE__END_RESIZING_STAGE + ), /* input_method */ - eq(FrameworkStatsLog.DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD), + eq( + FrameworkStatsLog + .DESKTOP_MODE_TASK_SIZE_UPDATED__INPUT_METHOD__UNKNOWN_INPUT_METHOD + ), /* desktop_mode_session_id */ eq(sessionId), /* instance_id */ @@ -582,9 +607,12 @@ class DesktopModeEventLoggerTest : ShellTestCase() { fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() { desktopModeEventLogger.logTaskInfoStateInit() verify { - FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), + FrameworkStatsLog.write( + eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE), /* task_event */ - eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD), + eq( + FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD + ), /* instance_id */ eq(0), /* uid */ @@ -604,13 +632,14 @@ class DesktopModeEventLoggerTest : ShellTestCase() { /* unminimize_reason */ eq(UNSET_UNMINIMIZE_REASON), /* visible_task_count */ - eq(0) + eq(0), ) } } private fun createTaskInfo(): RunningTaskInfo { - return TestRunningTaskInfoBuilder().setTaskId(TASK_ID) + return TestRunningTaskInfoBuilder() + .setTaskId(TASK_ID) .setUid(TASK_UID) .setBounds(Rect(TASK_X, TASK_Y, TASK_WIDTH, TASK_HEIGHT)) .build() @@ -628,27 +657,42 @@ class DesktopModeEventLoggerTest : ShellTestCase() { private const val DISPLAY_HEIGHT = 500 private const val DISPLAY_AREA = DISPLAY_HEIGHT * DISPLAY_WIDTH - private val TASK_UPDATE = TaskUpdate( - TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, - visibleTaskCount = TASK_COUNT, - ) + private val TASK_UPDATE = + TaskUpdate( + TASK_ID, + TASK_UID, + TASK_HEIGHT, + TASK_WIDTH, + TASK_X, + TASK_Y, + visibleTaskCount = TASK_COUNT, + ) - private val TASK_SIZE_UPDATE = TaskSizeUpdate( - resizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, - inputMethod = InputMethod.UNKNOWN_INPUT_METHOD, - TASK_ID, - TASK_UID, - TASK_HEIGHT, - TASK_WIDTH, - DISPLAY_AREA, - ) + private val TASK_SIZE_UPDATE = + TaskSizeUpdate( + resizeTrigger = ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, + inputMethod = InputMethod.UNKNOWN_INPUT_METHOD, + TASK_ID, + TASK_UID, + TASK_HEIGHT, + TASK_WIDTH, + DISPLAY_AREA, + ) private fun createTaskUpdate( minimizeReason: MinimizeReason? = null, unminimizeReason: UnminimizeReason? = null, - ) = TaskUpdate( - TASK_ID, TASK_UID, TASK_HEIGHT, TASK_WIDTH, TASK_X, TASK_Y, minimizeReason, - unminimizeReason, TASK_COUNT - ) + ) = + TaskUpdate( + TASK_ID, + TASK_UID, + TASK_HEIGHT, + TASK_WIDTH, + TASK_X, + TASK_Y, + minimizeReason, + unminimizeReason, + TASK_COUNT, + ) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt index e57ae2a86859..413e7bc5d1d6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeKeyGestureHandlerTest.kt @@ -30,49 +30,49 @@ import android.view.Display.DEFAULT_DISPLAY import android.view.KeyEvent import android.window.DisplayAreaInfo import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer +import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn +import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession +import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.hardware.input.Flags.FLAG_USE_KEY_GESTURE_EVENT_HANDLER import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS import com.android.window.flags.Flags.FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT +import com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS import com.android.wm.shell.MockToken import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask -import com.android.wm.shell.transition.FocusTransitionObserver -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.ExperimentalCoroutinesApi -import org.junit.Before -import org.junit.Rule -import org.junit.Test -import org.junit.runner.RunWith -import org.mockito.kotlin.eq -import org.mockito.kotlin.whenever -import com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer -import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn -import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession -import com.android.dx.mockito.inline.extended.StaticMockitoSession -import com.android.window.flags.Flags.FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.transition.FocusTransitionObserver import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel +import com.google.common.truth.Truth.assertThat import java.util.Optional import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob import kotlinx.coroutines.cancel import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith import org.mockito.Mockito.anyInt import org.mockito.Mockito.spy import org.mockito.Mockito.verify import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** @@ -130,21 +130,24 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) doAnswer { - keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler) - null - }.whenever(inputManager).registerKeyGestureEventHandler(any()) + keyGestureEventHandler = (it.arguments[0] as KeyGestureEventHandler) + null + } + .whenever(inputManager) + .registerKeyGestureEventHandler(any()) shellInit.init() - desktopModeKeyGestureHandler = DesktopModeKeyGestureHandler( - context, - Optional.of(desktopModeWindowDecorViewModel), - Optional.of(desktopTasksController), - inputManager, - shellTaskOrganizer, - focusTransitionObserver, - testExecutor, - displayController - ) + desktopModeKeyGestureHandler = + DesktopModeKeyGestureHandler( + context, + Optional.of(desktopModeWindowDecorViewModel), + Optional.of(desktopTasksController), + inputManager, + shellTaskOrganizer, + focusTransitionObserver, + testExecutor, + displayController, + ) } @After @@ -160,7 +163,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { @EnableFlags( FLAG_ENABLE_DISPLAY_FOCUS_IN_SHELL_TRANSITIONS, FLAG_ENABLE_MOVE_TO_NEXT_DISPLAY_SHORTCUT, - FLAG_USE_KEY_GESTURE_EVENT_HANDLER + FLAG_USE_KEY_GESTURE_EVENT_HANDLER, ) fun keyGestureMoveToNextDisplay_shouldMoveToNextDisplay() { // Set up two display ids @@ -176,12 +179,13 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) - val event = KeyGestureEvent.Builder() - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY) - .setDisplayId(SECOND_DISPLAY) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D)) - .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON) - .build() + val event = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MOVE_TO_NEXT_DISPLAY) + .setDisplayId(SECOND_DISPLAY) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_D)) + .setModifierState(KeyEvent.META_META_ON or KeyEvent.META_CTRL_ON) + .build() val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() @@ -190,108 +194,102 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { } @Test - @EnableFlags( - FLAG_USE_KEY_GESTURE_EVENT_HANDLER, - FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS - ) + @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) fun keyGestureSnapLeft_shouldSnapResizeTaskToLeft() { val task = setUpFreeformTask() task.isFocused = true whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) - val event = KeyGestureEvent.Builder() - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET)) - .setModifierState(KeyEvent.META_META_ON) - .build() + val event = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_LEFT_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_LEFT_BRACKET)) + .setModifierState(KeyEvent.META_META_ON) + .build() val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() assertThat(result).isTrue() - verify(desktopModeWindowDecorViewModel).onSnapResize( - task.taskId, - true, - DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, - /* fromMenu= */ false - ) + verify(desktopModeWindowDecorViewModel) + .onSnapResize( + task.taskId, + true, + DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, + /* fromMenu= */ false, + ) } @Test - @EnableFlags( - FLAG_USE_KEY_GESTURE_EVENT_HANDLER, - FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS - ) + @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) fun keyGestureSnapRight_shouldSnapResizeTaskToRight() { val task = setUpFreeformTask() task.isFocused = true whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) - val event = KeyGestureEvent.Builder() - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET)) - .setModifierState(KeyEvent.META_META_ON) - .build() + val event = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_SNAP_RIGHT_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_RIGHT_BRACKET)) + .setModifierState(KeyEvent.META_META_ON) + .build() val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() assertThat(result).isTrue() - verify(desktopModeWindowDecorViewModel).onSnapResize( - task.taskId, - false, - DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, - /* fromMenu= */ false - ) + verify(desktopModeWindowDecorViewModel) + .onSnapResize( + task.taskId, + false, + DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, + /* fromMenu= */ false, + ) } @Test - @EnableFlags( - FLAG_USE_KEY_GESTURE_EVENT_HANDLER, - FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS - ) + @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) fun keyGestureToggleFreeformWindowSize_shouldToggleTaskSize() { val task = setUpFreeformTask() task.isFocused = true whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) - val event = KeyGestureEvent.Builder() - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS)) - .setModifierState(KeyEvent.META_META_ON) - .build() + val event = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_TOGGLE_MAXIMIZE_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_EQUALS)) + .setModifierState(KeyEvent.META_META_ON) + .build() val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() assertThat(result).isTrue() - verify(desktopTasksController).toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - isMaximized = isTaskMaximized(task, displayController), - source = ToggleTaskSizeInteraction.Source.KEYBOARD_SHORTCUT, - inputMethod = - DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, - ), - ) + verify(desktopTasksController) + .toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + isMaximized = isTaskMaximized(task, displayController), + source = ToggleTaskSizeInteraction.Source.KEYBOARD_SHORTCUT, + inputMethod = DesktopModeEventLogger.Companion.InputMethod.KEYBOARD, + ), + ) } @Test - @EnableFlags( - FLAG_USE_KEY_GESTURE_EVENT_HANDLER, - FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS - ) + @EnableFlags(FLAG_USE_KEY_GESTURE_EVENT_HANDLER, FLAG_ENABLE_TASK_RESIZING_KEYBOARD_SHORTCUTS) fun keyGestureMinimizeFreeformWindow_shouldMinimizeTask() { val task = setUpFreeformTask() task.isFocused = true whenever(shellTaskOrganizer.getRunningTasks()).thenReturn(arrayListOf(task)) whenever(focusTransitionObserver.hasGlobalFocus(eq(task))).thenReturn(true) - val event = KeyGestureEvent.Builder() - .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW) - .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS)) - .setModifierState(KeyEvent.META_META_ON) - .build() + val event = + KeyGestureEvent.Builder() + .setKeyGestureType(KeyGestureEvent.KEY_GESTURE_TYPE_MINIMIZE_FREEFORM_WINDOW) + .setKeycodes(intArrayOf(KeyEvent.KEYCODE_MINUS)) + .setModifierState(KeyEvent.META_META_ON) + .build() val result = keyGestureEventHandler.handleKeyGestureEvent(event, null) testExecutor.flushAll() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index 7c4ce4acfc9c..43684fb92b64 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -87,700 +87,735 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { - @JvmField - @Rule - val extendedMockitoRule = - ExtendedMockitoRule.Builder(this) - .mockStatic(DesktopModeStatus::class.java) - .mockStatic(SystemProperties::class.java) - .mockStatic(Trace::class.java) - .build()!! - - private val testExecutor = mock<ShellExecutor>() - private val mockShellInit = mock<ShellInit>() - private val transitions = mock<Transitions>() - private val context = mock<Context>() - - private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver - private lateinit var shellInit: ShellInit - private lateinit var desktopModeEventLogger: DesktopModeEventLogger - - @Before - fun setup() { - whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) - shellInit = spy(ShellInit(testExecutor)) - desktopModeEventLogger = mock<DesktopModeEventLogger>() - - transitionObserver = DesktopModeLoggerTransitionObserver( - context, mockShellInit, transitions, desktopModeEventLogger) - val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) - verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) - initRunnableCaptor.value.run() - // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a - // consistent state with no outstanding interactions when test cases start executing. - verify(desktopModeEventLogger).logTaskInfoStateInit() - } - - @Test - fun testInitialiseVisibleTasksSystemProperty() { - ExtendedMockito.verify { - SystemProperties.set( - eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY), - eq(DesktopModeLoggerTransitionObserver - .VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE)) - } - } - - @Test - fun testRegistersObserverAtInit() { - verify(transitions).registerObserver(same(transitionObserver)) - } - - @Test - fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, never()).logSessionEnter(any()) - verify(desktopModeEventLogger, never()).logTaskAdded(any()) - } - - @Test - fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() { - val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_FREEFORM_INTENT, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - // task change is finalised when drag ends - val transitionInfo = - TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) - .addChange(change) - .build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0) - .addChange(change) - .build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_MENU_BUTTON, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) - .addChange(change) - .build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0) - .addChange(change) - .build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.KEYBOARD_SHORTCUT_ENTER, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitToFront_logTaskAddedAndEnterReasonOverview() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { - // previous exit to overview transition - // add a freeform task - val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) - transitionObserver.isSessionActive = true - val previousTransitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) - .build() - - callOnTransitionReady(previousTransitionInfo) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - - // Enter desktop mode from cancelled recents has no transition. Enter is detected on the - // next transition involving freeform windows - - // TRANSIT_TO_FRONT - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { - // previous exit to overview transition - // add a freeform task - val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) - transitionObserver.isSessionActive = true - val previousTransitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) - .build() - - callOnTransitionReady(previousTransitionInfo) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - - // Enter desktop mode from cancelled recents has no transition. Enter is detected on the - // next transition involving freeform windows - - // TRANSIT_CHANGE - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { - // previous exit to overview transition - // add a freeform task - val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) - transitionObserver.isSessionActive = true - val previousTransitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) - .build() - - callOnTransitionReady(previousTransitionInfo) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - - // Enter desktop mode from cancelled recents has no transition. Enter is detected on the - // next transition involving freeform windows - - // TRANSIT_OPEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - @Suppress("ktlint:standard:max-line-length") - fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() { - // Tests for AppFromOverview precedence in compared to cancelled Overview - - // previous exit to overview transition - // add a freeform task - val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) - transitionObserver.isSessionActive = true - val previousTransitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) - .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) - .build() - - callOnTransitionReady(previousTransitionInfo) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - - // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) - .addChange(change) - .build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_FROM_OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.UNKNOWN_ENTER, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitWake_logTaskAddedAndEnterReasonScreenOn() { - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitBack_previousExitReasonScreenOff_logTaskAddedAndEnterReasonScreenOn() { - val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) - // Previous Exit reason recorded as Screen Off - transitionObserver.addTaskInfosToCachedMap(freeformTask) - transitionObserver.isSessionActive = true - callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) - verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) - // Enter desktop through back transition, this happens when user enters after dismissing - // keyguard - val change = createChange(TRANSIT_TO_FRONT, freeformTask) - val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_BACK, 0).addChange(change).build() - - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.SCREEN_ON, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitEndDragToDesktop_previousExitReasonScreenOff_logTaskAddedAndEnterReasonAppDrag() { - val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) - // Previous Exit reason recorded as Screen Off - transitionObserver.addTaskInfosToCachedMap(freeformTask) - transitionObserver.isSessionActive = true - callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) - verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) - - // Enter desktop through app handle drag. This represents cases where instead of moving to - // desktop right after turning the screen on, we move to fullscreen then move another task - // to desktop - val transitionInfo = - TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) - .addChange(createChange(TRANSIT_TO_FRONT, freeformTask)) - .build() - callOnTransitionReady(transitionInfo) - - verifyTaskAddedAndEnterLogging(EnterReason.APP_HANDLE_DRAG, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun transitSleep_logTaskRemovedAndExitReasonScreenOff() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) - .addChange(change) - .build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // window mode changing from FREEFORM to FULLSCREEN - val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // recents transition - val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM)) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - } - - @Test - fun transitClose_logTaskRemovedAndExitReasonTaskFinished() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // task closing - val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verifyTaskRemovedAndExitLogging(ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE) - } - - @Test - fun transitMinimize_logExitReasongMinimized() { - // add a freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // minimize the task - val change = createChange(TRANSIT_MINIMIZE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_MINIMIZE).addChange(change).build() - callOnTransitionReady(transitionInfo) - - assertFalse(transitionObserver.isSessionActive) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED)) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE)) - verifyZeroInteractions(desktopModeEventLogger) - } - - @Test - fun sessionExitByRecents_cancelledAnimation_sessionRestored() { - // add a freeform task to an existing session - val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(taskInfo) - transitionObserver.isSessionActive = true - - // recents transition sent freeform window to back - val change = createChange(TRANSIT_TO_BACK, taskInfo) - val transitionInfo1 = - TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS).addChange(change).build() - callOnTransitionReady(transitionInfo1) - - verifyTaskRemovedAndExitLogging( - ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE - ) - - val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() - callOnTransitionReady(transitionInfo2) - - verifyTaskAddedAndEnterLogging(EnterReason.OVERVIEW, - DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1)) - } - - @Test - fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { - // add an existing freeform task - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.isSessionActive = true - - // new freeform task added - val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)) - .logTaskAdded(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2))) - verify(desktopModeEventLogger, never()).logSessionEnter(any()) - } - - @Test - fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() { - // add an existing freeform task - val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(taskInfo) - transitionObserver.isSessionActive = true - - // task position changed - val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_CHANGE, 0) - .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) - .build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)) - .logTaskInfoChanged( - eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1)) + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .mockStatic(SystemProperties::class.java) + .mockStatic(Trace::class.java) + .build()!! + + private val testExecutor = mock<ShellExecutor>() + private val mockShellInit = mock<ShellInit>() + private val transitions = mock<Transitions>() + private val context = mock<Context>() + + private lateinit var transitionObserver: DesktopModeLoggerTransitionObserver + private lateinit var shellInit: ShellInit + private lateinit var desktopModeEventLogger: DesktopModeEventLogger + + @Before + fun setup() { + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + shellInit = spy(ShellInit(testExecutor)) + desktopModeEventLogger = mock<DesktopModeEventLogger>() + + transitionObserver = + DesktopModeLoggerTransitionObserver( + context, + mockShellInit, + transitions, + desktopModeEventLogger, + ) + val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java) + verify(mockShellInit) + .addInitCallback(initRunnableCaptor.capture(), same(transitionObserver)) + initRunnableCaptor.value.run() + // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a + // consistent state with no outstanding interactions when test cases start executing. + verify(desktopModeEventLogger).logTaskInfoStateInit() + } + + @Test + fun testInitialiseVisibleTasksSystemProperty() { + ExtendedMockito.verify { + SystemProperties.set( + eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY), + eq( + DesktopModeLoggerTransitionObserver + .VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE + ), + ) + } + } + + @Test + fun testRegistersObserverAtInit() { + verify(transitions).registerObserver(same(transitionObserver)) + } + + @Test + fun transitOpen_notFreeformWindow_doesNotLogTaskAddedOrSessionEnter() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, never()).logSessionEnter(any()) + verify(desktopModeEventLogger, never()).logTaskAdded(any()) + } + + @Test + fun transitOpen_logTaskAddedAndEnterReasonAppFreeformIntent() { + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_FREEFORM_INTENT, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), ) - verifyZeroInteractions(desktopModeEventLogger) - } - - @Test - fun sessionAlreadyStarted_taskResized_logsTaskUpdate() { - // add an existing freeform task - val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) - transitionObserver.addTaskInfosToCachedMap(taskInfo) - transitionObserver.isSessionActive = true - - // task resized - val newTaskInfo = - createTaskInfo( - WINDOWING_MODE_FREEFORM, - taskWidth = DEFAULT_TASK_WIDTH + 100, - taskHeight = DEFAULT_TASK_HEIGHT - 100) - val transitionInfo = - TransitionInfoBuilder(TRANSIT_CHANGE, 0) - .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) - .build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)) - .logTaskInfoChanged( - eq( - DEFAULT_TASK_UPDATE.copy( - taskWidth = DEFAULT_TASK_WIDTH + 100, taskHeight = DEFAULT_TASK_HEIGHT - 100, - visibleTaskCount = 1)) + } + + @Test + fun transitEndDragToDesktop_logTaskAddedAndEnterReasonAppHandleDrag() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + // task change is finalised when drag ends + val transitionInfo = + TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_HANDLE_DRAG, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), ) - verifyZeroInteractions(desktopModeEventLogger) - } - - @Test - fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() { - // add 2 existing freeform task - val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM) - val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2) - transitionObserver.addTaskInfosToCachedMap(taskInfo1) - transitionObserver.addTaskInfosToCachedMap(taskInfo2) - transitionObserver.isSessionActive = true - - // task 1 position update - val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) - val transitionInfo1 = - TransitionInfoBuilder(TRANSIT_CHANGE, 0) - .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1)) - .build() - callOnTransitionReady(transitionInfo1) - - verify(desktopModeEventLogger, times(1)) - .logTaskInfoChanged( - eq(DEFAULT_TASK_UPDATE.copy( - taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2)) + } + + @Test + fun transitEnterDesktopByButtonTap_logTaskAddedAndEnterReasonButtonTap() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_HANDLE_MENU_BUTTON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), ) - verifyZeroInteractions(desktopModeEventLogger) - - // task 2 resize - val newTaskInfo2 = - createTaskInfo( - WINDOWING_MODE_FREEFORM, - id = 2, - taskWidth = DEFAULT_TASK_WIDTH + 100, - taskHeight = DEFAULT_TASK_HEIGHT - 100) - val transitionInfo2 = - TransitionInfoBuilder(TRANSIT_CHANGE, 0) - .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2)) - .build() - - callOnTransitionReady(transitionInfo2) - - verify(desktopModeEventLogger, times(1)) - .logTaskInfoChanged( - eq( - DEFAULT_TASK_UPDATE.copy( - instanceId = 2, - taskWidth = DEFAULT_TASK_WIDTH + 100, - taskHeight = DEFAULT_TASK_HEIGHT - 100, - visibleTaskCount = 2)), - ) - verifyZeroInteractions(desktopModeEventLogger) - } - - @Test - fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { - // add two existing freeform tasks - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) - transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) - transitionObserver.isSessionActive = true - - // new freeform task closed - val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) - val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() - callOnTransitionReady(transitionInfo) - - verify(desktopModeEventLogger, times(1)) - .logTaskRemoved( - eq(DEFAULT_TASK_UPDATE.copy( - instanceId = 2, visibleTaskCount = 1)) + } + + @Test + fun transitEnterDesktopFromAppFromOverview_logTaskAddedAndEnterReasonAppFromOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_FROM_OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), ) - verify(desktopModeEventLogger, never()).logSessionExit(any()) - } - - /** Simulate calling the onTransitionReady() method */ - private fun callOnTransitionReady(transitionInfo: TransitionInfo) { - val transition = mock<IBinder>() - val startT = mock<SurfaceControl.Transaction>() - val finishT = mock<SurfaceControl.Transaction>() - - transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) - } - - private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) { - assertTrue(transitionObserver.isSessionActive) - verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(enterReason)) - verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(taskUpdate)) - ExtendedMockito.verify { - Trace.setCounter( - eq(Trace.TRACE_TAG_WINDOW_MANAGER), - eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME), - eq(taskUpdate.visibleTaskCount.toLong())) - } - ExtendedMockito.verify { - SystemProperties.set( - eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY), - eq(taskUpdate.visibleTaskCount.toString())) - } - verifyZeroInteractions(desktopModeEventLogger) - } - - private fun verifyTaskRemovedAndExitLogging( - exitReason: ExitReason, - taskUpdate: TaskUpdate - ) { - assertFalse(transitionObserver.isSessionActive) - verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(taskUpdate)) - verify(desktopModeEventLogger, times(1)).logSessionExit(eq(exitReason)) - verifyZeroInteractions(desktopModeEventLogger) - } - - private companion object { - const val DEFAULT_TASK_ID = 1 - const val DEFAULT_TASK_UID = 2 - const val DEFAULT_TASK_HEIGHT = 100 - const val DEFAULT_TASK_WIDTH = 200 - const val DEFAULT_TASK_X = 30 - const val DEFAULT_TASK_Y = 70 - const val DEFAULT_VISIBLE_TASK_COUNT = 0 - val DEFAULT_TASK_UPDATE = - TaskUpdate( - DEFAULT_TASK_ID, - DEFAULT_TASK_UID, - DEFAULT_TASK_HEIGHT, - DEFAULT_TASK_WIDTH, - DEFAULT_TASK_X, - DEFAULT_TASK_Y, - visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT, + } + + @Test + fun transitEnterDesktopFromKeyboardShortcut_logTaskAddedAndEnterReasonKeyboardShortcut() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.KEYBOARD_SHORTCUT_ENTER, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), ) + } + + @Test + fun transitToFront_logTaskAddedAndEnterReasonOverview() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() - fun createTaskInfo( - windowMode: Int, - id: Int = DEFAULT_TASK_ID, - uid: Int = DEFAULT_TASK_UID, - taskHeight: Int = DEFAULT_TASK_HEIGHT, - taskWidth: Int = DEFAULT_TASK_WIDTH, - taskX: Int = DEFAULT_TASK_X, - taskY: Int = DEFAULT_TASK_Y, - ) = - ActivityManager.RunningTaskInfo().apply { - taskId = id - effectiveUid = uid - configuration.windowConfiguration.apply { - windowingMode = windowMode - positionInParent = Point(taskX, taskY) - bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight)) - } + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitToFront_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.isSessionActive = true + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_TO_FRONT + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_FRONT, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitChange_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.isSessionActive = true + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_CHANGE + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CHANGE, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitOpen_previousTransitionExitToOverview_logTaskAddedAndEnterReasonOverview() { + // previous exit to overview transition + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.isSessionActive = true + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // Enter desktop mode from cancelled recents has no transition. Enter is detected on the + // next transition involving freeform windows + + // TRANSIT_OPEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + @Suppress("ktlint:standard:max-line-length") + fun transitEnterDesktopFromAppFromOverview_previousTransitionExitToOverview_logTaskAddedAndEnterReasonAppFromOverview() { + // Tests for AppFromOverview precedence in compared to cancelled Overview + + // previous exit to overview transition + // add a freeform task + val previousTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(previousTaskInfo) + transitionObserver.isSessionActive = true + val previousTransitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(createChange(TRANSIT_TO_BACK, previousTaskInfo)) + .build() + + callOnTransitionReady(previousTransitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + // TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW, 0) + .addChange(change) + .build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_FROM_OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitEnterDesktopFromUnknown_logTaskAddedAndEnterReasonUnknown() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.UNKNOWN_ENTER, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitWake_logTaskAddedAndEnterReasonScreenOn() { + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_WAKE, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.SCREEN_ON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitBack_previousExitReasonScreenOff_logTaskAddedAndEnterReasonScreenOn() { + val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) + // Previous Exit reason recorded as Screen Off + transitionObserver.addTaskInfosToCachedMap(freeformTask) + transitionObserver.isSessionActive = true + callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) + verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + // Enter desktop through back transition, this happens when user enters after dismissing + // keyguard + val change = createChange(TRANSIT_TO_FRONT, freeformTask) + val transitionInfo = TransitionInfoBuilder(TRANSIT_TO_BACK, 0).addChange(change).build() + + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.SCREEN_ON, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitEndDragToDesktop_previousExitReasonScreenOff_logTaskAddedAndEnterReasonAppDrag() { + val freeformTask = createTaskInfo(WINDOWING_MODE_FREEFORM) + // Previous Exit reason recorded as Screen Off + transitionObserver.addTaskInfosToCachedMap(freeformTask) + transitionObserver.isSessionActive = true + callOnTransitionReady(TransitionInfoBuilder(TRANSIT_SLEEP).build()) + verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + + // Enter desktop through app handle drag. This represents cases where instead of moving to + // desktop right after turning the screen on, we move to fullscreen then move another task + // to desktop + val transitionInfo = + TransitionInfoBuilder(Transitions.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, 0) + .addChange(createChange(TRANSIT_TO_FRONT, freeformTask)) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskAddedAndEnterLogging( + EnterReason.APP_HANDLE_DRAG, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun transitSleep_logTaskRemovedAndExitReasonScreenOff() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + val transitionInfo = TransitionInfoBuilder(TRANSIT_SLEEP).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.SCREEN_OFF, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopTaskDrag_logTaskRemovedAndExitReasonDragToExit() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.DRAG_TO_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopAppHandleButton_logTaskRemovedAndExitReasonButton() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.APP_HANDLE_MENU_BUTTON_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopUsingKeyboard_logTaskRemovedAndExitReasonKeyboard() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.KEYBOARD_SHORTCUT_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitExitDesktopUnknown_logTaskRemovedAndExitReasonUnknown() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // window mode changing from FREEFORM to FULLSCREEN + val change = createChange(TRANSIT_TO_FRONT, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.UNKNOWN_EXIT, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitToFrontWithFlagRecents_logTaskRemovedAndExitReasonOverview() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // recents transition + val change = createChange(TRANSIT_TO_BACK, createTaskInfo(WINDOWING_MODE_FREEFORM)) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitClose_logTaskRemovedAndExitReasonTaskFinished() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // task closing + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verifyTaskRemovedAndExitLogging(ExitReason.TASK_FINISHED, DEFAULT_TASK_UPDATE) + } + + @Test + fun transitMinimize_logExitReasongMinimized() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // minimize the task + val change = createChange(TRANSIT_MINIMIZE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_MINIMIZE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + assertFalse(transitionObserver.isSessionActive) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED)) + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE)) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionExitByRecents_cancelledAnimation_sessionRestored() { + // add a freeform task to an existing session + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.isSessionActive = true + + // recents transition sent freeform window to back + val change = createChange(TRANSIT_TO_BACK, taskInfo) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_TO_FRONT, TRANSIT_FLAG_IS_RECENTS) + .addChange(change) + .build() + callOnTransitionReady(transitionInfo1) + + verifyTaskRemovedAndExitLogging(ExitReason.RETURN_HOME_OR_OVERVIEW, DEFAULT_TASK_UPDATE) + + val transitionInfo2 = TransitionInfoBuilder(TRANSIT_NONE).build() + callOnTransitionReady(transitionInfo2) + + verifyTaskAddedAndEnterLogging( + EnterReason.OVERVIEW, + DEFAULT_TASK_UPDATE.copy(visibleTaskCount = 1), + ) + } + + @Test + fun sessionAlreadyStarted_newFreeformTaskAdded_logsTaskAdded() { + // add an existing freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // new freeform task added + val change = createChange(TRANSIT_OPEN, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskAdded(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 2))) + verify(desktopModeEventLogger, never()).logSessionEnter(any()) + } + + @Test + fun sessionAlreadyStarted_taskPositionChanged_logsTaskUpdate() { + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.isSessionActive = true + + // task position changed + val newTaskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 1)) + ) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_taskResized_logsTaskUpdate() { + // add an existing freeform task + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) + transitionObserver.addTaskInfosToCachedMap(taskInfo) + transitionObserver.isSessionActive = true + + // task resized + val newTaskInfo = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100, + ) + val transitionInfo = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo)) + .build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq( + DEFAULT_TASK_UPDATE.copy( + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100, + visibleTaskCount = 1, + ) + ) + ) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_multipleTasksUpdated_logsTaskUpdateForCorrectTask() { + // add 2 existing freeform task + val taskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM) + val taskInfo2 = createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2) + transitionObserver.addTaskInfosToCachedMap(taskInfo1) + transitionObserver.addTaskInfosToCachedMap(taskInfo2) + transitionObserver.isSessionActive = true + + // task 1 position update + val newTaskInfo1 = createTaskInfo(WINDOWING_MODE_FREEFORM, taskX = DEFAULT_TASK_X + 100) + val transitionInfo1 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo1)) + .build() + callOnTransitionReady(transitionInfo1) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq(DEFAULT_TASK_UPDATE.copy(taskX = DEFAULT_TASK_X + 100, visibleTaskCount = 2)) + ) + verifyZeroInteractions(desktopModeEventLogger) + + // task 2 resize + val newTaskInfo2 = + createTaskInfo( + WINDOWING_MODE_FREEFORM, + id = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100, + ) + val transitionInfo2 = + TransitionInfoBuilder(TRANSIT_CHANGE, 0) + .addChange(createChange(TRANSIT_CHANGE, newTaskInfo2)) + .build() + + callOnTransitionReady(transitionInfo2) + + verify(desktopModeEventLogger, times(1)) + .logTaskInfoChanged( + eq( + DEFAULT_TASK_UPDATE.copy( + instanceId = 2, + taskWidth = DEFAULT_TASK_WIDTH + 100, + taskHeight = DEFAULT_TASK_HEIGHT - 100, + visibleTaskCount = 2, + ) + ) + ) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test + fun sessionAlreadyStarted_freeformTaskRemoved_logsTaskRemoved() { + // add two existing freeform tasks + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + transitionObserver.isSessionActive = true + + // new freeform task closed + val change = createChange(TRANSIT_CLOSE, createTaskInfo(WINDOWING_MODE_FREEFORM, id = 2)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build() + callOnTransitionReady(transitionInfo) + + verify(desktopModeEventLogger, times(1)) + .logTaskRemoved(eq(DEFAULT_TASK_UPDATE.copy(instanceId = 2, visibleTaskCount = 1))) + verify(desktopModeEventLogger, never()).logSessionExit(any()) + } + + /** Simulate calling the onTransitionReady() method */ + private fun callOnTransitionReady(transitionInfo: TransitionInfo) { + val transition = mock<IBinder>() + val startT = mock<SurfaceControl.Transaction>() + val finishT = mock<SurfaceControl.Transaction>() + + transitionObserver.onTransitionReady(transition, transitionInfo, startT, finishT) + } + + private fun verifyTaskAddedAndEnterLogging(enterReason: EnterReason, taskUpdate: TaskUpdate) { + assertTrue(transitionObserver.isSessionActive) + verify(desktopModeEventLogger, times(1)).logSessionEnter(eq(enterReason)) + verify(desktopModeEventLogger, times(1)).logTaskAdded(eq(taskUpdate)) + ExtendedMockito.verify { + Trace.setCounter( + eq(Trace.TRACE_TAG_WINDOW_MANAGER), + eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_NAME), + eq(taskUpdate.visibleTaskCount.toLong()), + ) } + ExtendedMockito.verify { + SystemProperties.set( + eq(DesktopModeLoggerTransitionObserver.VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY), + eq(taskUpdate.visibleTaskCount.toString()), + ) + } + verifyZeroInteractions(desktopModeEventLogger) + } + + private fun verifyTaskRemovedAndExitLogging(exitReason: ExitReason, taskUpdate: TaskUpdate) { + assertFalse(transitionObserver.isSessionActive) + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(taskUpdate)) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(exitReason)) + verifyZeroInteractions(desktopModeEventLogger) + } + + private companion object { + const val DEFAULT_TASK_ID = 1 + const val DEFAULT_TASK_UID = 2 + const val DEFAULT_TASK_HEIGHT = 100 + const val DEFAULT_TASK_WIDTH = 200 + const val DEFAULT_TASK_X = 30 + const val DEFAULT_TASK_Y = 70 + const val DEFAULT_VISIBLE_TASK_COUNT = 0 + val DEFAULT_TASK_UPDATE = + TaskUpdate( + DEFAULT_TASK_ID, + DEFAULT_TASK_UID, + DEFAULT_TASK_HEIGHT, + DEFAULT_TASK_WIDTH, + DEFAULT_TASK_X, + DEFAULT_TASK_Y, + visibleTaskCount = DEFAULT_VISIBLE_TASK_COUNT, + ) - fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { - val change = - Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>()) - change.mode = mode - change.taskInfo = taskInfo - return change + fun createTaskInfo( + windowMode: Int, + id: Int = DEFAULT_TASK_ID, + uid: Int = DEFAULT_TASK_UID, + taskHeight: Int = DEFAULT_TASK_HEIGHT, + taskWidth: Int = DEFAULT_TASK_WIDTH, + taskX: Int = DEFAULT_TASK_X, + taskY: Int = DEFAULT_TASK_Y, + ) = + ActivityManager.RunningTaskInfo().apply { + taskId = id + effectiveUid = uid + configuration.windowConfiguration.apply { + windowingMode = windowMode + positionInParent = Point(taskX, taskY) + bounds.set(Rect(taskX, taskY, taskX + taskWidth, taskY + taskHeight)) + } + } + + fun createChange(mode: Int, taskInfo: ActivityManager.RunningTaskInfo): Change { + val change = + Change(WindowContainerToken(mock<IWindowContainerToken>()), mock<SurfaceControl>()) + change.mode = mode + change.taskInfo = taskInfo + return change + } } - } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt index db4e93de9541..f6eed5da6cad 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeTransitionTypesTest.kt @@ -18,18 +18,18 @@ package com.android.wm.shell.desktopmode import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest -import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON -import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG -import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_HANDLE_MENU_BUTTON +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG +import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getEnterTransitionType import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.getExitTransitionType -import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_FROM_OVERVIEW +import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.KEYBOARD_SHORTCUT import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.TASK_DRAG import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN @@ -53,8 +53,7 @@ class DesktopModeTransitionTypesTest { .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_HANDLE_MENU_BUTTON) assertThat(APP_FROM_OVERVIEW.getEnterTransitionType()) .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_APP_FROM_OVERVIEW) - assertThat(TASK_DRAG.getEnterTransitionType()) - .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) + assertThat(TASK_DRAG.getEnterTransitionType()).isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_UNKNOWN) assertThat(KEYBOARD_SHORTCUT.getEnterTransitionType()) .isEqualTo(TRANSIT_ENTER_DESKTOP_FROM_KEYBOARD_SHORTCUT) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt index 94698e2fc0fb..72b1fd9af117 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLoggerTest.kt @@ -16,7 +16,6 @@ package com.android.wm.shell.desktopmode - import android.content.ComponentName import android.content.pm.ApplicationInfo import android.content.pm.PackageManager @@ -67,8 +66,7 @@ class DesktopModeUiEventLoggerTest : ShellTestCase() { @Test fun log_eventLogged() { - val event = - DESKTOP_WINDOW_EDGE_DRAG_RESIZE + val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE logger.log(UID, PACKAGE_NAME, event) assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id) @@ -97,8 +95,7 @@ class DesktopModeUiEventLoggerTest : ShellTestCase() { @Test fun logWithInstanceId_eventLogged() { - val event = - DESKTOP_WINDOW_EDGE_DRAG_RESIZE + val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE logger.logWithInstanceId(INSTANCE_ID, UID, PACKAGE_NAME, event) assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) assertThat(uiEventLoggerFake.eventId(0)).isEqualTo(event.id) @@ -109,12 +106,12 @@ class DesktopModeUiEventLoggerTest : ShellTestCase() { @Test fun logWithTaskInfo_eventLogged() { - val event = - DESKTOP_WINDOW_EDGE_DRAG_RESIZE - val taskInfo = TestRunningTaskInfoBuilder() - .setUserId(USER_ID) - .setBaseActivity(ComponentName(PACKAGE_NAME, "test")) - .build() + val event = DESKTOP_WINDOW_EDGE_DRAG_RESIZE + val taskInfo = + TestRunningTaskInfoBuilder() + .setUserId(USER_ID) + .setBaseActivity(ComponentName(PACKAGE_NAME, "test")) + .build() whenever(mockPackageManager.getApplicationInfoAsUser(PACKAGE_NAME, /* flags= */ 0, USER_ID)) .thenReturn(ApplicationInfo().apply { uid = UID }) logger.log(taskInfo, event) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt index 935e6d052f5e..e46d2c7147ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicatorTest.kt @@ -66,74 +66,109 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { fun testFullscreenRegionCalculation() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, - 2 * STABLE_INSETS.top)) + assertThat(testRegion.bounds) + .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, 2 * STABLE_INSETS.top)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) val transitionHeight = SystemBarUtils.getStatusBarHeight(context) - val toFullscreenScale = mContext.resources.getFloat( - R.dimen.desktop_mode_fullscreen_region_scale - ) + val toFullscreenScale = + mContext.resources.getFloat(R.dimen.desktop_mode_fullscreen_region_scale) val toFullscreenWidth = displayLayout.width() * toFullscreenScale - assertThat(testRegion.bounds).isEqualTo(Rect( - (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(), - Short.MIN_VALUE.toInt(), - (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(), - transitionHeight)) + assertThat(testRegion.bounds) + .isEqualTo( + Rect( + (DISPLAY_BOUNDS.width() / 2f - toFullscreenWidth / 2f).toInt(), + Short.MIN_VALUE.toInt(), + (DISPLAY_BOUNDS.width() / 2f + toFullscreenWidth / 2f).toInt(), + transitionHeight, + ) + ) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, - 2 * STABLE_INSETS.top)) + assertThat(testRegion.bounds) + .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, 2 * STABLE_INSETS.top)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) testRegion = visualIndicator.calculateFullscreenRegion(displayLayout, CAPTION_HEIGHT) - assertThat(testRegion.bounds).isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, - transitionHeight)) + assertThat(testRegion.bounds) + .isEqualTo(Rect(0, Short.MIN_VALUE.toInt(), 2400, transitionHeight)) } @Test fun testSplitLeftRegionCalculation() { - val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_split_from_desktop_height) + val transitionHeight = + context.resources.getDimensionPixelSize(R.dimen.desktop_mode_split_from_desktop_height) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) - var testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + var testRegion = + visualIndicator.calculateSplitLeftRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) - testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitLeftRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(0, transitionHeight, 32, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) - testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitLeftRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) - testRegion = visualIndicator.calculateSplitLeftRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitLeftRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(0, -50, 32, 1600)) } @Test fun testSplitRightRegionCalculation() { - val transitionHeight = context.resources.getDimensionPixelSize( - R.dimen.desktop_mode_split_from_desktop_height) + val transitionHeight = + context.resources.getDimensionPixelSize(R.dimen.desktop_mode_split_from_desktop_height) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) - var testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + var testRegion = + visualIndicator.calculateSplitRightRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FREEFORM) - testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitRightRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(2368, transitionHeight, 2400, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) - testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitRightRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) - testRegion = visualIndicator.calculateSplitRightRegion(displayLayout, - TRANSITION_AREA_WIDTH, CAPTION_HEIGHT) + testRegion = + visualIndicator.calculateSplitRightRegion( + displayLayout, + TRANSITION_AREA_WIDTH, + CAPTION_HEIGHT, + ) assertThat(testRegion.bounds).isEqualTo(Rect(2368, -50, 2400, 1600)) } @@ -141,10 +176,12 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { fun testDefaultIndicators() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) - assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) result = visualIndicator.updateIndicatorType(PointF(10000f, 500f)) - assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) result = visualIndicator.updateIndicatorType(PointF(500f, 10000f)) assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) @@ -154,8 +191,16 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { } private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { - visualIndicator = DesktopModeVisualIndicator(syncQueue, taskInfo, displayController, - context, taskSurface, taskDisplayAreaOrganizer, dragStartState) + visualIndicator = + DesktopModeVisualIndicator( + syncQueue, + taskInfo, + displayController, + context, + taskSurface, + taskDisplayAreaOrganizer, + dragStartState, + ) } companion object { @@ -163,11 +208,12 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { private const val CAPTION_HEIGHT = 50 private val DISPLAY_BOUNDS = Rect(0, 0, 2400, 1600) private const val NAVBAR_HEIGHT = 50 - private val STABLE_INSETS = Rect( - DISPLAY_BOUNDS.left, - DISPLAY_BOUNDS.top + CAPTION_HEIGHT, - DISPLAY_BOUNDS.right, - DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT - ) + private val STABLE_INSETS = + Rect( + DISPLAY_BOUNDS.left, + DISPLAY_BOUNDS.top + CAPTION_HEIGHT, + DISPLAY_BOUNDS.right, + DISPLAY_BOUNDS.bottom - NAVBAR_HEIGHT, + ) } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt index 344140d91ab3..e777ec7b55f6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopRepositoryTest.kt @@ -76,15 +76,9 @@ class DesktopRepositoryTest : ShellTestCase() { datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - repo = - DesktopRepository( - persistentRepository, - datastoreScope, - DEFAULT_USER_ID - ) - whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( - Desktop.getDefaultInstance() - ) + repo = DesktopRepository(persistentRepository, datastoreScope, DEFAULT_USER_ID) + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) + .thenReturn(Desktop.getDefaultInstance()) shellInit.init() } @@ -245,7 +239,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(1)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf() + freeformTasksInZOrder = arrayListOf(), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -253,7 +247,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(1, 2)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf() + freeformTasksInZOrder = arrayListOf(), ) } } @@ -441,8 +435,8 @@ class DesktopRepositoryTest : ShellTestCase() { } /** - * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY. - * This tests that task is removed from the last parent display when it vanishes. + * When a task vanishes, the displayId of the task is set to INVALID_DISPLAY. This tests that + * task is removed from the last parent display when it vanishes. */ @Test fun updateTask_removeVisibleTasksRemovesTaskWithInvalidDisplay() { @@ -562,7 +556,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(5) + freeformTasksInZOrder = arrayListOf(5), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -570,7 +564,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(5)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(6, 5) + freeformTasksInZOrder = arrayListOf(6, 5), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -578,10 +572,10 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(5, 6)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(7, 6, 5) + freeformTasksInZOrder = arrayListOf(7, 6, 5), ) } - } + } @Test fun addTask_alreadyExists_movesToTop() { @@ -628,7 +622,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(5) + freeformTasksInZOrder = arrayListOf(5), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -636,7 +630,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(5)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(6, 5) + freeformTasksInZOrder = arrayListOf(6, 5), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -644,7 +638,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(5, 6)), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(7, 6, 5) + freeformTasksInZOrder = arrayListOf(7, 6, 5), ) verify(persistentRepository, times(2)) .addOrUpdateDesktop( @@ -652,10 +646,10 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(arrayOf(5, 7)), minimizedTasks = ArraySet(arrayOf(6)), - freeformTasksInZOrder = arrayListOf(7, 6, 5) + freeformTasksInZOrder = arrayListOf(7, 6, 5), ) } - } + } @Test fun addTask_taskIsUnminimized_noop() { @@ -694,7 +688,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(1) + freeformTasksInZOrder = arrayListOf(1), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -702,7 +696,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = ArrayList() + freeformTasksInZOrder = ArrayList(), ) } } @@ -731,7 +725,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(1) + freeformTasksInZOrder = arrayListOf(1), ) verify(persistentRepository) .addOrUpdateDesktop( @@ -739,7 +733,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = ArrayList() + freeformTasksInZOrder = ArrayList(), ) } } @@ -768,7 +762,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = arrayListOf(1) + freeformTasksInZOrder = arrayListOf(1), ) verify(persistentRepository, never()) .addOrUpdateDesktop( @@ -776,7 +770,7 @@ class DesktopRepositoryTest : ShellTestCase() { DEFAULT_DESKTOP_ID, visibleTasks = ArraySet(), minimizedTasks = ArraySet(), - freeformTasksInZOrder = ArrayList() + freeformTasksInZOrder = ArrayList(), ) } } @@ -928,7 +922,6 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.isMinimizedTask(taskId = 2)).isFalse() } - @Test fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() { repo.minimizeTask(displayId = 10, taskId = 2) @@ -1056,6 +1049,7 @@ class DesktopRepositoryTest : ShellTestCase() { class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 + override fun onActiveTasksChanged(displayId: Int) { when (displayId) { DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++ @@ -1093,4 +1087,4 @@ class DesktopRepositoryTest : ShellTestCase() { private const val DEFAULT_USER_ID = 1000 private const val DEFAULT_DESKTOP_ID = 0 } -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt index b4daa6637f83..19ab9113bc7a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt @@ -22,7 +22,6 @@ import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION -import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask @@ -45,167 +44,144 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopTaskChangeListenerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule() - private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener + private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener - private val desktopUserRepositories = mock<DesktopUserRepositories>() - private val desktopRepository = mock<DesktopRepository>() + private val desktopUserRepositories = mock<DesktopUserRepositories>() + private val desktopRepository = mock<DesktopRepository>() - @Before - fun setUp() { - desktopTaskChangeListener = DesktopTaskChangeListener(desktopUserRepositories) + @Before + fun setUp() { + desktopTaskChangeListener = DesktopTaskChangeListener(desktopUserRepositories) - whenever(desktopUserRepositories.current).thenReturn(desktopRepository) - whenever(desktopUserRepositories.getProfile(anyInt())).thenReturn(desktopRepository) - } + whenever(desktopUserRepositories.current).thenReturn(desktopRepository) + whenever(desktopUserRepositories.getProfile(anyInt())).thenReturn(desktopRepository) + } - @Test - fun onTaskOpening_fullscreenTask_notActiveDesktopTask_noop() { - val task = createFullscreenTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(false) + @Test + fun onTaskOpening_fullscreenTask_notActiveDesktopTask_noop() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(false) - desktopTaskChangeListener.onTaskOpening(task) + desktopTaskChangeListener.onTaskOpening(task) - verify(desktopUserRepositories.current, never()) - .addTask(task.displayId, task.taskId, task.isVisible) - verify(desktopUserRepositories.current, never()) - .removeFreeformTask(task.displayId, task.taskId) - } + verify(desktopUserRepositories.current, never()) + .addTask(task.displayId, task.taskId, task.isVisible) + verify(desktopUserRepositories.current, never()) + .removeFreeformTask(task.displayId, task.taskId) + } - @Test - fun onTaskOpening_freeformTask_activeDesktopTask_removesTaskFromRepo() { - val task = createFullscreenTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskOpening(task) - - verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) - } + @Test + fun onTaskOpening_freeformTask_activeDesktopTask_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) - @Test - fun onTaskOpening_freeformTask_visibleDesktopTask_addsTaskToRepository() { - val task = createFreeformTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(false) - - desktopTaskChangeListener.onTaskOpening(task) - - verify(desktopUserRepositories.current) - .addTask(task.displayId, task.taskId, task.isVisible) - } - - @Test - fun onTaskOpening_freeformTask_nonVisibleDesktopTask_addsTaskToRepository() { - val task = createFreeformTask().apply { isVisible = false } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskOpening(task) - - verify(desktopUserRepositories.current) - .addTask(task.displayId, task.taskId, task.isVisible) - } - - @Test - fun onTaskChanging_freeformTaskOutsideDesktop_removesTaskFromRepo() { - val task = createFullscreenTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskChanging(task) - - verify(desktopUserRepositories.current) - .removeFreeformTask(task.displayId, task.taskId) - } - - @Test - fun onTaskChanging_visibleTaskInDesktop_updatesTaskVisibility() { - val task = createFreeformTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskChanging(task) - - verify(desktopUserRepositories.current) - .updateTask(task.displayId, task.taskId, task.isVisible) - } - - @Test - fun onTaskChanging_nonVisibleTask_updatesTaskVisibility() { - val task = createFreeformTask().apply { isVisible = false } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskChanging(task) - - verify(desktopUserRepositories.current) - .updateTask(task.displayId, task.taskId, task.isVisible) - } - - @Test - fun onTaskMovingToFront_freeformTaskOutsideDesktop_removesTaskFromRepo() { - val task = createFullscreenTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskMovingToFront(task) - - verify(desktopUserRepositories.current) - .removeFreeformTask(task.displayId, task.taskId) - } - - @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() { - val task = createFreeformTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - whenever(desktopUserRepositories.current.isClosingTask(task.taskId)) - .thenReturn(false) - - desktopTaskChangeListener.onTaskClosing(task) - - verify(desktopUserRepositories.current) - .updateTask(task.displayId, task.taskId, isVisible = false) - verify(desktopUserRepositories.current) - .minimizeTask(task.displayId, task.taskId) - } - - @Test - @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun onTaskClosing_backNavDisabled_closingTask_removesTaskInRepo() { - val task = createFreeformTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - whenever(desktopUserRepositories.current.isClosingTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskClosing(task) - - verify(desktopUserRepositories.current, never()) - .minimizeTask(task.displayId, task.taskId) - verify(desktopUserRepositories.current) - .removeClosingTask(task.taskId) - verify(desktopUserRepositories.current) - .removeFreeformTask(task.displayId, task.taskId) - } - - @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun onTaskClosing_backNavEnabled_closingTask_removesTaskFromRepo() { - val task = createFreeformTask().apply { isVisible = true } - whenever(desktopUserRepositories.current.isActiveTask(task.taskId)) - .thenReturn(true) - whenever(desktopUserRepositories.current.isClosingTask(task.taskId)) - .thenReturn(true) - - desktopTaskChangeListener.onTaskClosing(task) - - verify(desktopUserRepositories.current).removeClosingTask(task.taskId) - verify(desktopUserRepositories.current) - .removeFreeformTask(task.displayId, task.taskId) - } + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + fun onTaskOpening_freeformTask_visibleDesktopTask_addsTaskToRepository() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopUserRepositories.current).addTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskOpening_freeformTask_nonVisibleDesktopTask_addsTaskToRepository() { + val task = createFreeformTask().apply { isVisible = false } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopUserRepositories.current).addTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskChanging_freeformTaskOutsideDesktop_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + fun onTaskChanging_visibleTaskInDesktop_updatesTaskVisibility() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopUserRepositories.current) + .updateTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskChanging_nonVisibleTask_updatesTaskVisibility() { + val task = createFreeformTask().apply { isVisible = false } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopUserRepositories.current) + .updateTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskMovingToFront_freeformTaskOutsideDesktop_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskMovingToFront(task) + + verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopUserRepositories.current) + .updateTask(task.displayId, task.taskId, isVisible = false) + verify(desktopUserRepositories.current).minimizeTask(task.displayId, task.taskId) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavDisabled_closingTask_removesTaskInRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopUserRepositories.current, never()).minimizeTask(task.displayId, task.taskId) + verify(desktopUserRepositories.current).removeClosingTask(task.taskId) + verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavEnabled_closingTask_removesTaskFromRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopUserRepositories.current.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopUserRepositories.current.isClosingTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopUserRepositories.current).removeClosingTask(task.taskId) + verify(desktopUserRepositories.current).removeFreeformTask(task.displayId, task.taskId) + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 0b12d228a0c2..0eb88e368054 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt @@ -81,9 +81,9 @@ import com.android.dx.mockito.inline.extended.ExtendedMockito.never import com.android.dx.mockito.inline.extended.StaticMockitoSession import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags -import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.window.flags.Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP +import com.android.window.flags.Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY import com.android.wm.shell.MockToken import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -96,7 +96,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.common.MultiInstanceHelper import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SyncTransactionQueue -import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.InputMethod import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.ResizeTrigger @@ -109,6 +108,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.createHomeTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSplitScreenTask import com.android.wm.shell.desktopmode.EnterDesktopTaskTransitionHandler.FREEFORM_ANIMATION_DURATION import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION +import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository @@ -137,8 +137,8 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage -import java.util.function.Consumer import java.util.Optional +import java.util.function.Consumer import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import kotlin.test.assertIs @@ -167,8 +167,8 @@ import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock import org.mockito.Mockito.spy -import org.mockito.Mockito.verify import org.mockito.Mockito.times +import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.argumentCaptor @@ -189,4415 +189,4737 @@ import org.mockito.quality.Strictness @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopTasksControllerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - - @Mock lateinit var testExecutor: ShellExecutor - @Mock lateinit var shellCommandHandler: ShellCommandHandler - @Mock lateinit var shellController: ShellController - @Mock lateinit var displayController: DisplayController - @Mock lateinit var displayLayout: DisplayLayout - @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer - @Mock lateinit var syncQueue: SyncTransactionQueue - @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer - @Mock lateinit var transitions: Transitions - @Mock lateinit var keyguardManager: KeyguardManager - @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator - @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler - @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler - @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler - @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler - @Mock - lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler - @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler - @Mock - lateinit var mMockDesktopImmersiveController: DesktopImmersiveController - @Mock lateinit var splitScreenController: SplitScreenController - @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler - @Mock lateinit var dragAndDropController: DragAndDropController - @Mock lateinit var multiInstanceHelper: MultiInstanceHelper - @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator - @Mock lateinit var recentTasksController: RecentTasksController - @Mock - private lateinit var mockInteractionJankMonitor: InteractionJankMonitor - @Mock private lateinit var mockSurface: SurfaceControl - @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener - @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter - @Mock private lateinit var mockHandler: Handler - @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger - @Mock private lateinit var desktopModeUiEventLogger: DesktopModeUiEventLogger - @Mock lateinit var persistentRepository: DesktopPersistentRepository - @Mock lateinit var motionEvent: MotionEvent - @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer - @Mock private lateinit var mockToast: Toast - private lateinit var mockitoSession: StaticMockitoSession - @Mock - private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel - @Mock - private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration - @Mock private lateinit var resources: Resources - @Mock - lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener - @Mock private lateinit var userManager: UserManager - private lateinit var controller: DesktopTasksController - private lateinit var shellInit: ShellInit - private lateinit var taskRepository: DesktopRepository - private lateinit var userRepositories: DesktopUserRepositories - private lateinit var desktopTasksLimiter: DesktopTasksLimiter - private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener - private lateinit var testScope: CoroutineScope - - private val shellExecutor = TestShellExecutor() - - // Mock running tasks are registered here so we can get the list from mock shell task organizer - private val runningTasks = mutableListOf<RunningTaskInfo>() - - private val DISPLAY_DIMENSION_SHORT = 1600 - private val DISPLAY_DIMENSION_LONG = 2560 - private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275) - private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085) - private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635) - private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275) - private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611) - private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275) - - @Before - fun setUp() { - Dispatchers.setMain(StandardTestDispatcher()) - mockitoSession = - mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .spyStatic(Toast::class.java) - .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - shellInit = spy(ShellInit(testExecutor)) - userRepositories = - DesktopUserRepositories( - context, - shellInit, - shellController, - persistentRepository, - repositoryInitializer, - testScope, - userManager) - desktopTasksLimiter = - DesktopTasksLimiter( + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + @Mock lateinit var testExecutor: ShellExecutor + @Mock lateinit var shellCommandHandler: ShellCommandHandler + @Mock lateinit var shellController: ShellController + @Mock lateinit var displayController: DisplayController + @Mock lateinit var displayLayout: DisplayLayout + @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer + @Mock lateinit var syncQueue: SyncTransactionQueue + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer + @Mock lateinit var transitions: Transitions + @Mock lateinit var keyguardManager: KeyguardManager + @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator + @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler + @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler + @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler + @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler + @Mock + lateinit var toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler + @Mock lateinit var dragToDesktopTransitionHandler: DragToDesktopTransitionHandler + @Mock lateinit var mMockDesktopImmersiveController: DesktopImmersiveController + @Mock lateinit var splitScreenController: SplitScreenController + @Mock lateinit var recentsTransitionHandler: RecentsTransitionHandler + @Mock lateinit var dragAndDropController: DragAndDropController + @Mock lateinit var multiInstanceHelper: MultiInstanceHelper + @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator + @Mock lateinit var recentTasksController: RecentTasksController + @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockSurface: SurfaceControl + @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener + @Mock private lateinit var freeformTaskTransitionStarter: FreeformTaskTransitionStarter + @Mock private lateinit var mockHandler: Handler + @Mock private lateinit var desktopModeEventLogger: DesktopModeEventLogger + @Mock private lateinit var desktopModeUiEventLogger: DesktopModeUiEventLogger + @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var motionEvent: MotionEvent + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer + @Mock private lateinit var mockToast: Toast + private lateinit var mockitoSession: StaticMockitoSession + @Mock private lateinit var desktopTilingDecorViewModel: DesktopTilingDecorViewModel + @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration + @Mock private lateinit var resources: Resources + @Mock + lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener + @Mock private lateinit var userManager: UserManager + private lateinit var controller: DesktopTasksController + private lateinit var shellInit: ShellInit + private lateinit var taskRepository: DesktopRepository + private lateinit var userRepositories: DesktopUserRepositories + private lateinit var desktopTasksLimiter: DesktopTasksLimiter + private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener + private lateinit var testScope: CoroutineScope + + private val shellExecutor = TestShellExecutor() + + // Mock running tasks are registered here so we can get the list from mock shell task organizer + private val runningTasks = mutableListOf<RunningTaskInfo>() + + private val DISPLAY_DIMENSION_SHORT = 1600 + private val DISPLAY_DIMENSION_LONG = 2560 + private val DEFAULT_LANDSCAPE_BOUNDS = Rect(320, 75, 2240, 1275) + private val DEFAULT_PORTRAIT_BOUNDS = Rect(200, 165, 1400, 2085) + private val RESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 435, 1575, 1635) + private val RESIZABLE_PORTRAIT_BOUNDS = Rect(680, 75, 1880, 1275) + private val UNRESIZABLE_LANDSCAPE_BOUNDS = Rect(25, 449, 1575, 1611) + private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275) + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + mockitoSession = + mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .spyStatic(Toast::class.java) + .startMocking() + doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + + testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + shellInit = spy(ShellInit(testExecutor)) + userRepositories = + DesktopUserRepositories( + context, + shellInit, + shellController, + persistentRepository, + repositoryInitializer, + testScope, + userManager, + ) + desktopTasksLimiter = + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + MAX_TASK_LIMIT, + mockInteractionJankMonitor, + mContext, + mockHandler, + ) + + whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } + whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }) + .thenReturn(Desktop.getDefaultInstance()) + doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } + + val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + any<RunningTaskInfo>(), + any(), + ) + ) + .thenReturn(ExitResult.NoExit) + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + anyInt(), + anyOrNull(), + any(), + ) + ) + .thenReturn(ExitResult.NoExit) + + controller = createController() + controller.setSplitScreenController(splitScreenController) + controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter + controller.desktopModeEnterExitTransitionListener = desktopModeEnterExitTransitionListener + + shellInit.init() + + val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) + recentsTransitionStateListener = captor.value + + controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + + assumeTrue(ENABLE_SHELL_TRANSITIONS) + + taskRepository = userRepositories.current + } + + private fun createController(): DesktopTasksController { + return DesktopTasksController( + context, + shellInit, + shellCommandHandler, + shellController, + displayController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + dragAndDropController, transitions, + keyguardManager, + mReturnToDragStartAnimator, + desktopMixedTransitionHandler, + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, + dragAndDropTransitionHandler, + toggleResizeDesktopTaskTransitionHandler, + dragToDesktopTransitionHandler, + mMockDesktopImmersiveController, userRepositories, - shellTaskOrganizer, - MAX_TASK_LIMIT, + recentsTransitionHandler, + multiInstanceHelper, + shellExecutor, + Optional.of(desktopTasksLimiter), + recentTasksController, mockInteractionJankMonitor, - mContext, - mockHandler) - - whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } - whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } - whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) - whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(STABLE_BOUNDS) - } - whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( - Desktop.getDefaultInstance() - ) - doReturn(mockToast).`when` { Toast.makeText(any(), anyInt(), anyInt()) } - - val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>(), any())) - .thenReturn(ExitResult.NoExit) - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any())) - .thenReturn(ExitResult.NoExit) - - controller = createController() - controller.setSplitScreenController(splitScreenController) - controller.freeformTaskTransitionStarter = freeformTaskTransitionStarter - controller.desktopModeEnterExitTransitionListener = desktopModeEnterExitTransitionListener - - shellInit.init() - - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) - verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value - - controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener - - assumeTrue(ENABLE_SHELL_TRANSITIONS) - - taskRepository = userRepositories.current - } - - private fun createController(): DesktopTasksController { - return DesktopTasksController( - context, - shellInit, - shellCommandHandler, - shellController, - displayController, - shellTaskOrganizer, - syncQueue, - rootTaskDisplayAreaOrganizer, - dragAndDropController, - transitions, - keyguardManager, - mReturnToDragStartAnimator, - desktopMixedTransitionHandler, - enterDesktopTransitionHandler, - exitDesktopTransitionHandler, - dragAndDropTransitionHandler, - toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, - mMockDesktopImmersiveController, - userRepositories, - recentsTransitionHandler, - multiInstanceHelper, - shellExecutor, - Optional.of(desktopTasksLimiter), - recentTasksController, - mockInteractionJankMonitor, - mockHandler, - desktopModeEventLogger, - desktopModeUiEventLogger, - desktopTilingDecorViewModel, - ) - } - - @After - fun tearDown() { - mockitoSession.finishMocking() - - runningTasks.clear() - testScope.cancel() - } - - @Test - fun instantiate_addInitCallback() { - verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() { - setUpFreeformTask() - - assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse() - } - - @Test - fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { - val task1 = setUpFreeformTask() - - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) - controller.toggleDesktopTaskSize( - task1, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) + mockHandler, + desktopModeEventLogger, + desktopModeUiEventLogger, + desktopTilingDecorViewModel, + ) + } - verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task1, - STABLE_BOUNDS.width(), - STABLE_BOUNDS.height(), - displayController - ) - assertThat(argumentCaptor.value).isTrue() - } - - @Test - fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() { - val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } - setUpFreeformTask(bounds = stableBounds, active = true) - assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() - } - - @Test - fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfMaximizedTask_returnFalse() { - val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } - val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) - controller.toggleDesktopTaskSize( - task1, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.RESTORE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, - InputMethod.TOUCH - ) - ) + @After + fun tearDown() { + mockitoSession.finishMocking() - verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - eq(ResizeTrigger.MAXIMIZE_BUTTON), - eq(InputMethod.TOUCH), - eq(task1), - anyOrNull(), - anyOrNull(), - eq(displayController), - anyOrNull() - ) - assertThat(argumentCaptor.value).isFalse() - } - - @Test - fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() { - val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } - setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) - - assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() - } - - - @Test - fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { - whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) - clearInvocations(shellInit) - - createController() - - verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - fun isDesktopModeShowing_noTasks_returnsFalse() { - assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_noTasksVisible_returnsFalse() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskHidden(task2) - - assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() - } - - @Test - fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskVisible(task1) - markTaskHidden(task2) - - assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { - val homeTask = setUpHomeTask(SECOND_DISPLAY) - val task1 = setUpFreeformTask(SECOND_DISPLAY) - val task2 = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 (no wallpaper intent) - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskVisible(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { - val homeTask = setUpHomeTask(SECOND_DISPLAY) - val task1 = setUpFreeformTask(SECOND_DISPLAY) - val task2 = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(task1) - markTaskHidden(task2) - - controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskVisible(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: home, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - markTaskHidden(task1) - markTaskVisible(task2) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Expect order to be from bottom: wallpaper intent, task1, task2 - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - wct.assertReorderAt(index = 1, task1) - wct.assertReorderAt(index = 2, task2) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, homeTask) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { - val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) - val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) - setUpHomeTask(SECOND_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(taskDefaultDisplay) - markTaskHidden(taskSecondDisplay) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Expect order to be from bottom: home, task - wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) - wct.assertReorderAt(index = 1, taskDefaultDisplay) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { - val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) - val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) - setUpHomeTask(SECOND_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) - markTaskHidden(taskDefaultDisplay) - markTaskHidden(taskSecondDisplay) - - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Move home to front - wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) - // Add desktop wallpaper activity - wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) - // Move freeform task to front - wct.assertReorderAt(index = 2, taskDefaultDisplay) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val minimizedTask = setUpFreeformTask() - - markTaskHidden(freeformTask) - markTaskHidden(minimizedTask) - taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(2) - // Reorder home and freeform task to top, don't reorder the minimized task - wct.assertReorderAt(index = 0, homeTask, toTop = true) - wct.assertReorderAt(index = 1, freeformTask, toTop = true) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val minimizedTask = setUpFreeformTask() - - markTaskHidden(freeformTask) - markTaskHidden(minimizedTask) - taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) - controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) - - val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(3) - // Move home to front - wct.assertReorderAt(index = 0, homeTask, toTop = true) - // Add desktop wallpaper activity - wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) - // Reorder freeform task to top, don't reorder the minimized task - wct.assertReorderAt(index = 2, freeformTask, toTop = true) - } - - @Test - fun visibleTaskCount_noTasks_returnsZero() { - assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) - } - - @Test - fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskVisible) - assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) - } - - @Test - fun visibleTaskCount_twoTasks_oneVisible_returnsOne() { - setUpHomeTask() - setUpFreeformTask().also(::markTaskVisible) - setUpFreeformTask().also(::markTaskHidden) - assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) - } - - @Test - fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { - setUpHomeTask() - setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) - setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) - assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) - } - - @Test - fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(gravity = Gravity.LEFT) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) - } - - @Test - fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(gravity = Gravity.RIGHT) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) - } - - @Test - fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(gravity = Gravity.TOP) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) - } - - @Test - fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(gravity = Gravity.BOTTOM) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(finalBounds).isEqualTo(Rect()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) - val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS, active = false) - - val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) - - assertNotNull(wct, "should handle request") - val finalBounds = findBoundsChange(wct, freeformTask) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.BottomRight) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) - val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) - - val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) - - assertNull(wct, "should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_positionBottomRight() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.BottomRight) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_positionTopLeft() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.TopLeft) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_positionBottomLeft() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.BottomLeft) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_positionTopRight() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.TopRight) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_positionResetsToCenter() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.Center) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - // Add freeform task with half display size snap bounds at left side. - setUpFreeformTask(bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom)) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.Center) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - // Add freeform task with half display size snap bounds at right side. - setUpFreeformTask(bounds = Rect( - stableBounds.right - 500, stableBounds.top, stableBounds.right, stableBounds.bottom)) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.Center) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - // Add maximised freeform task. - setUpFreeformTask(bounds = Rect(stableBounds)) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.Center) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) - fun addMoveToDesktopChanges_defaultToCenterIfFree() { - setUpLandscapeDisplay() - val stableBounds = Rect() - displayLayout.getStableBoundsForDesktopMode(stableBounds) - - val minTouchTarget = context.resources.getDimensionPixelSize( - R.dimen.freeform_required_visible_empty_space_in_header) - addFreeformTaskAtPosition(DesktopTaskPosition.Center, stableBounds, - Rect(0, 0, 1600, 1200), Point(0, minTouchTarget + 1)) - - val task = setUpFullscreenTask() - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - val finalBounds = findBoundsChange(wct, task) - assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) - .isEqualTo(DesktopTaskPosition.Center) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(enableUserFullscreenOverride = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_landscapeDevice_portraitResizableApp_aspectRatioOverridden() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, - shouldLetterbox = true, aspectRatioOverrideApplied = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() { - setUpPortraitDisplay() - val task = setUpFullscreenTask(enableUserFullscreenOverride = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() { - setUpPortraitDisplay() - val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun addMoveToDesktopChanges_portraitDevice_landscapeResizableApp_aspectRatioOverridden() { - setUpPortraitDisplay() - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - deviceOrientation = ORIENTATION_PORTRAIT, - shouldLetterbox = true, aspectRatioOverrideApplied = true) - val wct = WindowContainerTransaction() - controller.addMoveToDesktopChanges(wct, task) - - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { - val task = setUpFullscreenTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() { - val task = setUpFullscreenTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - fun moveTaskToDesktop_nonExistentTask_doesNothing() { - controller.moveTaskToDesktop(999, transitionSource = UNKNOWN) - verifyEnterDesktopWCTNotExecuted() - verify(desktopModeEnterExitTransitionListener, times(0)).onEnterDesktopModeTransitionStarted(anyInt()) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveTaskToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() { - val task = createTaskInfo(1) - whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) - whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) - - with(getLatestEnterDesktopWct()) { - assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) - } - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { - val task = createTaskInfo(1) - whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) - whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) - - with(getLatestEnterDesktopWct()) { - // Add desktop wallpaper activity - assertPendingIntentAt(index = 0, desktopWallpaperIntent) - // Launch task - assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM) - } - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() { - val task = - setUpFullscreenTask().apply { - isActivityStackTransparent = true - isTopActivityNoDisplay = true - numActivities = 1 - } - - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() { - val task = - setUpFullscreenTask().apply { - isActivityStackTransparent = true - isTopActivityNoDisplay = false - numActivities = 1 - } - - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - verifyEnterDesktopWCTNotExecuted() - verify(desktopModeEnterExitTransitionListener, times(0)).onEnterDesktopModeTransitionStarted( - FREEFORM_ANIMATION_DURATION - ) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_systemUIActivityWithDisplay_doesNothing() { - // Set task as systemUI package - val systemUIPackageName = context.resources.getString( - com.android.internal.R.string.config_systemUi) - val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - val task = - setUpFullscreenTask().apply { - baseActivity = baseComponent - isTopActivityNoDisplay = false - } - - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - verifyEnterDesktopWCTNotExecuted() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() { - // Set task as systemUI package - val systemUIPackageName = context.resources.getString( - com.android.internal.R.string.config_systemUi) - val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - val task = - setUpFullscreenTask().apply { - baseActivity = baseComponent - isTopActivityNoDisplay = true - } - - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) - whenever( - transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) - ).thenReturn(Binder()) - - val task = createTaskInfo(1) - whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) - whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop( - taskId = task.taskId, - transitionSource = UNKNOWN, - remoteTransition = RemoteTransition(spy(TestRemoteTransition()))) - - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) - } - - - @Test - fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) - whenever( - transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) - ).thenReturn(Binder()) - - controller.moveRunningTaskToDesktop( - task = setUpFullscreenTask(), - transitionSource = UNKNOWN, - remoteTransition = RemoteTransition(spy(TestRemoteTransition()))) - - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - val fullscreenTask = setUpFullscreenTask() - markTaskHidden(freeformTask) - - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) - - with(getLatestEnterDesktopWct()) { - // Operations should include home task, freeform task - assertThat(hierarchyOps).hasSize(3) - assertReorderSequence(homeTask, freeformTask, fullscreenTask) - assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { - val freeformTask = setUpFreeformTask() - val fullscreenTask = setUpFullscreenTask() - markTaskHidden(freeformTask) - - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) - - with(getLatestEnterDesktopWct()) { - // Operations should include wallpaper intent, freeform task, fullscreen task - assertThat(hierarchyOps).hasSize(3) - assertPendingIntentAt(index = 0, desktopWallpaperIntent) - assertReorderAt(index = 1, freeformTask) - assertReorderAt(index = 2, fullscreenTask) - assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { - setUpHomeTask(displayId = DEFAULT_DISPLAY) - val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) - markTaskHidden(freeformTaskDefault) - - val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) - val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) - markTaskHidden(freeformTaskSecond) - - controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) - - with(getLatestEnterDesktopWct()) { - // Check that hierarchy operations do not include tasks from second display - assertThat(hierarchyOps.map { it.container }).doesNotContain(homeTaskSecond.token.asBinder()) - assertThat(hierarchyOps.map { it.container }) - .doesNotContain(freeformTaskSecond.token.asBinder()) - } - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - } - - @Test - fun moveRunningTaskToDesktop_splitTaskExitsSplit() { - val task = setUpSplitScreenTask() - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - verify(splitScreenController) - .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) - } - - @Test - fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() { - val task = setUpFullscreenTask() - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - verify(splitScreenController, never()) - .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - val newTask = setUpFullscreenTask() - val homeTask = setUpHomeTask() - - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) - - val wct = getLatestEnterDesktopWct() - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home - wct.assertReorderAt(0, homeTask) - wct.assertReorderSequenceInRange( - range = 1..<(MAX_TASK_LIMIT + 1), - *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] - newTask) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - val newTask = setUpFullscreenTask() - val homeTask = setUpHomeTask() - - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) - - val wct = getLatestEnterDesktopWct() - verify(desktopModeEnterExitTransitionListener).onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper - // Move home to front - wct.assertReorderAt(0, homeTask) - // Add desktop wallpaper activity - wct.assertPendingIntentAt(1, desktopWallpaperIntent) - // Bring freeform tasks to front - wct.assertReorderSequenceInRange( - range = 2..<(MAX_TASK_LIMIT + 2), - *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] - newTask) - } - - @Test - fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { - val task = setUpFreeformTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) - val wct = getLatestExitDesktopWct() - verify(desktopModeEnterExitTransitionListener, times(1)).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) - verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) - // Removes wallpaper activity when leaving desktop - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { - val task = setUpFreeformTask() - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - } - - @Test - fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - - controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) - assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) - verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - // Removes wallpaper activity when leaving desktop - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { - val task1 = setUpFreeformTask() - // Setup task2 - setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) - assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) - verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - // Does not remove wallpaper activity, as desktop still has a visible desktop task - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun moveToFullscreen_nonExistentTask_doesNothing() { - controller.moveToFullscreen(999, transitionSource = UNKNOWN) - verifyExitDesktopWCTNotExecuted() - } - - @Test - fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { - val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) - - with(getLatestExitDesktopWct()) { - assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) - assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) - } - verify(desktopModeEnterExitTransitionListener).onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - } - - @Test - fun moveTaskToFront_postsWctWithReorderOp() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - controller.moveTaskToFront(task1, remoteTransition = null) - - val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, task1) - } - - @Test - fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { - setUpHomeTask() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } - whenever(desktopMixedTransitionHandler.startLaunchTransition( - eq(TRANSIT_TO_FRONT), - any(), - eq(freeformTasks[0].taskId), - anyOrNull(), - anyOrNull(), - )).thenReturn(Binder()) - - controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) - - val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize - wct.assertReorderAt(0, freeformTasks[0], toTop = true) - wct.assertReorderAt(1, freeformTasks[1], toTop = false) - } - - @Test - fun moveTaskToFront_remoteTransition_usesOneshotHandler() { - setUpHomeTask() - val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) - whenever( - transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) - ).thenReturn(Binder()) - - controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) - } - - @Test - fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { - setUpHomeTask() - val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) - whenever( - transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture()) - ).thenReturn(Binder()) - - controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - - assertThat(transitionHandlerArgCaptor.value) - .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) - } - - @Test - fun moveTaskToFront_backgroundTask_launchesTask() { - val task = createTaskInfo(1) - whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) - - controller.moveTaskToFront(task.taskId, remoteTransition = null) - - val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - val task = createTaskInfo(1001) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) - whenever(desktopMixedTransitionHandler - .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull(), anyOrNull())) - .thenReturn(Binder()) - - controller.moveTaskToFront(task.taskId, remoteTransition = null) - - val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) - assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize - wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) - wct.assertReorderAt(1, freeformTasks[0], toTop = false) - } - - @Test - fun moveToNextDisplay_noOtherDisplays() { - whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - verifyWCTNotExecuted() - } - - @Test - fun moveToNextDisplay_moveFromFirstToSecondDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: second display - val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) - .thenReturn(secondDisplayArea) - - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - controller.moveToNextDisplay(task.taskId) - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() - } - } - - @Test - fun moveToNextDisplay_moveFromSecondToFirstDisplay() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: default display - val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) - .thenReturn(defaultDisplayArea) - - val task = setUpFreeformTask(displayId = SECOND_DISPLAY) - controller.moveToNextDisplay(task.taskId) - - with(getLatestWct(type = TRANSIT_CHANGE)) { - assertThat(hierarchyOps).hasSize(1) - assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) - assertThat(hierarchyOps[0].isReparent).isTrue() - assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) - assertThat(hierarchyOps[0].toTop).isTrue() - } - } - - @Test - @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) - fun moveToNextDisplay_removeWallpaper() { - // Set up two display ids - whenever(rootTaskDisplayAreaOrganizer.displayIds) - .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) - // Create a mock for the target display area: second display - val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) - whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) - .thenReturn(secondDisplayArea) - // Add a task and a wallpaper - val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - controller.moveToNextDisplay(task.taskId) - - with(getLatestWct(type = TRANSIT_CHANGE)) { - val wallpaperChange = hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() } - assertThat(wallpaperChange).isNotNull() - assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) - } - } - - @Test - fun getTaskWindowingMode() { - val fullscreenTask = setUpFullscreenTask() - val freeformTask = setUpFreeformTask() - - assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) - .isEqualTo(WINDOWING_MODE_FULLSCREEN) - assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) - .isEqualTo(WINDOWING_MODE_FREEFORM) - assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun onDesktopWindowClose_noActiveTasks() { - val task = setUpFreeformTask(active = false) - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { - val task = setUpFreeformTask() - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) - // Adds remove wallpaper operation - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowClose_singleActiveTask_isClosing() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_singleActiveTask_isMinimized() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_multipleActiveTasks() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) - // Doesn't modify transaction - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) - // Adds remove wallpaper operation - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - - val wct = WindowContainerTransaction() - controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) - // Adds remove wallpaper operation - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { - val task = setUpFreeformTask(active = false) - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - controller.minimizeTask(task) - - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() - } - } - - @Test - fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() { - val task = setUpPipTask(autoEnterEnabled = true) - val handler = mock(TransitionHandler::class.java) - whenever(freeformTaskTransitionStarter.startPipTransition(any())) - .thenReturn(Binder()) - whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) - .thenReturn(android.util.Pair(handler, WindowContainerTransaction()) + runningTasks.clear() + testScope.cancel() + } + + @Test + fun instantiate_addInitCallback() { + verify(shellInit).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_onlyFreeFormTaskIsRunning_returnFalse() { + setUpFreeformTask() + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { + val task1 = setUpFreeformTask() + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize( + task1, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task1, + STABLE_BOUNDS.width(), + STABLE_BOUNDS.height(), + displayController, + ) + assertThat(argumentCaptor.value).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_fullScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask(bounds = stableBounds, active = true) + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfMaximizedTask_returnFalse() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + val task1 = setUpFreeformTask(bounds = stableBounds, active = true) + + val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + controller.toggleDesktopTaskSize( + task1, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.RESTORE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, + InputMethod.TOUCH, + ), + ) + + verify(taskbarDesktopTaskListener).onTaskbarCornerRoundingUpdate(argumentCaptor.capture()) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + eq(ResizeTrigger.MAXIMIZE_BUTTON), + eq(InputMethod.TOUCH), + eq(task1), + anyOrNull(), + anyOrNull(), + eq(displayController), + anyOrNull(), + ) + assertThat(argumentCaptor.value).isFalse() + } + + @Test + fun doesAnyTaskRequireTaskbarRounding_splitScreenTaskIsRunning_returnTrue() { + val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } + setUpFreeformTask( + bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom) + ) + + assertThat(controller.doesAnyTaskRequireTaskbarRounding(DEFAULT_DISPLAY)).isTrue() + } + + @Test + fun instantiate_cannotEnterDesktopMode_doNotAddInitCallback() { + whenever(DesktopModeStatus.canEnterDesktopMode(context)).thenReturn(false) + clearInvocations(shellInit) + + createController() + + verify(shellInit, never()).addInitCallback(any(), any<DesktopTasksController>()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + fun isDesktopModeShowing_noTasks_returnsFalse() { + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_noTasksVisible_returnsFalse() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() + } + + @Test + fun isDesktopModeShowing_tasksActiveAndVisible_returnsTrue() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskHidden(task2) + + assertThat(controller.isDesktopModeShowing(displayId = 0)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 (no wallpaper intent) + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { + val homeTask = setUpHomeTask(SECOND_DISPLAY) + val task1 = setUpFreeformTask(SECOND_DISPLAY) + val task2 = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(task1) + markTaskHidden(task2) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskVisible(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: home, task1, task2 + wct.assertReorderAt(index = 0, homeTask) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + markTaskHidden(task1) + markTaskVisible(task2) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Expect order to be from bottom: wallpaper intent, task1, task2 + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, homeTask) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntentAt(index = 0, desktopWallpaperIntent) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Expect order to be from bottom: home, task + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + wct.assertReorderAt(index = 1, taskDefaultDisplay) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) + val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(SECOND_DISPLAY) + markTaskHidden(taskDefaultDisplay) + markTaskHidden(taskSecondDisplay) + + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTaskDefaultDisplay) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + // Move freeform task to front + wct.assertReorderAt(index = 2, taskDefaultDisplay) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(2) + // Reorder home and freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 0, homeTask, toTop = true) + wct.assertReorderAt(index = 1, freeformTask, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val minimizedTask = setUpFreeformTask() + + markTaskHidden(freeformTask) + markTaskHidden(minimizedTask) + taskRepository.minimizeTask(DEFAULT_DISPLAY, minimizedTask.taskId) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + assertThat(wct.hierarchyOps).hasSize(3) + // Move home to front + wct.assertReorderAt(index = 0, homeTask, toTop = true) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) + // Reorder freeform task to top, don't reorder the minimized task + wct.assertReorderAt(index = 2, freeformTask, toTop = true) + } + + @Test + fun visibleTaskCount_noTasks_returnsZero() { + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) + } + + @Test + fun visibleTaskCount_twoTasks_bothVisible_returnsTwo() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskVisible) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) + } + + @Test + fun visibleTaskCount_twoTasks_oneVisible_returnsOne() { + setUpHomeTask() + setUpFreeformTask().also(::markTaskVisible) + setUpFreeformTask().also(::markTaskHidden) + assertThat(controller.visibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) + } + + @Test + fun visibleTaskCount_twoTasksVisibleOnDifferentDisplays_returnsOne() { + setUpHomeTask() + setUpFreeformTask(DEFAULT_DISPLAY).also(::markTaskVisible) + setUpFreeformTask(SECOND_DISPLAY).also(::markTaskVisible) + assertThat(controller.visibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) + } + + @Test + fun addMoveToDesktopChanges_gravityLeft_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.LEFT) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityRight_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.RIGHT) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityTop_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.TOP) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + fun addMoveToDesktopChanges_gravityBottom_noBoundsApplied() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(gravity = Gravity.BOTTOM) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(finalBounds).isEqualTo(Rect()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun handleRequest_newFreeformTaskLaunch_cascadeApplied() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS, active = false) + + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + val finalBounds = findBoundsChange(wct, freeformTask) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomRight) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun handleRequest_freeformTaskAlreadyExistsInDesktopMode_cascadeNotApplied() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + val freeformTask = setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNull(wct, "should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionBottomRight() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + setUpFreeformTask(bounds = DEFAULT_LANDSCAPE_BOUNDS) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomRight) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionTopLeft() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + addFreeformTaskAtPosition(DesktopTaskPosition.BottomRight, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.TopLeft) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionBottomLeft() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + addFreeformTaskAtPosition(DesktopTaskPosition.TopLeft, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.BottomLeft) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionTopRight() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + addFreeformTaskAtPosition(DesktopTaskPosition.BottomLeft, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.TopRight) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + addFreeformTaskAtPosition(DesktopTaskPosition.TopRight, stableBounds) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapLeft_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at left side. + setUpFreeformTask( + bounds = Rect(stableBounds.left, stableBounds.top, 500, stableBounds.bottom) + ) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowSnapRight_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add freeform task with half display size snap bounds at right side. + setUpFreeformTask( + bounds = + Rect( + stableBounds.right - 500, + stableBounds.top, + stableBounds.right, + stableBounds.bottom, + ) + ) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_lastWindowMaximised_positionResetsToCenter() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + // Add maximised freeform task. + setUpFreeformTask(bounds = Rect(stableBounds)) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CASCADING_WINDOWS) + fun addMoveToDesktopChanges_defaultToCenterIfFree() { + setUpLandscapeDisplay() + val stableBounds = Rect() + displayLayout.getStableBoundsForDesktopMode(stableBounds) + + val minTouchTarget = + context.resources.getDimensionPixelSize( + R.dimen.freeform_required_visible_empty_space_in_header + ) + addFreeformTaskAtPosition( + DesktopTaskPosition.Center, + stableBounds, + Rect(0, 0, 1600, 1200), + Point(0, minTouchTarget + 1), + ) + + val task = setUpFullscreenTask() + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + assertThat(stableBounds.getDesktopTaskPosition(finalBounds!!)) + .isEqualTo(DesktopTaskPosition.Center) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_landscapeDevice_portraitResizableApp_aspectRatioOverridden() { + setUpLandscapeDisplay() + val task = + setUpFullscreenTask( + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true, + aspectRatioOverrideApplied = true, + ) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_userFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableUserFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_systemFullscreenOverride_defaultPortraitBounds() { + setUpPortraitDisplay() + val task = setUpFullscreenTask(enableSystemFullscreenOverride = true) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun addMoveToDesktopChanges_portraitDevice_landscapeResizableApp_aspectRatioOverridden() { + setUpPortraitDisplay() + val task = + setUpFullscreenTask( + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + deviceOrientation = ORIENTATION_PORTRAIT, + shouldLetterbox = true, + aspectRatioOverrideApplied = true, + ) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + fun moveToDesktop_tdaFullscreen_windowingModeSetToFreeform() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + fun moveRunningTaskToDesktop_tdaFreeform_windowingModeSetToUndefined() { + val task = setUpFullscreenTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + fun moveTaskToDesktop_nonExistentTask_doesNothing() { + controller.moveTaskToDesktop(999, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + verify(desktopModeEnterExitTransitionListener, times(0)) + .onEnterDesktopModeTransitionStarted(anyInt()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveTaskToDesktop_desktopWallpaperDisabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + + controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + + controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Add desktop wallpaper activity + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + // Launch task + assertLaunchTaskAt(index = 1, task.taskId, WINDOWING_MODE_FREEFORM) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() { + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() { + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = false + numActivities = 1 + } + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + verify(desktopModeEnterExitTransitionListener, times(0)) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_systemUIActivityWithDisplay_doesNothing() { + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = false + } + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() { + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { + val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) + .thenReturn(Binder()) + + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) + controller.moveTaskToDesktop( + taskId = task.taskId, + transitionSource = UNKNOWN, + remoteTransition = RemoteTransition(spy(TestRemoteTransition())), + ) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + } + + @Test + fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { + val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) + .thenReturn(Binder()) + + controller.moveRunningTaskToDesktop( + task = setUpFullscreenTask(), + transitionSource = UNKNOWN, + remoteTransition = RemoteTransition(spy(TestRemoteTransition())), + ) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Operations should include home task, freeform task + assertThat(hierarchyOps).hasSize(3) + assertReorderSequence(homeTask, freeformTask, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Operations should include wallpaper intent, freeform task, fullscreen task + assertThat(hierarchyOps).hasSize(3) + assertPendingIntentAt(index = 0, desktopWallpaperIntent) + assertReorderAt(index = 1, freeformTask) + assertReorderAt(index = 2, fullscreenTask) + assertThat(changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { + setUpHomeTask(displayId = DEFAULT_DISPLAY) + val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val fullscreenTaskDefault = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + markTaskHidden(freeformTaskDefault) + + val homeTaskSecond = setUpHomeTask(displayId = SECOND_DISPLAY) + val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) + markTaskHidden(freeformTaskSecond) + + controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + + with(getLatestEnterDesktopWct()) { + // Check that hierarchy operations do not include tasks from second display + assertThat(hierarchyOps.map { it.container }) + .doesNotContain(homeTaskSecond.token.asBinder()) + assertThat(hierarchyOps.map { it.container }) + .doesNotContain(freeformTaskSecond.token.asBinder()) + } + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + fun moveRunningTaskToDesktop_splitTaskExitsSplit() { + val task = setUpSplitScreenTask() + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + verify(splitScreenController) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test + fun moveRunningTaskToDesktop_fullscreenTaskDoesNotExitSplit() { + val task = setUpFullscreenTask() + controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + verify(splitScreenController, never()) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + val homeTask = setUpHomeTask() + + controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 1) // visible tasks + home + wct.assertReorderAt(0, homeTask) + wct.assertReorderSequenceInRange( + range = 1..<(MAX_TASK_LIMIT + 1), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + val newTask = setUpFullscreenTask() + val homeTask = setUpHomeTask() + + controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + assertThat(wct.hierarchyOps.size).isEqualTo(MAX_TASK_LIMIT + 2) // tasks + home + wallpaper + // Move home to front + wct.assertReorderAt(0, homeTask) + // Add desktop wallpaper activity + wct.assertPendingIntentAt(1, desktopWallpaperIntent) + // Bring freeform tasks to front + wct.assertReorderSequenceInRange( + range = 2..<(MAX_TASK_LIMIT + 2), + *freeformTasks.drop(1).toTypedArray(), // Skipping freeformTasks[0] + newTask, + ) + } + + @Test + fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + verify(desktopModeEnterExitTransitionListener, times(1)) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveToFullscreen_tdaFreeform_windowingModeSetToFullscreen() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + } + + @Test + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task.token.asBinder()]) + assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Removes wallpaper activity when leaving desktop + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FULLSCREEN + + controller.moveToFullscreen(task1.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val task1Change = assertNotNull(wct.changes[task1.token.asBinder()]) + assertThat(task1Change.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Does not remove wallpaper activity, as desktop still has a visible desktop task + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun moveToFullscreen_nonExistentTask_doesNothing() { + controller.moveToFullscreen(999, transitionSource = UNKNOWN) + verifyExitDesktopWCTNotExecuted() + } + + @Test + fun moveToFullscreen_secondDisplayTaskHasFreeform_secondDisplayNotAffected() { + val taskDefaultDisplay = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val taskSecondDisplay = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToFullscreen(taskDefaultDisplay.taskId, transitionSource = UNKNOWN) + + with(getLatestExitDesktopWct()) { + assertThat(changes.keys).contains(taskDefaultDisplay.token.asBinder()) + assertThat(changes.keys).doesNotContain(taskSecondDisplay.token.asBinder()) + } + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + } + + @Test + fun moveTaskToFront_postsWctWithReorderOp() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + controller.moveTaskToFront(task1, remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, task1) + } + + @Test + fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { + setUpHomeTask() + val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(freeformTasks[0].taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[0], toTop = true) + wct.assertReorderAt(1, freeformTasks[1], toTop = false) + } + + @Test + fun moveTaskToFront_remoteTransition_usesOneshotHandler() { + setUpHomeTask() + val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } + val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) + .thenReturn(Binder()) + + controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) + + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + } + + @Test + fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { + setUpHomeTask() + val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } + val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) + .thenReturn(Binder()) + + controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) + + assertThat(transitionHandlerArgCaptor.value) + .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) + } + + @Test + fun moveTaskToFront_backgroundTask_launchesTask() { + val task = createTaskInfo(1) + whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) + + controller.moveTaskToFront(task.taskId, remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + } + + @Test + fun moveTaskToFront_backgroundTaskBringsTasksOverLimit_minimizesBackTask() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + val task = createTaskInfo(1001) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + eq(task.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) + + controller.moveTaskToFront(task.taskId, remoteTransition = null) + + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) + assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize + wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) + wct.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test + fun moveToNextDisplay_noOtherDisplays() { + whenever(rootTaskDisplayAreaOrganizer.displayIds).thenReturn(intArrayOf(DEFAULT_DISPLAY)) + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + verifyWCTNotExecuted() + } + + @Test + fun moveToNextDisplay_moveFromFirstToSecondDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + controller.moveToNextDisplay(task.taskId) + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(secondDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + fun moveToNextDisplay_moveFromSecondToFirstDisplay() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: default display + val defaultDisplayArea = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .thenReturn(defaultDisplayArea) + + val task = setUpFreeformTask(displayId = SECOND_DISPLAY) + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + assertThat(hierarchyOps).hasSize(1) + assertThat(hierarchyOps[0].container).isEqualTo(task.token.asBinder()) + assertThat(hierarchyOps[0].isReparent).isTrue() + assertThat(hierarchyOps[0].newParent).isEqualTo(defaultDisplayArea.token.asBinder()) + assertThat(hierarchyOps[0].toTop).isTrue() + } + } + + @Test + @EnableFlags(FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) + fun moveToNextDisplay_removeWallpaper() { + // Set up two display ids + whenever(rootTaskDisplayAreaOrganizer.displayIds) + .thenReturn(intArrayOf(DEFAULT_DISPLAY, SECOND_DISPLAY)) + // Create a mock for the target display area: second display + val secondDisplayArea = DisplayAreaInfo(MockToken().token(), SECOND_DISPLAY, 0) + whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(SECOND_DISPLAY)) + .thenReturn(secondDisplayArea) + // Add a task and a wallpaper + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + controller.moveToNextDisplay(task.taskId) + + with(getLatestWct(type = TRANSIT_CHANGE)) { + val wallpaperChange = + hierarchyOps.find { op -> op.container == wallpaperToken.asBinder() } + assertThat(wallpaperChange).isNotNull() + assertThat(wallpaperChange!!.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + } + } + + @Test + fun getTaskWindowingMode() { + val fullscreenTask = setUpFullscreenTask() + val freeformTask = setUpFreeformTask() + + assertThat(controller.getTaskWindowingMode(fullscreenTask.taskId)) + .isEqualTo(WINDOWING_MODE_FULLSCREEN) + assertThat(controller.getTaskWindowingMode(freeformTask.taskId)) + .isEqualTo(WINDOWING_MODE_FREEFORM) + assertThat(controller.getTaskWindowingMode(999)).isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun onDesktopWindowClose_noActiveTasks() { + val task = setUpFreeformTask(active = false) + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_noWallpaperActivityToken() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_hasWallpaperActivityToken() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_singleActiveTask_isClosing() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_singleActiveTask_isMinimized() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) + // Doesn't modify transaction + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks_isOnlyNonClosingTask() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowClose_multipleActiveTasks_hasMinimized() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + val wct = WindowContainerTransaction() + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task1) + // Adds remove wallpaper operation + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { + val task = setUpFreeformTask(active = false) + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test + fun onDesktopWindowMinimize_pipTask_autoEnterEnabled_startPipTransition() { + val task = setUpPipTask(autoEnterEnabled = true) + val handler = mock(TransitionHandler::class.java) + whenever(freeformTaskTransitionStarter.startPipTransition(any())).thenReturn(Binder()) + whenever(transitions.dispatchRequest(any(), any(), anyOrNull())) + .thenReturn(android.util.Pair(handler, WindowContainerTransaction())) + + controller.minimizeTask(task) + + verify(freeformTaskTransitionStarter).startPipTransition(any()) + verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any()) + } + + @Test + fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() { + val task = setUpPipTask(autoEnterEnabled = false) + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(Binder()) + + controller.minimizeTask(task) + + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any()) + verify(freeformTaskTransitionStarter, never()).startPipTransition(any()) + } + + @Test + fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { + val task = setUpFreeformTask(active = true) + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + } + + @Test + fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { + val task = setUpFreeformTask() + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + // The only active task is being minimized. + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + // Adds remove wallpaper operation + captor.value.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() { + val task = setUpFreeformTask() + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) + + // The only active task is already minimized. + controller.minimizeTask(task) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test + fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() { + val task1 = setUpFreeformTask(active = true) + setUpFreeformTask(active = true) + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + + controller.minimizeTask(task1) + + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + captor.value.hierarchyOps.none { hop -> + hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() + } + } + + @Test + fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() { + val task1 = setUpFreeformTask(active = true) + val task2 = setUpFreeformTask(active = true) + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + val wallpaperToken = MockToken().token() + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + // task1 is the only visible task as task2 is minimized. + controller.minimizeTask(task1) + // Adds remove wallpaper operation + val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + // Adds remove wallpaper operation + captor.value.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun onDesktopWindowMinimize_triesToExitImmersive() { + val task = setUpFreeformTask() + val transition = Binder() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + + controller.minimizeTask(task) + + verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task), any()) + } + + @Test + fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() { + val task = setUpFreeformTask() + val transition = Binder() + val runOnTransit = RunOnStartTransitionCallback() + whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + .thenReturn(transition) + whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any())) + .thenReturn( + ExitResult.Exit(exitingTask = task.taskId, runOnTransitionStart = runOnTransit) + ) + + controller.minimizeTask(task) + + assertThat(runOnTransit.invocations).isEqualTo(1) + assertThat(runOnTransit.lastInvoked).isEqualTo(transition) + } + + @Test + fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + + assertThat(wct.hierarchyOps).hasSize(1) + } + + @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { + val homeTask = setUpHomeTask() + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + + // There are 5 hops that are happening in this case: + // 1. Moving the fullscreen task to top as we add moveToDesktop() changes + // 2. Bringing home task to front + // 3. Pending intent for the wallpaper + // 4. Bringing the existing freeform task to top + // 5. Bringing the fullscreen task back at the top + assertThat(wct.hierarchyOps).hasSize(5) + wct.assertReorderAt(1, homeTask, toTop = true) + wct.assertReorderAt(4, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(2) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + wct.assertReorderAt(1, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + // Make sure we reorder the new task to top, and the back task to the bottom + assertThat(wct!!.hierarchyOps.size).isEqualTo(9) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + wct.assertReorderAt(8, freeformTasks[0], toTop = false) + } + + @Test + fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { + val minimizedTask = setUpFreeformTask() + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val homeTask = setUpHomeTask() + val fullscreenTask = createFullscreenTask() + fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) + + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct!!.hierarchyOps.size).isEqualTo(10) + wct.assertReorderAt(0, fullscreenTask, toTop = true) + // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized + // task is under the home task. + wct.assertReorderAt(1, homeTask, toTop = true) + wct.assertReorderAt(9, freeformTasks[0], toTop = false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + assertThat(wct.hierarchyOps).hasSize(3) + // There are 3 hops that are happening in this case: + // 1. Moving the fullscreen task to top as we add moveToDesktop() changes + // 2. Pending intent for the wallpaper + // 3. Bringing the fullscreen task back at the top + wct.assertPendingIntentAt(1, desktopWallpaperIntent) + wct.assertReorderAt(2, fullscreenTask, toTop = true) + } + + @Test + fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + val fullscreenTask = createFullscreenTask() + val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) + + assertThat(wct).isNull() + } + + @Test + fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { + val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = + controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) + assertThat(result).isNull() + } + + @Test + fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { + val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } + freeformTasks.forEach { markTaskVisible(it) } + val newFreeformTask = createFreeformTask() + + val wct = + controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) + + assertThat(wct?.hierarchyOps?.size).isEqualTo(1) + wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom + } + + @Test + fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertFalse(wct.anyWindowingModeChange(freeformTask.token)) + } + + @Test + fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + + val freeformTask = setUpFreeformTask() + markTaskHidden(freeformTask) + val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNotNull(wct, "should handle request") + assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { + val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) + val result = + controller.handleRequest( + Binder(), + createTransition(freeformTask2, type = TRANSIT_TO_FRONT), + ) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + result.assertReorderAt(1, freeformTask2, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + val freeformTask1 = setUpFreeformTask() + val freeformTask2 = createFreeformTask() + + markTaskHidden(freeformTask1) + val result = + controller.handleRequest( + Binder(), + createTransition(freeformTask2, type = TRANSIT_TO_FRONT), + ) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(3) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring active desktop tasks to front + result.assertReorderAt(1, freeformTask1, toTop = true) + // Bring new task to front + result.assertReorderAt(2, freeformTask2, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, task, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, task, toTop = true) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(1) + result.assertReorderAt(0, taskDefaultDisplay, toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) + // Second display task + createFreeformTask(displayId = SECOND_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) + + assertNotNull(result, "Should handle request") + assertThat(result.hierarchyOps?.size).isEqualTo(2) + // Add desktop wallpaper activity + result.assertPendingIntentAt(0, desktopWallpaperIntent) + // Bring new task to front + result.assertReorderAt(1, taskDefaultDisplay, toTop = true) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest( + freeformTask2.token.asBinder(), + createTransition(freeformTask2), + ) + assertFalse(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { + whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true) + + val freeformTask1 = setUpFreeformTask() + markTaskVisible(freeformTask1) + + val freeformTask2 = createFreeformTask() + val result = + controller.handleRequest( + freeformTask2.token.asBinder(), + createTransition(freeformTask2), + ) + assertTrue(result.anyDensityConfigChange(freeformTask2.token)) + } + + @Test + fun handleRequest_freeformTask_keyguardLocked_returnNull() { + whenever(keyguardManager.isKeyguardLocked).thenReturn(true) + val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY) + + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + + assertNull(result, "Should NOT handle request") + } + + @Test + fun handleRequest_notOpenOrToFrontTransition_returnNull() { + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build() + val transition = createTransition(task = task, type = TRANSIT_CLOSE) + val result = controller.handleRequest(Binder(), transition) + assertThat(result).isNull() + } + + @Test + fun handleRequest_noTriggerTask_returnNull() { + assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotStandard_returnNull() { + val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { + val task = + TestRunningTaskInfoBuilder() + .setActivityType(ACTIVITY_TYPE_STANDARD) + .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) + .build() + assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() + } + + @Test + fun handleRequest_recentsAnimationRunning_returnNull() { + // Set up a visible freeform task so a fullscreen task should be converted to freeform + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Mark recents animation running + recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING) + + // Open a fullscreen task, check that it does not result in a WCT with changes to it + val fullscreenTask = createFullscreenTask() + assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() + } + + @Test + fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() { + // Set up a visible freeform task + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Mark recents animation running + recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING) + + // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. + val result = controller.handleRequest(Binder(), createTransition(freeformTask)) + assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val task = + setUpFreeformTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = false + numActivities = 1 + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + val task = + setUpFreeformTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = false + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { + val task = setUpFreeformTask() + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() { + val task = setUpFreeformTask() + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() { + val task = setUpFreeformTask() + markTaskHidden(task) + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() { + val task = setUpFreeformTask() + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { + val task = setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleTasks_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, ) + fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) + val result = + controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { + val task = setUpFreeformTask() + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() { + val task = setUpFreeformTask() + + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { + val task = setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() { + val task = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + val result = + controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() { + val task1 = setUpFreeformTask() + setUpFreeformTask() + + taskRepository.wallpaperActivityToken = MockToken().token() + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + assertNull(result, "Should not handle request") + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + val result = + controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) + + // Should create remove wallpaper transaction + assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() { + val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val wallpaperToken = MockToken().token() + + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) + // Task is being minimized so mark it as not visible. + taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) + val result = + controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) + + assertNull(result, "Should not handle request") + } - controller.minimizeTask(task) - - verify(freeformTaskTransitionStarter).startPipTransition(any()) - verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any()) - } - - @Test - fun onDesktopWindowMinimize_pipTask_autoEnterDisabled_startMinimizeTransition() { - val task = setUpPipTask(autoEnterEnabled = false) - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(Binder()) - - controller.minimizeTask(task) - - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any()) - verify(freeformTaskTransitionStarter, never()).startPipTransition(any()) - } - - @Test - fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { - val task = setUpFreeformTask(active = true) - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - - controller.minimizeTask(task) - - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK - } - } - - @Test - fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { - val task = setUpFreeformTask() - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - // The only active task is being minimized. - controller.minimizeTask(task) - - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - // Adds remove wallpaper operation - captor.value.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() { - val task = setUpFreeformTask() - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) - - // The only active task is already minimized. - controller.minimizeTask(task) - - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() - } - } - - @Test - fun onDesktopWindowMinimize_multipleActiveTasks_doesntRemoveWallpaper() { - val task1 = setUpFreeformTask(active = true) - setUpFreeformTask(active = true) - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - - controller.minimizeTask(task1) - - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() - } - } - - @Test - fun onDesktopWindowMinimize_multipleActiveTasks_minimizesTheOnlyVisibleTask_removesWallpaper() { - val task1 = setUpFreeformTask(active = true) - val task2 = setUpFreeformTask(active = true) - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - val wallpaperToken = MockToken().token() - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - - // task1 is the only visible task as task2 is minimized. - controller.minimizeTask(task1) - // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - // Adds remove wallpaper operation - captor.value.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun onDesktopWindowMinimize_triesToExitImmersive() { - val task = setUpFreeformTask() - val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - - controller.minimizeTask(task) - - verify(mMockDesktopImmersiveController).exitImmersiveIfApplicable(any(), eq(task), any()) - } - - @Test - fun onDesktopWindowMinimize_invokesImmersiveTransitionStartCallback() { - val task = setUpFreeformTask() - val transition = Binder() - val runOnTransit = RunOnStartTransitionCallback() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) - .thenReturn(transition) - whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = task.taskId, - runOnTransitionStart = runOnTransit, - )) - - controller.minimizeTask(task) - - assertThat(runOnTransit.invocations).isEqualTo(1) - assertThat(runOnTransit.lastInvoked).isEqualTo(transition) - } - - @Test - fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - assertNotNull(wct, "should handle request") - assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - - assertThat(wct.hierarchyOps).hasSize(1) - } - - @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_freeformVisible_returnSwitchToFreeformWCT() { - val homeTask = setUpHomeTask() - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - assertNotNull(wct, "should handle request") - assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - - // There are 5 hops that are happening in this case: - // 1. Moving the fullscreen task to top as we add moveToDesktop() changes - // 2. Bringing home task to front - // 3. Pending intent for the wallpaper - // 4. Bringing the existing freeform task to top - // 5. Bringing the fullscreen task back at the top - assertThat(wct.hierarchyOps).hasSize(5) - wct.assertReorderAt(1, homeTask, toTop = true) - wct.assertReorderAt(4, fullscreenTask, toTop = true) - } - - @Test - fun handleRequest_fullscreenTaskToFreeform_underTaskLimit_dontMinimize() { - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - val fullscreenTask = createFullscreenTask() - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - // Make sure we only reorder the new task to top (we don't reorder the old task to bottom) - assertThat(wct?.hierarchyOps?.size).isEqualTo(1) - wct!!.assertReorderAt(0, fullscreenTask, toTop = true) - } - - @Test - fun handleRequest_fullscreenTaskToFreeform_bringsTasksOverLimit_otherTaskIsMinimized() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val fullscreenTask = createFullscreenTask() - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(2) - wct.assertReorderAt(0, fullscreenTask, toTop = true) - wct.assertReorderAt(1, freeformTasks[0], toTop = false) - } - - @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_bringsTasksOverLimit_otherTaskIsMinimized() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val fullscreenTask = createFullscreenTask() - fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - // Make sure we reorder the new task to top, and the back task to the bottom - assertThat(wct!!.hierarchyOps.size).isEqualTo(9) - wct.assertReorderAt(0, fullscreenTask, toTop = true) - wct.assertReorderAt(8, freeformTasks[0], toTop = false) - } - - @Test - fun handleRequest_fullscreenTaskWithTaskOnHome_beyondLimit_existingAndNewTasksAreMinimized() { - val minimizedTask = setUpFreeformTask() - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = minimizedTask.taskId) - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val homeTask = setUpHomeTask() - val fullscreenTask = createFullscreenTask() - fullscreenTask.baseIntent.setFlags(Intent.FLAG_ACTIVITY_TASK_ON_HOME) - - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - assertThat(wct!!.hierarchyOps.size).isEqualTo(10) - wct.assertReorderAt(0, fullscreenTask, toTop = true) - // Make sure we reorder the home task to the top, desktop tasks to top of them and minimized - // task is under the home task. - wct.assertReorderAt(1, homeTask, toTop = true) - wct.assertReorderAt(9, freeformTasks[0], toTop = false) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { - whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - - val fullscreenTask = createFullscreenTask() - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - assertNotNull(wct, "should handle request") - assertThat(wct.changes[fullscreenTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - assertThat(wct.hierarchyOps).hasSize(3) - // There are 3 hops that are happening in this case: - // 1. Moving the fullscreen task to top as we add moveToDesktop() changes - // 2. Pending intent for the wallpaper - // 3. Bringing the fullscreen task back at the top - wct.assertPendingIntentAt(1, desktopWallpaperIntent) - wct.assertReorderAt(2, fullscreenTask, toTop = true) - } - - @Test - fun handleRequest_fullscreenTask_noTasks_enforceDesktop_fullscreenDisplay_returnNull() { - whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - val fullscreenTask = createFullscreenTask() - val wct = controller.handleRequest(Binder(), createTransition(fullscreenTask)) - - assertThat(wct).isNull() - } - - @Test - fun handleRequest_fullscreenTask_freeformNotVisible_returnNull() { - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_fullscreenTask_noOtherTasks_returnNull() { - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_fullscreenTask_freeformTaskOnOtherDisplay_returnNull() { - val fullscreenTaskDefaultDisplay = createFullscreenTask(displayId = DEFAULT_DISPLAY) - createFreeformTask(displayId = SECOND_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(fullscreenTaskDefaultDisplay)) - assertThat(result).isNull() - } - - @Test - fun handleRequest_freeformTask_freeformVisible_aboveTaskLimit_minimize() { - val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } - freeformTasks.forEach { markTaskVisible(it) } - val newFreeformTask = createFreeformTask() - - val wct = controller.handleRequest(Binder(), createTransition(newFreeformTask, TRANSIT_OPEN)) - - assertThat(wct?.hierarchyOps?.size).isEqualTo(1) - wct!!.assertReorderAt(0, freeformTasks[0], toTop = false) // Reorder to the bottom - } - - @Test - fun handleRequest_freeformTask_relaunchActiveTask_taskBecomesUndefined() { - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - - val wct = - controller.handleRequest(Binder(), createTransition(freeformTask)) - - // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. - assertNotNull(wct, "should handle request") - assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun handleRequest_freeformTask_relaunchTask_enforceDesktop_freeformDisplay_noWinModeChange() { - whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM - - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) - - assertNotNull(wct, "should handle request") - assertFalse(wct.anyWindowingModeChange(freeformTask.token)) - } - - @Test - fun handleRequest_freeformTask_relaunchTask_enforceDesktop_fullscreenDisplay_becomesUndefined() { - whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - - val freeformTask = setUpFreeformTask() - markTaskHidden(freeformTask) - val wct = controller.handleRequest(Binder(), createTransition(freeformTask)) - - assertNotNull(wct, "should handle request") - assertThat(wct.changes[freeformTask.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperDisabled_freeformNotVisible_reorderedToTop() { - val freeformTask1 = setUpFreeformTask() - val freeformTask2 = createFreeformTask() - - markTaskHidden(freeformTask1) - val result = - controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(2) - result.assertReorderAt(1, freeformTask2, toTop = true) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { - val freeformTask1 = setUpFreeformTask() - val freeformTask2 = createFreeformTask() - - markTaskHidden(freeformTask1) - val result = - controller.handleRequest(Binder(), createTransition(freeformTask2, type = TRANSIT_TO_FRONT)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(3) - // Add desktop wallpaper activity - result.assertPendingIntentAt(0, desktopWallpaperIntent) - // Bring active desktop tasks to front - result.assertReorderAt(1, freeformTask1, toTop = true) - // Bring new task to front - result.assertReorderAt(2, freeformTask2, toTop = true) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperDisabled_noOtherTasks_reorderedToTop() { - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(1) - result.assertReorderAt(0, task, toTop = true) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { - val task = createFreeformTask() - val result = controller.handleRequest(Binder(), createTransition(task)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(2) - // Add desktop wallpaper activity - result.assertPendingIntentAt(0, desktopWallpaperIntent) - // Bring new task to front - result.assertReorderAt(1, task, toTop = true) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_dskWallpaperDisabled_freeformOnOtherDisplayOnly_reorderedToTop() { - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - // Second display task - createFreeformTask(displayId = SECOND_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(1) - result.assertReorderAt(0, taskDefaultDisplay, toTop = true) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { - val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) - // Second display task - createFreeformTask(displayId = SECOND_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(taskDefaultDisplay)) - - assertNotNull(result, "Should handle request") - assertThat(result.hierarchyOps?.size).isEqualTo(2) - // Add desktop wallpaper activity - result.assertPendingIntentAt(0, desktopWallpaperIntent) - // Bring new task to front - result.assertReorderAt(1, taskDefaultDisplay, toTop = true) - } - - @Test - fun handleRequest_freeformTask_alreadyInDesktop_noOverrideDensity_noConfigDensityChange() { - whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(false) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = - controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) - assertFalse(result.anyDensityConfigChange(freeformTask2.token)) - } - - @Test - fun handleRequest_freeformTask_alreadyInDesktop_overrideDensity_hasConfigDensityChange() { - whenever(DesktopModeStatus.useDesktopOverrideDensity()).thenReturn(true) - - val freeformTask1 = setUpFreeformTask() - markTaskVisible(freeformTask1) - - val freeformTask2 = createFreeformTask() - val result = - controller.handleRequest(freeformTask2.token.asBinder(), createTransition(freeformTask2)) - assertTrue(result.anyDensityConfigChange(freeformTask2.token)) - } - - @Test - fun handleRequest_freeformTask_keyguardLocked_returnNull() { - whenever(keyguardManager.isKeyguardLocked).thenReturn(true) - val freeformTask = createFreeformTask(displayId = DEFAULT_DISPLAY) - - val result = controller.handleRequest(Binder(), createTransition(freeformTask)) - - assertNull(result, "Should NOT handle request") - } - - @Test - fun handleRequest_notOpenOrToFrontTransition_returnNull() { - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_FULLSCREEN) - .build() - val transition = createTransition(task = task, type = TRANSIT_CLOSE) - val result = controller.handleRequest(Binder(), transition) - assertThat(result).isNull() - } - - @Test - fun handleRequest_noTriggerTask_returnNull() { - assertThat(controller.handleRequest(Binder(), createTransition(task = null))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotStandard_returnNull() { - val task = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_triggerTaskNotFullscreenOrFreeform_returnNull() { - val task = - TestRunningTaskInfoBuilder() - .setActivityType(ACTIVITY_TYPE_STANDARD) - .setWindowingMode(WINDOWING_MODE_MULTI_WINDOW) - .build() - assertThat(controller.handleRequest(Binder(), createTransition(task))).isNull() - } - - @Test - fun handleRequest_recentsAnimationRunning_returnNull() { - // Set up a visible freeform task so a fullscreen task should be converted to freeform - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Mark recents animation running - recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING) - - // Open a fullscreen task, check that it does not result in a WCT with changes to it - val fullscreenTask = createFullscreenTask() - assertThat(controller.handleRequest(Binder(), createTransition(fullscreenTask))).isNull() - } - - @Test - fun handleRequest_recentsAnimationRunning_relaunchActiveTask_taskBecomesUndefined() { - // Set up a visible freeform task - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Mark recents animation running - recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_ANIMATING) - - // Should become undefined as the TDA is set to fullscreen. It will inherit from the TDA. - val result = controller.handleRequest(Binder(), createTransition(freeformTask)) - assertThat(result?.changes?.get(freeformTask.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_topActivityTransparentWithoutDisplay_returnSwitchToFreeformWCT() { - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - val task = - setUpFullscreenTask().apply { - isActivityStackTransparent = true - isTopActivityNoDisplay = true - numActivities = 1 - } - - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + @Test + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { + val task1 = setUpFullscreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_topActivityTransparentWithDisplay_returnSwitchToFullscreenWCT() { - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - val task = - setUpFreeformTask().apply { - isActivityStackTransparent = true - isTopActivityNoDisplay = false - numActivities = 1 - } - - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + } + + @Test + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { + val task1 = setUpSplitScreenTask() + val task2 = setUpFullscreenTask() + val task3 = setUpFullscreenTask() + val task4 = setUpSplitScreenTask() + + task1.isFocused = true + task2.isFocused = false + task3.isFocused = false + task4.isFocused = true + + task4.parentTaskId = task1.taskId + + controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + verify(splitScreenController) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test + fun moveFocusedTaskToFullscreen() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT() { - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Set task as systemUI package - val systemUIPackageName = context.resources.getString( - com.android.internal.R.string.config_systemUi) - val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - val task = - setUpFreeformTask().apply { - baseActivity = baseComponent - isTopActivityNoDisplay = false - } - - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + } + + @Test + fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) + + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + wct.assertRemoveAt(index = 0, wallpaperToken) + } + + @Test + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - } + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wct.hierarchyOps).isEmpty() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun removeDesktop_multipleTasks_removesAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + + controller.removeDesktop(displayId = DEFAULT_DISPLAY) + + val wct = getLatestWct(TRANSIT_CLOSE) + assertThat(wct.hierarchyOps).hasSize(3) + wct.assertRemoveAt(index = 0, task1.token) + wct.assertRemoveAt(index = 1, task2.token) + wct.assertRemoveAt(index = 2, task3.token) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) + whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null) + + controller.removeDesktop(displayId = DEFAULT_DISPLAY) + + val wct = getLatestWct(TRANSIT_CLOSE) + assertThat(wct.hierarchyOps).hasSize(2) + wct.assertRemoveAt(index = 0, task1.token) + wct.assertRemoveAt(index = 1, task2.token) + verify(recentTasksController).removeBackgroundTask(task3.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun handleRequest_systemUIActivityWithoutDisplay_returnSwitchToFreeformWCT() { - val freeformTask = setUpFreeformTask() - markTaskVisible(freeformTask) - - // Set task as systemUI package - val systemUIPackageName = context.resources.getString( - com.android.internal.R.string.config_systemUi) - val baseComponent = ComponentName(systemUIPackageName, /* class */ "") - val task = - setUpFullscreenTask().apply { - baseActivity = baseComponent - isTopActivityNoDisplay = true - } - - val result = controller.handleRequest(Binder(), createTransition(task)) - assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_backTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_removesTask() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - ) - fun handleRequest_backTransition_singleTaskNoToken_withWallpaper_notInDesktop_doesNotHandle() { - val task = setUpFreeformTask() - markTaskHidden(task) - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleTaskNoToken_doesNotHandle() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_backTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { - val task = setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_singleTaskWithToken_removesWallpaper() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_backTransition_multipleTasks_noWallpaper_doesNotHandle() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_backTransition_multipleTasks_doesNotHandle() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION - ) - fun handleRequest_backTransition_multipleTasksSingleNonClosing_removesWallpaperAndTask() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @EnableFlags( - Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - ) - fun handleRequest_backTransition_multipleTasksSingleNonMinimized_removesWallpaperAndTask() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_TO_BACK)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_backTransition_nonMinimizadTask_withWallpaper_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - // Task is being minimized so mark it as not visible. - taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) - val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_closeTransition_singleTaskNoToken_noWallpaper_doesNotHandle() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleTaskNoToken_doesNotHandle() { - val task = setUpFreeformTask() - - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - assertNull(result, "Should not handle request") - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleTaskWithToken_noWallpaper_doesNotHandle() { - val task = setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_singleTaskWithToken_withWallpaper_removesWallpaper() { - val task = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - val result = controller.handleRequest(Binder(), createTransition(task, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleTasks_noWallpaper_doesNotHandle() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleTasksFlagEnabled_doesNotHandle() { - val task1 = setUpFreeformTask() - setUpFreeformTask() - - taskRepository.wallpaperActivityToken = MockToken().token() - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - assertNull(result, "Should not handle request") - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleTasksSingleNonClosing_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.addClosingTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun handleRequest_closeTransition_multipleTasksSingleNonMinimized_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - val result = controller.handleRequest(Binder(), createTransition(task1, type = TRANSIT_CLOSE)) - - // Should create remove wallpaper transaction - assertNotNull(result, "Should handle request").assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY,) - fun handleRequest_closeTransition_minimizadTask_withWallpaper_removesWallpaper() { - val task1 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val task2 = setUpFreeformTask(displayId = DEFAULT_DISPLAY) - val wallpaperToken = MockToken().token() - - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) - // Task is being minimized so mark it as not visible. - taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) - val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) - - assertNull(result, "Should not handle request") - } - - @Test - fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { - val task1 = setUpFullscreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - - controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) - - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task1.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { - val task1 = setUpSplitScreenTask() - val task2 = setUpFullscreenTask() - val task3 = setUpFullscreenTask() - val task4 = setUpSplitScreenTask() - - task1.isFocused = true - task2.isFocused = false - task3.isFocused = false - task4.isFocused = true - - task4.parentTaskId = task1.taskId - - controller.moveFocusedTaskToDesktop(DEFAULT_DISPLAY, transitionSource = UNKNOWN) - - val wct = getLatestEnterDesktopWct() - assertThat(wct.changes[task4.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_FREEFORM) - verify(splitScreenController) - .prepareExitSplitScreen(any(), anyInt(), eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE)) - } - - @Test - fun moveFocusedTaskToFullscreen() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - - controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task2.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - } - - @Test - fun moveFocusedTaskToFullscreen_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) - taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) - - controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) - assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - wct.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - taskRepository.wallpaperActivityToken = wallpaperToken - controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) - - val wct = getLatestExitDesktopWct() - val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) - assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wct.hierarchyOps).isEmpty() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasks_removesAll() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - - controller.removeDesktop(displayId = DEFAULT_DISPLAY) - - val wct = getLatestWct(TRANSIT_CLOSE) - assertThat(wct.hierarchyOps).hasSize(3) - wct.assertRemoveAt(index = 0, task1.token) - wct.assertRemoveAt(index = 1, task2.token) - wct.assertRemoveAt(index = 2, task3.token) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task3.taskId)).thenReturn(null) - - controller.removeDesktop(displayId = DEFAULT_DISPLAY) - - val wct = getLatestWct(TRANSIT_CLOSE) - assertThat(wct.hierarchyOps).hasSize(2) - wct.assertRemoveAt(index = 0, task1.token) - wct.assertRemoveAt(index = 1, task2.token) - verify(recentTasksController).removeBackgroundTask(task3.taskId) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask() - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_PORTRAIT, shouldLetterbox = true) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask(isResizable = false, screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask( - isResizable = false, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT, - shouldLetterbox = true) - setUpLandscapeDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask( - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_PORTRAIT) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) - fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { - val spyController = spy(controller) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) - - val task = - setUpFullscreenTask( - isResizable = false, - deviceOrientation = ORIENTATION_PORTRAIT, - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, - shouldLetterbox = true) - setUpPortraitDisplay() - - spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) - val wct = getLatestDragToDesktopWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) - } - - @Test - fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { - val task = setUpFreeformTask() - val spyController = spy(controller) - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000)) - - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) - spyController.onDragPositioningEnd( - task, - mockSurface, - Point(100, -100), /* position */ - PointF(200f, -200f), /* inputCoordinate */ - Rect(100, -100, 500, 1000), /* currentDragBounds */ - Rect(0, 50, 2000, 2000), /* validDragArea */ - Rect() /* dragStartBounds */, - motionEvent, - desktopWindowDecoration, + val task = setUpFullscreenTask() + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_landscapeOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(screenOrientation = SCREEN_ORIENTATION_LANDSCAPE) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_resizable_portraitOrientation_resizablePortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true, + ) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_landscapeOrientation_defaultLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + ) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_landscapeDevice_unResizable_portraitOrientation_unResizablePortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + shouldLetterbox = true, + ) + setUpLandscapeDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_undefinedOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = setUpFullscreenTask(deviceOrientation = ORIENTATION_PORTRAIT) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_portraitOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + ) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_resizable_landscapeOrientation_resizableLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true, + ) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(RESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_portraitOrientation_defaultPortraitBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + ) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(DEFAULT_PORTRAIT_BOUNDS) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) + fun dragToDesktop_portraitDevice_unResizable_landscapeOrientation_unResizableLandscapeBounds() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + + val task = + setUpFullscreenTask( + isResizable = false, + deviceOrientation = ORIENTATION_PORTRAIT, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + shouldLetterbox = true, + ) + setUpPortraitDisplay() + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) + val wct = getLatestDragToDesktopWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_LANDSCAPE_BOUNDS) + } + + @Test + fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { + val task = setUpFreeformTask() + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, -100, 500, 1000)) + + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, -100), /* position */ + PointF(200f, -200f), /* inputCoordinate */ + Rect(100, -100, 500, 1000), /* currentDragBounds */ + Rect(0, 50, 2000, 2000), /* validDragArea */ + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration, ) - val rectAfterEnd = Rect(100, 50, 500, 1150) - verify(transitions) - .startTransition( - eq(TRANSIT_CHANGE), - Mockito.argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - change.configuration.windowConfiguration.bounds == rectAfterEnd - } - }, - eq(null)) - } - - @Test - fun onDesktopDragEnd_noIndicator_updatesTaskBounds() { - val task = setUpFreeformTask() - val spyController = spy(controller) - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) - - val currentDragBounds = Rect(100, 200, 500, 1000) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) - - spyController.onDragPositioningEnd( - task, - mockSurface, - Point(100, 200), /* position */ - PointF(200f, 300f), /* inputCoordinate */ - currentDragBounds, /* currentDragBounds */ - Rect(0, 50, 2000, 2000) /* validDragArea */, - Rect() /* dragStartBounds */, - motionEvent, - desktopWindowDecoration, - ) - - - verify(transitions) - .startTransition( - eq(TRANSIT_CHANGE), - Mockito.argThat { wct -> - return@argThat wct.changes.any { (token, change) -> - change.configuration.windowConfiguration.bounds == currentDragBounds - } - }, - eq(null)) - } - - @Test - fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() { - val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) - val spyController = spy(controller) - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(STABLE_BOUNDS) - } - whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) - - // Drag move the task to the top edge - spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) - spyController.onDragPositioningEnd( - task, - mockSurface, - Point(100, 50), /* position */ - PointF(200f, 300f), /* inputCoordinate */ - Rect(100, 50, 500, 1000), /* currentDragBounds */ - Rect(0, 50, 2000, 2000) /* validDragArea */, - Rect() /* dragStartBounds */, - motionEvent, - desktopWindowDecoration) - - // Assert the task exits desktop mode - val wct = getLatestExitDesktopWct() - assertThat(wct.changes[task.token.asBinder()]?.windowingMode) - .isEqualTo(WINDOWING_MODE_UNDEFINED) - } - - @Test - fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() { - val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) - val spyController = spy(controller) - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(STABLE_BOUNDS) - } - whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) - - // Drag move the task to the top edge - val currentDragBounds = Rect(100, 50, 500, 1000) - spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) - spyController.onDragPositioningEnd( - task, - mockSurface, - Point(100, 50), /* position */ - PointF(200f, 300f), /* inputCoordinate */ - currentDragBounds, - Rect(0, 50, 2000, 2000) /* validDragArea */, - Rect() /* dragStartBounds */, - motionEvent, - desktopWindowDecoration) - - // Assert bounds set to stable bounds - val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) - assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) - // Assert event is properly logged - verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( - ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, - InputMethod.UNKNOWN_INPUT_METHOD, - task, - task.configuration.windowConfiguration.bounds.width(), - task.configuration.windowConfiguration.bounds.height(), - displayController - ) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, - InputMethod.UNKNOWN_INPUT_METHOD, - task, - STABLE_BOUNDS.width(), - STABLE_BOUNDS.height(), - displayController - ) - } - - @Test - fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() { - val task = setUpFreeformTask(bounds = STABLE_BOUNDS) - val spyController = spy(controller) - val mockSurface = mock(SurfaceControl::class.java) - val mockDisplayLayout = mock(DisplayLayout::class.java) - val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! - tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN - whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) - whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) - whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(STABLE_BOUNDS) - } - whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) - .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) - - // Drag move the task to the top edge - val currentDragBounds = Rect(100, 50, 500, 1000) - spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) - spyController.onDragPositioningEnd( - task, - mockSurface, - Point(100, 50), /* position */ - PointF(200f, 300f), /* inputCoordinate */ - currentDragBounds, /* currentDragBounds */ - Rect(0, 50, 2000, 2000) /* validDragArea */, - Rect() /* dragStartBounds */, - motionEvent, - desktopWindowDecoration) - - // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) - // Assert that task leash is updated via Surface Animations - verify(mReturnToDragStartAnimator).start( - eq(task.taskId), - eq(mockSurface), - eq(currentDragBounds), - eq(STABLE_BOUNDS), - anyOrNull(), - ) - // Assert no event is logged - verify(desktopModeEventLogger, never()).logTaskResizingStarted( - any(), any(), any(), any(), any(), any(), any() - ) - verify(desktopModeEventLogger, never()).logTaskResizingEnded( - any(), any(), any(), any(), any(), any(), any() - ) - } - - @Test - fun enterSplit_freeformTaskIsMovedToSplit() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - - controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - - verify(splitScreenController) - .requestEnterSplitSelect( - eq(task2), - any(), - eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), - eq(task2.configuration.windowConfiguration.bounds)) - } - - @Test - fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - taskRepository.wallpaperActivityToken = wallpaperToken - taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) - taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) - - controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(splitScreenController) - .requestEnterSplitSelect( - eq(task2), - wctArgument.capture(), - eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), - eq(task2.configuration.windowConfiguration.bounds)) - // Removes wallpaper activity when leaving desktop - wctArgument.value.assertRemoveAt(index = 0, wallpaperToken) - } - - @Test - fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { - val task1 = setUpFreeformTask() - val task2 = setUpFreeformTask() - val task3 = setUpFreeformTask() - val wallpaperToken = MockToken().token() - - task1.isFocused = false - task2.isFocused = true - task3.isFocused = false - taskRepository.wallpaperActivityToken = wallpaperToken - - controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(splitScreenController) - .requestEnterSplitSelect( - eq(task2), - wctArgument.capture(), - eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), - eq(task2.configuration.windowConfiguration.bounds)) - // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun newWindow_fromFullscreenOpensInSplit() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) - runOpenNewWindow(task) - verify(splitScreenController) - .startIntent(any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED) - ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) - .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun newWindow_fromSplitOpensInSplit() { - setUpLandscapeDisplay() - val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) - runOpenNewWindow(task) - verify(splitScreenController) - .startIntent( - any(), anyInt(), any(), any(), - optionsCaptor.capture(), anyOrNull(), eq(true), eq(SPLIT_INDEX_UNDEFINED) - ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) - .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun newWindow_fromFreeformAddsNewWindow() { - setUpLandscapeDisplay() - val task = setUpFreeformTask() - val wctCaptor = argumentCaptor<WindowContainerTransaction>() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any())) - .thenReturn(ExitResult.NoExit) - whenever(desktopMixedTransitionHandler - .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull())) - .thenReturn(transition) - - runOpenNewWindow(task) - - verify(desktopMixedTransitionHandler) - .startLaunchTransition(anyInt(), wctCaptor.capture(), anyOrNull(), anyOrNull(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions) - .launchWindowingMode).isEqualTo(WINDOWING_MODE_FREEFORM) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun newWindow_fromFreeform_exitsImmersiveIfNeeded() { - setUpLandscapeDisplay() - val immersiveTask = setUpFreeformTask() - val task = setUpFreeformTask() - val runOnStart = RunOnStartTransitionCallback() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull(), any())) - .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart)) - whenever(desktopMixedTransitionHandler - .startLaunchTransition(anyInt(), any(), anyOrNull(), anyOrNull(), anyOrNull())) - .thenReturn(transition) - - runOpenNewWindow(task) - - runOnStart.assertOnlyInvocation(transition) - } - - private fun runOpenNewWindow(task: RunningTaskInfo) { - markTaskVisible(task) - task.baseActivity = mock(ComponentName::class.java) - task.isFocused = true - runningTasks.add(task) - controller.openNewWindow(task) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFullscreenOpensInSplit() { - setUpLandscapeDisplay() - val task = setUpFullscreenTask() - val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) - runOpenInstance(task, taskToRequest.taskId) - verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) - .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromSplitOpensInSplit() { - setUpLandscapeDisplay() - val task = setUpSplitScreenTask() - val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) - runOpenInstance(task, taskToRequest.taskId) - verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) - .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeformAddsNewWindow() { - setUpLandscapeDisplay() - val task = setUpFreeformTask() - val taskToRequest = setUpFreeformTask() - runOpenInstance(task, taskToRequest.taskId) - verify(desktopMixedTransitionHandler).startLaunchTransition(anyInt(), any(), anyInt(), - anyOrNull(), anyOrNull()) - val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) - assertThat(wct.hierarchyOps).hasSize(1) - wct.assertReorderAt(index = 0, taskToRequest) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeform_minimizesIfNeeded() { - setUpLandscapeDisplay() - val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } - val oldestTask = freeformTasks.first() - val newestTask = freeformTasks.last() - - val transition = Binder() - val wctCaptor = argumentCaptor<WindowContainerTransaction>() - whenever(desktopMixedTransitionHandler.startLaunchTransition(anyInt(), wctCaptor.capture(), - anyInt(), anyOrNull(), anyOrNull() - )) - .thenReturn(transition) - - runOpenInstance(newestTask, freeformTasks[1].taskId) - - val wct = wctCaptor.firstValue - assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize - wct.assertReorderAt(0, freeformTasks[1], toTop = true) - wct.assertReorderAt(1, oldestTask, toTop = false) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) - fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { - setUpLandscapeDisplay() - val freeformTask = setUpFreeformTask() - val immersiveTask = setUpFreeformTask() - taskRepository.setTaskInFullImmersiveState( - displayId = immersiveTask.displayId, - taskId = immersiveTask.taskId, - immersive = true - ) - val runOnStartTransit = RunOnStartTransitionCallback() - val transition = Binder() - whenever(desktopMixedTransitionHandler.startLaunchTransition(anyInt(), any(), anyInt(), - anyOrNull(), anyOrNull() - )) - .thenReturn(transition) - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable( - any(), eq(DEFAULT_DISPLAY), eq(freeformTask.taskId), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = immersiveTask.taskId, - runOnTransitionStart = runOnStartTransit, - )) - - runOpenInstance(immersiveTask, freeformTask.taskId) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId), any()) - runOnStartTransit.assertOnlyInvocation(transition) - } - - private fun runOpenInstance( - callingTask: RunningTaskInfo, - requestedTaskId: Int - ) { - markTaskVisible(callingTask) - callingTask.baseActivity = mock(ComponentName::class.java) - callingTask.isFocused = true - runningTasks.add(callingTask) - controller.openInstance(callingTask, requestedTaskId) - } - - @Test - fun toggleBounds_togglesToStableBounds() { - val bounds = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) + val rectAfterEnd = Rect(100, 50, 500, 1150) + verify(transitions) + .startTransition( + eq(TRANSIT_CHANGE), + Mockito.argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + change.configuration.windowConfiguration.bounds == rectAfterEnd + } + }, + eq(null), + ) + } - // Assert bounds set to stable bounds - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - STABLE_BOUNDS.width(), - STABLE_BOUNDS.height(), - displayController - ) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) - fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() { - val bounds = Rect(100, 100, 300, 300) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { - topActivityInfo = ActivityInfo().apply { - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE - configuration.windowConfiguration.appBounds = bounds - } - isResizeable = true - } - - val currentDragBounds = Rect(0, 100, 200, 300) - val expectedBounds = Rect( - STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom - ) + @Test + fun onDesktopDragEnd_noIndicator_updatesTaskBounds() { + val task = setUpFreeformTask() + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + + val currentDragBounds = Rect(100, 200, 500, 1000) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 200), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + currentDragBounds, /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration, + ) - controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, - ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, desktopWindowDecoration) - // Assert bounds set to stable bounds - val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) - assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.SNAP_LEFT_MENU, - InputMethod.TOUCH, - task, - expectedBounds.width(), - expectedBounds.height(), - displayController - ) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) - fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() { - // Set up task to already be in snapped-left bounds - val bounds = Rect( - STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom - ) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { - topActivityInfo = ActivityInfo().apply { - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE - configuration.windowConfiguration.appBounds = bounds - } - isResizeable = true - } - - // Attempt to snap left again - val currentDragBounds = Rect(bounds).apply { offset(-100, 0) } - controller.snapToHalfScreen(task, mockSurface, currentDragBounds, SnapPosition.LEFT, - ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, desktopWindowDecoration) - // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) - - // Assert that task leash is updated via Surface Animations - verify(mReturnToDragStartAnimator).start( - eq(task.taskId), - eq(mockSurface), - eq(currentDragBounds), - eq(bounds), - anyOrNull(), - ) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.SNAP_LEFT_MENU, - InputMethod.TOUCH, - task, - bounds.width(), - bounds.height(), - displayController - ) - } + verify(transitions) + .startTransition( + eq(TRANSIT_CHANGE), + Mockito.argThat { wct -> + return@argThat wct.changes.any { (token, change) -> + change.configuration.windowConfiguration.bounds == currentDragBounds + } + }, + eq(null), + ) + } - @Test - @DisableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, Flags.FLAG_ENABLE_TILE_RESIZING) - fun handleSnapResizingTaskOnDrag_nonResizable_snapsToHalfScreen() { - val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { - isResizeable = false + @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() { + val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + Rect(100, 50, 500, 1000), /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration, + ) + + // Assert the task exits desktop mode + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) } - val preDragBounds = Rect(100, 100, 400, 500) - val currentDragBounds = Rect(0, 100, 300, 500) - val expectedBounds = - Rect(STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom) - controller.handleSnapResizingTaskOnDrag( + @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() { + val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + val currentDragBounds = Rect(100, 50, 500, 1000) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + currentDragBounds, + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration, + ) - task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration - ) - val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) - assertThat(findBoundsChange(wct, task)).isEqualTo( - expectedBounds - ) - verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( - ResizeTrigger.DRAG_LEFT, - InputMethod.UNKNOWN_INPUT_METHOD, - task, - preDragBounds.width(), - preDragBounds.height(), - displayController - ) - } - - @Test - @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) - fun handleSnapResizingTaskOnDrag_nonResizable_startsRepositionAnimation() { - val task = setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { - isResizeable = false - } - val preDragBounds = Rect(100, 100, 400, 500) - val currentDragBounds = Rect(0, 100, 300, 500) - - controller.handleSnapResizingTaskOnDrag( - task, SnapPosition.LEFT, mockSurface, currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration) - verify(mReturnToDragStartAnimator).start( - eq(task.taskId), - eq(mockSurface), - eq(currentDragBounds), - eq(preDragBounds), - any(), - ) - verify(desktopModeEventLogger, never()).logTaskResizingStarted( - any(), - any(), - any(), - any(), - any(), - any(), - any() - ) - } - - @Test - @EnableFlags( - Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING - ) - fun handleInstantSnapResizingTask_nonResizable_animatorNotStartedAndShowsToast() { - val taskBounds = Rect(0, 0, 200, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { - isResizeable = false - } - - controller.handleInstantSnapResizingTask( - task, - SnapPosition.LEFT, - ResizeTrigger.SNAP_LEFT_MENU, - InputMethod.MOUSE, - desktopWindowDecoration - ) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + // Assert event is properly logged + verify(desktopModeEventLogger, times(1)) + .logTaskResizingStarted( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + InputMethod.UNKNOWN_INPUT_METHOD, + task, + task.configuration.windowConfiguration.bounds.width(), + task.configuration.windowConfiguration.bounds.height(), + displayController, + ) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.DRAG_TO_TOP_RESIZE_TRIGGER, + InputMethod.UNKNOWN_INPUT_METHOD, + task, + STABLE_BOUNDS.width(), + STABLE_BOUNDS.height(), + displayController, + ) + } - // Assert that task is NOT updated via WCT - verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) - verify(mockToast).show() - } - - @Test - @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) - @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) - fun handleInstantSnapResizingTask_resizable_snapsToHalfScreenAndNotShowToast() { - val taskBounds = Rect(0, 0, 200, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { - isResizeable = true - } - val expectedBounds = Rect( - STABLE_BOUNDS.left, STABLE_BOUNDS.top, STABLE_BOUNDS.right / 2, STABLE_BOUNDS.bottom - ) + @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() { + val task = setUpFreeformTask(bounds = STABLE_BOUNDS) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + val currentDragBounds = Rect(100, 50, 500, 1000) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + currentDragBounds, /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration, + ) - controller.handleInstantSnapResizingTask( - task, SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration - ) + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + // Assert that task leash is updated via Surface Animations + verify(mReturnToDragStartAnimator) + .start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(STABLE_BOUNDS), + anyOrNull(), + ) + // Assert no event is logged + verify(desktopModeEventLogger, never()) + .logTaskResizingStarted(any(), any(), any(), any(), any(), any(), any()) + verify(desktopModeEventLogger, never()) + .logTaskResizingEnded(any(), any(), any(), any(), any(), any(), any()) + } - // Assert bounds set to half of the stable bounds - val wct = getLatestToggleResizeDesktopTaskWct(taskBounds) - assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) - verify(mockToast, never()).show() - verify(desktopModeEventLogger, times(1)).logTaskResizingStarted( - ResizeTrigger.SNAP_LEFT_MENU, - InputMethod.MOUSE, - task, - taskBounds.width(), - taskBounds.height(), - displayController - ) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.SNAP_LEFT_MENU, - InputMethod.MOUSE, - task, - expectedBounds.width(), - expectedBounds.height(), - displayController - ) - } - - @Test - fun toggleBounds_togglesToCalculatedBoundsForNonResizable() { - val bounds = Rect(0, 0, 200, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { - topActivityInfo = ActivityInfo().apply { - screenOrientation = SCREEN_ORIENTATION_LANDSCAPE - configuration.windowConfiguration.appBounds = bounds - } - appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width() - appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height() - isResizeable = false - } - - // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds - val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750) - - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) + @Test + fun enterSplit_freeformTaskIsMovedToSplit() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + any(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds), + ) + } - // Assert bounds set to stable bounds - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - expectedBounds.width(), - expectedBounds.height(), - displayController - ) - } - - @Test - fun toggleBounds_lastBoundsBeforeMaximizeSaved() { - val bounds = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) - - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) + @Test + fun enterSplit_onlyVisibleNonMinimizedTask_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) + taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds), + ) + // Removes wallpaper activity when leaving desktop + wctArgument.value.assertRemoveAt(index = 0, wallpaperToken) + } - assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) - verify(desktopModeEventLogger, never()).logTaskResizingEnded( - any(), any(), any(), any(), - any(), any(), any() - ) - } - - @Test - fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) - - // Maximize - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) - task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) - - // Restore - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.RESTORE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, - InputMethod.TOUCH - ) - ) + @Test + fun enterSplit_multipleVisibleNonMinimizedTasks_removesWallpaperActivity() { + val task1 = setUpFreeformTask() + val task2 = setUpFreeformTask() + val task3 = setUpFreeformTask() + val wallpaperToken = MockToken().token() + + task1.isFocused = false + task2.isFocused = true + task3.isFocused = false + taskRepository.wallpaperActivityToken = wallpaperToken + + controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) + + val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(splitScreenController) + .requestEnterSplitSelect( + eq(task2), + wctArgument.capture(), + eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), + eq(task2.configuration.windowConfiguration.bounds), + ) + // Does not remove wallpaper activity, as desktop still has visible desktop tasks + assertThat(wctArgument.value.hierarchyOps).isEmpty() + } - // Assert bounds set to last bounds before maximize - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - boundsBeforeMaximize.width(), - boundsBeforeMaximize.height(), - displayController - ) - } - - @Test - fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualWidth() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { - isResizeable = false - } - - // Maximize - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) - task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS.left, - boundsBeforeMaximize.top, STABLE_BOUNDS.right, boundsBeforeMaximize.bottom) - - // Restore - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.RESTORE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, - InputMethod.TOUCH - ) - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent( + any(), + anyInt(), + any(), + any(), + optionsCaptor.capture(), + anyOrNull(), + eq(true), + eq(SPLIT_INDEX_UNDEFINED), + ) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } - // Assert bounds set to last bounds before maximize - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - boundsBeforeMaximize.width(), - boundsBeforeMaximize.height(), - displayController - ) - } - - @Test - fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualHeight() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { - isResizeable = false - } - - // Maximize - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) - task.configuration.windowConfiguration.bounds.set(boundsBeforeMaximize.left, - STABLE_BOUNDS.top, boundsBeforeMaximize.right, STABLE_BOUNDS.bottom) - - // Restore - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.RESTORE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, - InputMethod.TOUCH - ) - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenNewWindow(task) + verify(splitScreenController) + .startIntent( + any(), + anyInt(), + any(), + any(), + optionsCaptor.capture(), + anyOrNull(), + eq(true), + eq(SPLIT_INDEX_UNDEFINED), + ) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } - // Assert bounds set to last bounds before maximize - val wct = getLatestToggleResizeDesktopTaskWct() - assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - boundsBeforeMaximize.width(), - boundsBeforeMaximize.height(), - displayController - ) - } - - @Test - fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { - val boundsBeforeMaximize = Rect(0, 0, 100, 100) - val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) - - // Maximize - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.MAXIMIZE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, - InputMethod.TOUCH - ) - ) - task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) - - // Restore - controller.toggleDesktopTaskSize( - task, - ToggleTaskSizeInteraction( - ToggleTaskSizeInteraction.Direction.RESTORE, - ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, - InputMethod.TOUCH - ) - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val wctCaptor = argumentCaptor<WindowContainerTransaction>() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + anyInt(), + anyOrNull(), + any(), + ) + ) + .thenReturn(ExitResult.NoExit) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + any(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + runOpenNewWindow(task) + + verify(desktopMixedTransitionHandler) + .startLaunchTransition( + anyInt(), + wctCaptor.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + assertThat( + ActivityOptions.fromBundle(wctCaptor.firstValue.hierarchyOps[0].launchOptions) + .launchWindowingMode + ) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } - // Assert last bounds before maximize removed after use - assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() - verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( - ResizeTrigger.MAXIMIZE_BUTTON, - InputMethod.TOUCH, - task, - boundsBeforeMaximize.width(), - boundsBeforeMaximize.height(), - displayController - ) - } - - @Test - fun onUnhandledDrag_newFreeformIntent() { - testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, - PointF(1200f, 700f), - Rect(240, 700, 2160, 1900)) - } - - @Test - fun onUnhandledDrag_newFreeformIntentSplitLeft() { - testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, - PointF(50f, 700f), - Rect(0, 0, 500, 1000)) - } - - @Test - fun onUnhandledDrag_newFreeformIntentSplitRight() { - testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, - PointF(2500f, 700f), - Rect(500, 0, 1000, 1000)) - } - - @Test - fun onUnhandledDrag_newFullscreenIntent() { - testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, - PointF(1200f, 50f), - Rect()) - } - - @Test - fun shellController_registersUserChangeListener() { - verify(shellController, times(2)).addUserChangeListener(any()) - } - - @Test - @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_exits() { - val task = setUpFreeformTask(DEFAULT_DISPLAY) - taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true) - - task.requestedVisibleTypes = WindowInsets.Type.statusBars() - controller.onTaskInfoChanged(task) - - verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any()) - } - - @Test - @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun onTaskInfoChanged_notInImmersiveUnrequestsImmersive_noReExit() { - val task = setUpFreeformTask(DEFAULT_DISPLAY) - taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = false) - - task.requestedVisibleTypes = WindowInsets.Type.statusBars() - controller.onTaskInfoChanged(task) - - verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any()) - } - - @Test - @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_inRecentsTransition_noExit() { - val task = setUpFreeformTask(DEFAULT_DISPLAY) - taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true) - recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_REQUESTED) - - task.requestedVisibleTypes = WindowInsets.Type.statusBars() - controller.onTaskInfoChanged(task) - - verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any()) - } - - @Test - fun moveTaskToDesktop_background_attemptsImmersiveExit() { - val task = setUpFreeformTask(background = true) - val wct = WindowContainerTransaction() - val runOnStartTransit = RunOnStartTransitionCallback() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = 5, - runOnTransitionStart = runOnStartTransit, - )) - whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) - runOnStartTransit.assertOnlyInvocation(transition) - } - - @Test - fun moveTaskToDesktop_foreground_attemptsImmersiveExit() { - val task = setUpFreeformTask(background = false) - val wct = WindowContainerTransaction() - val runOnStartTransit = RunOnStartTransitionCallback() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = 5, - runOnTransitionStart = runOnStartTransit, - )) - whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) - runOnStartTransit.assertOnlyInvocation(transition) - } - - @Test - fun moveTaskToFront_background_attemptsImmersiveExit() { - val task = setUpFreeformTask(background = true) - val runOnStartTransit = RunOnStartTransitionCallback() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = 5, - runOnTransitionStart = runOnStartTransit, - )) - whenever(desktopMixedTransitionHandler - .startLaunchTransition(any(), any(), anyInt(), anyOrNull(), anyOrNull())) - .thenReturn(transition) - - controller.moveTaskToFront(task.taskId, remoteTransition = null) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()) - runOnStartTransit.assertOnlyInvocation(transition) - } - - @Test - fun moveTaskToFront_foreground_attemptsImmersiveExit() { - val task = setUpFreeformTask(background = false) - val runOnStartTransit = RunOnStartTransitionCallback() - val transition = Binder() - whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any())) - .thenReturn( - ExitResult.Exit( - exitingTask = 5, - runOnTransitionStart = runOnStartTransit, - )) - whenever(desktopMixedTransitionHandler - .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull(), anyOrNull())) - .thenReturn(transition) - - controller.moveTaskToFront(task.taskId, remoteTransition = null) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()) - runOnStartTransit.assertOnlyInvocation(transition) - } - - @Test - fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() { - markTaskVisible(setUpFreeformTask()) - val task = setUpFreeformTask() - markTaskVisible(task) - val binder = Binder() - - controller.handleRequest(binder, createTransition(task)) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any()) - } - - @Test - fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() { - setUpFreeformTask() - val task = setUpFullscreenTask() - val binder = Binder() - - controller.handleRequest(binder, createTransition(task)) - - verify(mMockDesktopImmersiveController) - .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) - taskRepository.setTaskInFullImmersiveState( - displayId = triggerTask.displayId, - taskId = triggerTask.taskId, - immersive = true - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun newWindow_fromFreeform_exitsImmersiveIfNeeded() { + setUpLandscapeDisplay() + val immersiveTask = setUpFreeformTask() + val task = setUpFreeformTask() + val runOnStart = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + anyInt(), + anyOrNull(), + any(), + ) + ) + .thenReturn(ExitResult.Exit(immersiveTask.taskId, runOnStart)) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + any(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + runOpenNewWindow(task) + + runOnStart.assertOnlyInvocation(transition) + } - assertThat(controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - )).isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) - taskRepository.setTaskInFullImmersiveState( - displayId = triggerTask.displayId, - taskId = triggerTask.taskId, - immersive = true - ) + private fun runOpenNewWindow(task: RunningTaskInfo) { + markTaskVisible(task) + task.baseActivity = mock(ComponentName::class.java) + task.isFocused = true + runningTasks.add(task) + controller.openNewWindow(task) + } - assertThat(controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null) - )).isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5) - taskRepository.setTaskInFullImmersiveState( - displayId = triggerTask.displayId, - taskId = triggerTask.taskId, - immersive = false - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFullscreenOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpFullscreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } - assertThat(controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - )).isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() { - // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFullscreenTask(displayId = 5) - assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() - taskRepository.setTaskInFullImmersiveState( - displayId = existingTask.displayId, - taskId = existingTask.taskId, - immersive = true - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromSplitOpensInSplit() { + setUpLandscapeDisplay() + val task = setUpSplitScreenTask() + val taskToRequest = setUpFreeformTask() + val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + runOpenInstance(task, taskToRequest.taskId) + verify(splitScreenController) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) + } - assertThat( - controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - ) - ).isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { - val triggerTask = setUpFullscreenTask(displayId = 5) - assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() - - assertThat(controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - )).isFalse() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() { - // At least one freeform task to be in a desktop. - val existingTask = setUpFreeformTask(displayId = 5) - val triggerTask = setUpFreeformTask(displayId = 5, active = false) - assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() - taskRepository.setTaskInFullImmersiveState( - displayId = existingTask.displayId, - taskId = existingTask.taskId, - immersive = true - ) + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeformAddsNewWindow() { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + val taskToRequest = setUpFreeformTask() + runOpenInstance(task, taskToRequest.taskId) + verify(desktopMixedTransitionHandler) + .startLaunchTransition(anyInt(), any(), anyInt(), anyOrNull(), anyOrNull()) + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) + assertThat(wct.hierarchyOps).hasSize(1) + wct.assertReorderAt(index = 0, taskToRequest) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_minimizesIfNeeded() { + setUpLandscapeDisplay() + val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + val oldestTask = freeformTasks.first() + val newestTask = freeformTasks.last() + + val transition = Binder() + val wctCaptor = argumentCaptor<WindowContainerTransaction>() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + wctCaptor.capture(), + anyInt(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + runOpenInstance(newestTask, freeformTasks[1].taskId) + + val wct = wctCaptor.firstValue + assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize + wct.assertReorderAt(0, freeformTasks[1], toTop = true) + wct.assertReorderAt(1, oldestTask, toTop = false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES) + fun openInstance_fromFreeform_exitsImmersiveIfNeeded() { + setUpLandscapeDisplay() + val freeformTask = setUpFreeformTask() + val immersiveTask = setUpFreeformTask() + taskRepository.setTaskInFullImmersiveState( + displayId = immersiveTask.displayId, + taskId = immersiveTask.taskId, + immersive = true, + ) + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + anyInt(), + any(), + anyInt(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + eq(DEFAULT_DISPLAY), + eq(freeformTask.taskId), + any(), + ) + ) + .thenReturn( + ExitResult.Exit( + exitingTask = immersiveTask.taskId, + runOnTransitionStart = runOnStartTransit, + ) + ) + + runOpenInstance(immersiveTask, freeformTask.taskId) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable( + any(), + eq(immersiveTask.displayId), + eq(freeformTask.taskId), + any(), + ) + runOnStartTransit.assertOnlyInvocation(transition) + } + + private fun runOpenInstance(callingTask: RunningTaskInfo, requestedTaskId: Int) { + markTaskVisible(callingTask) + callingTask.baseActivity = mock(ComponentName::class.java) + callingTask.isFocused = true + runningTasks.add(callingTask) + controller.openInstance(callingTask, requestedTaskId) + } + + @Test + fun toggleBounds_togglesToStableBounds() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + STABLE_BOUNDS.width(), + STABLE_BOUNDS.height(), + displayController, + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) + fun snapToHalfScreen_getSnapBounds_calculatesBoundsForResizable() { + val bounds = Rect(100, 100, 300, 300) + val task = + setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = + ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + isResizeable = true + } + + val currentDragBounds = Rect(0, 100, 200, 300) + val expectedBounds = + Rect( + STABLE_BOUNDS.left, + STABLE_BOUNDS.top, + STABLE_BOUNDS.right / 2, + STABLE_BOUNDS.bottom, + ) + + controller.snapToHalfScreen( + task, + mockSurface, + currentDragBounds, + SnapPosition.LEFT, + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.TOUCH, + desktopWindowDecoration, + ) + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.TOUCH, + task, + expectedBounds.width(), + expectedBounds.height(), + displayController, + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) + fun snapToHalfScreen_snapBoundsWhenAlreadySnapped_animatesSurfaceWithoutWCT() { + // Set up task to already be in snapped-left bounds + val bounds = + Rect( + STABLE_BOUNDS.left, + STABLE_BOUNDS.top, + STABLE_BOUNDS.right / 2, + STABLE_BOUNDS.bottom, + ) + val task = + setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = + ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + isResizeable = true + } + + // Attempt to snap left again + val currentDragBounds = Rect(bounds).apply { offset(-100, 0) } + controller.snapToHalfScreen( + task, + mockSurface, + currentDragBounds, + SnapPosition.LEFT, + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.TOUCH, + desktopWindowDecoration, + ) + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + + // Assert that task leash is updated via Surface Animations + verify(mReturnToDragStartAnimator) + .start(eq(task.taskId), eq(mockSurface), eq(currentDragBounds), eq(bounds), anyOrNull()) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.TOUCH, + task, + bounds.width(), + bounds.height(), + displayController, + ) + } - assertThat( - controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - ) - ).isTrue() - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { - val triggerTask = setUpFreeformTask(displayId = 5, active = false) - assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() - - assertThat(controller.shouldPlayDesktopAnimation( - TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) - )).isFalse() - } - - private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { - var invocations = 0 - private set - var lastInvoked: IBinder? = null - private set - - override fun invoke(transition: IBinder) { - invocations++ - lastInvoked = transition - } - } - - private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) { - assertThat(invocations).isEqualTo(1) - assertThat(lastInvoked).isEqualTo(transition) - } - - /** - * Assert that an unhandled drag event launches a PendingIntent with the - * windowing mode and bounds we are expecting. - */ - private fun testOnUnhandledDrag( - indicatorType: DesktopModeVisualIndicator.IndicatorType, - inputCoordinate: PointF, - expectedBounds: Rect - ) { - setUpLandscapeDisplay() - val task = setUpFreeformTask() - markTaskVisible(task) - task.isFocused = true - val runningTasks = ArrayList<RunningTaskInfo>() - runningTasks.add(task) - val spyController = spy(controller) - val mockPendingIntent = mock(PendingIntent::class.java) - val mockDragEvent = mock(DragEvent::class.java) - val mockCallback = mock(Consumer::class.java) - val b = SurfaceControl.Builder() - b.setName("test surface") - val dragSurface = b.build() - whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks) - whenever(mockDragEvent.dragSurface).thenReturn(dragSurface) - whenever(mockDragEvent.x).thenReturn(inputCoordinate.x) - whenever(mockDragEvent.y).thenReturn(inputCoordinate.y) - whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true) - whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) - doReturn(indicatorType) - .whenever(spyController).updateVisualIndicator( - eq(task), - anyOrNull(), - anyOrNull(), - anyOrNull(), - eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) - ) - - spyController.onUnhandledDrag( - mockPendingIntent, - mockDragEvent, - mockCallback as Consumer<Boolean> + @Test + @DisableFlags( + Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING, + Flags.FLAG_ENABLE_TILE_RESIZING, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - var expectedWindowingMode: Int - if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { - expectedWindowingMode = WINDOWING_MODE_FULLSCREEN - // Fullscreen launches currently use default transitions - verify(transitions).startTransition(any(), capture(arg), anyOrNull()) - } else { - expectedWindowingMode = WINDOWING_MODE_FREEFORM - // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) - } - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) - .launchWindowingMode).isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) - .launchBounds).isEqualTo(expectedBounds) - } - - private val desktopWallpaperIntent: Intent - get() = Intent(context, DesktopWallpaperActivity::class.java) - - private fun addFreeformTaskAtPosition( - pos: DesktopTaskPosition, - stableBounds: Rect, - bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS, - offsetPos: Point = Point(0, 0) - ): RunningTaskInfo { - val offset = pos.getTopLeftCoordinates(stableBounds, bounds) - val prevTaskBounds = Rect(bounds) - prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y) - return setUpFreeformTask(bounds = prevTaskBounds) - } - - private fun setUpFreeformTask( - displayId: Int = DEFAULT_DISPLAY, - bounds: Rect? = null, - active: Boolean = true, - background: Boolean = false, - ): RunningTaskInfo { - val task = createFreeformTask(displayId, bounds) - val activityInfo = ActivityInfo() - task.topActivityInfo = activityInfo - if (background) { - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) - whenever(recentTasksController.findTaskInBackground(task.taskId)) - .thenReturn(createTaskInfo(task.taskId)) - } else { - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - } - taskRepository.addTask(displayId, task.taskId, isVisible = active) - if (!background) { - runningTasks.add(task) - } - return task - } - - private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { - return setUpFreeformTask().apply { - pictureInPictureParams = PictureInPictureParams.Builder() - .setAutoEnterEnabled(autoEnterEnabled) - .build() - } - } - - private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createHomeTask(displayId) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpFullscreenTask( - displayId: Int = DEFAULT_DISPLAY, - isResizable: Boolean = true, - windowingMode: Int = WINDOWING_MODE_FULLSCREEN, - deviceOrientation: Int = ORIENTATION_LANDSCAPE, - screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, - shouldLetterbox: Boolean = false, - gravity: Int = Gravity.NO_GRAVITY, - enableUserFullscreenOverride: Boolean = false, - enableSystemFullscreenOverride: Boolean = false, - aspectRatioOverrideApplied: Boolean = false - ): RunningTaskInfo { - val task = createFullscreenTask(displayId) - val activityInfo = ActivityInfo() - activityInfo.screenOrientation = screenOrientation - activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0) - with(task) { - topActivityInfo = activityInfo - isResizeable = isResizable - configuration.orientation = deviceOrientation - configuration.windowConfiguration.windowingMode = windowingMode - appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride - appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride - - if (deviceOrientation == ORIENTATION_LANDSCAPE) { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) - appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG - appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT - } else { - configuration.windowConfiguration.appBounds = - Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) - appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT - appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG - } - - if (shouldLetterbox) { - appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied) - if (deviceOrientation == ORIENTATION_LANDSCAPE && - screenOrientation == SCREEN_ORIENTATION_PORTRAIT) { - // Letterbox to portrait size - appCompatTaskInfo.setTopActivityLetterboxed(true) - appCompatTaskInfo.topActivityLetterboxAppWidth = 1200 - appCompatTaskInfo.topActivityLetterboxAppHeight = 1600 - } else if (deviceOrientation == ORIENTATION_PORTRAIT && - screenOrientation == SCREEN_ORIENTATION_LANDSCAPE) { - // Letterbox to landscape size - appCompatTaskInfo.setTopActivityLetterboxed(true) - appCompatTaskInfo.topActivityLetterboxAppWidth = 1600 - appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 + fun handleSnapResizingTaskOnDrag_nonResizable_snapsToHalfScreen() { + val task = + setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false } + val preDragBounds = Rect(100, 100, 400, 500) + val currentDragBounds = Rect(0, 100, 300, 500) + val expectedBounds = + Rect( + STABLE_BOUNDS.left, + STABLE_BOUNDS.top, + STABLE_BOUNDS.right / 2, + STABLE_BOUNDS.bottom, + ) + + controller.handleSnapResizingTaskOnDrag( + task, + SnapPosition.LEFT, + mockSurface, + currentDragBounds, + preDragBounds, + motionEvent, + desktopWindowDecoration, + ) + val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingStarted( + ResizeTrigger.DRAG_LEFT, + InputMethod.UNKNOWN_INPUT_METHOD, + task, + preDragBounds.width(), + preDragBounds.height(), + displayController, + ) + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun handleSnapResizingTaskOnDrag_nonResizable_startsRepositionAnimation() { + val task = + setUpFreeformTask(DEFAULT_DISPLAY, Rect(0, 0, 200, 100)).apply { isResizeable = false } + val preDragBounds = Rect(100, 100, 400, 500) + val currentDragBounds = Rect(0, 100, 300, 500) + + controller.handleSnapResizingTaskOnDrag( + task, + SnapPosition.LEFT, + mockSurface, + currentDragBounds, + preDragBounds, + motionEvent, + desktopWindowDecoration, + ) + verify(mReturnToDragStartAnimator) + .start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(preDragBounds), + any(), + ) + verify(desktopModeEventLogger, never()) + .logTaskResizingStarted(any(), any(), any(), any(), any(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + fun handleInstantSnapResizingTask_nonResizable_animatorNotStartedAndShowsToast() { + val taskBounds = Rect(0, 0, 200, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { isResizeable = false } + + controller.handleInstantSnapResizingTask( + task, + SnapPosition.LEFT, + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.MOUSE, + desktopWindowDecoration, + ) + + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + verify(mockToast).show() + } + + @Test + @EnableFlags(Flags.FLAG_DISABLE_NON_RESIZABLE_APP_SNAP_RESIZING) + @DisableFlags(Flags.FLAG_ENABLE_TILE_RESIZING) + fun handleInstantSnapResizingTask_resizable_snapsToHalfScreenAndNotShowToast() { + val taskBounds = Rect(0, 0, 200, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, taskBounds).apply { isResizeable = true } + val expectedBounds = + Rect( + STABLE_BOUNDS.left, + STABLE_BOUNDS.top, + STABLE_BOUNDS.right / 2, + STABLE_BOUNDS.bottom, + ) + + controller.handleInstantSnapResizingTask( + task, + SnapPosition.LEFT, + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.MOUSE, + desktopWindowDecoration, + ) + + // Assert bounds set to half of the stable bounds + val wct = getLatestToggleResizeDesktopTaskWct(taskBounds) + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(mockToast, never()).show() + verify(desktopModeEventLogger, times(1)) + .logTaskResizingStarted( + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.MOUSE, + task, + taskBounds.width(), + taskBounds.height(), + displayController, + ) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.SNAP_LEFT_MENU, + InputMethod.MOUSE, + task, + expectedBounds.width(), + expectedBounds.height(), + displayController, + ) + } + + @Test + fun toggleBounds_togglesToCalculatedBoundsForNonResizable() { + val bounds = Rect(0, 0, 200, 100) + val task = + setUpFreeformTask(DEFAULT_DISPLAY, bounds).apply { + topActivityInfo = + ActivityInfo().apply { + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } + appCompatTaskInfo.topActivityLetterboxAppWidth = bounds.width() + appCompatTaskInfo.topActivityLetterboxAppHeight = bounds.height() + isResizeable = false + } + + // Bounds should be 1000 x 500, vertically centered in the 1000 x 1000 stable bounds + val expectedBounds = Rect(STABLE_BOUNDS.left, 250, STABLE_BOUNDS.right, 750) + + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + expectedBounds.width(), + expectedBounds.height(), + displayController, + ) + } + + @Test + fun toggleBounds_lastBoundsBeforeMaximizeSaved() { + val bounds = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, bounds) + + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + + assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isEqualTo(bounds) + verify(desktopModeEventLogger, never()) + .logTaskResizingEnded(any(), any(), any(), any(), any(), any(), any()) + } + + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.RESTORE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, + InputMethod.TOUCH, + ), + ) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + boundsBeforeMaximize.width(), + boundsBeforeMaximize.height(), + displayController, + ) + } + + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualWidth() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = + setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { isResizeable = false } + + // Maximize + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + task.configuration.windowConfiguration.bounds.set( + STABLE_BOUNDS.left, + boundsBeforeMaximize.top, + STABLE_BOUNDS.right, + boundsBeforeMaximize.bottom, + ) + + // Restore + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.RESTORE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, + InputMethod.TOUCH, + ), + ) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + boundsBeforeMaximize.width(), + boundsBeforeMaximize.height(), + displayController, + ) + } + + @Test + fun toggleBounds_togglesFromStableBoundsToLastBoundsBeforeMaximize_nonResizeableEqualHeight() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = + setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize).apply { isResizeable = false } + + // Maximize + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + task.configuration.windowConfiguration.bounds.set( + boundsBeforeMaximize.left, + STABLE_BOUNDS.top, + boundsBeforeMaximize.right, + STABLE_BOUNDS.bottom, + ) + + // Restore + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.RESTORE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, + InputMethod.TOUCH, + ), + ) + + // Assert bounds set to last bounds before maximize + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(boundsBeforeMaximize) + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + boundsBeforeMaximize.width(), + boundsBeforeMaximize.height(), + displayController, + ) + } + + @Test + fun toggleBounds_removesLastBoundsBeforeMaximizeAfterRestoringBounds() { + val boundsBeforeMaximize = Rect(0, 0, 100, 100) + val task = setUpFreeformTask(DEFAULT_DISPLAY, boundsBeforeMaximize) + + // Maximize + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.MAXIMIZE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_MAXIMIZE, + InputMethod.TOUCH, + ), + ) + task.configuration.windowConfiguration.bounds.set(STABLE_BOUNDS) + + // Restore + controller.toggleDesktopTaskSize( + task, + ToggleTaskSizeInteraction( + ToggleTaskSizeInteraction.Direction.RESTORE, + ToggleTaskSizeInteraction.Source.HEADER_BUTTON_TO_RESTORE, + InputMethod.TOUCH, + ), + ) + + // Assert last bounds before maximize removed after use + assertThat(taskRepository.removeBoundsBeforeMaximize(task.taskId)).isNull() + verify(desktopModeEventLogger, times(1)) + .logTaskResizingEnded( + ResizeTrigger.MAXIMIZE_BUTTON, + InputMethod.TOUCH, + task, + boundsBeforeMaximize.width(), + boundsBeforeMaximize.height(), + displayController, + ) + } + + @Test + fun onUnhandledDrag_newFreeformIntent() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + PointF(1200f, 700f), + Rect(240, 700, 2160, 1900), + ) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitLeft() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + PointF(50f, 700f), + Rect(0, 0, 500, 1000), + ) + } + + @Test + fun onUnhandledDrag_newFreeformIntentSplitRight() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + PointF(2500f, 700f), + Rect(500, 0, 1000, 1000), + ) + } + + @Test + fun onUnhandledDrag_newFullscreenIntent() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + PointF(1200f, 50f), + Rect(), + ) + } + + @Test + fun shellController_registersUserChangeListener() { + verify(shellController, times(2)).addUserChangeListener(any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_exits() { + val task = setUpFreeformTask(DEFAULT_DISPLAY) + taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true) + + task.requestedVisibleTypes = WindowInsets.Type.statusBars() + controller.onTaskInfoChanged(task) + + verify(mMockDesktopImmersiveController).moveTaskToNonImmersive(eq(task), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun onTaskInfoChanged_notInImmersiveUnrequestsImmersive_noReExit() { + val task = setUpFreeformTask(DEFAULT_DISPLAY) + taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = false) + + task.requestedVisibleTypes = WindowInsets.Type.statusBars() + controller.onTaskInfoChanged(task) + + verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any()) + } + + @Test + @EnableFlags(FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun onTaskInfoChanged_inImmersiveUnrequestsImmersive_inRecentsTransition_noExit() { + val task = setUpFreeformTask(DEFAULT_DISPLAY) + taskRepository.setTaskInFullImmersiveState(DEFAULT_DISPLAY, task.taskId, immersive = true) + recentsTransitionStateListener.onTransitionStateChanged(TRANSITION_STATE_REQUESTED) + + task.requestedVisibleTypes = WindowInsets.Type.statusBars() + controller.onTaskInfoChanged(task) + + verify(mMockDesktopImmersiveController, never()).moveTaskToNonImmersive(eq(task), any()) + } + + @Test + fun moveTaskToDesktop_background_attemptsImmersiveExit() { + val task = setUpFreeformTask(background = true) + val wct = WindowContainerTransaction() + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + eq(wct), + eq(task.displayId), + eq(task.taskId), + any(), + ) + ) + .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) + whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) + + controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) + runOnStartTransit.assertOnlyInvocation(transition) + } + + @Test + fun moveTaskToDesktop_foreground_attemptsImmersiveExit() { + val task = setUpFreeformTask(background = false) + val wct = WindowContainerTransaction() + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + eq(wct), + eq(task.displayId), + eq(task.taskId), + any(), + ) + ) + .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) + whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) + + controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) + runOnStartTransit.assertOnlyInvocation(transition) + } + + @Test + fun moveTaskToFront_background_attemptsImmersiveExit() { + val task = setUpFreeformTask(background = true) + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + eq(task.displayId), + eq(task.taskId), + any(), + ) + ) + .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + any(), + any(), + anyInt(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + controller.moveTaskToFront(task.taskId, remoteTransition = null) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()) + runOnStartTransit.assertOnlyInvocation(transition) + } + + @Test + fun moveTaskToFront_foreground_attemptsImmersiveExit() { + val task = setUpFreeformTask(background = false) + val runOnStartTransit = RunOnStartTransitionCallback() + val transition = Binder() + whenever( + mMockDesktopImmersiveController.exitImmersiveIfApplicable( + any(), + eq(task.displayId), + eq(task.taskId), + any(), + ) + ) + .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + any(), + any(), + eq(task.taskId), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(transition) + + controller.moveTaskToFront(task.taskId, remoteTransition = null) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId), any()) + runOnStartTransit.assertOnlyInvocation(transition) + } + + @Test + fun handleRequest_freeformLaunchToDesktop_attemptsImmersiveExit() { + markTaskVisible(setUpFreeformTask()) + val task = setUpFreeformTask() + markTaskVisible(task) + val binder = Binder() + + controller.handleRequest(binder, createTransition(task)) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any()) + } + + @Test + fun handleRequest_fullscreenLaunchToDesktop_attemptsImmersiveExit() { + setUpFreeformTask() + val task = setUpFullscreenTask() + val binder = Binder() + + controller.handleRequest(binder, createTransition(task)) + + verify(mMockDesktopImmersiveController) + .exitImmersiveIfApplicable(eq(binder), any(), eq(task.displayId), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notShowingDesktop_doesNotPlay() { + val triggerTask = setUpFullscreenTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = true, + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notOpening_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = true, + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_CHANGE, triggerTask, /* remoteTransition= */ null) + ) + ) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_notImmersive_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5) + taskRepository.setTaskInFullImmersiveState( + displayId = triggerTask.displayId, + taskId = triggerTask.taskId, + immersive = false, + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_fullscreenEntersDesktop_plays() { + // At least one freeform task to be in a desktop. + val existingTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFullscreenTask(displayId = 5) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() + taskRepository.setTaskInFullImmersiveState( + displayId = existingTask.displayId, + taskId = existingTask.taskId, + immersive = true, + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_fullscreenStaysFullscreen_doesNotPlay() { + val triggerTask = setUpFullscreenTask(displayId = 5) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_freeformStaysInDesktop_plays() { + // At least one freeform task to be in a desktop. + val existingTask = setUpFreeformTask(displayId = 5) + val triggerTask = setUpFreeformTask(displayId = 5, active = false) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isTrue() + taskRepository.setTaskInFullImmersiveState( + displayId = existingTask.displayId, + taskId = existingTask.taskId, + immersive = true, + ) + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun shouldPlayDesktopAnimation_freeformExitsDesktop_doesNotPlay() { + val triggerTask = setUpFreeformTask(displayId = 5, active = false) + assertThat(controller.isDesktopModeShowing(triggerTask.displayId)).isFalse() + + assertThat( + controller.shouldPlayDesktopAnimation( + TransitionRequestInfo(TRANSIT_OPEN, triggerTask, /* remoteTransition= */ null) + ) + ) + .isFalse() + } + + private class RunOnStartTransitionCallback : ((IBinder) -> Unit) { + var invocations = 0 + private set + + var lastInvoked: IBinder? = null + private set + + override fun invoke(transition: IBinder) { + invocations++ + lastInvoked = transition } - } - } - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun setUpLandscapeDisplay() { - whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) - whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) - val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_LONG, - DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT - ) - whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(stableBounds) } - } - private fun setUpPortraitDisplay() { - whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) - whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) - val stableBounds = Rect(0, 0, DISPLAY_DIMENSION_SHORT, - DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT - ) - whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> - (i.arguments.first() as Rect).set(stableBounds) - } - } - - private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { - val task = createSplitScreenTask(displayId) - whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) - whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - runningTasks.add(task) - return task - } - - private fun markTaskVisible(task: RunningTaskInfo) { - taskRepository.updateTask(task.displayId, task.taskId, isVisible = true) - } - - private fun markTaskHidden(task: RunningTaskInfo) { - taskRepository.updateTask(task.displayId, task.taskId, isVisible = false) - } - - private fun getLatestWct( - @WindowManager.TransitionType type: Int = TRANSIT_OPEN, - handlerClass: Class<out TransitionHandler>? = null - ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (handlerClass == null) { - verify(transitions).startTransition(eq(type), arg.capture(), isNull()) - } else { - verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) - } - return arg.value - } - - private fun getLatestToggleResizeDesktopTaskWct( - currentBounds: Rect? = null - ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value - } - - private fun getLatestDesktopMixedTaskWct( - @WindowManager.TransitionType type: Int = TRANSIT_OPEN, - ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull()) - return arg.value - } - - private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value - } - - private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value - } - - private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value - } - - private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = - wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds - - private fun verifyWCTNotExecuted() { - verify(transitions, never()).startTransition(anyInt(), any(), isNull()) - } - - private fun verifyExitDesktopWCTNotExecuted() { - verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) - } - - private fun verifyEnterDesktopWCTNotExecuted() { - verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) - } - - private fun createTransition( - task: RunningTaskInfo?, - @WindowManager.TransitionType type: Int = TRANSIT_OPEN - ): TransitionRequestInfo { - return TransitionRequestInfo(type, task, null /* remoteTransition */) - } - - private companion object { - const val SECOND_DISPLAY = 2 - val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) - const val MAX_TASK_LIMIT = 6 - private const val TASKBAR_FRAME_HEIGHT = 200 - } + private fun RunOnStartTransitionCallback.assertOnlyInvocation(transition: IBinder) { + assertThat(invocations).isEqualTo(1) + assertThat(lastInvoked).isEqualTo(transition) + } + + /** + * Assert that an unhandled drag event launches a PendingIntent with the windowing mode and + * bounds we are expecting. + */ + private fun testOnUnhandledDrag( + indicatorType: DesktopModeVisualIndicator.IndicatorType, + inputCoordinate: PointF, + expectedBounds: Rect, + ) { + setUpLandscapeDisplay() + val task = setUpFreeformTask() + markTaskVisible(task) + task.isFocused = true + val runningTasks = ArrayList<RunningTaskInfo>() + runningTasks.add(task) + val spyController = spy(controller) + val mockPendingIntent = mock(PendingIntent::class.java) + val mockDragEvent = mock(DragEvent::class.java) + val mockCallback = mock(Consumer::class.java) + val b = SurfaceControl.Builder() + b.setName("test surface") + val dragSurface = b.build() + whenever(shellTaskOrganizer.runningTasks).thenReturn(runningTasks) + whenever(mockDragEvent.dragSurface).thenReturn(dragSurface) + whenever(mockDragEvent.x).thenReturn(inputCoordinate.x) + whenever(mockDragEvent.y).thenReturn(inputCoordinate.y) + whenever(multiInstanceHelper.supportsMultiInstanceSplit(anyOrNull())).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + doReturn(indicatorType) + .whenever(spyController) + .updateVisualIndicator( + eq(task), + anyOrNull(), + anyOrNull(), + anyOrNull(), + eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT), + ) + + spyController.onUnhandledDrag( + mockPendingIntent, + mockDragEvent, + mockCallback as Consumer<Boolean>, + ) + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + var expectedWindowingMode: Int + if (indicatorType == DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) { + expectedWindowingMode = WINDOWING_MODE_FULLSCREEN + // Fullscreen launches currently use default transitions + verify(transitions).startTransition(any(), capture(arg), anyOrNull()) + } else { + expectedWindowingMode = WINDOWING_MODE_FREEFORM + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + } + assertThat( + ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + .launchWindowingMode + ) + .isEqualTo(expectedWindowingMode) + assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + .isEqualTo(expectedBounds) + } + + private val desktopWallpaperIntent: Intent + get() = Intent(context, DesktopWallpaperActivity::class.java) + + private fun addFreeformTaskAtPosition( + pos: DesktopTaskPosition, + stableBounds: Rect, + bounds: Rect = DEFAULT_LANDSCAPE_BOUNDS, + offsetPos: Point = Point(0, 0), + ): RunningTaskInfo { + val offset = pos.getTopLeftCoordinates(stableBounds, bounds) + val prevTaskBounds = Rect(bounds) + prevTaskBounds.offsetTo(offset.x + offsetPos.x, offset.y + offsetPos.y) + return setUpFreeformTask(bounds = prevTaskBounds) + } + + private fun setUpFreeformTask( + displayId: Int = DEFAULT_DISPLAY, + bounds: Rect? = null, + active: Boolean = true, + background: Boolean = false, + ): RunningTaskInfo { + val task = createFreeformTask(displayId, bounds) + val activityInfo = ActivityInfo() + task.topActivityInfo = activityInfo + if (background) { + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + whenever(recentTasksController.findTaskInBackground(task.taskId)) + .thenReturn(createTaskInfo(task.taskId)) + } else { + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + } + taskRepository.addTask(displayId, task.taskId, isVisible = active) + if (!background) { + runningTasks.add(task) + } + return task + } + + private fun setUpPipTask(autoEnterEnabled: Boolean): RunningTaskInfo { + return setUpFreeformTask().apply { + pictureInPictureParams = + PictureInPictureParams.Builder().setAutoEnterEnabled(autoEnterEnabled).build() + } + } + + private fun setUpHomeTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + val task = createHomeTask(displayId) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun setUpFullscreenTask( + displayId: Int = DEFAULT_DISPLAY, + isResizable: Boolean = true, + windowingMode: Int = WINDOWING_MODE_FULLSCREEN, + deviceOrientation: Int = ORIENTATION_LANDSCAPE, + screenOrientation: Int = SCREEN_ORIENTATION_UNSPECIFIED, + shouldLetterbox: Boolean = false, + gravity: Int = Gravity.NO_GRAVITY, + enableUserFullscreenOverride: Boolean = false, + enableSystemFullscreenOverride: Boolean = false, + aspectRatioOverrideApplied: Boolean = false, + ): RunningTaskInfo { + val task = createFullscreenTask(displayId) + val activityInfo = ActivityInfo() + activityInfo.screenOrientation = screenOrientation + activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0) + with(task) { + topActivityInfo = activityInfo + isResizeable = isResizable + configuration.orientation = deviceOrientation + configuration.windowConfiguration.windowingMode = windowingMode + appCompatTaskInfo.isUserFullscreenOverrideEnabled = enableUserFullscreenOverride + appCompatTaskInfo.isSystemFullscreenOverrideEnabled = enableSystemFullscreenOverride + + if (deviceOrientation == ORIENTATION_LANDSCAPE) { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_LONG, DISPLAY_DIMENSION_SHORT) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_LONG + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_SHORT + } else { + configuration.windowConfiguration.appBounds = + Rect(0, 0, DISPLAY_DIMENSION_SHORT, DISPLAY_DIMENSION_LONG) + appCompatTaskInfo.topActivityLetterboxAppWidth = DISPLAY_DIMENSION_SHORT + appCompatTaskInfo.topActivityLetterboxAppHeight = DISPLAY_DIMENSION_LONG + } + + if (shouldLetterbox) { + appCompatTaskInfo.setHasMinAspectRatioOverride(aspectRatioOverrideApplied) + if ( + deviceOrientation == ORIENTATION_LANDSCAPE && + screenOrientation == SCREEN_ORIENTATION_PORTRAIT + ) { + // Letterbox to portrait size + appCompatTaskInfo.setTopActivityLetterboxed(true) + appCompatTaskInfo.topActivityLetterboxAppWidth = 1200 + appCompatTaskInfo.topActivityLetterboxAppHeight = 1600 + } else if ( + deviceOrientation == ORIENTATION_PORTRAIT && + screenOrientation == SCREEN_ORIENTATION_LANDSCAPE + ) { + // Letterbox to landscape size + appCompatTaskInfo.setTopActivityLetterboxed(true) + appCompatTaskInfo.topActivityLetterboxAppWidth = 1600 + appCompatTaskInfo.topActivityLetterboxAppHeight = 1200 + } + } + } + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun setUpLandscapeDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_LONG) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_SHORT) + val stableBounds = + Rect( + 0, + 0, + DISPLAY_DIMENSION_LONG, + DISPLAY_DIMENSION_SHORT - Companion.TASKBAR_FRAME_HEIGHT, + ) + whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } + } + + private fun setUpPortraitDisplay() { + whenever(displayLayout.width()).thenReturn(DISPLAY_DIMENSION_SHORT) + whenever(displayLayout.height()).thenReturn(DISPLAY_DIMENSION_LONG) + val stableBounds = + Rect( + 0, + 0, + DISPLAY_DIMENSION_SHORT, + DISPLAY_DIMENSION_LONG - Companion.TASKBAR_FRAME_HEIGHT, + ) + whenever(displayLayout.getStableBoundsForDesktopMode(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(stableBounds) + } + } + + private fun setUpSplitScreenTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { + val task = createSplitScreenTask(displayId) + whenever(splitScreenController.isTaskInSplitScreen(task.taskId)).thenReturn(true) + whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + runningTasks.add(task) + return task + } + + private fun markTaskVisible(task: RunningTaskInfo) { + taskRepository.updateTask(task.displayId, task.taskId, isVisible = true) + } + + private fun markTaskHidden(task: RunningTaskInfo) { + taskRepository.updateTask(task.displayId, task.taskId, isVisible = false) + } + + private fun getLatestWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + handlerClass: Class<out TransitionHandler>? = null, + ): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + if (handlerClass == null) { + verify(transitions).startTransition(eq(type), arg.capture(), isNull()) + } else { + verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) + } + return arg.value + } + + private fun getLatestToggleResizeDesktopTaskWct( + currentBounds: Rect? = null + ): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) + .startTransition(capture(arg), eq(currentBounds)) + return arg.value + } + + private fun getLatestDesktopMixedTaskWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN + ): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(desktopMixedTransitionHandler) + .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull()) + return arg.value + } + + private fun getLatestEnterDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) + return arg.value + } + + private fun getLatestDragToDesktopWct(): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) + return arg.value + } + + private fun getLatestExitDesktopWct(): WindowContainerTransaction { + val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) + return arg.value + } + + private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = + wct.changes[task.token.asBinder()]?.configuration?.windowConfiguration?.bounds + + private fun verifyWCTNotExecuted() { + verify(transitions, never()).startTransition(anyInt(), any(), isNull()) + } + + private fun verifyExitDesktopWCTNotExecuted() { + verify(exitDesktopTransitionHandler, never()).startTransition(any(), any(), any(), any()) + } + + private fun verifyEnterDesktopWCTNotExecuted() { + verify(enterDesktopTransitionHandler, never()).moveToDesktop(any(), any()) + } + + private fun createTransition( + task: RunningTaskInfo?, + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + ): TransitionRequestInfo { + return TransitionRequestInfo(type, task, null /* remoteTransition */) + } + + private companion object { + const val SECOND_DISPLAY = 2 + val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + const val MAX_TASK_LIMIT = 6 + private const val TASKBAR_FRAME_HEIGHT = 200 + } } private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { - assertWithMessage("WCT does not have a hierarchy operation at index $index") - .that(hierarchyOps.size) - .isGreaterThan(index) + assertWithMessage("WCT does not have a hierarchy operation at index $index") + .that(hierarchyOps.size) + .isGreaterThan(index) } private fun WindowContainerTransaction.assertReorderAt( index: Int, task: RunningTaskInfo, - toTop: Boolean? = null + toTop: Boolean? = null, ) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) - assertThat(op.container).isEqualTo(task.token.asBinder()) - toTop?.let { assertThat(op.toTop).isEqualTo(it) } + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REORDER) + assertThat(op.container).isEqualTo(task.token.asBinder()) + toTop?.let { assertThat(op.toTop).isEqualTo(it) } } private fun WindowContainerTransaction.assertReorderSequence(vararg tasks: RunningTaskInfo) { - for (i in tasks.indices) { - assertReorderAt(i, tasks[i]) - } + for (i in tasks.indices) { + assertReorderAt(i, tasks[i]) + } } /** Checks if the reorder hierarchy operations in [range] correspond to [tasks] list */ private fun WindowContainerTransaction.assertReorderSequenceInRange( - range: IntRange, - vararg tasks: RunningTaskInfo + range: IntRange, + vararg tasks: RunningTaskInfo, ) { - assertThat(hierarchyOps.slice(range).map { it.type to it.container }) - .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() }) - .inOrder() + assertThat(hierarchyOps.slice(range).map { it.type to it.container }) + .containsExactlyElementsIn(tasks.map { HIERARCHY_OP_TYPE_REORDER to it.token.asBinder() }) + .inOrder() } private fun WindowContainerTransaction.assertRemoveAt(index: Int, token: WindowContainerToken) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) - assertThat(op.container).isEqualTo(token.asBinder()) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) } private fun WindowContainerTransaction.assertNoRemoveAt(index: Int, token: WindowContainerToken) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) - assertThat(op.container).isEqualTo(token.asBinder()) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) } private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowContainerToken) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) - assertThat(op.container).isEqualTo(token.asBinder()) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) + assertThat(op.container).isEqualTo(token.asBinder()) } private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) - assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_PENDING_INTENT) + assertThat(op.pendingIntent?.intent?.component).isEqualTo(intent.component) } private fun WindowContainerTransaction.assertLaunchTaskAt( index: Int, taskId: Int, - windowingMode: Int + windowingMode: Int, ) { - val keyLaunchWindowingMode = "android.activity.windowingMode" - - assertIndexInBounds(index) - val op = hierarchyOps[index] - assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK) - assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId) - assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED)) - .isEqualTo(windowingMode) + val keyLaunchWindowingMode = "android.activity.windowingMode" + + assertIndexInBounds(index) + val op = hierarchyOps[index] + assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_LAUNCH_TASK) + assertThat(op.launchOptions?.getInt(LAUNCH_KEY_TASK_ID)).isEqualTo(taskId) + assertThat(op.launchOptions?.getInt(keyLaunchWindowingMode, WINDOWING_MODE_UNDEFINED)) + .isEqualTo(windowingMode) } private fun WindowContainerTransaction?.anyDensityConfigChange( token: WindowContainerToken ): Boolean { - return this?.changes?.any { change -> - change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) - } ?: false + return this?.changes?.any { change -> + change.key == token.asBinder() && ((change.value.configSetMask and CONFIG_DENSITY) != 0) + } ?: false } private fun WindowContainerTransaction?.anyWindowingModeChange( - token: WindowContainerToken + token: WindowContainerToken ): Boolean { -return this?.changes?.any { change -> - change.key == token.asBinder() && change.value.windowingMode >= 0 -} ?: false + return this?.changes?.any { change -> + change.key == token.asBinder() && change.value.windowingMode >= 0 + } ?: false } private fun createTaskInfo(id: Int) = RecentTaskInfo().apply { - taskId = id - token = WindowContainerToken(mock(IWindowContainerToken::class.java)) + taskId = id + token = WindowContainerToken(mock(IWindowContainerToken::class.java)) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt index 1e4d108a9cda..e6f1fcf7f14f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksLimiterTest.kt @@ -42,12 +42,12 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase -import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions @@ -85,9 +85,7 @@ import org.mockito.quality.Strictness @ExperimentalCoroutinesApi class DesktopTasksLimiterTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule() @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @@ -108,9 +106,12 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Before fun setUp() { - mockitoSession = ExtendedMockito.mockitoSession().strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java).startMocking() - doReturn(true).`when`{ DesktopModeStatus.canEnterDesktopMode(any()) } + mockitoSession = + ExtendedMockito.mockitoSession() + .strictness(Strictness.LENIENT) + .spyStatic(DesktopModeStatus::class.java) + .startMocking() + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } shellInit = spy(ShellInit(testExecutor)) Dispatchers.setMain(StandardTestDispatcher()) testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) @@ -123,12 +124,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { persistentRepository, repositoryInitializer, testScope, - userManager + userManager, ) desktopTaskRepo = userRepositories.current desktopTasksLimiter = - DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, MAX_TASK_LIMIT, - interactionJankMonitor, mContext, handler) + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + MAX_TASK_LIMIT, + interactionJankMonitor, + mContext, + handler, + ) } @After @@ -140,16 +148,30 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun createDesktopTasksLimiter_withZeroLimit_shouldThrow() { assertFailsWith<IllegalArgumentException> { - DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, 0, - interactionJankMonitor, mContext, handler) + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + 0, + interactionJankMonitor, + mContext, + handler, + ) } } @Test fun createDesktopTasksLimiter_withNegativeLimit_shouldThrow() { assertFailsWith<IllegalArgumentException> { - DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, -5, - interactionJankMonitor, mContext, handler) + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + -5, + interactionJankMonitor, + mContext, + handler, + ) } } @@ -168,11 +190,14 @@ class DesktopTasksLimiterTest : ShellTestCase() { val task = setUpFreeformTask() markTaskHidden(task) - desktopTasksLimiter.getTransitionObserver().onTransitionReady( + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( Binder() /* transition */, TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() } @@ -184,13 +209,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { val task = setUpFreeformTask() markTaskHidden(task) desktopTasksLimiter.addPendingMinimizeChange( - pendingTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + pendingTransition, + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) - desktopTasksLimiter.getTransitionObserver().onTransitionReady( - taskTransition /* transition */, - TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + taskTransition /* transition */, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() } @@ -201,13 +232,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { val task = setUpFreeformTask() markTaskVisible(task) desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + transition, + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) - desktopTasksLimiter.getTransitionObserver().onTransitionReady( + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( transition, TransitionInfoBuilder(TRANSIT_OPEN).build(), StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isFalse() } @@ -218,13 +255,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { val task = setUpFreeformTask() markTaskHidden(task) desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + transition, + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) - desktopTasksLimiter.getTransitionObserver().onTransitionReady( + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( transition, TransitionInfoBuilder(TRANSIT_OPEN).build(), StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() } @@ -234,13 +277,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { val transition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) + transition, + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) - desktopTasksLimiter.getTransitionObserver().onTransitionReady( + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( transition, TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() } @@ -251,22 +300,29 @@ class DesktopTasksLimiterTest : ShellTestCase() { val transition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) - - val change = TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply { - mode = TRANSIT_TO_BACK - taskInfo = task - setStartAbsBounds(bounds) - } - desktopTasksLimiter.getTransitionObserver().onTransitionReady( transition, - TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) }, - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) + + val change = + TransitionInfo.Change(task.token, mock(SurfaceControl::class.java)).apply { + mode = TRANSIT_TO_BACK + taskInfo = task + setStartAbsBounds(bounds) + } + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + transition, + TransitionInfo(TRANSIT_OPEN, TransitionInfo.FLAG_NONE).apply { addChange(change) }, + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() - assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)).isEqualTo( - bounds) + assertThat(desktopTaskRepo.removeBoundsBeforeMinimize(taskId = task.taskId)) + .isEqualTo(bounds) } @Test @@ -275,15 +331,22 @@ class DesktopTasksLimiterTest : ShellTestCase() { val newTransition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) - desktopTasksLimiter.getTransitionObserver().onTransitionMerged( - mergedTransition, newTransition) - - desktopTasksLimiter.getTransitionObserver().onTransitionReady( - newTransition, - TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + mergedTransition, + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) + desktopTasksLimiter + .getTransitionObserver() + .onTransitionMerged(mergedTransition, newTransition) + + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + newTransition, + TransitionInfoBuilder(TRANSIT_OPEN).addChange(TRANSIT_TO_BACK, task).build(), + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) assertThat(desktopTaskRepo.isMinimizedTask(taskId = task.taskId)).isTrue() } @@ -297,7 +360,9 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( - DEFAULT_DISPLAY, wct) + DEFAULT_DISPLAY, + wct, + ) assertThat(wct.isEmpty).isTrue() } @@ -307,7 +372,9 @@ class DesktopTasksLimiterTest : ShellTestCase() { fun removeLeftoverMinimizedTasks_noMinimizedTasks_doesNothing() { val wct = WindowContainerTransaction() desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( - DEFAULT_DISPLAY, wct) + DEFAULT_DISPLAY, + wct, + ) assertThat(wct.isEmpty).isTrue() } @@ -322,7 +389,9 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() desktopTasksLimiter.leftoverMinimizedTasksRemover.removeLeftoverMinimizedTasks( - DEFAULT_DISPLAY, wct) + DEFAULT_DISPLAY, + wct, + ) assertThat(wct.hierarchyOps).hasSize(2) assertThat(wct.hierarchyOps[0].type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) @@ -351,10 +420,11 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, - wct = wct, - newFrontTaskId = setUpFreeformTask().taskId) + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) assertThat(minimizedTaskId).isNull() assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added @@ -367,10 +437,11 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = DEFAULT_DISPLAY, - wct = wct, - newFrontTaskId = setUpFreeformTask().taskId) + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + displayId = DEFAULT_DISPLAY, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) assertThat(minimizedTaskId).isEqualTo(tasks.first().taskId) assertThat(wct.hierarchyOps.size).isEqualTo(1) @@ -385,10 +456,11 @@ class DesktopTasksLimiterTest : ShellTestCase() { val wct = WindowContainerTransaction() val minimizedTaskId = - desktopTasksLimiter.addAndGetMinimizeTaskChanges( - displayId = 0, - wct = wct, - newFrontTaskId = setUpFreeformTask().taskId) + desktopTasksLimiter.addAndGetMinimizeTaskChanges( + displayId = 0, + wct = wct, + newFrontTaskId = setUpFreeformTask().taskId, + ) assertThat(minimizedTaskId).isNull() assertThat(wct.hierarchyOps).isEmpty() // No reordering operations added @@ -398,8 +470,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { fun getTaskToMinimize_tasksWithinLimit_returnsNull() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( - visibleOrderedTasks = tasks.map { it.taskId }) + val minimizedTask = + desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId }) assertThat(minimizedTask).isNull() } @@ -408,8 +480,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { fun getTaskToMinimize_tasksAboveLimit_returnsBackTask() { val tasks = (1..MAX_TASK_LIMIT + 1).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( - visibleOrderedTasks = tasks.map { it.taskId }) + val minimizedTask = + desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId }) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last().taskId) @@ -418,12 +490,19 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAboveLimit_otherLimit_returnsBackTask() { desktopTasksLimiter = - DesktopTasksLimiter(transitions, userRepositories, shellTaskOrganizer, MAX_TASK_LIMIT2, - interactionJankMonitor, mContext, handler) + DesktopTasksLimiter( + transitions, + userRepositories, + shellTaskOrganizer, + MAX_TASK_LIMIT2, + interactionJankMonitor, + mContext, + handler, + ) val tasks = (1..MAX_TASK_LIMIT2 + 1).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( - visibleOrderedTasks = tasks.map { it.taskId }) + val minimizedTask = + desktopTasksLimiter.getTaskIdToMinimize(visibleOrderedTasks = tasks.map { it.taskId }) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last().taskId) @@ -433,9 +512,11 @@ class DesktopTasksLimiterTest : ShellTestCase() { fun getTaskToMinimize_withNewTask_tasksAboveLimit_returnsBackTask() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( + val minimizedTask = + desktopTasksLimiter.getTaskIdToMinimize( visibleOrderedTasks = tasks.map { it.taskId }, - newTaskIdInFront = setUpFreeformTask().taskId) + newTaskIdInFront = setUpFreeformTask().taskId, + ) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last().taskId) @@ -444,10 +525,12 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test fun getTaskToMinimize_tasksAtLimit_newIntentReturnsBackTask() { val tasks = (1..MAX_TASK_LIMIT).map { setUpFreeformTask() } - val minimizedTask = desktopTasksLimiter.getTaskIdToMinimize( - visibleOrderedTasks = tasks.map { it.taskId }, - newTaskIdInFront = null, - launchingNewIntent = true) + val minimizedTask = + desktopTasksLimiter.getTaskIdToMinimize( + visibleOrderedTasks = tasks.map { it.taskId }, + newTaskIdInFront = null, + launchingNewIntent = true, + ) // first == front, last == back assertThat(minimizedTask).isEqualTo(tasks.last().taskId) @@ -459,25 +542,28 @@ class DesktopTasksLimiterTest : ShellTestCase() { val transition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) - - desktopTasksLimiter.getTransitionObserver().onTransitionReady( transition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) + + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition) - verify(interactionJankMonitor).begin( - any(), - eq(mContext), - eq(handler), - eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + verify(interactionJankMonitor) + .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) - desktopTasksLimiter.getTransitionObserver().onTransitionFinished( - transition, - /* aborted = */ false) + desktopTasksLimiter + .getTransitionObserver() + .onTransitionFinished(transition, /* aborted= */ false) verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) } @@ -488,26 +574,28 @@ class DesktopTasksLimiterTest : ShellTestCase() { val transition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - transition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) - - desktopTasksLimiter.getTransitionObserver().onTransitionReady( transition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) + + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + transition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) desktopTasksLimiter.getTransitionObserver().onTransitionStarting(transition) - verify(interactionJankMonitor).begin( - any(), - eq(mContext), - eq(handler), - eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW), - ) + verify(interactionJankMonitor) + .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) - desktopTasksLimiter.getTransitionObserver().onTransitionFinished( - transition, - /* aborted = */ true) + desktopTasksLimiter + .getTransitionObserver() + .onTransitionFinished(transition, /* aborted= */ true) verify(interactionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) } @@ -519,25 +607,28 @@ class DesktopTasksLimiterTest : ShellTestCase() { val newTransition = Binder() val task = setUpFreeformTask() desktopTasksLimiter.addPendingMinimizeChange( - mergedTransition, displayId = DEFAULT_DISPLAY, taskId = task.taskId) - - desktopTasksLimiter.getTransitionObserver().onTransitionReady( mergedTransition, - TransitionInfoBuilder(TRANSIT_OPEN).build(), - StubTransaction() /* startTransaction */, - StubTransaction() /* finishTransaction */) + displayId = DEFAULT_DISPLAY, + taskId = task.taskId, + ) + + desktopTasksLimiter + .getTransitionObserver() + .onTransitionReady( + mergedTransition, + TransitionInfoBuilder(TRANSIT_OPEN).build(), + StubTransaction() /* startTransaction */, + StubTransaction(), /* finishTransaction */ + ) desktopTasksLimiter.getTransitionObserver().onTransitionStarting(mergedTransition) - verify(interactionJankMonitor).begin( - any(), - eq(mContext), - eq(handler), - eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) + verify(interactionJankMonitor) + .begin(any(), eq(mContext), eq(handler), eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) - desktopTasksLimiter.getTransitionObserver().onTransitionMerged( - mergedTransition, - newTransition) + desktopTasksLimiter + .getTransitionObserver() + .onTransitionMerged(mergedTransition, newTransition) verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) } @@ -550,19 +641,11 @@ class DesktopTasksLimiterTest : ShellTestCase() { } private fun markTaskVisible(task: RunningTaskInfo) { - desktopTaskRepo.updateTask( - task.displayId, - task.taskId, - isVisible = true - ) + desktopTaskRepo.updateTask(task.displayId, task.taskId, isVisible = true) } private fun markTaskHidden(task: RunningTaskInfo) { - desktopTaskRepo.updateTask( - task.displayId, - task.taskId, - isVisible = false - ) + desktopTaskRepo.updateTask(task.displayId, task.taskId, isVisible = false) } private companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt index b31a3f5fa642..c9623bcd5c16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserverTest.kt @@ -99,7 +99,7 @@ class DesktopTasksTransitionObserverTest { shellTaskOrganizer, mixedHandler, backAnimationController, - shellInit + shellInit, ) } @@ -139,7 +139,10 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).minimizeTask(task.displayId, task.taskId) val pendingTransition = DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( - transition, task.taskId, isLastTask = false) + transition, + task.taskId, + isLastTask = false, + ) verify(mixedHandler).addPendingMixedTransition(pendingTransition) } @@ -162,7 +165,10 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).minimizeTask(task.displayId, task.taskId) val pendingTransition = DesktopMixedTransitionHandler.PendingMixedTransition.Minimize( - transition, task.taskId, isLastTask = true) + transition, + task.taskId, + isLastTask = true, + ) verify(mixedHandler).addPendingMixedTransition(pendingTransition) } @@ -251,7 +257,8 @@ class DesktopTasksTransitionObserverTest { parent = null taskInfo = task flags = flags - }) + } + ) if (withWallpaper) { addChange( Change(mock(), mock()).apply { @@ -259,14 +266,15 @@ class DesktopTasksTransitionObserverTest { parent = null taskInfo = createWallpaperTaskInfo() flags = flags - }) + } + ) } } } private fun createOpenChangeTransition( task: RunningTaskInfo?, - type: Int = TRANSIT_OPEN + type: Int = TRANSIT_OPEN, ): TransitionInfo { return TransitionInfo(TRANSIT_OPEN, 0 /* flags */).apply { addChange( @@ -275,7 +283,8 @@ class DesktopTasksTransitionObserverTest { parent = null taskInfo = task flags = flags - }) + } + ) } } @@ -287,13 +296,14 @@ class DesktopTasksTransitionObserverTest { parent = null taskInfo = task flags = flags - }) + } + ) } } private fun getLatestWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN, - handlerClass: Class<out Transitions.TransitionHandler>? = null + handlerClass: Class<out Transitions.TransitionHandler>? = null, ): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) if (handlerClass == null) { @@ -330,8 +340,6 @@ class DesktopTasksTransitionObserverTest { RunningTaskInfo().apply { token = mock<WindowContainerToken>() baseIntent = - Intent().apply { - component = DesktopWallpaperActivity.wallpaperActivityComponent - } + Intent().apply { component = DesktopWallpaperActivity.wallpaperActivityComponent } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt index a2e939d86adb..b9e307fa5973 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopUserRepositoriesTest.kt @@ -82,20 +82,20 @@ class DesktopUserRepositoriesTest : ShellTestCase() { datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - val profiles: MutableList<UserInfo> = mutableListOf( - UserInfo(USER_ID_1, "User 1", 0), - UserInfo(PROFILE_ID_2, "Profile 2", 0)) + val profiles: MutableList<UserInfo> = + mutableListOf(UserInfo(USER_ID_1, "User 1", 0), UserInfo(PROFILE_ID_2, "Profile 2", 0)) whenever(userManager.getProfiles(USER_ID_1)).thenReturn(profiles) - userRepositories = DesktopUserRepositories( - context, - shellInit, - shellController, - persistentRepository, - repositoryInitializer, - datastoreScope, - userManager - ) + userRepositories = + DesktopUserRepositories( + context, + shellInit, + shellController, + persistentRepository, + repositoryInitializer, + datastoreScope, + userManager, + ) } @After diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt index 13528b947609..e4eff9f1d592 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandlerTest.kt @@ -61,9 +61,7 @@ import org.mockito.quality.Strictness @RunWithLooper @RunWith(AndroidTestingRunner::class) class DragToDesktopTransitionHandlerTest : ShellTestCase() { - @JvmField - @Rule - val mAnimatorTestRule = AnimatorTestRule(this) + @JvmField @Rule val mAnimatorTestRule = AnimatorTestRule(this) @Mock private lateinit var transitions: Transitions @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @@ -123,11 +121,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task + draggedTask = task, ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) verify(dragAnimator).startAnimation() @@ -137,13 +135,13 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun startDragToDesktop_cancelledBeforeReady_startCancelTransition() { performEarlyCancel( defaultHandler, - DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL, ) verify(transitions) .startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(defaultHandler) + eq(defaultHandler), ) } @@ -151,7 +149,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun startDragToDesktop_cancelledBeforeReady_verifySplitLeftCancel() { performEarlyCancel( defaultHandler, - DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_LEFT, ) verify(splitScreenController) .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_TOP_OR_LEFT), any()) @@ -161,7 +159,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun startDragToDesktop_cancelledBeforeReady_verifySplitRightCancel() { performEarlyCancel( defaultHandler, - DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT + DragToDesktopTransitionHandler.CancelState.CANCEL_SPLIT_RIGHT, ) verify(splitScreenController) .requestEnterSplitSelect(any(), any(), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), any()) @@ -214,7 +212,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .startTransition( eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), any(), - eq(defaultHandler) + eq(defaultHandler), ) } @@ -277,14 +275,18 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startToken = startDrag(defaultHandler) // Then user cancelled after it had already started. - val cancelToken = cancelDragToDesktopTransition( - defaultHandler, DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + val cancelToken = + cancelDragToDesktopTransition( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL, + ) defaultHandler.mergeAnimation( cancelToken, TransitionInfo(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, 0), mock<SurfaceControl.Transaction>(), startToken, - mock<Transitions.TransitionFinishCallback>()) + mock<Transitions.TransitionFinishCallback>(), + ) // Cancel animation should run since it had already started. verify(dragAnimator).cancelAnimator() @@ -296,8 +298,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { val startToken = startDrag(defaultHandler) // Then user cancelled after it had already started. - val cancelToken = cancelDragToDesktopTransition( - defaultHandler, DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL) + val cancelToken = + cancelDragToDesktopTransition( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.STANDARD_CANCEL, + ) defaultHandler.onTransitionConsumed(cancelToken, aborted = true, null) // Cancel animation should run since it had already started. @@ -360,7 +365,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(defaultHandler) + eq(defaultHandler), ) } @@ -374,7 +379,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { .startTransition( eq(TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP), any(), - eq(defaultHandler) + eq(defaultHandler), ) } @@ -390,7 +395,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), t = transaction, mergeTarget = mock(), - finishCallback = finishCallback + finishCallback = finishCallback, ) // Should NOT have any transaction changes @@ -414,11 +419,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task + draggedTask = task, ), t = mergedStartTransaction, mergeTarget = startTransition, - finishCallback = finishCallback + finishCallback = finishCallback, ) // Should show dragged task layer in start and finish transaction @@ -446,11 +451,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task + draggedTask = task, ), t = mergedStartTransaction, mergeTarget = startTransition, - finishCallback = finishCallback + finishCallback = finishCallback, ) // Should show dragged task layer in start and finish transaction @@ -475,7 +480,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { assertEquals( "Expects to return system properties stored value", /* expected= */ value, - /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name) + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name), ) } @@ -491,7 +496,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { assertEquals( "Expects to return scaled system properties stored value", /* expected= */ value / scale, - /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale) + /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue(name, scale = scale), ) } @@ -508,8 +513,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { /* expected= */ defaultValue, /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( name, - default = defaultValue - ) + default = defaultValue, + ), ) } @@ -530,8 +535,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { /* actual= */ SpringDragToDesktopTransitionHandler.propertyValue( name, default = defaultValue, - scale = scale - ) + scale = scale, + ), ) } @@ -542,8 +547,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler.onTransitionConsumed(transition, aborted = true, mock()) verify(mockInteractionJankMonitor).cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_HOLD)) - verify(mockInteractionJankMonitor, times(0)).cancel( - eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)) + verify(mockInteractionJankMonitor, times(0)) + .cancel(eq(CUJ_DESKTOP_MODE_ENTER_APP_HANDLE_DRAG_RELEASE)) } @Test @@ -554,13 +559,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler.onTaskResizeAnimationListener = mock() defaultHandler.mergeAnimation( transition = endTransition, - info = createTransitionInfo( - type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, - draggedTask = task - ), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), t = mock<SurfaceControl.Transaction>(), mergeTarget = startTransition, - finishCallback = mock<Transitions.TransitionFinishCallback>() + finishCallback = mock<Transitions.TransitionFinishCallback>(), ) defaultHandler.onTransitionConsumed(endTransition, aborted = true, mock()) @@ -574,7 +580,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun startDrag( handler: DragToDesktopTransitionHandler, task: RunningTaskInfo = createTask(), - finishTransaction: SurfaceControl.Transaction = mock() + finishTransaction: SurfaceControl.Transaction = mock(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -584,11 +590,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task + draggedTask = task, ), startTransaction = mock(), finishTransaction = finishTransaction, - finishCallback = {} + finishCallback = {}, ) return transition } @@ -596,14 +602,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun startDragToDesktopTransition( handler: DragToDesktopTransitionHandler, task: RunningTaskInfo, - dragAnimator: MoveToDesktopAnimator + dragAnimator: MoveToDesktopAnimator, ): IBinder { val token = mock<IBinder>() whenever( transitions.startTransition( eq(TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP), any(), - eq(handler) + eq(handler), ) ) .thenReturn(token) @@ -613,13 +619,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun cancelDragToDesktopTransition( handler: DragToDesktopTransitionHandler, - cancelState: DragToDesktopTransitionHandler.CancelState): IBinder { + cancelState: DragToDesktopTransitionHandler.CancelState, + ): IBinder { val token = mock<IBinder>() whenever( transitions.startTransition( eq(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP), any(), - eq(handler) + eq(handler), ) ) .thenReturn(token) @@ -630,7 +637,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { private fun performEarlyCancel( handler: DragToDesktopTransitionHandler, - cancelState: DragToDesktopTransitionHandler.CancelState + cancelState: DragToDesktopTransitionHandler.CancelState, ) { val task = createTask() // Simulate transition is started and is ready to animate. @@ -643,11 +650,11 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, - draggedTask = task + draggedTask = task, ), startTransaction = mock(), finishTransaction = mock(), - finishCallback = {} + finishCallback = {}, ) // Don't even animate the "drag" since it was already cancelled. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt index 38c6ed90241c..e10253992bb5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/WindowDecorCaptionHandleRepositoryTest.kt @@ -31,67 +31,71 @@ import org.junit.runner.RunWith @SmallTest @RunWith(AndroidTestingRunner::class) class WindowDecorCaptionHandleRepositoryTest { - private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository + private lateinit var captionHandleRepository: WindowDecorCaptionHandleRepository - @Before - fun setUp() { - captionHandleRepository = WindowDecorCaptionHandleRepository() - } + @Before + fun setUp() { + captionHandleRepository = WindowDecorCaptionHandleRepository() + } - @Test - fun initialState_noAction_returnsNoCaption() { - // Check the initial value of `captionStateFlow`. - assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) - } + @Test + fun initialState_noAction_returnsNoCaption() { + // Check the initial value of `captionStateFlow`. + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } - @Test - fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() { - val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME) - val appHandleCaptionState = - CaptionState.AppHandle( - runningTaskInfo = taskInfo, - isHandleMenuExpanded = false, - globalAppHandleBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), - isCapturedLinkAvailable = false) + @Test + fun notifyCaptionChange_toAppHandleVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FULLSCREEN, GMAIL_PACKAGE_NAME) + val appHandleCaptionState = + CaptionState.AppHandle( + runningTaskInfo = taskInfo, + isHandleMenuExpanded = false, + globalAppHandleBounds = + Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), + isCapturedLinkAvailable = false, + ) - captionHandleRepository.notifyCaptionChanged(appHandleCaptionState) + captionHandleRepository.notifyCaptionChanged(appHandleCaptionState) - assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState) - } + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHandleCaptionState) + } - @Test - fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() { - val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME) - val appHeaderCaptionState = - CaptionState.AppHeader( - runningTaskInfo = taskInfo, - isHeaderMenuExpanded = true, - globalAppChipBounds = Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), - isCapturedLinkAvailable = false) + @Test + fun notifyCaptionChange_toAppChipVisible_updatesStateWithCorrectData() { + val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM, GMAIL_PACKAGE_NAME) + val appHeaderCaptionState = + CaptionState.AppHeader( + runningTaskInfo = taskInfo, + isHeaderMenuExpanded = true, + globalAppChipBounds = + Rect(/* left= */ 0, /* top= */ 1, /* right= */ 2, /* bottom= */ 3), + isCapturedLinkAvailable = false, + ) - captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState) + captionHandleRepository.notifyCaptionChanged(appHeaderCaptionState) - assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState) - } + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(appHeaderCaptionState) + } - @Test - fun notifyCaptionChange_toNoCaption_updatesState() { - captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption) + @Test + fun notifyCaptionChange_toNoCaption_updatesState() { + captionHandleRepository.notifyCaptionChanged(CaptionState.NoCaption) - assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) - } + assertThat(captionHandleRepository.captionStateFlow.value).isEqualTo(CaptionState.NoCaption) + } - private fun createTaskInfo( - deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED, - runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME - ): RunningTaskInfo = - RunningTaskInfo().apply { - configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode } - topActivityInfo?.apply { packageName = runningTaskPackageName } - } + private fun createTaskInfo( + deviceWindowingMode: Int = WINDOWING_MODE_UNDEFINED, + runningTaskPackageName: String = LAUNCHER_PACKAGE_NAME, + ): RunningTaskInfo = + RunningTaskInfo().apply { + configuration.windowConfiguration.apply { windowingMode = deviceWindowingMode } + topActivityInfo?.apply { packageName = runningTaskPackageName } + } - private companion object { - const val GMAIL_PACKAGE_NAME = "com.google.android.gm" - const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" - } + private companion object { + const val GMAIL_PACKAGE_NAME = "com.google.android.gm" + const val LAUNCHER_PACKAGE_NAME = "com.google.android.apps.nexuslauncher" + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt index c33005e7cfcc..1569f9dc9b10 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/compatui/SystemModalsTransitionHandlerTest.kt @@ -25,10 +25,10 @@ import android.view.WindowManager.TRANSIT_OPEN import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTaskBuilder import com.android.wm.shell.desktopmode.DesktopTestHelpers.createSystemModalTask -import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder @@ -44,8 +44,8 @@ import org.mockito.kotlin.verify import org.mockito.kotlin.whenever /** - * Tests for {@link SystemModalsTransitionHandler} - * Usage: atest WMShellUnitTests:SystemModalsTransitionHandlerTest + * Tests for {@link SystemModalsTransitionHandler} Usage: atest + * WMShellUnitTests:SystemModalsTransitionHandlerTest */ @SmallTest @RunWith(AndroidTestingRunner::class) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt index 9c00c0cee8b1..5475032f35a9 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationControllerTest.kt @@ -69,431 +69,448 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @OptIn(ExperimentalCoroutinesApi::class) class AppHandleEducationControllerTest : ShellTestCase() { - @JvmField - @Rule - val extendedMockitoRule = - ExtendedMockitoRule.Builder(this) - .mockStatic(DesktopModeStatus::class.java) - .mockStatic(SystemProperties::class.java) - .build()!! - @JvmField @Rule val setFlagsRule = SetFlagsRule() - - private lateinit var educationController: AppHandleEducationController - private lateinit var testableContext: TestableContext - private val testScope = TestScope() - private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto()) - private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) - private val educationConfigCaptor = - argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>() - @Mock private lateinit var mockEducationFilter: AppHandleEducationFilter - @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository - @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository - @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController - - @Before - fun setUp() { - MockitoAnnotations.initMocks(this) - Dispatchers.setMain(StandardTestDispatcher(testScope.testScheduler)) - testableContext = TestableContext(mContext) - whenever(mockDataStoreRepository.dataStoreFlow).thenReturn(testDataStoreFlow) - whenever(mockCaptionHandleRepository.captionStateFlow).thenReturn(testCaptionStateFlow) - whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) - - educationController = - AppHandleEducationController( - testableContext, - mockEducationFilter, - mockDataStoreRepository, - mockCaptionHandleRepository, - mockTooltipController, - testScope.backgroundScope, - Dispatchers.Main) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleVisible_shouldCallShowEducationTooltip() = - testScope.runTest { - // App handle is visible. Should show education tooltip. + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this) + .mockStatic(DesktopModeStatus::class.java) + .mockStatic(SystemProperties::class.java) + .build()!! + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private lateinit var educationController: AppHandleEducationController + private lateinit var testableContext: TestableContext + private val testScope = TestScope() + private val testDataStoreFlow = MutableStateFlow(createWindowingEducationProto()) + private val testCaptionStateFlow = MutableStateFlow<CaptionState>(CaptionState.NoCaption) + private val educationConfigCaptor = + argumentCaptor<DesktopWindowingEducationTooltipController.TooltipEducationViewConfig>() + @Mock private lateinit var mockEducationFilter: AppHandleEducationFilter + @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository + @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository + @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController + + @Before + fun setUp() { + MockitoAnnotations.initMocks(this) + Dispatchers.setMain(StandardTestDispatcher(testScope.testScheduler)) + testableContext = TestableContext(mContext) + whenever(mockDataStoreRepository.dataStoreFlow).thenReturn(testDataStoreFlow) + whenever(mockCaptionHandleRepository.captionStateFlow).thenReturn(testCaptionStateFlow) + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(true) + + educationController = + AppHandleEducationController( + testableContext, + mockEducationFilter, + mockDataStoreRepository, + mockCaptionHandleRepository, + mockTooltipController, + testScope.backgroundScope, + Dispatchers.Main, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleVisible_shouldCallShowEducationTooltip() = + testScope.runTest { + // App handle is visible. Should show education tooltip. + setShouldShowAppHandleEducation(true) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_flagDisabled_shouldNotCallShowEducationTooltip() = + testScope.runTest { + // App handle visible but education aconfig flag disabled, should not show education + // tooltip. + whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) + setShouldShowAppHandleEducation(true) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() = + testScope.runTest { + // App handle is visible but [shouldShowAppHandleEducation] api returns false, should + // not + // show education tooltip. + setShouldShowAppHandleEducation(false) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() = + testScope.runTest { + // App handle is not visible, should not show education tooltip. + setShouldShowAppHandleEducation(true) + + // Simulate app handle is not visible. + testCaptionStateFlow.value = CaptionState.NoCaption + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() = + testScope.runTest { + // App handle is visible but app handle hint has been viewed before, + // should not show education tooltip. + // Mark app handle hint viewed. + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + setShouldShowAppHandleEducation(true) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, never()).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = + testScope.runTest { + // App handle is visible but app handle hint has been viewed before. + // But as we are overriding prerequisite conditions, we should show app + // handle tooltip. + // Mark app handle hint viewed. + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + val systemPropertiesKey = + "persist.desktop_windowing_app_handle_education_override_conditions" + whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + .thenReturn(true) + setShouldShowAppHandleEducation(true) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() = + testScope.runTest { + setShouldShowAppHandleEducation(false) + + // Simulate app handle visible and expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for some time before verifying + waitForBufferDelay() + + verify(mockDataStoreRepository, times(1)) + .updateAppHandleHintUsedTimestampMillis(eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() = + testScope.runTest { + // App handle is visible. Should show education tooltip. + setShouldShowAppHandleEducation(true) + + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockDataStoreRepository, times(1)) + .updateAppHandleHintViewedTimestampMillis(eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() = + testScope.runTest { + // After first tooltip is dismissed, app handle is expanded. Should show second + // education + // tooltip. + showAndDismissFirstTooltip() + + // Simulate app handle expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + + // [showEducationTooltip] should be called twice, once for each tooltip. + verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() = + testScope.runTest { + // After first tooltip is dismissed, app handle is expanded after timeout. Should not + // show + // second education tooltip. + showAndDismissFirstTooltip() + + // Wait for timeout to occur, after this timeout we should not listen for further + // triggers + // anymore. + advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) + runCurrent() + // Simulate app handle expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + + // [showEducationTooltip] should be called once, just for the first tooltip. + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() = + testScope.runTest { + // After first tooltip is dismissed, app handle is expanded twice. Should show second + // education tooltip only once. + showAndDismissFirstTooltip() + + // Simulate app handle expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + // Simulate app handle being expanded twice. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + waitForBufferDelay() + + // [showEducationTooltip] should not be called thrice, even if app handle was expanded + // twice. Should be called twice, once for each tooltip. + verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() = + testScope.runTest { + // After first tooltip is dismissed, app handle is not expanded. Should not show second + // education tooltip. + showAndDismissFirstTooltip() + + // Simulate app handle visible but not expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) + // Wait for next tooltip to showup. + waitForBufferDelay() + + // [showEducationTooltip] should be called once, just for the first tooltip. + verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() = + testScope.runTest { + // After first two tooltips are dismissed, app header is visible. Should show third + // education tooltip. + showAndDismissFirstTooltip() + showAndDismissSecondTooltip() + + // Simulate app header visible. + testCaptionStateFlow.value = createAppHeaderState() + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() = + testScope.runTest { + // After first two tooltips are dismissed, app header is visible after timeout. Should + // not + // show third education tooltip. + showAndDismissFirstTooltip() + showAndDismissSecondTooltip() + + // Wait for timeout to occur, after this timeout we should not listen for further + // triggers + // anymore. + advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) + runCurrent() + // Simulate app header visible. + testCaptionStateFlow.value = createAppHeaderState() + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() = + testScope.runTest { + // After first two tooltips are dismissed, app header is visible twice. Should show + // third + // education tooltip only once. + showAndDismissFirstTooltip() + showAndDismissSecondTooltip() + + // Simulate app header visible. + testCaptionStateFlow.value = createAppHeaderState() + // Wait for next tooltip to showup. + waitForBufferDelay() + testCaptionStateFlow.value = createAppHeaderState() + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() = + testScope.runTest { + // After first two tooltips are dismissed, app header is visible but expanded. Should + // not + // show third education tooltip. + showAndDismissFirstTooltip() + showAndDismissSecondTooltip() + + // Simulate app header visible. + testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = + testScope.runTest { + // App handle is visible. Should show education tooltip. + setShouldShowAppHandleEducation(true) + val mockOpenHandleMenuCallback: (Int) -> Unit = mock() + val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() + educationController.setAppHandleEducationTooltipCallbacks( + mockOpenHandleMenuCallback, + mockToDesktopModeCallback, + ) + // Simulate app handle visible. + testCaptionStateFlow.value = createAppHandleState() + // Wait for first tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, atLeastOnce()) + .showEducationTooltip(educationConfigCaptor.capture(), any()) + educationConfigCaptor.lastValue.onEducationClickAction.invoke() + + verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + @Ignore("b/371527084: revisit testcase after refactoring original logic") + fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = + testScope.runTest { + // After first tooltip is dismissed, app handle is expanded. Should show second + // education + // tooltip. + showAndDismissFirstTooltip() + val mockOpenHandleMenuCallback: (Int) -> Unit = mock() + val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() + educationController.setAppHandleEducationTooltipCallbacks( + mockOpenHandleMenuCallback, + mockToDesktopModeCallback, + ) + // Simulate app handle expanded. + testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) + // Wait for next tooltip to showup. + waitForBufferDelay() + + verify(mockTooltipController, atLeastOnce()) + .showEducationTooltip(educationConfigCaptor.capture(), any()) + educationConfigCaptor.lastValue.onEducationClickAction.invoke() + + verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) + } + + private suspend fun TestScope.showAndDismissFirstTooltip() { setShouldShowAppHandleEducation(true) - - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_flagDisabled_shouldNotCallShowEducationTooltip() = - testScope.runTest { - // App handle visible but education aconfig flag disabled, should not show education - // tooltip. - whenever(DesktopModeStatus.canEnterDesktopMode(any())).thenReturn(false) - setShouldShowAppHandleEducation(true) - - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, never()).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_shouldShowAppHandleEducationReturnsFalse_shouldNotCallShowEducationTooltip() = - testScope.runTest { - // App handle is visible but [shouldShowAppHandleEducation] api returns false, should not - // show education tooltip. - setShouldShowAppHandleEducation(false) - - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, never()).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleNotVisible_shouldNotCallShowEducationTooltip() = - testScope.runTest { - // App handle is not visible, should not show education tooltip. - setShouldShowAppHandleEducation(true) - - // Simulate app handle is not visible. - testCaptionStateFlow.value = CaptionState.NoCaption - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, never()).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleHintViewedAlready_shouldNotCallShowEducationTooltip() = - testScope.runTest { - // App handle is visible but app handle hint has been viewed before, - // should not show education tooltip. - // Mark app handle hint viewed. - testDataStoreFlow.value = - createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - setShouldShowAppHandleEducation(true) - - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, never()).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun overridePrerequisite_appHandleHintViewedAlready_shouldCallShowEducationTooltip() = - testScope.runTest { - // App handle is visible but app handle hint has been viewed before. - // But as we are overriding prerequisite conditions, we should show app - // handle tooltip. - // Mark app handle hint viewed. - testDataStoreFlow.value = - createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - val systemPropertiesKey = - "persist.desktop_windowing_app_handle_education_override_conditions" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) - .thenReturn(true) - setShouldShowAppHandleEducation(true) - // Simulate app handle visible. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) // Wait for first tooltip to showup. waitForBufferDelay() - - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_appHandleExpanded_shouldMarkAppHandleHintUsed() = - testScope.runTest { + // [shouldShowAppHandleEducation] should return false as education has been viewed + // before. setShouldShowAppHandleEducation(false) + // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. + captureAndInvokeOnDismissAction() + } - // Simulate app handle visible and expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for some time before verifying - waitForBufferDelay() - - verify(mockDataStoreRepository, times(1)).updateAppHandleHintUsedTimestampMillis(eq(true)) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun init_showFirstTooltip_shouldMarkAppHandleHintViewed() = - testScope.runTest { - // App handle is visible. Should show education tooltip. - setShouldShowAppHandleEducation(true) - - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockDataStoreRepository, times(1)).updateAppHandleHintViewedTimestampMillis(eq(true)) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpanded_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded. Should show second education - // tooltip. - showAndDismissFirstTooltip() - - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - - // [showEducationTooltip] should be called twice, once for each tooltip. - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpandedAfterTimeout_shouldCallShowEducationTooltipOnce() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded after timeout. Should not show - // second education tooltip. - showAndDismissFirstTooltip() - - // Wait for timeout to occur, after this timeout we should not listen for further triggers - // anymore. - advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) - runCurrent() - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - - // [showEducationTooltip] should be called once, just for the first tooltip. - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleExpandedTwice_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded twice. Should show second - // education tooltip only once. - showAndDismissFirstTooltip() - - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - // Simulate app handle being expanded twice. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - waitForBufferDelay() - - // [showEducationTooltip] should not be called thrice, even if app handle was expanded - // twice. Should be called twice, once for each tooltip. - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showWindowingImageButtonTooltip_appHandleNotExpanded_shouldCallShowEducationTooltipOnce() = - testScope.runTest { - // After first tooltip is dismissed, app handle is not expanded. Should not show second - // education tooltip. - showAndDismissFirstTooltip() - - // Simulate app handle visible but not expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for next tooltip to showup. - waitForBufferDelay() - - // [showEducationTooltip] should be called once, just for the first tooltip. - verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisible_shouldCallShowEducationTooltipThrice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible. Should show third - // education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisibleAfterTimeout_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible after timeout. Should not - // show third education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Wait for timeout to occur, after this timeout we should not listen for further triggers - // anymore. - advanceTimeBy(APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS) - runCurrent() - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderVisibleTwice_shouldCallShowEducationTooltipThrice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible twice. Should show third - // education tooltip only once. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - testCaptionStateFlow.value = createAppHeaderState() - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(3)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun showExitWindowingButtonTooltip_appHeaderExpanded_shouldCallShowEducationTooltipTwice() = - testScope.runTest { - // After first two tooltips are dismissed, app header is visible but expanded. Should not - // show third education tooltip. - showAndDismissFirstTooltip() - showAndDismissSecondTooltip() - - // Simulate app header visible. - testCaptionStateFlow.value = createAppHeaderState(isHeaderMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, times(2)).showEducationTooltip(any(), any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - fun setAppHandleEducationTooltipCallbacks_onAppHandleTooltipClicked_callbackInvoked() = - testScope.runTest { - // App handle is visible. Should show education tooltip. - setShouldShowAppHandleEducation(true) - val mockOpenHandleMenuCallback: (Int) -> Unit = mock() - val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() - educationController.setAppHandleEducationTooltipCallbacks( - mockOpenHandleMenuCallback, mockToDesktopModeCallback) - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState() - // Wait for first tooltip to showup. - waitForBufferDelay() - - verify(mockTooltipController, atLeastOnce()) - .showEducationTooltip(educationConfigCaptor.capture(), any()) - educationConfigCaptor.lastValue.onEducationClickAction.invoke() - - verify(mockOpenHandleMenuCallback, times(1)).invoke(any()) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) - @Ignore("b/371527084: revisit testcase after refactoring original logic") - fun setAppHandleEducationTooltipCallbacks_onWindowingImageButtonTooltipClicked_callbackInvoked() = - testScope.runTest { - // After first tooltip is dismissed, app handle is expanded. Should show second education - // tooltip. - showAndDismissFirstTooltip() - val mockOpenHandleMenuCallback: (Int) -> Unit = mock() - val mockToDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit = mock() - educationController.setAppHandleEducationTooltipCallbacks( - mockOpenHandleMenuCallback, mockToDesktopModeCallback) + private fun TestScope.showAndDismissSecondTooltip() { // Simulate app handle expanded. testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) // Wait for next tooltip to showup. waitForBufferDelay() + // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. + captureAndInvokeOnDismissAction() + } + private fun captureAndInvokeOnDismissAction() { verify(mockTooltipController, atLeastOnce()) .showEducationTooltip(educationConfigCaptor.capture(), any()) - educationConfigCaptor.lastValue.onEducationClickAction.invoke() - - verify(mockToDesktopModeCallback, times(1)).invoke(any(), any()) - } - - private suspend fun TestScope.showAndDismissFirstTooltip() { - setShouldShowAppHandleEducation(true) - // Simulate app handle visible. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = false) - // Wait for first tooltip to showup. - waitForBufferDelay() - // [shouldShowAppHandleEducation] should return false as education has been viewed - // before. - setShouldShowAppHandleEducation(false) - // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. - captureAndInvokeOnDismissAction() - } - - private fun TestScope.showAndDismissSecondTooltip() { - // Simulate app handle expanded. - testCaptionStateFlow.value = createAppHandleState(isHandleMenuExpanded = true) - // Wait for next tooltip to showup. - waitForBufferDelay() - // Dismiss previous tooltip, after this we should listen for next tooltip's trigger. - captureAndInvokeOnDismissAction() - } - - private fun captureAndInvokeOnDismissAction() { - verify(mockTooltipController, atLeastOnce()) - .showEducationTooltip(educationConfigCaptor.capture(), any()) - educationConfigCaptor.lastValue.onDismissAction.invoke() - } - - private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) = - whenever(mockEducationFilter.shouldShowAppHandleEducation(any())) - .thenReturn(shouldShowAppHandleEducation) - - /** - * Class under test waits for some time before showing education, simulate advance time before - * verifying or moving forward - */ - private fun TestScope.waitForBufferDelay() { - advanceTimeBy(APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS) - runCurrent() - } - - private companion object { - val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L - val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long = - APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L - } + educationConfigCaptor.lastValue.onDismissAction.invoke() + } + + private suspend fun setShouldShowAppHandleEducation(shouldShowAppHandleEducation: Boolean) = + whenever(mockEducationFilter.shouldShowAppHandleEducation(any())) + .thenReturn(shouldShowAppHandleEducation) + + /** + * Class under test waits for some time before showing education, simulate advance time before + * verifying or moving forward + */ + private fun TestScope.waitForBufferDelay() { + advanceTimeBy(APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS) + runCurrent() + } + + private companion object { + val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = + APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L + val APP_HANDLE_EDUCATION_TIMEOUT_BUFFER_MILLIS: Long = + APP_HANDLE_EDUCATION_TIMEOUT_MILLIS + 1000L + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt index 963890d1caa4..4db883d13551 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt @@ -49,85 +49,89 @@ import org.junit.runner.RunWith @RunWith(AndroidTestingRunner::class) @ExperimentalCoroutinesApi class AppHandleEducationDatastoreRepositoryTest { - private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext - private lateinit var testDatastore: DataStore<WindowingEducationProto> - private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository - private lateinit var datastoreScope: CoroutineScope - - @Before - fun setUp() { - Dispatchers.setMain(StandardTestDispatcher()) - datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) - testDatastore = - DataStoreFactory.create( - serializer = - AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer, - scope = datastoreScope) { - testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE) + private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext + private lateinit var testDatastore: DataStore<WindowingEducationProto> + private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository + private lateinit var datastoreScope: CoroutineScope + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + testDatastore = + DataStoreFactory.create( + serializer = + AppHandleEducationDatastoreRepository.Companion + .WindowingEducationProtoSerializer, + scope = datastoreScope, + ) { + testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE) } - datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore) - } - - @After - fun tearDown() { - File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") - .deleteRecursively() - - datastoreScope.cancel() - } - - @Test - fun getWindowingEducationProto_returnsCorrectProto() = - runTest(StandardTestDispatcher()) { - val windowingEducationProto = - createWindowingEducationProto( - appHandleHintViewedTimestampMillis = 123L, - appHandleHintUsedTimestampMillis = 124L, - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), - appUsageStatsLastUpdateTimestampMillis = 125L) - testDatastore.updateData { windowingEducationProto } - - val resultProto = datastoreRepository.windowingEducationProto() - - assertThat(resultProto).isEqualTo(windowingEducationProto) - } - - @Test - fun updateAppUsageStats_updatesDatastoreProto() = - runTest(StandardTestDispatcher()) { - val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3) - val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = appUsageStats, - appUsageStatsLastUpdateTimestampMillis = - appUsageStatsLastUpdateTimestamp.toMillis()) - - datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp) - - val result = testDatastore.data.first() - assertThat(result).isEqualTo(windowingEducationProto) - } - - @Test - fun updateAppHandleHintViewedTimestampMillis_updatesDatastoreProto() = - runTest(StandardTestDispatcher()) { - datastoreRepository.updateAppHandleHintViewedTimestampMillis(true) - - val result = testDatastore.data.first().hasAppHandleHintViewedTimestampMillis() - assertThat(result).isEqualTo(true) - } - - @Test - fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() = - runTest(StandardTestDispatcher()) { - datastoreRepository.updateAppHandleHintUsedTimestampMillis(true) - - val result = testDatastore.data.first().hasAppHandleHintUsedTimestampMillis() - assertThat(result).isEqualTo(true) - } - - companion object { - private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb" - } + datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore) + } + + @After + fun tearDown() { + File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore") + .deleteRecursively() + + datastoreScope.cancel() + } + + @Test + fun getWindowingEducationProto_returnsCorrectProto() = + runTest(StandardTestDispatcher()) { + val windowingEducationProto = + createWindowingEducationProto( + appHandleHintViewedTimestampMillis = 123L, + appHandleHintUsedTimestampMillis = 124L, + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = 125L, + ) + testDatastore.updateData { windowingEducationProto } + + val resultProto = datastoreRepository.windowingEducationProto() + + assertThat(resultProto).isEqualTo(windowingEducationProto) + } + + @Test + fun updateAppUsageStats_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + val appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 3) + val appUsageStatsLastUpdateTimestamp = Duration.ofMillis(123L) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = appUsageStats, + appUsageStatsLastUpdateTimestampMillis = + appUsageStatsLastUpdateTimestamp.toMillis(), + ) + + datastoreRepository.updateAppUsageStats(appUsageStats, appUsageStatsLastUpdateTimestamp) + + val result = testDatastore.data.first() + assertThat(result).isEqualTo(windowingEducationProto) + } + + @Test + fun updateAppHandleHintViewedTimestampMillis_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + datastoreRepository.updateAppHandleHintViewedTimestampMillis(true) + + val result = testDatastore.data.first().hasAppHandleHintViewedTimestampMillis() + assertThat(result).isEqualTo(true) + } + + @Test + fun updateAppHandleHintUsedTimestampMillis_updatesDatastoreProto() = + runTest(StandardTestDispatcher()) { + datastoreRepository.updateAppHandleHintUsedTimestampMillis(true) + + val result = testDatastore.data.first().hasAppHandleHintUsedTimestampMillis() + assertThat(result).isEqualTo(true) + } + + companion object { + private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb" + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt index e5edd69155b5..2fc36efb1a41 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationFilterTest.kt @@ -53,189 +53,221 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) @kotlinx.coroutines.ExperimentalCoroutinesApi class AppHandleEducationFilterTest : ShellTestCase() { - @JvmField - @Rule - val extendedMockitoRule = - ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!! - @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository - @Mock private lateinit var mockUsageStatsManager: UsageStatsManager - private lateinit var educationFilter: AppHandleEducationFilter - private lateinit var testableResources: TestableResources - private lateinit var testableContext: TestableContext - - @Before - fun setup() { - MockitoAnnotations.initMocks(this) - testableContext = TestableContext(mContext) - testableResources = - testableContext.orCreateTestableResources.apply { - addOverride( - R.array.desktop_windowing_app_handle_education_allowlist_apps, - arrayOf(GMAIL_PACKAGE_NAME)) - addOverride(R.integer.desktop_windowing_education_required_time_since_setup_seconds, 0) - addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) - addOverride( - R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, MAX_VALUE) - addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100) - } - testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager) - educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository) - } - - @Test - fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest { - // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation - // should return true - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isTrue() - } - - @Test - fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest { - // Pass Youtube as current focus app, it is not in allowlist hence #shouldShowAppHandleEducation - // should return false - testableResources.addOverride( - R.array.desktop_windowing_app_handle_education_allowlist_apps, arrayOf(GMAIL_PACKAGE_NAME)) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - val captionState = - createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME)) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(captionState) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest { - // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation should - // return false - testableResources.addOverride( - R.integer.desktop_windowing_education_required_time_since_setup_seconds, MAX_VALUE) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest { - // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return false - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appHandleHintViewedTimestampMillis = 123L, - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest { - // App handle hint has been used before, hence #shouldShowAppHandleEducation should return false - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appHandleHintUsedTimestampMillis = 123L, - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest { - // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence - // #shouldShowAppHandleEducation should return false - testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest { - // UsageStats caching interval is set to 0ms, that means caching should happen very frequently - testableResources.addOverride( - R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, 0) - // The DataStore currently holds a proto object where Gmail's app launch count is recorded as 4. - // This value exceeds the minimum required count of 3. - testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = 0) - // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail. - // This value is below the minimum required count of 3. - `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong())) - .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 })) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - // Result should be false as queried usage stats should be considered to determine the result - // instead of cached stats - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest { - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - // Simulate app handle menu is expanded - val captionState = createAppHandleState(isHandleMenuExpanded = true) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(captionState) - - // We should not show app handle education if app menu is expanded - assertThat(result).isFalse() - } - - @Test - fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest { - // Simulate that gmail app has been launched twice before, minimum app launch count is 3, hence - // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite - // conditions, #shouldShowAppHandleEducation should return true. - testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) - val systemPropertiesKey = "persist.desktop_windowing_app_handle_education_override_conditions" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())).thenReturn(true) - val windowingEducationProto = - createWindowingEducationProto( - appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), - appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE) - `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) - - val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) - - assertThat(result).isTrue() - } + @JvmField + @Rule + val extendedMockitoRule = + ExtendedMockitoRule.Builder(this).mockStatic(SystemProperties::class.java).build()!! + @Mock private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository + @Mock private lateinit var mockUsageStatsManager: UsageStatsManager + private lateinit var educationFilter: AppHandleEducationFilter + private lateinit var testableResources: TestableResources + private lateinit var testableContext: TestableContext + + @Before + fun setup() { + MockitoAnnotations.initMocks(this) + testableContext = TestableContext(mContext) + testableResources = + testableContext.orCreateTestableResources.apply { + addOverride( + R.array.desktop_windowing_app_handle_education_allowlist_apps, + arrayOf(GMAIL_PACKAGE_NAME), + ) + addOverride( + R.integer.desktop_windowing_education_required_time_since_setup_seconds, + 0, + ) + addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + addOverride( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, + MAX_VALUE, + ) + addOverride(R.integer.desktop_windowing_education_app_launch_interval_seconds, 100) + } + testableContext.addMockSystemService(Context.USAGE_STATS_SERVICE, mockUsageStatsManager) + educationFilter = AppHandleEducationFilter(testableContext, datastoreRepository) + } + + @Test + fun shouldShowAppHandleEducation_isTriggerValid_returnsTrue() = runTest { + // setup() makes sure that all of the conditions satisfy and #shouldShowAppHandleEducation + // should return true + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isTrue() + } + + @Test + fun shouldShowAppHandleEducation_focusAppNotInAllowlist_returnsFalse() = runTest { + // Pass Youtube as current focus app, it is not in allowlist hence + // #shouldShowAppHandleEducation + // should return false + testableResources.addOverride( + R.array.desktop_windowing_app_handle_education_allowlist_apps, + arrayOf(GMAIL_PACKAGE_NAME), + ) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(YOUTUBE_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + val captionState = + createAppHandleState(createTaskInfo(runningTaskPackageName = YOUTUBE_PACKAGE_NAME)) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(captionState) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_timeSinceSetupIsNotSufficient_returnsFalse() = runTest { + // Time required to have passed setup is > 100 years, hence #shouldShowAppHandleEducation + // should + // return false + testableResources.addOverride( + R.integer.desktop_windowing_education_required_time_since_setup_seconds, + MAX_VALUE, + ) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_appHandleHintViewedBefore_returnsFalse() = runTest { + // App handle hint has been viewed before, hence #shouldShowAppHandleEducation should return + // false + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appHandleHintViewedTimestampMillis = 123L, + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_appHandleHintUsedBefore_returnsFalse() = runTest { + // App handle hint has been used before, hence #shouldShowAppHandleEducation should return + // false + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appHandleHintUsedTimestampMillis = 123L, + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_doesNotHaveMinAppUsage_returnsFalse() = runTest { + // Simulate that gmail app has been launched twice before, minimum app launch count is 3, + // hence + // #shouldShowAppHandleEducation should return false + testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_appUsageStatsStale_queryAppUsageStats() = runTest { + // UsageStats caching interval is set to 0ms, that means caching should happen very + // frequently + testableResources.addOverride( + R.integer.desktop_windowing_education_app_usage_cache_interval_seconds, + 0, + ) + // The DataStore currently holds a proto object where Gmail's app launch count is recorded + // as 4. + // This value exceeds the minimum required count of 3. + testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = 0, + ) + // The mocked UsageStatsManager is configured to return a launch count of 2 for Gmail. + // This value is below the minimum required count of 3. + `when`(mockUsageStatsManager.queryAndAggregateUsageStats(anyLong(), anyLong())) + .thenReturn(mapOf(GMAIL_PACKAGE_NAME to UsageStats().apply { mAppLaunchCount = 2 })) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + // Result should be false as queried usage stats should be considered to determine the + // result + // instead of cached stats + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_appHandleMenuExpanded_returnsFalse() = runTest { + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 4), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + // Simulate app handle menu is expanded + val captionState = createAppHandleState(isHandleMenuExpanded = true) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(captionState) + + // We should not show app handle education if app menu is expanded + assertThat(result).isFalse() + } + + @Test + fun shouldShowAppHandleEducation_overridePrerequisite_returnsTrue() = runTest { + // Simulate that gmail app has been launched twice before, minimum app launch count is 3, + // hence + // #shouldShowAppHandleEducation should return false. But as we are overriding prerequisite + // conditions, #shouldShowAppHandleEducation should return true. + testableResources.addOverride(R.integer.desktop_windowing_education_min_app_launch_count, 3) + val systemPropertiesKey = + "persist.desktop_windowing_app_handle_education_override_conditions" + whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + .thenReturn(true) + val windowingEducationProto = + createWindowingEducationProto( + appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2), + appUsageStatsLastUpdateTimestampMillis = Long.MAX_VALUE, + ) + `when`(datastoreRepository.windowingEducationProto()).thenReturn(windowingEducationProto) + + val result = educationFilter.shouldShowAppHandleEducation(createAppHandleState()) + + assertThat(result).isTrue() + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt index 6a5d9f67e4a7..8d7fb5d7af85 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/minimize/DesktopWindowLimitRemoteHandlerTest.kt @@ -66,16 +66,27 @@ class DesktopWindowLimitRemoteHandlerTest { private fun createRemoteHandler(taskIdToMinimize: Int) = DesktopWindowLimitRemoteHandler( - shellExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskIdToMinimize) + shellExecutor, + rootTaskDisplayAreaOrganizer, + remoteTransition, + taskIdToMinimize, + ) @Test fun startAnimation_dontSetTransition_returnsFalse() { val minimizeTask = createDesktopTask() val remoteHandler = createRemoteHandler(taskIdToMinimize = minimizeTask.taskId) - assertThat(remoteHandler.startAnimation(transition, - createMinimizeTransitionInfo(minimizeTask), startT, finishT, finishCallback) - ).isFalse() + assertThat( + remoteHandler.startAnimation( + transition, + createMinimizeTransitionInfo(minimizeTask), + startT, + finishT, + finishCallback, + ) + ) + .isFalse() } @Test @@ -84,9 +95,8 @@ class DesktopWindowLimitRemoteHandlerTest { remoteHandler.setTransition(transition) val info = createToFrontTransitionInfo() - assertThat( - remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) - ).isFalse() + assertThat(remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback)) + .isFalse() } @Test @@ -96,9 +106,8 @@ class DesktopWindowLimitRemoteHandlerTest { remoteHandler.setTransition(transition) val info = createMinimizeTransitionInfo(minimizeTask) - assertThat( - remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) - ).isTrue() + assertThat(remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback)) + .isTrue() } @Test @@ -109,8 +118,7 @@ class DesktopWindowLimitRemoteHandlerTest { remoteHandler.startAnimation(transition, info, startT, finishT, finishCallback) - verify(rootTaskDisplayAreaOrganizer, times(0)) - .reparentToDisplayArea(anyInt(), any(), any()) + verify(rootTaskDisplayAreaOrganizer, times(0)).reparentToDisplayArea(anyInt(), any(), any()) } @Test @@ -154,14 +162,18 @@ class DesktopWindowLimitRemoteHandlerTest { private fun createToFrontTransitionInfo() = TransitionInfoBuilder(TRANSIT_TO_FRONT) - .addChange(TRANSIT_TO_FRONT, - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) + .addChange( + TRANSIT_TO_FRONT, + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(), + ) .build() private fun createMinimizeTransitionInfo(minimizeTask: ActivityManager.RunningTaskInfo) = TransitionInfoBuilder(TRANSIT_TO_FRONT) - .addChange(TRANSIT_TO_FRONT, - TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build()) + .addChange( + TRANSIT_TO_FRONT, + TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(), + ) .addChange(TRANSIT_TO_BACK, minimizeTask) .build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt index 4f7e80cf8330..eae206664021 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepositoryTest.kt @@ -63,9 +63,10 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { DataStoreFactory.create( serializer = DesktopPersistentRepository.Companion.DesktopPersistentRepositoriesSerializer, - scope = datastoreScope) { - testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE) - } + scope = datastoreScope, + ) { + testContext.dataStoreFile(DESKTOP_REPOSITORY_STATES_DATASTORE_TEST_FILE) + } datastoreRepository = DesktopPersistentRepository(testDatastore) } @@ -113,7 +114,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { visibleTasks = visibleTasks, minimizedTasks = minimizedTasks, freeformTasksInZOrder = freeformTasksInZOrder, - userId = DEFAULT_USER_ID) + userId = DEFAULT_USER_ID, + ) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) assertThat(actualDesktop?.tasksByTaskIdMap).hasSize(2) @@ -137,7 +139,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { visibleTasks = visibleTasks, minimizedTasks = minimizedTasks, freeformTasksInZOrder = freeformTasksInZOrder, - userId = DEFAULT_USER_ID) + userId = DEFAULT_USER_ID, + ) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) assertThat(actualDesktop?.tasksByTaskIdMap?.get(task.taskId)?.desktopTaskState) @@ -161,7 +164,8 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { visibleTasks = visibleTasks, minimizedTasks = minimizedTasks, freeformTasksInZOrder = freeformTasksInZOrder, - userId = DEFAULT_USER_ID) + userId = DEFAULT_USER_ID, + ) val actualDesktop = datastoreRepository.readDesktop(DEFAULT_USER_ID, DEFAULT_DESKTOP_ID) assertThat(actualDesktop?.tasksByTaskIdMap).isEmpty() @@ -194,7 +198,7 @@ class DesktopPersistentRepositoryTest : ShellTestCase() { fun createDesktopTask( taskId: Int, - state: DesktopTaskState = DesktopTaskState.VISIBLE + state: DesktopTaskState = DesktopTaskState.VISIBLE, ): DesktopTask = DesktopTask.newBuilder().setTaskId(taskId).setDesktopTaskState(state).build() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt index cdf064b075a1..a3c441698905 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -47,15 +47,12 @@ import org.mockito.Mockito.spy import org.mockito.kotlin.mock import org.mockito.kotlin.whenever - @SmallTest @RunWith(AndroidTestingRunner::class) @ExperimentalCoroutinesApi class DesktopRepositoryInitializerTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() + @JvmField @Rule val setFlagsRule = SetFlagsRule() private lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var shellInit: ShellInit @@ -82,7 +79,7 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { persistentRepository, repositoryInitializer, datastoreScope, - userManager + userManager, ) } @@ -90,101 +87,94 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE, FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) fun initWithPersistence_multipleUsers_addedCorrectly() = runTest(StandardTestDispatcher()) { - whenever(persistentRepository.getUserDesktopRepositoryMap()).thenReturn( - mapOf( - USER_ID_1 to desktopRepositoryState1, - USER_ID_2 to desktopRepositoryState2 + whenever(persistentRepository.getUserDesktopRepositoryMap()) + .thenReturn( + mapOf( + USER_ID_1 to desktopRepositoryState1, + USER_ID_2 to desktopRepositoryState2, + ) ) - ) whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1)) .thenReturn(desktopRepositoryState1) whenever(persistentRepository.getDesktopRepositoryState(USER_ID_2)) .thenReturn(desktopRepositoryState2) - whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)) - .thenReturn(desktop1) - whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)) - .thenReturn(desktop2) - whenever(persistentRepository.readDesktop(USER_ID_2, DESKTOP_ID_3)) - .thenReturn(desktop3) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2) + whenever(persistentRepository.readDesktop(USER_ID_2, DESKTOP_ID_3)).thenReturn(desktop3) repositoryInitializer.initialize(desktopUserRepositories) // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be updated + // since the repository currently doesn't handle desktops. This test logic should be + // updated // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getActiveTasks(DEFAULT_DISPLAY) - ) + desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + ) .containsExactly(1, 3, 4, 5) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) - ) + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksOrdered(DEFAULT_DISPLAY) + ) .containsExactly(5, 1) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getMinimizedTasks(DEFAULT_DISPLAY) - ) + desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + ) .containsExactly(3, 4) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_2) - .getActiveTasks(DEFAULT_DISPLAY) - ) + desktopUserRepositories.getProfile(USER_ID_2).getActiveTasks(DEFAULT_DISPLAY) + ) .containsExactly(7, 8) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_2) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) - ) + desktopUserRepositories + .getProfile(USER_ID_2) + .getExpandedTasksOrdered(DEFAULT_DISPLAY) + ) .contains(7) assertThat( - desktopUserRepositories.getProfile(USER_ID_2) - .getMinimizedTasks(DEFAULT_DISPLAY) - ).containsExactly(8) + desktopUserRepositories.getProfile(USER_ID_2).getMinimizedTasks(DEFAULT_DISPLAY) + ) + .containsExactly(8) } @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun initWithPersistence_singleUser_addedCorrectly() = runTest(StandardTestDispatcher()) { - whenever(persistentRepository.getUserDesktopRepositoryMap()).thenReturn( - mapOf( - USER_ID_1 to desktopRepositoryState1, - ) - ) + whenever(persistentRepository.getUserDesktopRepositoryMap()) + .thenReturn(mapOf(USER_ID_1 to desktopRepositoryState1)) whenever(persistentRepository.getDesktopRepositoryState(USER_ID_1)) .thenReturn(desktopRepositoryState1) - whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)) - .thenReturn(desktop1) - whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)) - .thenReturn(desktop2) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_1)).thenReturn(desktop1) + whenever(persistentRepository.readDesktop(USER_ID_1, DESKTOP_ID_2)).thenReturn(desktop2) repositoryInitializer.initialize(desktopUserRepositories) // Desktop Repository currently returns all tasks across desktops for a specific user - // since the repository currently doesn't handle desktops. This test logic should be updated + // since the repository currently doesn't handle desktops. This test logic should be + // updated // once the repository handles multiple desktops. assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getActiveTasks(DEFAULT_DISPLAY) - ) + desktopUserRepositories.getProfile(USER_ID_1).getActiveTasks(DEFAULT_DISPLAY) + ) .containsExactly(1, 3, 4, 5) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getExpandedTasksOrdered(DEFAULT_DISPLAY) - ) + desktopUserRepositories + .getProfile(USER_ID_1) + .getExpandedTasksOrdered(DEFAULT_DISPLAY) + ) .containsExactly(5, 1) .inOrder() assertThat( - desktopUserRepositories.getProfile(USER_ID_1) - .getMinimizedTasks(DEFAULT_DISPLAY) - ) + desktopUserRepositories.getProfile(USER_ID_1).getMinimizedTasks(DEFAULT_DISPLAY) + ) .containsExactly(3, 4) .inOrder() } @@ -202,70 +192,73 @@ class DesktopRepositoryInitializerTest : ShellTestCase() { const val DESKTOP_ID_3 = 4 val freeformTasksInZOrder1 = listOf(1, 3) - val desktop1: Desktop = Desktop.newBuilder() - .setDesktopId(DESKTOP_ID_1) - .addAllZOrderedTasks(freeformTasksInZOrder1) - .putTasksByTaskId( - 1, - DesktopTask.newBuilder() - .setTaskId(1) - .setDesktopTaskState(DesktopTaskState.VISIBLE) - .build() - ) - .putTasksByTaskId( - 3, - DesktopTask.newBuilder() - .setTaskId(3) - .setDesktopTaskState(DesktopTaskState.MINIMIZED) - .build() - ) - .build() + val desktop1: Desktop = + Desktop.newBuilder() + .setDesktopId(DESKTOP_ID_1) + .addAllZOrderedTasks(freeformTasksInZOrder1) + .putTasksByTaskId( + 1, + DesktopTask.newBuilder() + .setTaskId(1) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build(), + ) + .putTasksByTaskId( + 3, + DesktopTask.newBuilder() + .setTaskId(3) + .setDesktopTaskState(DesktopTaskState.MINIMIZED) + .build(), + ) + .build() val freeformTasksInZOrder2 = listOf(4, 5) - val desktop2: Desktop = Desktop.newBuilder() - .setDesktopId(DESKTOP_ID_2) - .addAllZOrderedTasks(freeformTasksInZOrder2) - .putTasksByTaskId( - 4, - DesktopTask.newBuilder() - .setTaskId(4) - .setDesktopTaskState(DesktopTaskState.MINIMIZED) - .build() - ) - .putTasksByTaskId( - 5, - DesktopTask.newBuilder() - .setTaskId(5) - .setDesktopTaskState(DesktopTaskState.VISIBLE) - .build() - ) - .build() + val desktop2: Desktop = + Desktop.newBuilder() + .setDesktopId(DESKTOP_ID_2) + .addAllZOrderedTasks(freeformTasksInZOrder2) + .putTasksByTaskId( + 4, + DesktopTask.newBuilder() + .setTaskId(4) + .setDesktopTaskState(DesktopTaskState.MINIMIZED) + .build(), + ) + .putTasksByTaskId( + 5, + DesktopTask.newBuilder() + .setTaskId(5) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build(), + ) + .build() val freeformTasksInZOrder3 = listOf(7, 8) - val desktop3: Desktop = Desktop.newBuilder() - .setDesktopId(DESKTOP_ID_3) - .addAllZOrderedTasks(freeformTasksInZOrder3) - .putTasksByTaskId( - 7, - DesktopTask.newBuilder() - .setTaskId(7) - .setDesktopTaskState(DesktopTaskState.VISIBLE) - .build() - ) - .putTasksByTaskId( - 8, - DesktopTask.newBuilder() - .setTaskId(8) - .setDesktopTaskState(DesktopTaskState.MINIMIZED) - .build() - ) - .build() - val desktopRepositoryState1: DesktopRepositoryState = DesktopRepositoryState.newBuilder() - .putDesktop(DESKTOP_ID_1, desktop1) - .putDesktop(DESKTOP_ID_2, desktop2) - .build() - val desktopRepositoryState2: DesktopRepositoryState = DesktopRepositoryState.newBuilder() - .putDesktop(DESKTOP_ID_3, desktop3) - .build() + val desktop3: Desktop = + Desktop.newBuilder() + .setDesktopId(DESKTOP_ID_3) + .addAllZOrderedTasks(freeformTasksInZOrder3) + .putTasksByTaskId( + 7, + DesktopTask.newBuilder() + .setTaskId(7) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build(), + ) + .putTasksByTaskId( + 8, + DesktopTask.newBuilder() + .setTaskId(8) + .setDesktopTaskState(DesktopTaskState.MINIMIZED) + .build(), + ) + .build() + val desktopRepositoryState1: DesktopRepositoryState = + DesktopRepositoryState.newBuilder() + .putDesktop(DESKTOP_ID_1, desktop1) + .putDesktop(DESKTOP_ID_2, desktop2) + .build() + val desktopRepositoryState2: DesktopRepositoryState = + DesktopRepositoryState.newBuilder().putDesktop(DESKTOP_ID_3, desktop3).build() } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java index 232ae0750c3a..ada7b4aff37d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTestUtils.java @@ -36,6 +36,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.common.split.SplitLayout; import com.android.wm.shell.common.split.SplitState; +import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.transition.Transitions; @@ -81,11 +82,13 @@ public class SplitTestUtils { ShellExecutor mainExecutor, Handler mainHandler, ShellExecutor bgExecutor, Optional<RecentTasksController> recentTasks, LaunchAdjacentController launchAdjacentController, - Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState) { + Optional<WindowDecorViewModel> windowDecorViewModel, SplitState splitState, + Optional<DesktopTasksController> desktopTasksController) { super(context, displayId, syncQueue, taskOrganizer, mainStage, sideStage, displayController, imeController, insetsController, splitLayout, transitions, transactionPool, mainExecutor, mainHandler, bgExecutor, - recentTasks, launchAdjacentController, windowDecorViewModel, splitState); + recentTasks, launchAdjacentController, windowDecorViewModel, splitState, + desktopTasksController); // Prepare root task for testing. mRootTask = new TestRunningTaskInfoBuilder().build(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java index 0d612c17c462..ffa8b6089660 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/SplitTransitionTests.java @@ -149,7 +149,7 @@ public class SplitTransitionTests extends ShellTestCase { mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), - mLaunchAdjacentController, Optional.empty(), mSplitState); + mLaunchAdjacentController, Optional.empty(), mSplitState, Optional.empty()); mStageCoordinator.setMixedHandler(mMixedHandler); mSplitScreenTransitions = mStageCoordinator.getSplitTransitions(); doAnswer((Answer<IBinder>) invocation -> mock(IBinder.class)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java index a6aeabd5bd19..9d1df864764f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/splitscreen/StageCoordinatorTests.java @@ -143,8 +143,9 @@ public class StageCoordinatorTests extends ShellTestCase { mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, - mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), - mLaunchAdjacentController, Optional.empty(), mSplitState)); + mMainExecutor, mMainHandler, mBgExecutor, Optional.empty(), + mLaunchAdjacentController, Optional.empty(), mSplitState, + Optional.empty())); mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1); diff --git a/media/java/android/media/AudioDevicePort.java b/media/java/android/media/AudioDevicePort.java index 4b3962e6dd74..9e9bbeea0635 100644 --- a/media/java/android/media/AudioDevicePort.java +++ b/media/java/android/media/AudioDevicePort.java @@ -210,6 +210,27 @@ public class AudioDevicePort extends AudioPort { return super.equals(o); } + /** + * Returns true if the AudioDevicePort passed as argument represents the same device (same + * type and same address). This is different from equals() in that the port IDs are not compared + * which allows matching devices across native audio server restarts. + * @param other the other audio device port to compare to. + * @return true if both device port correspond to the same audio device, false otherwise. + * @hide + */ + public boolean isSameAs(AudioDevicePort other) { + if (mType != other.type()) { + return false; + } + if (mAddress == null && other.address() != null) { + return false; + } + if (!mAddress.equals(other.address())) { + return false; + } + return true; + } + @Override public String toString() { String type = (mRole == ROLE_SOURCE ? diff --git a/media/java/android/media/AudioManager.java b/media/java/android/media/AudioManager.java index 9beeef4c160f..04fa90514dc9 100644 --- a/media/java/android/media/AudioManager.java +++ b/media/java/android/media/AudioManager.java @@ -8068,15 +8068,15 @@ public class AudioManager { ArrayList<AudioDevicePort> ports_A, ArrayList<AudioDevicePort> ports_B, int flags) { ArrayList<AudioDevicePort> delta_ports = new ArrayList<AudioDevicePort>(); - - AudioDevicePort cur_port = null; for (int cur_index = 0; cur_index < ports_B.size(); cur_index++) { boolean cur_port_found = false; - cur_port = ports_B.get(cur_index); + AudioDevicePort cur_port = ports_B.get(cur_index); for (int prev_index = 0; prev_index < ports_A.size() && !cur_port_found; prev_index++) { - cur_port_found = (cur_port.id() == ports_A.get(prev_index).id()); + // Do not compare devices by port ID as these change when the native + // audio server restarts + cur_port_found = cur_port.isSameAs(ports_A.get(prev_index)); } if (!cur_port_found) { @@ -8422,13 +8422,10 @@ public class AudioManager { * Callback method called when the mediaserver dies */ public void onServiceDied() { - synchronized (mDeviceCallbacks) { - broadcastDeviceListChange_sync(null); - } + // Nothing to do here } } - /** * @hide * Abstract class to receive event notification about audioserver process state. diff --git a/media/java/android/media/AudioPortEventHandler.java b/media/java/android/media/AudioPortEventHandler.java index 763eb2944d19..5685710e5f15 100644 --- a/media/java/android/media/AudioPortEventHandler.java +++ b/media/java/android/media/AudioPortEventHandler.java @@ -97,17 +97,15 @@ class AudioPortEventHandler { ArrayList<AudioPort> ports = new ArrayList<AudioPort>(); ArrayList<AudioPatch> patches = new ArrayList<AudioPatch>(); - if (msg.what != AUDIOPORT_EVENT_SERVICE_DIED) { - int status = AudioManager.updateAudioPortCache(ports, patches, null); - if (status != AudioManager.SUCCESS) { - // Since audio ports and audio patches are not null, the return - // value could be ERROR due to inconsistency between port generation - // and patch generation. In this case, we need to reschedule the - // message to make sure the native callback is done. - sendMessageDelayed(obtainMessage(msg.what, msg.obj), - RESCHEDULE_MESSAGE_DELAY_MS); - return; - } + int status = AudioManager.updateAudioPortCache(ports, patches, null); + if (status != AudioManager.SUCCESS) { + // Since audio ports and audio patches are not null, the return + // value could be ERROR due to inconsistency between port generation + // and patch generation. In this case, we need to reschedule the + // message to make sure the native callback is done. + sendMessageDelayed(obtainMessage(msg.what, msg.obj), + RESCHEDULE_MESSAGE_DELAY_MS); + return; } switch (msg.what) { diff --git a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt index 265c065e924e..bfaeb42d5a31 100644 --- a/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt +++ b/packages/SettingsLib/SettingsTheme/src/com/android/settingslib/widget/SettingsBasePreferenceFragment.kt @@ -16,6 +16,9 @@ package com.android.settingslib.widget +import android.os.Bundle +import android.view.View +import androidx.annotation.CallSuper import androidx.preference.PreferenceFragmentCompat import androidx.preference.PreferenceScreen import androidx.recyclerview.widget.RecyclerView @@ -23,9 +26,18 @@ import androidx.recyclerview.widget.RecyclerView /** Base class for Settings to use PreferenceFragmentCompat */ abstract class SettingsBasePreferenceFragment : PreferenceFragmentCompat() { + @CallSuper + override fun onViewCreated(view: View, savedInstanceState: Bundle?) { + super.onViewCreated(view, savedInstanceState) + if (SettingsThemeHelper.isExpressiveTheme(requireContext())) { + // Don't allow any divider in between the preferences in expressive design. + setDivider(null) + } + } + override fun onCreateAdapter(preferenceScreen: PreferenceScreen): RecyclerView.Adapter<*> { if (SettingsThemeHelper.isExpressiveTheme(requireContext())) return SettingsPreferenceGroupAdapter(preferenceScreen) return super.onCreateAdapter(preferenceScreen) } -}
\ No newline at end of file +} diff --git a/packages/SettingsLib/res/values/arrays.xml b/packages/SettingsLib/res/values/arrays.xml index 63c8929ef652..3d3dad379417 100644 --- a/packages/SettingsLib/res/values/arrays.xml +++ b/packages/SettingsLib/res/values/arrays.xml @@ -683,9 +683,9 @@ <!-- Values for showing shade on external display for developers --> <string-array name="shade_display_awareness_values" > - <item>device-display</item> - <item>external-display</item> - <item>focus-based</item> + <item>default_display</item> + <item>any_external_display</item> + <item>status_bar_latest_touch</item> </string-array> </resources> diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java index 6128d45831fb..55f48e3e367f 100644 --- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java +++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsProvider.java @@ -2089,7 +2089,33 @@ public class SettingsProvider extends ContentProvider { // setting. return false; } - final String mimeType = getContext().getContentResolver().getType(audioUri); + + // If the audioUri comes from FileProvider, the security check will fail. Currently, it + // should not have too many FileProvider Uri usage, using a workaround fix here. + // Only allow for caller is privileged apps + ApplicationInfo aInfo = null; + try { + aInfo = getCallingApplicationInfoOrThrow(); + } catch (IllegalStateException ignored) { + Slog.w(LOG_TAG, "isValidMediaUri: cannot get calling app info for setting: " + + name + " URI: " + audioUri); + return false; + } + final boolean isPrivilegedApp = aInfo != null ? aInfo.isPrivilegedApp() : false; + String mimeType = null; + if (isPrivilegedApp) { + final long identity = Binder.clearCallingIdentity(); + try { + mimeType = getContext().getContentResolver().getType(audioUri); + } finally { + Binder.restoreCallingIdentity(identity); + } + } else { + mimeType = getContext().getContentResolver().getType(audioUri); + } + if (DEBUG) { + Slog.v(LOG_TAG, "isValidMediaUri mimeType: " + mimeType); + } if (mimeType == null) { Slog.e(LOG_TAG, "mutateSystemSetting for setting: " + name + " URI: " + audioUri diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt index d5eaf829b746..f36fd34d0445 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt @@ -1020,7 +1020,7 @@ private fun EmptyStateCta(contentPadding: PaddingValues, viewModel: BaseCommunal text = titleForEmptyStateCTA, style = MaterialTheme.typography.displaySmall, textAlign = TextAlign.Center, - color = colors.primary, + color = colors.onPrimary, modifier = Modifier.focusable().semantics(mergeDescendants = true) { contentDescription = titleForEmptyStateCTA diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt index e78862e0e922..5c7ca97474b7 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt @@ -29,11 +29,8 @@ import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.getValue import androidx.compose.runtime.remember import androidx.compose.runtime.rememberCoroutineScope -import androidx.compose.runtime.setValue import androidx.compose.ui.Modifier -import androidx.compose.ui.platform.LocalContext import androidx.compose.ui.res.dimensionResource -import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.dp import androidx.compose.ui.viewinterop.AndroidView import androidx.lifecycle.compose.collectAsStateWithLifecycle @@ -52,7 +49,6 @@ import com.android.systemui.lifecycle.rememberViewModel import com.android.systemui.notifications.ui.composable.ConstrainedNotificationStack import com.android.systemui.notifications.ui.composable.SnoozeableHeadsUpNotificationSpace import com.android.systemui.res.R -import com.android.systemui.shade.LargeScreenHeaderHelper import com.android.systemui.shade.ShadeDisplayAware import com.android.systemui.statusbar.notification.icon.ui.viewbinder.AlwaysOnDisplayNotificationIconViewStore import com.android.systemui.statusbar.notification.icon.ui.viewbinder.NotificationIconContainerViewBinder @@ -179,16 +175,13 @@ constructor( return } - val splitShadeTopMargin: Dp = - LargeScreenHeaderHelper.getLargeScreenHeaderHeight(LocalContext.current).dp - ConstrainedNotificationStack( stackScrollView = stackScrollView.get(), viewModel = rememberViewModel("Notifications") { viewModelFactory.create() }, modifier = modifier .fillMaxWidth() - .thenIf(isShadeLayoutWide) { Modifier.padding(top = splitShadeTopMargin) } + .thenIf(isShadeLayoutWide) { Modifier.padding(top = 12.dp) } .let { if (burnInParams == null) { it diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt index a308c8ee38ca..3f4d3f8ba12a 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/common/ui/data/repository/ConfigurationRepositoryImplTest.kt @@ -98,6 +98,21 @@ class ConfigurationRepositoryImplTest : SysuiTestCase() { } @Test + fun onMovedToDisplays_updatesOnMovedToDisplay() = + testScope.runTest { + val lastOnMovedToDisplay by collectLastValue(underTest.onMovedToDisplay) + assertThat(lastOnMovedToDisplay).isNull() + + val configurationCallback = withArgCaptor { + verify(configurationController).addCallback(capture()) + } + + configurationCallback.onMovedToDisplay(1, Configuration()) + runCurrent() + assertThat(lastOnMovedToDisplay).isEqualTo(1) + } + + @Test fun onAnyConfigurationChange_updatesOnConfigChanged() = testScope.runTest { val lastAnyConfigurationChange by collectLastValue(underTest.onAnyConfigurationChange) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt new file mode 100644 index 000000000000..477e31e8a66c --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepositoryTest.kt @@ -0,0 +1,125 @@ +/* + * 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.systemui.keyboard.shortcut.data.repository + +import android.hardware.input.AppLaunchData +import android.hardware.input.AppLaunchData.RoleData +import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger +import android.hardware.input.fakeInputManager +import android.view.KeyEvent.KEYCODE_A +import android.view.KeyEvent.META_ALT_ON +import android.view.KeyEvent.META_CTRL_ON +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.shortcut.appLaunchDataRepository +import com.android.systemui.keyboard.shortcut.shared.model.shortcutCommand +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class AppLaunchDataRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val inputManager = kosmos.fakeInputManager.inputManager + private val testHelper = kosmos.shortcutHelperTestHelper + private val repo = kosmos.appLaunchDataRepository + + @Test + fun appLaunchData_returnsDataRetrievedFromApiBasedOnShortcutCommand() = + kosmos.runTest { + val inputGesture = simpleInputGestureDataForAppLaunchShortcut() + setApiAppLaunchBookmarks(listOf(inputGesture)) + + testHelper.toggle(TEST_DEVICE_ID) + + val appLaunchData = + repo.getAppLaunchDataForShortcutWithCommand( + shortcutCommand = + shortcutCommand { + key("Ctrl") + key("Alt") + key("A") + } + ) + + assertThat(appLaunchData).isEqualTo(inputGesture.action.appLaunchData()) + } + + @Test + fun appLaunchData_returnsSameDataForAnyOrderOfShortcutCommandKeys() = + kosmos.runTest { + val inputGesture = simpleInputGestureDataForAppLaunchShortcut() + setApiAppLaunchBookmarks(listOf(inputGesture)) + + testHelper.toggle(TEST_DEVICE_ID) + + val shortcutCommandCtrlAltA = shortcutCommand { + key("Ctrl") + key("Alt") + key("A") + } + + val shortcutCommandCtrlAAlt = shortcutCommand { + key("Ctrl") + key("A") + key("Alt") + } + + val shortcutCommandAltCtrlA = shortcutCommand { + key("Alt") + key("Ctrl") + key("A") + } + + assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandCtrlAltA)) + .isEqualTo(inputGesture.action.appLaunchData()) + + assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandCtrlAAlt)) + .isEqualTo(inputGesture.action.appLaunchData()) + + assertThat(repo.getAppLaunchDataForShortcutWithCommand(shortcutCommandAltCtrlA)) + .isEqualTo(inputGesture.action.appLaunchData()) + } + + private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List<InputGestureData>) { + whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks) + } + + private fun simpleInputGestureDataForAppLaunchShortcut( + keyCode: Int = KEYCODE_A, + modifiers: Int = META_CTRL_ON or META_ALT_ON, + appLaunchData: AppLaunchData = RoleData(TEST_ROLE), + ): InputGestureData { + return InputGestureData.Builder() + .setTrigger(createKeyTrigger(keyCode, modifiers)) + .setAppLaunchData(appLaunchData) + .build() + } + + private companion object { + private const val TEST_ROLE = "Test role" + private const val TEST_DEVICE_ID = 123 + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt index d12c04586ac2..4cfb26e6555b 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepositoryTest.kt @@ -18,14 +18,21 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.content.Context.INPUT_SERVICE +import android.hardware.input.AppLaunchData +import android.hardware.input.AppLaunchData.RoleData import android.hardware.input.InputGestureData +import android.hardware.input.InputGestureData.createKeyTrigger import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_ERROR_DOES_NOT_EXIST import android.hardware.input.InputManager.CUSTOM_INPUT_GESTURE_RESULT_SUCCESS +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION import android.hardware.input.fakeInputManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags +import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.KEYCODE_SLASH +import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CAPS_LOCK_ON +import android.view.KeyEvent.META_CTRL_ON import android.view.KeyEvent.META_META_ON import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest @@ -44,14 +51,15 @@ import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.allCusto import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.customizableInputGestureWithUnknownKeyGestureType import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.expectedShortcutCategoriesWithSimpleShortcutCombination import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.goHomeInputGestureData +import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.launchCalendarShortcutAddRequest import com.android.systemui.keyboard.shortcut.data.source.TestShortcuts.standardKeyCombination import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.Add -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.Delete +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper import com.android.systemui.kosmos.testScope +import com.android.systemui.kosmos.useUnconfinedTestDispatcher import com.android.systemui.res.R import com.android.systemui.settings.FakeUserTracker import com.android.systemui.settings.userTracker @@ -72,7 +80,7 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { private val mockUserContext: Context = mock() private val kosmos = - testKosmos().also { + testKosmos().useUnconfinedTestDispatcher().also { it.userTracker = FakeUserTracker(onCreateCurrentUserContext = { mockUserContext }) } @@ -242,6 +250,32 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { } @Test + fun buildInputGestureDataForAppLaunchShortcut_keyGestureTypeIsTypeLaunchApp() = + testScope.runTest { + setApiAppLaunchBookmarks(listOf(simpleInputGestureDataForAppLaunchShortcut())) + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(launchCalendarShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized() + + assertThat(inputGestureData?.action?.keyGestureType()) + .isEqualTo(KEY_GESTURE_TYPE_LAUNCH_APPLICATION) + } + + @Test + fun buildInputGestureDataForAppLaunchShortcut_appLaunchDataIsAdded() = + testScope.runTest { + setApiAppLaunchBookmarks(listOf(simpleInputGestureDataForAppLaunchShortcut())) + helper.toggle(deviceId = 123) + repo.onCustomizationRequested(launchCalendarShortcutAddRequest) + repo.updateUserKeyCombination(standardKeyCombination) + + val inputGestureData = repo.buildInputGestureDataForShortcutBeingCustomized() + assertThat(inputGestureData?.action?.appLaunchData()).isNotNull() + } + + @Test @EnableFlags(FLAG_ENABLE_CUSTOMIZABLE_INPUT_GESTURES, FLAG_USE_KEY_GESTURE_EVENT_HANDLER) fun deleteShortcut_successfullyRetrievesGestureDataAndDeletesShortcut() { testScope.runTest { @@ -304,17 +338,17 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { private suspend fun customizeShortcut( customizationRequest: ShortcutCustomizationRequestInfo, - keyCombination: KeyCombination? = null - ): ShortcutCustomizationRequestResult{ + keyCombination: KeyCombination? = null, + ): ShortcutCustomizationRequestResult { repo.onCustomizationRequested(customizationRequest) repo.updateUserKeyCombination(keyCombination) return when (customizationRequest) { - is Add -> { + is SingleShortcutCustomization.Add -> { repo.confirmAndSetShortcutCurrentlyBeingCustomized() } - is Delete -> { + is SingleShortcutCustomization.Delete -> { repo.deleteShortcutCurrentlyBeingCustomized() } @@ -352,4 +386,19 @@ class CustomShortcutCategoriesRepositoryTest : SysuiTestCase() { assertThat(categories).isEmpty() } } + + private fun setApiAppLaunchBookmarks(appLaunchBookmarks: List<InputGestureData>) { + whenever(inputManager.appLaunchBookmarks).thenReturn(appLaunchBookmarks) + } + + private fun simpleInputGestureDataForAppLaunchShortcut( + keyCode: Int = KEYCODE_A, + modifiers: Int = META_CTRL_ON or META_ALT_ON, + appLaunchData: AppLaunchData = RoleData("Test role"), + ): InputGestureData { + return InputGestureData.Builder() + .setTrigger(createKeyTrigger(keyCode, modifiers)) + .setAppLaunchData(appLaunchData) + .build() + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt index f78c692ee4c2..96410597e20c 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapterTest.kt @@ -28,6 +28,7 @@ import android.hardware.input.AppLaunchData import android.hardware.input.AppLaunchData.RoleData import android.hardware.input.InputGestureData import android.hardware.input.InputGestureData.createKeyTrigger +import android.hardware.input.KeyGestureEvent.KEY_GESTURE_TYPE_LAUNCH_APPLICATION import android.view.KeyEvent.KEYCODE_A import android.view.KeyEvent.META_ALT_ON import android.view.KeyEvent.META_CTRL_ON @@ -55,14 +56,15 @@ import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever - @SmallTest @RunWith(AndroidJUnit4::class) class InputGestureDataAdapterTest : SysuiTestCase() { - private val kosmos = testKosmos().also { kosmos -> - kosmos.userTracker = FakeUserTracker(onCreateCurrentUserContext = { kosmos.mockedContext }) - } + private val kosmos = + testKosmos().also { kosmos -> + kosmos.userTracker = + FakeUserTracker(onCreateCurrentUserContext = { kosmos.mockedContext }) + } private val adapter = kosmos.inputGestureDataAdapter private val roleManager = kosmos.roleManager private val packageManager: PackageManager = kosmos.packageManager @@ -139,24 +141,40 @@ class InputGestureDataAdapterTest : SysuiTestCase() { val inputGestureData = buildInputGestureDataForAppLaunchShortcut() val internalGroups = adapter.toInternalGroupSources(listOf(inputGestureData)) - assertThat(internalGroups).containsExactly( - InternalGroupsSource( - type = ShortcutCategoryType.AppCategories, - groups = listOf( - InternalKeyboardShortcutGroup( - label = APPLICATION_SHORTCUT_GROUP_LABEL, - items = listOf( - InternalKeyboardShortcutInfo( - label = expectedShortcutLabelForFirstAppMatchingIntent, - keycode = KEYCODE_A, - modifiers = META_CTRL_ON or META_ALT_ON, - isCustomShortcut = true + assertThat(internalGroups) + .containsExactly( + InternalGroupsSource( + type = ShortcutCategoryType.AppCategories, + groups = + listOf( + InternalKeyboardShortcutGroup( + label = APPLICATION_SHORTCUT_GROUP_LABEL, + items = + listOf( + InternalKeyboardShortcutInfo( + label = + expectedShortcutLabelForFirstAppMatchingIntent, + keycode = KEYCODE_A, + modifiers = META_CTRL_ON or META_ALT_ON, + isCustomShortcut = true, + ) + ), ) - ) - ) + ), + ) + ) + } + + @Test + fun keyGestureType_returnsTypeLaunchApplicationForAppLaunchShortcutCategory() = + kosmos.runTest { + assertThat( + adapter.getKeyGestureTypeForShortcut( + shortcutLabel = "Test Shortcut label", + shortcutCategoryType = ShortcutCategoryType.AppCategories, ) ) - ) + .isEqualTo(KEY_GESTURE_TYPE_LAUNCH_APPLICATION) } private fun setApiToRetrieveResolverActivity() { @@ -169,11 +187,10 @@ class InputGestureDataAdapterTest : SysuiTestCase() { .thenReturn(fakeActivityInfo) } - private fun buildInputGestureDataForAppLaunchShortcut( keyCode: Int = KEYCODE_A, modifiers: Int = META_CTRL_ON or META_ALT_ON, - appLaunchData: AppLaunchData = RoleData(TEST_ROLE) + appLaunchData: AppLaunchData = RoleData(TEST_ROLE), ): InputGestureData { return InputGestureData.Builder() .setTrigger(createKeyTrigger(keyCode, modifiers)) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt new file mode 100644 index 000000000000..ded2d223aab4 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepositoryTest.kt @@ -0,0 +1,67 @@ +/* + * 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.systemui.keyboard.shortcut.data.repository + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.keyboard.shortcut.shortcutHelperInputDeviceRepository +import com.android.systemui.keyboard.shortcut.shortcutHelperTestHelper +import com.android.systemui.kosmos.collectLastValue +import com.android.systemui.kosmos.runTest +import com.android.systemui.kosmos.useUnconfinedTestDispatcher +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class ShortcutHelperInputDeviceRepositoryTest : SysuiTestCase() { + private val kosmos = testKosmos().useUnconfinedTestDispatcher() + private val testHelper = kosmos.shortcutHelperTestHelper + private val repo = kosmos.shortcutHelperInputDeviceRepository + + @Test + fun activeInputDevice_nullByDefault() = + kosmos.runTest { + val activeInputDevice by collectLastValue(repo.activeInputDevice) + + assertThat(activeInputDevice).isNull() + } + + @Test + fun activeInputDevice_nonNullWhenHelperIsShown() = + kosmos.runTest { + val activeInputDevice by collectLastValue(repo.activeInputDevice) + + testHelper.showFromActivity() + + assertThat(activeInputDevice).isNotNull() + } + + @Test + fun activeInputDevice_nullWhenHelperIsClosed() = + kosmos.runTest { + val activeInputDevice by collectLastValue(repo.activeInputDevice) + + testHelper.showFromActivity() + testHelper.hideFromActivity() + + assertThat(activeInputDevice).isNull() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt index 7dc7016e5e74..7c88d76f28bd 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/shortcut/data/source/TestShortcuts.kt @@ -43,11 +43,12 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import com.android.systemui.keyboard.shortcut.shared.model.ShortcutSubCategory import com.android.systemui.keyboard.shortcut.shared.model.shortcut import com.android.systemui.keyboard.shortcut.shared.model.shortcutCategory +import com.android.systemui.keyboard.shortcut.shared.model.shortcutCommand import com.android.systemui.res.R object TestShortcuts { @@ -596,14 +597,27 @@ object TestShortcuts { ) val allAppsShortcutAddRequest = - ShortcutCustomizationRequestInfo.Add( + SingleShortcutCustomization.Add( label = "Open apps list", categoryType = System, subCategoryLabel = "System controls", ) + val launchCalendarShortcutAddRequest = + SingleShortcutCustomization.Add( + label = "Calendar", + categoryType = ShortcutCategoryType.AppCategories, + subCategoryLabel = "Applications", + shortcutCommand = + shortcutCommand { + key("Ctrl") + key("Alt") + key("A") + }, + ) + val allAppsShortcutDeleteRequest = - ShortcutCustomizationRequestInfo.Delete( + SingleShortcutCustomization.Delete( label = "Open apps list", categoryType = System, subCategoryLabel = "System controls", @@ -698,7 +712,7 @@ object TestShortcuts { ) val standardAddShortcutRequest = - ShortcutCustomizationRequestInfo.Add( + SingleShortcutCustomization.Add( label = "Standard shortcut", categoryType = System, subCategoryLabel = "Standard subcategory", diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt index 5d1ce7c5ca05..929537dcf757 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowViewTest.kt @@ -253,6 +253,16 @@ class NotificationShadeWindowViewTest : SysuiTestCase() { verify(configurationForwarder).onConfigurationChanged(eq(config)) } + @Test + @EnableFlags(AConfigFlags.FLAG_SHADE_WINDOW_GOES_AROUND) + fun onMovedToDisplay_configForwarderSet_propagatesConfig() { + val config = Configuration() + + underTest.onMovedToDisplay(1, config) + + verify(configurationForwarder).dispatchOnMovedToDisplay(eq(1), eq(config)) + } + private fun captureInteractionEventHandler() { verify(underTest).setInteractionEventHandler(interactionEventHandlerCaptor.capture()) interactionEventHandler = interactionEventHandlerCaptor.value diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt index 096675962d80..007a0fb87953 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryTest.kt @@ -16,17 +16,20 @@ package com.android.systemui.shade.data.repository +import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues +import com.android.systemui.display.data.repository.displayRepository import com.android.systemui.kosmos.testScope -import com.android.systemui.shade.display.ShadeDisplayPolicy -import com.android.systemui.shade.display.SpecificDisplayIdPolicy +import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy +import com.android.systemui.shade.display.DefaultDisplayShadePolicy +import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeGlobalSettings import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.flow.MutableStateFlow -import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith @@ -36,54 +39,72 @@ import org.junit.runner.RunWith class ShadeDisplaysRepositoryTest : SysuiTestCase() { private val kosmos = testKosmos() private val testScope = kosmos.testScope - private val defaultPolicy = SpecificDisplayIdPolicy(0) - - private val shadeDisplaysRepository = - ShadeDisplaysRepositoryImpl(defaultPolicy, testScope.backgroundScope) + private val globalSettings = kosmos.fakeGlobalSettings + private val displayRepository = kosmos.displayRepository + private val defaultPolicy = DefaultDisplayShadePolicy() + private val policies = kosmos.shadeDisplayPolicies + + private val underTest = + ShadeDisplaysRepositoryImpl( + globalSettings, + defaultPolicy, + testScope.backgroundScope, + policies, + ) @Test fun policy_changing_propagatedFromTheLatestPolicy() = testScope.runTest { - val displayIds by collectValues(shadeDisplaysRepository.displayId) - val policy1 = MutablePolicy() - val policy2 = MutablePolicy() + val displayIds by collectValues(underTest.displayId) assertThat(displayIds).containsExactly(0) - shadeDisplaysRepository.policy.value = policy1 + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display") - policy1.sendDisplayId(1) + displayRepository.addDisplay(displayId = 1) assertThat(displayIds).containsExactly(0, 1) - policy1.sendDisplayId(2) + displayRepository.addDisplay(displayId = 2) - assertThat(displayIds).containsExactly(0, 1, 2) + assertThat(displayIds).containsExactly(0, 1) - shadeDisplaysRepository.policy.value = policy2 + displayRepository.removeDisplay(displayId = 1) - assertThat(displayIds).containsExactly(0, 1, 2, 0) + assertThat(displayIds).containsExactly(0, 1, 2) - policy1.sendDisplayId(4) + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display") - // Changes to the first policy don't affect the output now assertThat(displayIds).containsExactly(0, 1, 2, 0) + } + + @Test + fun policy_updatesBasedOnSettingValue_defaultDisplay() = + testScope.runTest { + val policy by collectLastValue(underTest.policy) - policy2.sendDisplayId(5) + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "default_display") - assertThat(displayIds).containsExactly(0, 1, 2, 0, 5) + assertThat(policy).isInstanceOf(DefaultDisplayShadePolicy::class.java) } - private class MutablePolicy : ShadeDisplayPolicy { - fun sendDisplayId(id: Int) { - _displayId.value = id + @Test + fun policy_updatesBasedOnSettingValue_anyExternal() = + testScope.runTest { + val policy by collectLastValue(underTest.policy) + + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "any_external_display") + + assertThat(policy).isInstanceOf(AnyExternalShadeDisplayPolicy::class.java) } - private val _displayId = MutableStateFlow(0) - override val name: String - get() = "mutable_policy" + @Test + fun policy_updatesBasedOnSettingValue_focusBased() = + testScope.runTest { + val policy by collectLastValue(underTest.policy) - override val displayId: StateFlow<Int> - get() = _displayId - } + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, "status_bar_latest_touch") + + assertThat(policy).isInstanceOf(StatusBarTouchShadeDisplayPolicy::class.java) + } } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt index d584dc9ceef2..eeb3e6b31c69 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/data/repository/ShadePrimaryDisplayCommandTest.kt @@ -28,6 +28,7 @@ import com.android.systemui.shade.ShadePrimaryDisplayCommand import com.android.systemui.shade.display.ShadeDisplayPolicy import com.android.systemui.statusbar.commandline.commandRegistry import com.android.systemui.testKosmos +import com.android.systemui.util.settings.fakeGlobalSettings import com.google.common.truth.StringSubject import com.google.common.truth.Truth.assertThat import java.io.PrintWriter @@ -44,18 +45,17 @@ import org.junit.runner.RunWith class ShadePrimaryDisplayCommandTest : SysuiTestCase() { private val kosmos = testKosmos().useUnconfinedTestDispatcher() private val testScope = kosmos.testScope + private val globalSettings = kosmos.fakeGlobalSettings private val commandRegistry = kosmos.commandRegistry private val displayRepository = kosmos.displayRepository private val defaultPolicy = kosmos.defaultShadeDisplayPolicy - private val policy1 = makePolicy("policy_1") private val shadeDisplaysRepository = kosmos.shadeDisplaysRepository + private val policies = kosmos.shadeDisplayPolicies private val pw = PrintWriter(StringWriter()) - private val policies = - setOf(defaultPolicy, policy1, makePolicy("policy_2"), makePolicy("policy_3")) - private val underTest = ShadePrimaryDisplayCommand( + globalSettings, commandRegistry, displayRepository, shadeDisplaysRepository, @@ -69,30 +69,16 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { } @Test - fun commandDisplayOverride_updatesDisplayId() = - testScope.runTest { - val displayId by collectLastValue(shadeDisplaysRepository.displayId) - assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) - - val newDisplayId = 2 - commandRegistry.onShellCommand( - pw, - arrayOf("shade_display_override", newDisplayId.toString()), - ) - - assertThat(displayId).isEqualTo(newDisplayId) - } - - @Test fun commandShadeDisplayOverride_resetsDisplayId() = testScope.runTest { val displayId by collectLastValue(shadeDisplaysRepository.displayId) assertThat(displayId).isEqualTo(Display.DEFAULT_DISPLAY) val newDisplayId = 2 + displayRepository.addDisplay(displayId = newDisplayId) commandRegistry.onShellCommand( pw, - arrayOf("shade_display_override", newDisplayId.toString()), + arrayOf("shade_display_override", "any_external_display"), ) assertThat(displayId).isEqualTo(newDisplayId) @@ -108,7 +94,10 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { val newDisplayId = 2 displayRepository.addDisplay(displayId = newDisplayId) - commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", "any_external")) + commandRegistry.onShellCommand( + pw, + arrayOf("shade_display_override", "any_external_display"), + ) assertThat(displayId).isEqualTo(newDisplayId) } @@ -127,13 +116,14 @@ class ShadePrimaryDisplayCommandTest : SysuiTestCase() { } @Test - fun policies_setsSpecificPolicy() = + fun policies_setsNewPolicy() = testScope.runTest { val policy by collectLastValue(shadeDisplaysRepository.policy) + val newPolicy = policies.last().name - commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", policy1.name)) + commandRegistry.onShellCommand(pw, arrayOf("shade_display_override", newPolicy)) - assertThat(policy!!.name).isEqualTo(policy1.name) + assertThat(policy!!.name).isEqualTo(newPolicy) } private fun makePolicy(policyName: String): ShadeDisplayPolicy { diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt index 942ea65ec49e..e87077db8e75 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/ConfigurationControllerImplTest.kt @@ -21,11 +21,13 @@ import android.content.res.Configuration.UI_MODE_NIGHT_NO import android.content.res.Configuration.UI_MODE_NIGHT_YES import android.content.res.Configuration.UI_MODE_TYPE_CAR import android.os.LocaleList +import android.view.Display import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.statusbar.policy.ConfigurationController.ConfigurationListener import com.google.common.truth.Truth.assertThat +import java.util.Locale import org.junit.Before import org.junit.Ignore import org.junit.Test @@ -34,7 +36,6 @@ import org.mockito.Mockito.doAnswer import org.mockito.Mockito.mock import org.mockito.Mockito.never import org.mockito.Mockito.verify -import java.util.Locale @RunWith(AndroidJUnit4::class) @SmallTest @@ -64,9 +65,11 @@ class ConfigurationControllerImplTest : SysuiTestCase() { mConfigurationController.addCallback(listener2) doAnswer { - mConfigurationController.removeCallback(listener2) - null - }.`when`(listener).onThemeChanged() + mConfigurationController.removeCallback(listener2) + null + } + .`when`(listener) + .onThemeChanged() mConfigurationController.notifyThemeChanged() verify(listener).onThemeChanged() @@ -208,7 +211,6 @@ class ConfigurationControllerImplTest : SysuiTestCase() { assertThat(listener.maxBoundsChanged).isTrue() } - @Test fun localeListChanged_listenerNotified() { val config = mContext.resources.configuration @@ -289,7 +291,6 @@ class ConfigurationControllerImplTest : SysuiTestCase() { assertThat(listener.orientationChanged).isTrue() } - @Test fun multipleUpdates_listenerNotifiedOfAll() { val config = mContext.resources.configuration @@ -313,6 +314,17 @@ class ConfigurationControllerImplTest : SysuiTestCase() { } @Test + fun onMovedToDisplay_dispatchedToChildren() { + val config = mContext.resources.configuration + val listener = createAndAddListener() + + mConfigurationController.dispatchOnMovedToDisplay(newDisplayId = 1, config) + + assertThat(listener.display).isEqualTo(1) + assertThat(listener.changedConfig).isEqualTo(config) + } + + @Test @Ignore("b/261408895") fun equivalentConfigObject_listenerNotNotified() { val config = mContext.resources.configuration @@ -343,35 +355,49 @@ class ConfigurationControllerImplTest : SysuiTestCase() { var localeListChanged = false var layoutDirectionChanged = false var orientationChanged = false + var display = Display.DEFAULT_DISPLAY override fun onConfigChanged(newConfig: Configuration?) { changedConfig = newConfig } + override fun onDensityOrFontScaleChanged() { densityOrFontScaleChanged = true } + override fun onSmallestScreenWidthChanged() { smallestScreenWidthChanged = true } + override fun onMaxBoundsChanged() { maxBoundsChanged = true } + override fun onUiModeChanged() { uiModeChanged = true } + override fun onThemeChanged() { themeChanged = true } + override fun onLocaleListChanged() { localeListChanged = true } + override fun onLayoutDirectionChanged(isLayoutRtl: Boolean) { layoutDirectionChanged = true } + override fun onOrientationChanged(orientation: Int) { orientationChanged = true } + override fun onMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration?) { + display = newDisplayId + changedConfig = newConfiguration + } + fun assertNoMethodsCalled() { assertThat(densityOrFontScaleChanged).isFalse() assertThat(smallestScreenWidthChanged).isFalse() @@ -391,6 +417,7 @@ class ConfigurationControllerImplTest : SysuiTestCase() { themeChanged = false localeListChanged = false layoutDirectionChanged = false + display = Display.DEFAULT_DISPLAY } } } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt index 4d804d06fe87..747a2a9bd887 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/data/repository/ConfigurationRepository.kt @@ -53,8 +53,12 @@ interface ConfigurationRepository { val onConfigurationChange: Flow<Unit> val scaleForResolution: Flow<Float> + val configurationValues: Flow<Configuration> + /** Emits the latest display this configuration controller has been moved to. */ + val onMovedToDisplay: Flow<Int> + fun getResolutionScale(): Float /** Convenience to context.resources.getDimensionPixelSize() */ @@ -117,6 +121,20 @@ constructor( configurationController.addCallback(callback) awaitClose { configurationController.removeCallback(callback) } } + override val onMovedToDisplay: Flow<Int> + get() = conflatedCallbackFlow { + val callback = + object : ConfigurationController.ConfigurationListener { + override fun onMovedToDisplay( + newDisplayId: Int, + newConfiguration: Configuration?, + ) { + trySend(newDisplayId) + } + } + configurationController.addCallback(callback) + awaitClose { configurationController.removeCallback(callback) } + } override val scaleForResolution: StateFlow<Float> = onConfigurationChange diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt index 3020e5dedd17..b59713696055 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/model/InternalShortcutModels.kt @@ -49,7 +49,7 @@ data class InternalKeyboardShortcutGroup( * @param isCustomShortcut If Shortcut is user customized or system defined. */ data class InternalKeyboardShortcutInfo( - val label: String, + val label: String = "", val keycode: Int, val modifiers: Int, val baseCharacter: Char = Char.MIN_VALUE, @@ -60,4 +60,4 @@ data class InternalKeyboardShortcutInfo( data class InternalGroupsSource( val groups: List<InternalKeyboardShortcutGroup>, val type: ShortcutCategoryType, -)
\ No newline at end of file +) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt new file mode 100644 index 000000000000..b029b03ec8b8 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/AppLaunchDataRepository.kt @@ -0,0 +1,109 @@ +/* + * 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.systemui.keyboard.shortcut.data.repository + +import android.hardware.input.AppLaunchData +import android.hardware.input.InputGestureData.KeyTrigger +import android.hardware.input.InputManager +import android.util.Log +import android.view.InputDevice +import com.android.systemui.Flags.shortcutHelperKeyGlyph +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import javax.inject.Inject + +@SysUISingleton +class AppLaunchDataRepository +@Inject +constructor( + private val inputManager: InputManager, + @Background private val backgroundScope: CoroutineScope, + private val shortcutCategoriesUtils: ShortcutCategoriesUtils, + inputDeviceRepository: ShortcutHelperInputDeviceRepository, +) { + + private val shortcutCommandToAppLaunchDataMap: + StateFlow<Map<ShortcutCommandKey, AppLaunchData>> = + inputDeviceRepository.activeInputDevice + .map { inputDevice -> + if (inputDevice == null) { + emptyMap() + } + else{ + buildCommandToAppLaunchDataMap(inputDevice) + } + } + .stateIn( + scope = backgroundScope, + started = SharingStarted.Eagerly, + initialValue = mapOf(), + ) + + fun getAppLaunchDataForShortcutWithCommand(shortcutCommand: ShortcutCommand): AppLaunchData? { + val shortcutCommandAsKey = ShortcutCommandKey(shortcutCommand) + return shortcutCommandToAppLaunchDataMap.value[shortcutCommandAsKey] + } + + private fun buildCommandToAppLaunchDataMap(inputDevice: InputDevice): + Map<ShortcutCommandKey, AppLaunchData> { + val commandToAppLaunchDataMap = + mutableMapOf<ShortcutCommandKey, AppLaunchData>() + val appLaunchInputGestures = inputManager.appLaunchBookmarks + appLaunchInputGestures.forEach { inputGesture -> + val keyGlyphMap = + if (shortcutHelperKeyGlyph()) { + inputManager.getKeyGlyphMap(inputDevice.id) + } else null + + val shortcutCommand = + shortcutCategoriesUtils.toShortcutCommand( + keyGlyphMap, + inputDevice.keyCharacterMap, + inputGesture.trigger as KeyTrigger, + ) + + if (shortcutCommand != null) { + commandToAppLaunchDataMap[ShortcutCommandKey(shortcutCommand)] = + inputGesture.action.appLaunchData()!! + } else { + Log.w( + TAG, + "could not get Shortcut Command. inputGesture: $inputGesture", + ) + } + } + + return commandToAppLaunchDataMap + } + + private data class ShortcutCommandKey(val keys: List<ShortcutKey>) { + constructor( + shortcutCommand: ShortcutCommand + ) : this(shortcutCommand.keys.sortedBy { it.toString() }) + } + + private companion object { + private const val TAG = "AppLaunchDataRepository" + } +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt index 8afec04a621c..18ca877775df 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/CustomShortcutCategoriesRepository.kt @@ -30,48 +30,37 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.keyboard.shared.model.ShortcutCustomizationRequestResult import com.android.systemui.keyboard.shortcut.shared.model.KeyCombination import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategory +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo.SingleShortcutCustomization import com.android.systemui.keyboard.shortcut.shared.model.ShortcutKey import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine -import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext import javax.inject.Inject -import kotlin.coroutines.CoroutineContext @SysUISingleton class CustomShortcutCategoriesRepository @Inject constructor( - stateRepository: ShortcutHelperStateRepository, + inputDeviceRepository: ShortcutHelperInputDeviceRepository, @Background private val backgroundScope: CoroutineScope, - @Background private val bgCoroutineContext: CoroutineContext, private val shortcutCategoriesUtils: ShortcutCategoriesUtils, private val inputGestureDataAdapter: InputGestureDataAdapter, private val customInputGesturesRepository: CustomInputGesturesRepository, - private val inputManager: InputManager + private val inputManager: InputManager, + private val appLaunchDataRepository: AppLaunchDataRepository, ) : ShortcutCategoriesRepository { private val _selectedKeyCombination = MutableStateFlow<KeyCombination?>(null) private val _shortcutBeingCustomized = mutableStateOf<ShortcutCustomizationRequestInfo?>(null) - private val activeInputDevice = - stateRepository.state.map { - if (it is Active) { - withContext(bgCoroutineContext) { inputManager.getInputDevice(it.deviceId) } - } else { - null - } - } - val pressedKeys = _selectedKeyCombination - .combine(activeInputDevice) { keyCombination, inputDevice -> + .combine(inputDeviceRepository.activeInputDevice) { keyCombination, inputDevice -> if (inputDevice == null || keyCombination == null) { return@combine emptyList() } else { @@ -105,8 +94,10 @@ constructor( ) override val categories: Flow<List<ShortcutCategory>> = - combine(activeInputDevice, customInputGesturesRepository.customInputGestures) - { inputDevice, inputGestures -> + combine( + inputDeviceRepository.activeInputDevice, + customInputGesturesRepository.customInputGestures, + ) { inputDevice, inputGestures -> if (inputDevice == null) { emptyList() } else { @@ -147,10 +138,10 @@ constructor( fun buildInputGestureDataForShortcutBeingCustomized(): InputGestureData? { try { return Builder() - .addKeyGestureTypeFromShortcutLabel() + .addKeyGestureTypeForShortcutBeingCustomized() .addTriggerFromSelectedKeyCombination() + .addAppLaunchDataFromShortcutBeingCustomized() .build() - // TODO(b/379648200) add app launch data after dynamic label/icon mapping implementation } catch (e: IllegalArgumentException) { Log.w(TAG, "could not add custom shortcut: $e") return null @@ -158,9 +149,10 @@ constructor( } private fun retrieveInputGestureDataForShortcutBeingDeleted(): InputGestureData? { - val keyGestureType = getKeyGestureTypeFromShortcutBeingDeletedLabel() - return customInputGesturesRepository.retrieveCustomInputGestures() - .firstOrNull { it.action.keyGestureType() == keyGestureType } + val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized() + return customInputGesturesRepository.retrieveCustomInputGestures().firstOrNull { + it.action.keyGestureType() == keyGestureType + } } suspend fun confirmAndSetShortcutCurrentlyBeingCustomized(): @@ -183,8 +175,8 @@ constructor( return customInputGesturesRepository.resetAllCustomInputGestures() } - private fun Builder.addKeyGestureTypeFromShortcutLabel(): Builder { - val keyGestureType = getKeyGestureTypeFromShortcutBeingCustomizedLabel() + private fun Builder.addKeyGestureTypeForShortcutBeingCustomized(): Builder { + val keyGestureType = getKeyGestureTypeForShortcutBeingCustomized() if (keyGestureType == null) { Log.w( @@ -193,31 +185,28 @@ constructor( ) return this } - return setKeyGestureType(keyGestureType) } - @KeyGestureType - private fun getKeyGestureTypeFromShortcutBeingCustomizedLabel(): Int? { + private fun Builder.addAppLaunchDataFromShortcutBeingCustomized(): Builder { val shortcutBeingCustomized = - getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Add + (_shortcutBeingCustomized.value as? SingleShortcutCustomization) ?: return this - if (shortcutBeingCustomized == null) { - Log.w( - TAG, - "Requested key gesture type from label but shortcut being customized is null", - ) - return null + if (shortcutBeingCustomized.categoryType != ShortcutCategoryType.AppCategories) { + return this } - return inputGestureDataAdapter - .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label) + val defaultShortcutCommand = shortcutBeingCustomized.shortcutCommand + + val appLaunchData = + appLaunchDataRepository.getAppLaunchDataForShortcutWithCommand(defaultShortcutCommand) + + return if (appLaunchData == null) this else this.setAppLaunchData(appLaunchData) } @KeyGestureType - private fun getKeyGestureTypeFromShortcutBeingDeletedLabel(): Int? { - val shortcutBeingCustomized = - getShortcutBeingCustomized() as? ShortcutCustomizationRequestInfo.Delete + private fun getKeyGestureTypeForShortcutBeingCustomized(): Int? { + val shortcutBeingCustomized = getShortcutBeingCustomized() as? SingleShortcutCustomization if (shortcutBeingCustomized == null) { Log.w( @@ -227,8 +216,10 @@ constructor( return null } - return inputGestureDataAdapter - .getKeyGestureTypeFromShortcutLabel(shortcutBeingCustomized.label) + return inputGestureDataAdapter.getKeyGestureTypeForShortcut( + shortcutLabel = shortcutBeingCustomized.label, + shortcutCategoryType = shortcutBeingCustomized.categoryType, + ) } private fun Builder.addTriggerFromSelectedKeyCombination(): Builder { diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt index 5bb7cdd03b8f..db35d49e7598 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/DefaultShortcutCategoriesRepository.kt @@ -16,7 +16,6 @@ package com.android.systemui.keyboard.shortcut.data.repository -import android.hardware.input.InputManager import android.view.KeyboardShortcutGroup import android.view.KeyboardShortcutInfo import com.android.systemui.dagger.SysUISingleton @@ -36,29 +35,24 @@ import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType. import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.InputMethodEditor import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.MultiTasking import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType.System -import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active import javax.inject.Inject -import kotlinx.coroutines.CoroutineDispatcher import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.stateIn -import kotlinx.coroutines.withContext @SysUISingleton class DefaultShortcutCategoriesRepository @Inject constructor( @Background private val backgroundScope: CoroutineScope, - @Background private val backgroundDispatcher: CoroutineDispatcher, @SystemShortcuts private val systemShortcutsSource: KeyboardShortcutGroupsSource, @MultitaskingShortcuts private val multitaskingShortcutsSource: KeyboardShortcutGroupsSource, @AppCategoriesShortcuts private val appCategoriesShortcutsSource: KeyboardShortcutGroupsSource, @InputShortcuts private val inputShortcutsSource: KeyboardShortcutGroupsSource, @CurrentAppShortcuts private val currentAppShortcutsSource: KeyboardShortcutGroupsSource, - private val inputManager: InputManager, - stateRepository: ShortcutHelperStateRepository, + inputDeviceRepository: ShortcutHelperInputDeviceRepository, shortcutCategoriesUtils: ShortcutCategoriesUtils, ) : ShortcutCategoriesRepository { @@ -83,17 +77,8 @@ constructor( ), ) - private val activeInputDevice = - stateRepository.state.map { - if (it is Active) { - withContext(backgroundDispatcher) { inputManager.getInputDevice(it.deviceId) } - } else { - null - } - } - override val categories: Flow<List<ShortcutCategory>> = - activeInputDevice + inputDeviceRepository.activeInputDevice .map { inputDevice -> if (inputDevice == null) { return@map emptyList() diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt index df7101e21cce..6e754a37ebca 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/InputGestureDataAdapter.kt @@ -48,49 +48,54 @@ import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import javax.inject.Inject - +/** + * Serves as a bridge for converting InputGestureData API Models to Shortcut Helper Data Layer + * Models and vice versa. + */ class InputGestureDataAdapter @Inject constructor( private val userTracker: UserTracker, private val inputGestureMaps: InputGestureMaps, - private val context: Context + private val context: Context, ) { private val userContext: Context get() = userTracker.createCurrentUserContext(userTracker.userContext) - fun toInternalGroupSources( - inputGestures: List<InputGestureData> - ): List<InternalGroupsSource> { + fun toInternalGroupSources(inputGestures: List<InputGestureData>): List<InternalGroupsSource> { val ungroupedInternalGroupSources = inputGestures.mapNotNull { gestureData -> val keyTrigger = gestureData.trigger as KeyTrigger val keyGestureType = gestureData.action.keyGestureType() val appLaunchData: AppLaunchData? = gestureData.action.appLaunchData() fetchGroupLabelByGestureType(keyGestureType)?.let { groupLabel -> - toInternalKeyboardShortcutInfo( - keyGestureType, - keyTrigger, - appLaunchData - )?.let { internalKeyboardShortcutInfo -> - val group = - InternalKeyboardShortcutGroup( - label = groupLabel, - items = listOf(internalKeyboardShortcutInfo), - ) - - fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let { - InternalGroupsSource(groups = listOf(group), type = it) + toInternalKeyboardShortcutInfo(keyGestureType, keyTrigger, appLaunchData) + ?.let { internalKeyboardShortcutInfo -> + val group = + InternalKeyboardShortcutGroup( + label = groupLabel, + items = listOf(internalKeyboardShortcutInfo), + ) + + fetchShortcutCategoryTypeByGestureType(keyGestureType)?.let { + InternalGroupsSource(groups = listOf(group), type = it) + } } - } } } return ungroupedInternalGroupSources } - fun getKeyGestureTypeFromShortcutLabel(label: String): Int? { - return inputGestureMaps.shortcutLabelToKeyGestureTypeMap[label] + fun getKeyGestureTypeForShortcut( + shortcutLabel: String, + shortcutCategoryType: ShortcutCategoryType, + ): Int? { + if (shortcutCategoryType == ShortcutCategoryType.AppCategories) { + return KEY_GESTURE_TYPE_LAUNCH_APPLICATION + } + val result = inputGestureMaps.shortcutLabelToKeyGestureTypeMap[shortcutLabel] + return result } private fun toInternalKeyboardShortcutInfo( @@ -104,16 +109,14 @@ constructor( keycode = keyTrigger.keycode, modifiers = keyTrigger.modifierState, isCustomShortcut = true, - icon = appLaunchData?.let { fetchShortcutIconByAppLaunchData(appLaunchData) } + icon = appLaunchData?.let { fetchShortcutIconByAppLaunchData(appLaunchData) }, ) } return null } @SuppressLint("QueryPermissionsNeeded") - private fun fetchShortcutIconByAppLaunchData( - appLaunchData: AppLaunchData - ): Icon? { + private fun fetchShortcutIconByAppLaunchData(appLaunchData: AppLaunchData): Icon? { val intent = fetchIntentFromAppLaunchData(appLaunchData) ?: return null val resolvedActivity = resolveSingleMatchingActivityFrom(intent) @@ -132,7 +135,7 @@ constructor( private fun fetchShortcutLabelByGestureType( @KeyGestureType keyGestureType: Int, - appLaunchData: AppLaunchData? + appLaunchData: AppLaunchData?, ): String? { inputGestureMaps.gestureToInternalKeyboardShortcutInfoLabelResIdMap[keyGestureType]?.let { return context.getString(it) @@ -152,16 +155,14 @@ constructor( return if (resolvedActivity == null) { getIntentCategoryLabel(intent.selector?.categories?.iterator()?.next()) } else resolvedActivity.loadLabel(userContext.packageManager).toString() - } @SuppressLint("QueryPermissionsNeeded") private fun resolveSingleMatchingActivityFrom(intent: Intent): ActivityInfo? { val packageManager = userContext.packageManager - val resolvedActivity = intent.resolveActivityInfo( - packageManager, - /* flags= */ MATCH_DEFAULT_ONLY - ) ?: return null + val resolvedActivity = + intent.resolveActivityInfo(packageManager, /* flags= */ MATCH_DEFAULT_ONLY) + ?: return null val matchesMultipleActivities = ResolverActivity::class.qualifiedName.equals(resolvedActivity.name) @@ -172,22 +173,26 @@ constructor( } private fun getIntentCategoryLabel(category: String?): String? { - val categoryLabelRes = when (category.toString()) { - Intent.CATEGORY_APP_BROWSER -> R.string.keyboard_shortcut_group_applications_browser - Intent.CATEGORY_APP_CONTACTS -> R.string.keyboard_shortcut_group_applications_contacts - Intent.CATEGORY_APP_EMAIL -> R.string.keyboard_shortcut_group_applications_email - Intent.CATEGORY_APP_CALENDAR -> R.string.keyboard_shortcut_group_applications_calendar - Intent.CATEGORY_APP_MAPS -> R.string.keyboard_shortcut_group_applications_maps - Intent.CATEGORY_APP_MUSIC -> R.string.keyboard_shortcut_group_applications_music - Intent.CATEGORY_APP_MESSAGING -> R.string.keyboard_shortcut_group_applications_sms - Intent.CATEGORY_APP_CALCULATOR -> R.string.keyboard_shortcut_group_applications_calculator - else -> { - Log.w(TAG, ("No label for app category $category")) - null + val categoryLabelRes = + when (category.toString()) { + Intent.CATEGORY_APP_BROWSER -> R.string.keyboard_shortcut_group_applications_browser + Intent.CATEGORY_APP_CONTACTS -> + R.string.keyboard_shortcut_group_applications_contacts + Intent.CATEGORY_APP_EMAIL -> R.string.keyboard_shortcut_group_applications_email + Intent.CATEGORY_APP_CALENDAR -> + R.string.keyboard_shortcut_group_applications_calendar + Intent.CATEGORY_APP_MAPS -> R.string.keyboard_shortcut_group_applications_maps + Intent.CATEGORY_APP_MUSIC -> R.string.keyboard_shortcut_group_applications_music + Intent.CATEGORY_APP_MESSAGING -> R.string.keyboard_shortcut_group_applications_sms + Intent.CATEGORY_APP_CALCULATOR -> + R.string.keyboard_shortcut_group_applications_calculator + else -> { + Log.w(TAG, ("No label for app category $category")) + null + } } - } - return if (categoryLabelRes == null){ + return if (categoryLabelRes == null) { return null } else { context.getString(categoryLabelRes) @@ -196,41 +201,48 @@ constructor( private fun fetchIntentFromAppLaunchData(appLaunchData: AppLaunchData): Intent? { return when (appLaunchData) { - is CategoryData -> Intent.makeMainSelectorActivity( - /* selectorAction= */ ACTION_MAIN, - /* selectorCategory= */ appLaunchData.category - ) + is CategoryData -> + Intent.makeMainSelectorActivity( + /* selectorAction= */ ACTION_MAIN, + /* selectorCategory= */ appLaunchData.category, + ) is RoleData -> getRoleLaunchIntent(appLaunchData.role) - is ComponentData -> resolveComponentNameIntent( - packageName = appLaunchData.packageName, - className = appLaunchData.className - ) + is ComponentData -> + resolveComponentNameIntent( + packageName = appLaunchData.packageName, + className = appLaunchData.className, + ) else -> null } } private fun resolveComponentNameIntent(packageName: String, className: String): Intent? { - buildIntentFromComponentName(ComponentName(packageName, className))?.let { return it } - buildIntentFromComponentName(ComponentName( - userContext.packageManager.canonicalToCurrentPackageNames(arrayOf(packageName))[0], - className - ))?.let { return it } + buildIntentFromComponentName(ComponentName(packageName, className))?.let { + return it + } + buildIntentFromComponentName( + ComponentName( + userContext.packageManager + .canonicalToCurrentPackageNames(arrayOf(packageName))[0], + className, + ) + ) + ?.let { + return it + } return null } private fun buildIntentFromComponentName(componentName: ComponentName): Intent? { - try{ + try { val flags = MATCH_DIRECT_BOOT_UNAWARE or MATCH_DIRECT_BOOT_AWARE or MATCH_UNINSTALLED_PACKAGES // attempt to retrieve activity info to see if a NameNotFoundException is thrown. userContext.packageManager.getActivityInfo(componentName, flags) } catch (e: NameNotFoundException) { - Log.w( - TAG, - "Unable to find activity info for componentName: $componentName" - ) + Log.w(TAG, "Unable to find activity info for componentName: $componentName") return null } @@ -246,8 +258,9 @@ constructor( val roleManager = userContext.getSystemService(RoleManager::class.java)!! if (roleManager.isRoleAvailable(role)) { roleManager.getDefaultApplication(role)?.let { rolePackage -> - packageManager.getLaunchIntentForPackage(rolePackage)?.let { return it } - ?: Log.w(TAG, "No launch intent for role $role") + packageManager.getLaunchIntentForPackage(rolePackage)?.let { + return it + } ?: Log.w(TAG, "No launch intent for role $role") } ?: Log.w(TAG, "No default application for role $role, user= ${userContext.user}") } else { Log.w(TAG, "Role $role is not available.") @@ -264,4 +277,4 @@ constructor( private companion object { private const val TAG = "InputGestureDataUtils" } -}
\ No newline at end of file +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt index 4a725ec8abad..cf5460fef0e2 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutCategoriesUtils.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyboard.shortcut.data.repository import android.content.Context import android.graphics.drawable.Icon +import android.hardware.input.InputGestureData.KeyTrigger import android.hardware.input.InputManager import android.hardware.input.KeyGlyphMap import android.util.Log @@ -137,8 +138,7 @@ constructor( label = shortcutInfo.label, icon = toShortcutIcon(keepIcon, shortcutInfo), commands = listOf(shortcutCommand), - isCustomizable = - shortcutHelperExclusions.isShortcutCustomizable(shortcutInfo.label), + isCustomizable = shortcutHelperExclusions.isShortcutCustomizable(shortcutInfo.label), ) } @@ -158,6 +158,22 @@ constructor( return ShortcutIcon(packageName = icon.resPackage, resourceId = icon.resId) } + fun toShortcutCommand( + keyGlyphMap: KeyGlyphMap?, + keyCharacterMap: KeyCharacterMap, + keyTrigger: KeyTrigger, + ): ShortcutCommand? { + return toShortcutCommand( + keyGlyphMap = keyGlyphMap, + keyCharacterMap = keyCharacterMap, + info = + InternalKeyboardShortcutInfo( + keycode = keyTrigger.keycode, + modifiers = keyTrigger.modifierState, + ), + ) + } + private fun toShortcutCommand( keyGlyphMap: KeyGlyphMap?, keyCharacterMap: KeyCharacterMap, diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt new file mode 100644 index 000000000000..13613733c2bc --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/data/repository/ShortcutHelperInputDeviceRepository.kt @@ -0,0 +1,48 @@ +/* + * 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.systemui.keyboard.shortcut.data.repository + +import android.hardware.input.InputManager +import com.android.systemui.dagger.qualifiers.Background +import com.android.systemui.keyboard.shortcut.shared.model.ShortcutHelperState.Active +import javax.inject.Inject +import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.withContext + +class ShortcutHelperInputDeviceRepository +@Inject +constructor( + stateRepository: ShortcutHelperStateRepository, + @Background private val backgroundScope: CoroutineScope, + @Background private val bgCoroutineContext: CoroutineContext, + private val inputManager: InputManager, +) { + val activeInputDevice = + stateRepository.state + .map { + if (it is Active) { + withContext(bgCoroutineContext) { inputManager.getInputDevice(it.deviceId) } + } else { + null + } + } + .stateIn(scope = backgroundScope, started = SharingStarted.Lazily, initialValue = null) +} diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt index c7e6b43b9624..d8bad2590280 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCommand.kt @@ -18,7 +18,10 @@ package com.android.systemui.keyboard.shortcut.shared.model import androidx.annotation.DrawableRes -data class ShortcutCommand(val keys: List<ShortcutKey>, val isCustom: Boolean = false) +data class ShortcutCommand( + val keys: List<ShortcutKey> = emptyList(), + val isCustom: Boolean = false, +) class ShortcutCommandBuilder { private val keys = mutableListOf<ShortcutKey>() diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt index 095de41237cf..f183247bb355 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/shared/model/ShortcutCustomizationRequestInfo.kt @@ -17,17 +17,27 @@ package com.android.systemui.keyboard.shortcut.shared.model sealed interface ShortcutCustomizationRequestInfo { - data class Add( - val label: String = "", - val categoryType: ShortcutCategoryType = ShortcutCategoryType.System, - val subCategoryLabel: String = "", - ) : ShortcutCustomizationRequestInfo - data class Delete( - val label: String = "", - val categoryType: ShortcutCategoryType = ShortcutCategoryType.System, - val subCategoryLabel: String = "", - ) : ShortcutCustomizationRequestInfo + sealed interface SingleShortcutCustomization: ShortcutCustomizationRequestInfo { + val label: String + val categoryType: ShortcutCategoryType + val subCategoryLabel: String + val shortcutCommand: ShortcutCommand + + data class Add( + override val label: String = "", + override val categoryType: ShortcutCategoryType = ShortcutCategoryType.System, + override val subCategoryLabel: String = "", + override val shortcutCommand: ShortcutCommand = ShortcutCommand(), + ) : SingleShortcutCustomization + + data class Delete( + override val label: String = "", + override val categoryType: ShortcutCategoryType = ShortcutCategoryType.System, + override val subCategoryLabel: String = "", + override val shortcutCommand: ShortcutCommand = ShortcutCommand(), + ) : SingleShortcutCustomization + } data object Reset : ShortcutCustomizationRequestInfo } diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt index af6f0cb2ec83..aea583d67289 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/composable/ShortcutHelper.kt @@ -115,7 +115,6 @@ import androidx.compose.ui.util.fastForEach import androidx.compose.ui.util.fastForEachIndexed import com.android.compose.modifiers.thenIf import com.android.compose.ui.graphics.painter.rememberDrawablePainter -import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCategoryType import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCommand import com.android.systemui.keyboard.shortcut.shared.model.ShortcutCustomizationRequestInfo @@ -127,6 +126,7 @@ import com.android.systemui.keyboard.shortcut.ui.model.ShortcutCategoryUi import com.android.systemui.keyboard.shortcut.ui.model.ShortcutsUiState import com.android.systemui.res.R import kotlinx.coroutines.delay +import com.android.systemui.keyboard.shortcut.shared.model.Shortcut as ShortcutModel @Composable fun ShortcutHelper( @@ -505,10 +505,10 @@ private fun EndSidePanel( isCustomizing = isCustomizing and category.type.includeInCustomization, onCustomizationRequested = { requestInfo -> when (requestInfo) { - is ShortcutCustomizationRequestInfo.Add -> + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> onCustomizationRequested(requestInfo.copy(categoryType = category.type)) - is ShortcutCustomizationRequestInfo.Delete -> + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> onCustomizationRequested(requestInfo.copy(categoryType = category.type)) ShortcutCustomizationRequestInfo.Reset -> @@ -568,12 +568,12 @@ private fun SubCategoryContainerDualPane( isCustomizing = isCustomizing && shortcut.isCustomizable, onCustomizationRequested = { requestInfo -> when (requestInfo) { - is ShortcutCustomizationRequestInfo.Add -> + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> onCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) - is ShortcutCustomizationRequestInfo.Delete -> + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> onCustomizationRequested( requestInfo.copy(subCategoryLabel = subCategory.label) ) @@ -644,12 +644,18 @@ private fun Shortcut( isCustomizing = isCustomizing, onAddShortcutRequested = { onCustomizationRequested( - ShortcutCustomizationRequestInfo.Add(label = shortcut.label) + ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add( + label = shortcut.label, + shortcutCommand = shortcut.commands.first(), + ) ) }, onDeleteShortcutRequested = { onCustomizationRequested( - ShortcutCustomizationRequestInfo.Delete(label = shortcut.label) + ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete( + label = shortcut.label, + shortcutCommand = shortcut.commands.first(), + ) ) }, ) diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt index 92e25929fe4f..373eb250d61d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyboard/shortcut/ui/viewmodel/ShortcutCustomizationViewModel.kt @@ -66,8 +66,9 @@ constructor( } fun onShortcutCustomizationRequested(requestInfo: ShortcutCustomizationRequestInfo) { + shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) when (requestInfo) { - is ShortcutCustomizationRequestInfo.Add -> { + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Add -> { _shortcutCustomizationUiState.value = AddShortcutDialog( shortcutLabel = requestInfo.label, @@ -75,12 +76,10 @@ constructor( shortcutCustomizationInteractor.getDefaultCustomShortcutModifierKey(), pressedKeys = emptyList(), ) - shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) } - is ShortcutCustomizationRequestInfo.Delete -> { + is ShortcutCustomizationRequestInfo.SingleShortcutCustomization.Delete -> { _shortcutCustomizationUiState.value = DeleteShortcutDialog - shortcutCustomizationInteractor.onCustomizationRequested(requestInfo) } ShortcutCustomizationRequestInfo.Reset -> { diff --git a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java index 3c03d2830327..a7b51faaed57 100644 --- a/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java +++ b/packages/SystemUI/src/com/android/systemui/scrim/ScrimDrawable.java @@ -35,7 +35,7 @@ import android.view.animation.DecelerateInterpolator; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.graphics.ColorUtils; import com.android.systemui.statusbar.notification.stack.StackStateAnimator; -import static com.android.systemui.Flags.notificationShadeBlur; +import com.android.systemui.window.flag.WindowBlurFlag; /** * Drawable used on SysUI scrims. @@ -214,8 +214,9 @@ public class ScrimDrawable extends Drawable { public void draw(@NonNull Canvas canvas) { mPaint.setColor(mMainColor); mPaint.setAlpha(mAlpha); - if (notificationShadeBlur()) { + if (WindowBlurFlag.isEnabled()) { // TODO(b/370555223): Match the alpha to the visual spec when it is finalized. + // TODO (b/381263600), wire this at ScrimController, move it to PrimaryBouncerTransition mPaint.setAlpha((int) (0.5f * mAlpha)); } if (mConcaveInfo != null) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java index bf672be3c8d0..0a9aa9b49e8c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowView.java @@ -169,6 +169,10 @@ public class NotificationShadeWindowView extends WindowRootView { public void onMovedToDisplay(int displayId, Configuration config) { super.onMovedToDisplay(displayId, config); ShadeWindowGoesAround.isUnexpectedlyInLegacyMode(); + ShadeTraceLogger.INSTANCE.logOnMovedToDisplay(displayId, config); + if (mConfigurationForwarder != null) { + mConfigurationForwarder.dispatchOnMovedToDisplay(displayId, config); + } // When the window is moved we're only receiving a call to this method instead of the // onConfigurationChange itself. Let's just trigegr a normal config change. onConfigurationChanged(config); @@ -177,6 +181,7 @@ public class NotificationShadeWindowView extends WindowRootView { @Override protected void onConfigurationChanged(Configuration newConfig) { super.onConfigurationChanged(newConfig); + ShadeTraceLogger.INSTANCE.logOnConfigChanged(newConfig); if (mConfigurationForwarder != null) { ShadeWindowGoesAround.isUnexpectedlyInLegacyMode(); mConfigurationForwarder.onConfigurationChanged(newConfig); diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt index a54f6b9c6743..7bfe40c3d811 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadePrimaryDisplayCommand.kt @@ -16,23 +16,23 @@ package com.android.systemui.shade -import android.view.Display +import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS import com.android.systemui.CoreStartable import com.android.systemui.dagger.SysUISingleton import com.android.systemui.display.data.repository.DisplayRepository import com.android.systemui.shade.data.repository.MutableShadeDisplaysRepository import com.android.systemui.shade.display.ShadeDisplayPolicy -import com.android.systemui.shade.display.SpecificDisplayIdPolicy import com.android.systemui.statusbar.commandline.Command import com.android.systemui.statusbar.commandline.CommandRegistry +import com.android.systemui.util.settings.GlobalSettings import java.io.PrintWriter import javax.inject.Inject -import kotlin.text.toIntOrNull @SysUISingleton class ShadePrimaryDisplayCommand @Inject constructor( + private val globalSettings: GlobalSettings, private val commandRegistry: CommandRegistry, private val displaysRepository: DisplayRepository, private val positionRepository: MutableShadeDisplaysRepository, @@ -45,7 +45,7 @@ constructor( } override fun help(pw: PrintWriter) { - pw.println("shade_display_override (<displayId>|<policyName>) ") + pw.println("shade_display_override <policyName> ") pw.println("Set the display which is holding the shade, or the policy that defines it.") pw.println() pw.println("shade_display_override policies") @@ -56,9 +56,6 @@ constructor( pw.println() pw.println("shade_display_override (list|status) ") pw.println("Lists available displays and which has the shade") - pw.println() - pw.println("shade_display_override any_external") - pw.println("Moves the shade to the first not-default display available") } override fun execute(pw: PrintWriter, args: List<String>) { @@ -74,28 +71,24 @@ constructor( fun execute() { when (val command = args.getOrNull(0)?.lowercase()) { "reset" -> reset() + "policies" -> printPolicies() "list", "status" -> printStatus() - "policies" -> printPolicies() - "any_external" -> anyExternal() null -> help(pw) else -> parsePolicy(command) } } private fun parsePolicy(policyIdentifier: String) { - val displayId = policyIdentifier.toIntOrNull() - when { - displayId != null -> changeDisplay(displayId = displayId) - policies.any { it.name == policyIdentifier } -> { - positionRepository.policy.value = policies.first { it.name == policyIdentifier } - } - else -> help(pw) + if (policies.any { it.name == policyIdentifier }) { + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, policyIdentifier) + } else { + help(pw) } } private fun reset() { - positionRepository.policy.value = defaultPolicy + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, defaultPolicy.name) pw.println("Reset shade display policy to default policy: ${defaultPolicy.name}") } @@ -117,30 +110,5 @@ constructor( pw.println(if (currentPolicyName == it.name) " (Current policy)" else "") } } - - private fun anyExternal() { - val anyExternalDisplay = - displaysRepository.displays.value.firstOrNull { - it.displayId != Display.DEFAULT_DISPLAY - } - if (anyExternalDisplay == null) { - pw.println("No external displays available.") - return - } - setDisplay(anyExternalDisplay.displayId) - } - - private fun changeDisplay(displayId: Int) { - if (displayId < 0) { - pw.println("Error: display id should be positive integer") - } - - setDisplay(displayId) - } - - private fun setDisplay(id: Int) { - positionRepository.policy.value = SpecificDisplayIdPolicy(id) - pw.println("New shade primary display id is $id") - } } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt new file mode 100644 index 000000000000..a596d4f64638 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeTraceLogger.kt @@ -0,0 +1,59 @@ +/* + * 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.systemui.shade + +import android.content.res.Configuration +import android.os.Trace +import com.android.app.tracing.TraceUtils.traceAsync + +/** + * Centralized logging for shade-related events to a dedicated Perfetto track. + * + * Used by shade components to log events to a track named [TAG]. This consolidates shade-specific + * events into a single track for easier analysis in Perfetto, rather than scattering them across + * various threads' logs. + */ +object ShadeTraceLogger { + private const val TAG = "ShadeTraceLogger" + + fun logOnMovedToDisplay(displayId: Int, config: Configuration) { + if (!Trace.isEnabled()) return + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + TAG, + "onMovedToDisplay(displayId=$displayId, dpi=" + config.densityDpi + ")", + ) + } + + fun logOnConfigChanged(config: Configuration) { + if (!Trace.isEnabled()) return + Trace.instantForTrack( + Trace.TRACE_TAG_APP, + TAG, + "onConfigurationChanged(dpi=" + config.densityDpi + ")", + ) + } + + fun logMoveShadeWindowTo(displayId: Int) { + if (!Trace.isEnabled()) return + Trace.instantForTrack(Trace.TRACE_TAG_APP, TAG, "moveShadeWindowTo(displayId=$displayId)") + } + + fun traceReparenting(r: () -> Unit) { + traceAsync(TAG, { "reparenting" }) { r() } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt index 756241e9b071..af48231e0a99 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepository.kt @@ -16,17 +16,22 @@ package com.android.systemui.shade.data.repository +import android.provider.Settings.Global.DEVELOPMENT_SHADE_DISPLAY_AWARENESS import android.view.Display import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.shade.display.ShadeDisplayPolicy +import com.android.systemui.util.settings.GlobalSettings +import com.android.systemui.util.settings.SettingsProxyExt.observerFlow import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi -import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn /** Source of truth for the display currently holding the shade. */ @@ -38,7 +43,7 @@ interface ShadeDisplaysRepository { /** Allows to change the policy that determines in which display the Shade window is visible. */ interface MutableShadeDisplaysRepository : ShadeDisplaysRepository { /** Updates the policy to select where the shade is visible. */ - val policy: MutableStateFlow<ShadeDisplayPolicy> + val policy: StateFlow<ShadeDisplayPolicy> } /** Keeps the policy and propagates the display id for the shade from it. */ @@ -46,9 +51,27 @@ interface MutableShadeDisplaysRepository : ShadeDisplaysRepository { @OptIn(ExperimentalCoroutinesApi::class) class ShadeDisplaysRepositoryImpl @Inject -constructor(defaultPolicy: ShadeDisplayPolicy, @Background bgScope: CoroutineScope) : - MutableShadeDisplaysRepository { - override val policy = MutableStateFlow<ShadeDisplayPolicy>(defaultPolicy) +constructor( + globalSettings: GlobalSettings, + defaultPolicy: ShadeDisplayPolicy, + @Background bgScope: CoroutineScope, + policies: Set<@JvmSuppressWildcards ShadeDisplayPolicy>, +) : MutableShadeDisplaysRepository { + + override val policy: StateFlow<ShadeDisplayPolicy> = + globalSettings + .observerFlow(DEVELOPMENT_SHADE_DISPLAY_AWARENESS) + .onStart { emit(Unit) } + .map { + val current = globalSettings.getString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS) + for (policy in policies) { + if (policy.name == current) return@map policy + } + globalSettings.putString(DEVELOPMENT_SHADE_DISPLAY_AWARENESS, defaultPolicy.name) + return@map defaultPolicy + } + .distinctUntilChanged() + .stateIn(bgScope, SharingStarted.WhileSubscribed(), defaultPolicy) override val displayId: StateFlow<Int> = policy diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/SpecificDisplayIdPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/DefaultDisplayShadePolicy.kt index d43aad70368e..3819c6ffae08 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/SpecificDisplayIdPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/DefaultDisplayShadePolicy.kt @@ -21,12 +21,9 @@ import javax.inject.Inject import kotlinx.coroutines.flow.MutableStateFlow import kotlinx.coroutines.flow.StateFlow -/** Policy to specify a display id explicitly. */ -open class SpecificDisplayIdPolicy(id: Int) : ShadeDisplayPolicy { - override val name: String = "display_${id}_policy" +/** Policy to specify a default display explicitly. */ +class DefaultDisplayShadePolicy @Inject constructor() : ShadeDisplayPolicy { + override val name: String = "default_display" - override val displayId: StateFlow<Int> = MutableStateFlow(id) + override val displayId: StateFlow<Int> = MutableStateFlow(Display.DEFAULT_DISPLAY) } - -class DefaultDisplayShadePolicy @Inject constructor() : - SpecificDisplayIdPolicy(Display.DEFAULT_DISPLAY) diff --git a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt index bb96b0b3ce50..17b5e5b584b4 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/display/ShadeDisplayPolicy.kt @@ -23,6 +23,10 @@ import kotlinx.coroutines.flow.StateFlow /** Describes the display the shade should be shown in. */ interface ShadeDisplayPolicy { + /** + * String used to identify each policy and used to set policy via adb command. This value must + * match a value defined in the SettingsLib shade_display_awareness_values string array. + */ val name: String /** The display id the shade should be at, according to this policy. */ diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt index 08c03e28d596..8d536accaf76 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeDisplaysInteractor.kt @@ -27,6 +27,8 @@ import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.scene.ui.view.WindowRootView import com.android.systemui.shade.ShadeDisplayAware +import com.android.systemui.shade.ShadeTraceLogger.logMoveShadeWindowTo +import com.android.systemui.shade.ShadeTraceLogger.traceReparenting import com.android.systemui.shade.data.repository.ShadeDisplaysRepository import com.android.systemui.shade.shared.flag.ShadeWindowGoesAround import com.android.systemui.util.kotlin.getOrNull @@ -68,6 +70,7 @@ constructor( /** Tries to move the shade. If anything wrong happens, fails gracefully without crashing. */ private suspend fun moveShadeWindowTo(destinationId: Int) { Log.d(TAG, "Trying to move shade window to display with id $destinationId") + logMoveShadeWindowTo(destinationId) // Why using the shade context here instead of the view's Display? // The context's display is updated before the view one, so it is a better indicator of // which display the shade is supposed to be at. The View display is updated after the first @@ -83,7 +86,9 @@ constructor( return } try { - withContext(mainThreadContext) { reparentToDisplayId(id = destinationId) } + withContext(mainThreadContext) { + traceReparenting { reparentToDisplayId(id = destinationId) } + } } catch (e: IllegalStateException) { Log.e( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt index 684466ad839b..3408f4ffd082 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/NotificationShadeDepthController.kt @@ -34,6 +34,7 @@ import androidx.dynamicanimation.animation.SpringAnimation import androidx.dynamicanimation.animation.SpringForce import com.android.app.animation.Interpolators import com.android.systemui.Dumpable +import com.android.systemui.Flags.notificationShadeBlur import com.android.systemui.animation.ShadeInterpolation import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dump.DumpManager @@ -53,7 +54,6 @@ import java.io.PrintWriter import javax.inject.Inject import kotlin.math.max import kotlin.math.sign -import com.android.systemui.Flags.notificationShadeBlur /** * Responsible for blurring the notification shade window, and applying a zoom effect to the @@ -212,19 +212,13 @@ constructor( shadeRadius = 0f } - var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(shadeRadius)) var blur = shadeRadius.toInt() - - if (inSplitShade) { - zoomOut = 0f - } - + val zoomOut = blurRadiusToZoomOut(blurRadius = shadeRadius) // Make blur be 0 if it is necessary to stop blur effect. if (scrimsVisible) { if (!notificationShadeBlur()) { blur = 0 } - zoomOut = 0f } if (!blurUtils.supportsBlursOnWindows()) { @@ -237,24 +231,43 @@ constructor( return Pair(blur, zoomOut) } + private fun blurRadiusToZoomOut(blurRadius: Float): Float { + var zoomOut = MathUtils.saturate(blurUtils.ratioOfBlurRadius(blurRadius)) + if (inSplitShade) { + zoomOut = 0f + } + + if (scrimsVisible) { + zoomOut = 0f + } + return zoomOut + } + + private val shouldBlurBeOpaque: Boolean + get() = if (notificationShadeBlur()) false else scrimsVisible && !blursDisabledForAppLaunch + /** Callback that updates the window blur value and is called only once per frame. */ @VisibleForTesting val updateBlurCallback = Choreographer.FrameCallback { updateScheduled = false - val (blur, zoomOut) = computeBlurAndZoomOut() - val opaque = if (notificationShadeBlur()) false else scrimsVisible && !blursDisabledForAppLaunch + val (blur, zoomOutFromShadeRadius) = computeBlurAndZoomOut() + val opaque = shouldBlurBeOpaque Trace.traceCounter(Trace.TRACE_TAG_APP, "shade_blur_radius", blur) blurUtils.applyBlur(root.viewRootImpl, blur, opaque) - lastAppliedBlur = blur - wallpaperController.setNotificationShadeZoom(zoomOut) - listeners.forEach { - it.onWallpaperZoomOutChanged(zoomOut) - it.onBlurRadiusChanged(blur) - } - notificationShadeWindowController.setBackgroundBlurRadius(blur) + onBlurApplied(blur, zoomOutFromShadeRadius) } + private fun onBlurApplied(appliedBlurRadius: Int, zoomOutFromShadeRadius: Float) { + lastAppliedBlur = appliedBlurRadius + wallpaperController.setNotificationShadeZoom(zoomOutFromShadeRadius) + listeners.forEach { + it.onWallpaperZoomOutChanged(zoomOutFromShadeRadius) + it.onBlurRadiusChanged(appliedBlurRadius) + } + notificationShadeWindowController.setBackgroundBlurRadius(appliedBlurRadius) + } + /** Animate blurs when unlocking. */ private val keyguardStateCallback = object : KeyguardStateController.Callback { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt index 858cac111525..9c7af181284e 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationControllerImpl.kt @@ -65,6 +65,13 @@ constructor(@Assisted private val context: Context) : listeners.filterForEach({ this.listeners.contains(it) }) { it.onThemeChanged() } } + override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) { + val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } + listeners.filterForEach({ this.listeners.contains(it) }) { + it.onMovedToDisplay(newDisplayId, newConfiguration) + } + } + override fun onConfigurationChanged(newConfig: Configuration) { // Avoid concurrent modification exception val listeners = synchronized(this.listeners) { ArrayList(this.listeners) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt index 3fd46fc484a9..537e3e1893b9 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ConfigurationForwarder.kt @@ -28,4 +28,13 @@ import android.content.res.Configuration interface ConfigurationForwarder { /** Should be called when a new configuration is received. */ fun onConfigurationChanged(newConfiguration: Configuration) + + /** + * Should be called when the view associated to this configuration forwarded moved to another + * display, usually as a consequence of [View.onMovedToDisplay]. + * + * For the default configuration forwarder (associated with the global configuration) this is + * never expected to be called. + */ + fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java index 1bb4e8c66ef1..c77f6c1b8552 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ConfigurationController.java @@ -45,5 +45,6 @@ public interface ConfigurationController extends CallbackController<Configuratio default void onLocaleListChanged() {} default void onLayoutDirectionChanged(boolean isLayoutRtl) {} default void onOrientationChanged(int orientation) {} + default void onMovedToDisplay(int newDisplayId, Configuration newConfiguration) {} } } diff --git a/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt b/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt new file mode 100644 index 000000000000..8b6c8601f5d2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/window/flag/WindowBlurFlag.kt @@ -0,0 +1,32 @@ +/* + * 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.systemui.window.flag + +import com.android.systemui.Flags + +/** + * Flag that controls whether the background surface is blurred or not while on the + * lockscreen/shade/bouncer. This makes the background of scrim, bouncer and few other opaque + * surfaces transparent so that we can see the blur effect on the background surface (wallpaper). + */ +object WindowBlurFlag { + /** Whether the blur is enabled or not */ + @JvmStatic + val isEnabled + // Add flags here that require scrims/background surfaces to be transparent. + get() = Flags.notificationShadeBlur() || Flags.bouncerUiRevamp() +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt index 4d74254cf9f8..487049740079 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/common/ui/data/repository/FakeConfigurationRepository.kt @@ -17,6 +17,7 @@ package com.android.systemui.common.ui.data.repository import android.content.res.Configuration +import android.view.Display import com.android.systemui.dagger.SysUISingleton import dagger.Binds import dagger.Module @@ -25,6 +26,7 @@ import kotlinx.coroutines.channels.BufferOverflow import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.MutableSharedFlow import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.asSharedFlow import kotlinx.coroutines.flow.asStateFlow @@ -46,6 +48,10 @@ class FakeConfigurationRepository @Inject constructor() : ConfigurationRepositor override val configurationValues: Flow<Configuration> = _configurationChangeValues.asSharedFlow() + private val _onMovedToDisplay = MutableStateFlow<Int>(Display.DEFAULT_DISPLAY) + override val onMovedToDisplay: StateFlow<Int> + get() = _onMovedToDisplay + private val _scaleForResolution = MutableStateFlow(1f) override val scaleForResolution: Flow<Float> = _scaleForResolution.asStateFlow() @@ -64,6 +70,10 @@ class FakeConfigurationRepository @Inject constructor() : ConfigurationRepositor onAnyConfigurationChange() } + fun onMovedToDisplay(newDisplayId: Int) { + _onMovedToDisplay.value = newDisplayId + } + fun setScaleForResolution(scale: Float) { _scaleForResolution.value = scale } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt index 4cb8a416124f..2641070a1a59 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyboard/shortcut/KeyboardShortcutHelperKosmos.kt @@ -22,12 +22,14 @@ import android.content.res.mainResources import android.hardware.input.fakeInputManager import android.view.windowManager import com.android.systemui.broadcast.broadcastDispatcher +import com.android.systemui.keyboard.shortcut.data.repository.AppLaunchDataRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomInputGesturesRepository import com.android.systemui.keyboard.shortcut.data.repository.CustomShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.DefaultShortcutCategoriesRepository import com.android.systemui.keyboard.shortcut.data.repository.InputGestureDataAdapter import com.android.systemui.keyboard.shortcut.data.repository.InputGestureMaps import com.android.systemui.keyboard.shortcut.data.repository.ShortcutCategoriesUtils +import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperInputDeviceRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperStateRepository import com.android.systemui.keyboard.shortcut.data.repository.ShortcutHelperTestHelper import com.android.systemui.keyboard.shortcut.data.source.AppCategoriesShortcutsSource @@ -47,6 +49,7 @@ import com.android.systemui.keyguard.data.repository.fakeCommandQueue import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.kosmos.backgroundCoroutineContext +import com.android.systemui.kosmos.backgroundScope import com.android.systemui.kosmos.testDispatcher import com.android.systemui.kosmos.testScope import com.android.systemui.model.sysUiState @@ -99,35 +102,54 @@ val Kosmos.defaultShortcutCategoriesRepository by Kosmos.Fixture { DefaultShortcutCategoriesRepository( applicationCoroutineScope, - testDispatcher, shortcutHelperSystemShortcutsSource, shortcutHelperMultiTaskingShortcutsSource, shortcutHelperAppCategoriesShortcutsSource, shortcutHelperInputShortcutsSource, shortcutHelperCurrentAppShortcutsSource, - fakeInputManager.inputManager, - shortcutHelperStateRepository, + shortcutHelperInputDeviceRepository, shortcutCategoriesUtils, ) } val Kosmos.inputGestureMaps by Kosmos.Fixture { InputGestureMaps(applicationContext) } -val Kosmos.inputGestureDataAdapter by Kosmos.Fixture { InputGestureDataAdapter(userTracker, inputGestureMaps, applicationContext)} +val Kosmos.inputGestureDataAdapter by + Kosmos.Fixture { InputGestureDataAdapter(userTracker, inputGestureMaps, applicationContext) } val Kosmos.customInputGesturesRepository by Kosmos.Fixture { CustomInputGesturesRepository(userTracker, testDispatcher) } +val Kosmos.shortcutHelperInputDeviceRepository by + Kosmos.Fixture { + ShortcutHelperInputDeviceRepository( + shortcutHelperStateRepository, + backgroundScope, + backgroundCoroutineContext, + fakeInputManager.inputManager, + ) + } + +val Kosmos.appLaunchDataRepository by + Kosmos.Fixture { + AppLaunchDataRepository( + fakeInputManager.inputManager, + backgroundScope, + shortcutCategoriesUtils, + shortcutHelperInputDeviceRepository, + ) + } + val Kosmos.customShortcutCategoriesRepository by Kosmos.Fixture { CustomShortcutCategoriesRepository( - shortcutHelperStateRepository, + shortcutHelperInputDeviceRepository, applicationCoroutineScope, - testDispatcher, shortcutCategoriesUtils, inputGestureDataAdapter, customInputGesturesRepository, fakeInputManager.inputManager, + appLaunchDataRepository, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt index 7488397dde1f..636cb37adf03 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/data/repository/ShadeDisplaysRepositoryKosmos.kt @@ -16,20 +16,53 @@ package com.android.systemui.shade.data.repository -import android.view.Display +import com.android.systemui.display.data.repository.displayRepository +import com.android.systemui.keyguard.data.repository.keyguardRepository import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope +import com.android.systemui.shade.display.AnyExternalShadeDisplayPolicy +import com.android.systemui.shade.display.DefaultDisplayShadePolicy import com.android.systemui.shade.display.ShadeDisplayPolicy -import com.android.systemui.shade.display.SpecificDisplayIdPolicy +import com.android.systemui.shade.display.StatusBarTouchShadeDisplayPolicy +import com.android.systemui.util.settings.fakeGlobalSettings -val Kosmos.defaultShadeDisplayPolicy: ShadeDisplayPolicy by - Kosmos.Fixture { SpecificDisplayIdPolicy(Display.DEFAULT_DISPLAY) } +val Kosmos.defaultShadeDisplayPolicy: DefaultDisplayShadePolicy by + Kosmos.Fixture { DefaultDisplayShadePolicy() } + +val Kosmos.anyExternalShadeDisplayPolicy: AnyExternalShadeDisplayPolicy by + Kosmos.Fixture { + AnyExternalShadeDisplayPolicy( + bgScope = testScope.backgroundScope, + displayRepository = displayRepository, + ) + } + +val Kosmos.focusBasedShadeDisplayPolicy: StatusBarTouchShadeDisplayPolicy by + Kosmos.Fixture { + StatusBarTouchShadeDisplayPolicy( + displayRepository = displayRepository, + backgroundScope = testScope.backgroundScope, + keyguardRepository = keyguardRepository, + shadeOnDefaultDisplayWhenLocked = false, + ) + } val Kosmos.shadeDisplaysRepository: MutableShadeDisplaysRepository by Kosmos.Fixture { ShadeDisplaysRepositoryImpl( - defaultPolicy = defaultShadeDisplayPolicy, bgScope = testScope.backgroundScope, + globalSettings = fakeGlobalSettings, + policies = shadeDisplayPolicies, + defaultPolicy = defaultShadeDisplayPolicy, + ) + } + +val Kosmos.shadeDisplayPolicies: Set<ShadeDisplayPolicy> by + Kosmos.Fixture { + setOf( + defaultShadeDisplayPolicy, + anyExternalShadeDisplayPolicy, + focusBasedShadeDisplayPolicy, ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt index 32191277c94a..13673d16bf3c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/policy/FakeConfigurationController.kt @@ -27,6 +27,10 @@ class FakeConfigurationController @Inject constructor() : listeners.forEach { it.onConfigChanged(newConfiguration) } } + override fun dispatchOnMovedToDisplay(newDisplayId: Int, newConfiguration: Configuration) { + listeners.forEach { it.onMovedToDisplay(newDisplayId, newConfiguration) } + } + override fun notifyThemeChanged() { listeners.forEach { it.onThemeChanged() } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java index 111c40d49efc..9cf25e8df727 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/utils/leaks/FakeConfigurationController.java @@ -16,6 +16,8 @@ package com.android.systemui.utils.leaks; import android.content.res.Configuration; +import androidx.annotation.NonNull; + import com.android.systemui.statusbar.policy.ConfigurationController; public class FakeConfigurationController @@ -43,4 +45,10 @@ public class FakeConfigurationController public String getNightModeName() { return "undefined"; } + + @Override + public void dispatchOnMovedToDisplay(int newDisplayId, + @NonNull Configuration newConfiguration) { + + } } diff --git a/services/core/java/com/android/server/display/DisplayDevice.java b/services/core/java/com/android/server/display/DisplayDevice.java index 4bbddaeb53a2..2bdb5c25d0d5 100644 --- a/services/core/java/com/android/server/display/DisplayDevice.java +++ b/services/core/java/com/android/server/display/DisplayDevice.java @@ -306,6 +306,14 @@ abstract class DisplayDevice { } /** + * Returns if the display should only mirror another display rather than showing other content + * until it is destroyed. + */ + public boolean shouldOnlyMirror() { + return false; + } + + /** * Sets the display layer stack while in a transaction. */ public final void setLayerStackLocked(SurfaceControl.Transaction t, int layerStack, diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java index 0b633bd9c549..bad5b8b9567a 100644 --- a/services/core/java/com/android/server/display/DisplayManagerService.java +++ b/services/core/java/com/android/server/display/DisplayManagerService.java @@ -199,7 +199,6 @@ import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.atomic.AtomicLong; import java.util.function.Consumer; - /** * Manages attached displays. * <p> @@ -906,6 +905,16 @@ public final class DisplayManagerService extends SystemService { } } + @VisibleForTesting + ContentObserver getSettingsObserver() { + return mSettingsObserver; + } + + @VisibleForTesting + boolean shouldMirrorBuiltInDisplay() { + return mMirrorBuiltInDisplay; + } + DisplayNotificationManager getDisplayNotificationManager() { return mDisplayNotificationManager; } @@ -1230,11 +1239,6 @@ public final class DisplayManagerService extends SystemService { } private void updateMirrorBuiltInDisplaySettingLocked() { - if (!mFlags.isDisplayContentModeManagementEnabled()) { - Slog.e(TAG, "MirrorBuiltInDisplay setting shouldn't be updated when the flag is off."); - return; - } - synchronized (mSyncRoot) { ContentResolver resolver = mContext.getContentResolver(); final boolean mirrorBuiltInDisplay = Settings.Secure.getIntForUser(resolver, @@ -1243,6 +1247,9 @@ public final class DisplayManagerService extends SystemService { return; } mMirrorBuiltInDisplay = mirrorBuiltInDisplay; + if (mFlags.isDisplayContentModeManagementEnabled()) { + mLogicalDisplayMapper.forEachLocked(this::updateCanHostTasksIfNeededLocked); + } } } @@ -2308,6 +2315,10 @@ public final class DisplayManagerService extends SystemService { mDisplayBrightnesses.append(displayId, new BrightnessPair(brightnessDefault, brightnessDefault)); + if (mFlags.isDisplayContentModeManagementEnabled()) { + updateCanHostTasksIfNeededLocked(display); + } + DisplayManagerGlobal.invalidateLocalDisplayInfoCaches(); } @@ -2630,6 +2641,12 @@ public final class DisplayManagerService extends SystemService { } } + private void updateCanHostTasksIfNeededLocked(LogicalDisplay display) { + if (display.setCanHostTasksLocked(!mMirrorBuiltInDisplay)) { + sendDisplayEventIfEnabledLocked(display, DisplayManagerGlobal.EVENT_DISPLAY_CHANGED); + } + } + private void recordTopInsetLocked(@Nullable LogicalDisplay d) { // We must only persist the inset after boot has completed, otherwise we will end up // overwriting the persisted value before the masking flag has been loaded from the diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java index 9387e9ede532..730b95cf1eac 100644 --- a/services/core/java/com/android/server/display/DisplayPowerController.java +++ b/services/core/java/com/android/server/display/DisplayPowerController.java @@ -2235,7 +2235,6 @@ final class DisplayPowerController implements AutomaticBrightnessController.Call setReportedScreenState(REPORTED_TO_POLICY_SCREEN_TURNING_OFF); blockScreenOff(); mWindowManagerPolicy.screenTurningOff(mDisplayId, mPendingScreenOffUnblocker); - unblockScreenOff(); } else if (mPendingScreenOffUnblocker != null) { // Abort doing the state change until screen off is unblocked. return false; diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java index f34d2cc6e684..519763a1c3db 100644 --- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java +++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java @@ -217,8 +217,10 @@ class ExternalDisplayPolicy { mExternalDisplayStatsService.onDisplayConnected(logicalDisplay); - if ((Build.IS_ENG || Build.IS_USERDEBUG) - && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) { + if (((Build.IS_ENG || Build.IS_USERDEBUG) + && SystemProperties.getBoolean(ENABLE_ON_CONNECT, false)) + || (mFlags.isDisplayContentModeManagementEnabled() + && logicalDisplay.canHostTasksLocked())) { Slog.w(TAG, "External display is enabled by default, bypassing user consent."); mInjector.sendExternalDisplayEventLocked(logicalDisplay, EVENT_DISPLAY_CONNECTED); return; diff --git a/services/core/java/com/android/server/display/LogicalDisplay.java b/services/core/java/com/android/server/display/LogicalDisplay.java index 1de9c9589fb9..f9d413732e3e 100644 --- a/services/core/java/com/android/server/display/LogicalDisplay.java +++ b/services/core/java/com/android/server/display/LogicalDisplay.java @@ -17,6 +17,7 @@ package com.android.server.display; import static com.android.server.display.DisplayDeviceInfo.TOUCH_NONE; +import static com.android.server.display.layout.Layout.Display.POSITION_REAR; import static com.android.server.wm.utils.DisplayInfoOverrides.WM_OVERRIDE_FIELDS; import static com.android.server.wm.utils.DisplayInfoOverrides.copyDisplayInfoFields; @@ -227,6 +228,8 @@ final class LogicalDisplay { */ private final boolean mIsAnisotropyCorrectionEnabled; + private boolean mCanHostTasks; + LogicalDisplay(int displayId, int layerStack, DisplayDevice primaryDisplayDevice) { this(displayId, layerStack, primaryDisplayDevice, false, false); } @@ -245,6 +248,7 @@ final class LogicalDisplay { mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId; mIsAnisotropyCorrectionEnabled = isAnisotropyCorrectionEnabled; mAlwaysRotateDisplayDeviceEnabled = isAlwaysRotateDisplayDeviceEnabled; + mCanHostTasks = (mDisplayId == Display.DEFAULT_DISPLAY); } public void setDevicePositionLocked(int position) { @@ -568,6 +572,7 @@ final class LogicalDisplay { mBaseDisplayInfo.layoutLimitedRefreshRate = mLayoutLimitedRefreshRate; mBaseDisplayInfo.thermalRefreshRateThrottling = mThermalRefreshRateThrottling; mBaseDisplayInfo.thermalBrightnessThrottlingDataId = mThermalBrightnessThrottlingDataId; + mBaseDisplayInfo.canHostTasks = mCanHostTasks; mPrimaryDisplayDeviceInfo = deviceInfo; mInfo.set(null); @@ -927,6 +932,61 @@ final class LogicalDisplay { return handleLogicalDisplayChangedLocked; } + boolean canHostTasksLocked() { + return mCanHostTasks; + } + + /** + * Sets whether the display can host tasks. + * + * @param canHostTasks Whether the display can host tasks according to the user's setting. + * @return Whether Display Manager should call sendDisplayEventIfEnabledLocked(). + */ + boolean setCanHostTasksLocked(boolean canHostTasks) { + canHostTasks = validateCanHostTasksLocked(canHostTasks); + if (mBaseDisplayInfo.canHostTasks == canHostTasks) { + return false; + } + + mCanHostTasks = canHostTasks; + mBaseDisplayInfo.canHostTasks = canHostTasks; + mInfo.set(null); + return true; + } + + /** + * Checks whether the display's ability to host tasks should be determined independently of the + * user's setting value. If so, returns the actual validated value based on the display's + * usage; otherwise, returns the user's setting value. + * + * @param canHostTasks Whether the display can host tasks according to the user's setting. + * @return Whether the display can actually host task after configuration. + */ + private boolean validateCanHostTasksLocked(boolean canHostTasks) { + // The default display can always host tasks. + if (getDisplayIdLocked() == Display.DEFAULT_DISPLAY) { + return true; + } + + // The display that should only mirror can never host tasks. + if (mPrimaryDisplayDevice.shouldOnlyMirror()) { + return false; + } + + // The display that has its own content can always host tasks. + final boolean isRearDisplay = getDevicePositionLocked() == POSITION_REAR; + final boolean ownContent = + ((mPrimaryDisplayDevice.getDisplayDeviceInfoLocked().flags + & DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY) + != 0) + || isRearDisplay; + if (ownContent) { + return true; + } + + return canHostTasks; + } + /** * Swap the underlying {@link DisplayDevice} with the specified LogicalDisplay. * diff --git a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java index f14e452ab8d3..558afd1da380 100644 --- a/services/core/java/com/android/server/display/VirtualDisplayAdapter.java +++ b/services/core/java/com/android/server/display/VirtualDisplayAdapter.java @@ -307,13 +307,14 @@ public class VirtualDisplayAdapter extends DisplayAdapter { private VirtualDisplayDevice removeVirtualDisplayDeviceLocked(IBinder appToken) { if (getFeatureFlags().isVirtualDisplayLimitEnabled()) { - int ownerUid = mOwnerUids.get(appToken); - int noOfDevices = mNoOfDevicesPerPackage.get(ownerUid, /* valueIfKeyNotFound= */ 0); - if (noOfDevices <= 1) { - mNoOfDevicesPerPackage.delete(ownerUid); - mOwnerUids.remove(appToken); - } else { - mNoOfDevicesPerPackage.put(ownerUid, noOfDevices - 1); + Integer ownerUid = mOwnerUids.remove(appToken); + if (ownerUid != null) { + int noOfDevices = mNoOfDevicesPerPackage.get(ownerUid, /* valueIfKeyNotFound= */ 0); + if (noOfDevices <= 1) { + mNoOfDevicesPerPackage.delete(ownerUid); + } else { + mNoOfDevicesPerPackage.put(ownerUid, noOfDevices - 1); + } } } return mVirtualDisplayDevices.remove(appToken); @@ -500,6 +501,11 @@ public class VirtualDisplayAdapter extends DisplayAdapter { mPendingChanges = 0; } + @Override + public boolean shouldOnlyMirror() { + return mProjection != null || ((mFlags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) != 0); + } + public void setSurfaceLocked(Surface surface) { if (!mStopped && mSurface != surface) { if (mDisplayState == Display.STATE_ON diff --git a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java index b076aebe5210..4eaa11bac016 100644 --- a/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java +++ b/services/core/java/com/android/server/wm/DeferredDisplayUpdater.java @@ -432,7 +432,8 @@ class DeferredDisplayUpdater { || !first.thermalRefreshRateThrottling.contentEquals( second.thermalRefreshRateThrottling) || !Objects.equals(first.thermalBrightnessThrottlingDataId, - second.thermalBrightnessThrottlingDataId)) { + second.thermalBrightnessThrottlingDataId) + || first.canHostTasks != second.canHostTasks) { diff |= DIFF_NOT_WM_DEFERRABLE; } diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java index 724f083018f2..f96294ed4ca8 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java @@ -30,6 +30,7 @@ import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_D import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION; import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; +import static android.provider.Settings.Secure.MIRROR_BUILT_IN_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY; import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK; import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID; @@ -88,6 +89,7 @@ import android.content.pm.PackageManager; import android.content.pm.PackageManagerInternal; import android.content.pm.UserInfo; import android.content.res.Resources; +import android.database.ContentObserver; import android.graphics.Insets; import android.graphics.Rect; import android.hardware.Sensor; @@ -3830,6 +3832,96 @@ public class DisplayManagerServiceTest { assertThat(callback.receivedEvents()).isEmpty(); } + @Test + public void testMirrorBuiltInDisplay_flagEnabled() { + when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true); + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.systemReady(/* safeMode= */ false); + assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse(); + + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); + final ContentObserver observer = displayManager.getSettingsObserver(); + observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); + assertThat(displayManager.shouldMirrorBuiltInDisplay()).isTrue(); + } + + @Test + public void testMirrorBuiltInDisplay_flagDisabled() { + when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(false); + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.systemReady(/* safeMode= */ false); + assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse(); + + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); + final ContentObserver observer = displayManager.getSettingsObserver(); + observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); + assertThat(displayManager.shouldMirrorBuiltInDisplay()).isFalse(); + } + + @Test + public void testShouldNotNotifyDefaultDisplayChanges_whenMirrorBuiltInDisplayChanges() { + when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true); + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.systemReady(/* safeMode= */ false); + + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + Handler handler = displayManager.getDisplayHandler(); + waitForIdleHandler(handler); + + FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); + displayManagerBinderService.registerCallbackWithEventMask( + callback, STANDARD_DISPLAY_EVENTS); + waitForIdleHandler(handler); + + // Create a default display device + createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_INTERNAL); + + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); + final ContentObserver observer = displayManager.getSettingsObserver(); + observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); + waitForIdleHandler(handler); + + assertThat(callback.receivedEvents()).doesNotContain(EVENT_DISPLAY_CHANGED); + } + + @Test + public void testShouldNotifyNonDefaultDisplayChanges_whenMirrorBuiltInDisplayChanges() { + when(mMockFlags.isDisplayContentModeManagementEnabled()).thenReturn(true); + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 0); + + DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector); + displayManager.systemReady(/* safeMode= */ false); + + DisplayManagerService.BinderService displayManagerBinderService = + displayManager.new BinderService(); + Handler handler = displayManager.getDisplayHandler(); + waitForIdleHandler(handler); + + FakeDisplayManagerCallback callback = new FakeDisplayManagerCallback(); + displayManagerBinderService.registerCallbackWithEventMask( + callback, STANDARD_DISPLAY_EVENTS); + waitForIdleHandler(handler); + + // Create a default display device + createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_INTERNAL); + // Create a non-default display device + createFakeDisplayDevice(displayManager, new float[] {60f}, Display.TYPE_EXTERNAL); + + Settings.Secure.putInt(mContext.getContentResolver(), MIRROR_BUILT_IN_DISPLAY, 1); + final ContentObserver observer = displayManager.getSettingsObserver(); + observer.onChange(false, Settings.Secure.getUriFor(MIRROR_BUILT_IN_DISPLAY)); + waitForIdleHandler(handler); + + assertThat(callback.receivedEvents()).contains(EVENT_DISPLAY_CHANGED); + } + private void initDisplayPowerController(DisplayManagerInternal localService) { localService.initPowerManagement(new DisplayManagerInternal.DisplayPowerCallbacks() { @Override diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java index 241dc10747ac..1a0ab252f128 100644 --- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java +++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayTest.java @@ -609,4 +609,69 @@ public class LogicalDisplayTest { DisplayInfo info = mLogicalDisplay.getDisplayInfoLocked(); assertArrayEquals(appSupportedModes, info.appsSupportedModes); } + + @Test + public void testSetCanHostTasks_defaultDisplay() { + mLogicalDisplay = new LogicalDisplay(Display.DEFAULT_DISPLAY, LAYER_STACK, mDisplayDevice); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(true); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(false); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + } + + @Test + public void testSetCanHostTasks_nonDefaultNormalDisplay() { + mLogicalDisplay = + new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice); + + mLogicalDisplay.setCanHostTasksLocked(true); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(false); + assertFalse(mLogicalDisplay.canHostTasksLocked()); + } + + @Test + public void testSetCanHostTasks_nonDefaultVirtualMirrorDisplay() { + mDisplayDeviceInfo.type = Display.TYPE_VIRTUAL; + when(mDisplayDevice.shouldOnlyMirror()).thenReturn(true); + mLogicalDisplay = + new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice); + mLogicalDisplay.updateLocked(mDeviceRepo, mSyntheticModeManager); + + mLogicalDisplay.setCanHostTasksLocked(true); + assertFalse(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(false); + assertFalse(mLogicalDisplay.canHostTasksLocked()); + } + + @Test + public void testSetCanHostTasks_nonDefaultRearDisplay() { + mLogicalDisplay = + new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice); + mLogicalDisplay.setDevicePositionLocked(Layout.Display.POSITION_REAR); + + mLogicalDisplay.setCanHostTasksLocked(true); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(false); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + } + + @Test + public void testSetCanHostTasks_nonDefaultOwnContentOnly() { + mDisplayDeviceInfo.flags = DisplayDeviceInfo.FLAG_OWN_CONTENT_ONLY; + mLogicalDisplay = + new LogicalDisplay(Display.DEFAULT_DISPLAY + 1, LAYER_STACK, mDisplayDevice); + + mLogicalDisplay.setCanHostTasksLocked(true); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + + mLogicalDisplay.setCanHostTasksLocked(false); + assertTrue(mLogicalDisplay.canHostTasksLocked()); + } } |