diff options
Diffstat (limited to 'libs')
495 files changed, 19571 insertions, 4038 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java index fcf3a3759f7a..de3146e5bd11 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/WindowExtensionsImpl.java @@ -85,10 +85,7 @@ class WindowExtensionsImpl implements WindowExtensions { if (Flags.wlinfoOncreate()) { return EXTENSIONS_VERSION_V9; } - if (Flags.aeBackStackRestore()) { - return EXTENSIONS_VERSION_V8; - } - return EXTENSIONS_VERSION_V7; + return EXTENSIONS_VERSION_V8; } private String generateLogMessage() { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java index 76eb207a31c9..8e04855f7d14 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/area/WindowAreaComponentImpl.java @@ -126,7 +126,10 @@ public class WindowAreaComponentImpl implements WindowAreaComponent, return state.getIdentifier(); } } - return INVALID_DEVICE_STATE_IDENTIFIER; + + // If RDMV2 flag is enabled but not properly configured, let's fall back to RDMV1 if + // possible. + return getRdmV1Identifier(currentSupportedDeviceStates); } public WindowAreaComponentImpl(@NonNull Context context) { diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index 1bcb0bb91515..b0fadb06b7e3 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -288,7 +288,7 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen mSplitRules.clear(); mSplitRules.addAll(rules); - if (!Flags.aeBackStackRestore() || !mPresenter.isWaitingToRebuildTaskContainers()) { + if (!mPresenter.isWaitingToRebuildTaskContainers()) { return; } @@ -2893,10 +2893,6 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen @Override public void setAutoSaveEmbeddingState(boolean saveEmbeddingState) { - if (!Flags.aeBackStackRestore()) { - return; - } - synchronized (mLock) { mPresenter.setAutoSaveEmbeddingState(saveEmbeddingState); } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java index 9a2f32e9ee99..eb59d6efdeff 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java @@ -169,12 +169,8 @@ class SplitPresenter extends JetpackTaskFragmentOrganizer { mWindowLayoutComponent = windowLayoutComponent; mController = controller; final Bundle outSavedState = new Bundle(); - if (Flags.aeBackStackRestore()) { - outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); - registerOrganizer(false /* isSystemOrganizer */, outSavedState); - } else { - registerOrganizer(); - } + outSavedState.setClassLoader(ParcelableTaskContainerData.class.getClassLoader()); + registerOrganizer(false /* isSystemOrganizer */, outSavedState); mBackupHelper = new BackupHelper(controller, this, outSavedState); if (!SplitController.ENABLE_SHELL_TRANSITIONS) { // TODO(b/207070762): cleanup with legacy app transition diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java index d677fef5c22c..b7983bdaf4cf 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/area/WindowAreaComponentImplTests.java @@ -143,13 +143,20 @@ public class WindowAreaComponentImplTests { } @Test - public void testRdmV2Identifier_whenStateIsProperlyConfigured() { + public void testFallsBackToRdmV1() { + // Test that if we try to get RDMV2 but it's not available, that we get RDMV1 if it is + // available. final List<DeviceState> supportedStates = new ArrayList<>(); - supportedStates.add(REAR_DISPLAY_STATE_V1); - assertEquals(INVALID_DEVICE_STATE_IDENTIFIER, + assertEquals(REAR_DISPLAY_STATE_V1.getIdentifier(), WindowAreaComponentImpl.getRdmV2Identifier(supportedStates)); + } + @Test + public void testRdmV2Identifier_whenStateIsProperlyConfigured() { + final List<DeviceState> supportedStates = new ArrayList<>(); + + supportedStates.add(REAR_DISPLAY_STATE_V1); supportedStates.add(REAR_DISPLAY_STATE_V2); assertEquals(REAR_DISPLAY_STATE_V2.getIdentifier(), WindowAreaComponentImpl.getRdmV2Identifier(supportedStates)); diff --git a/libs/WindowManager/Shell/OWNERS b/libs/WindowManager/Shell/OWNERS index 394093c6ab30..68970e68de07 100644 --- a/libs/WindowManager/Shell/OWNERS +++ b/libs/WindowManager/Shell/OWNERS @@ -1,7 +1,7 @@ -xutan@google.com +jorgegil@google.com pbdr@google.com pragyabajoria@google.com # Give submodule owners in shell resource approval -per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, jorgegil@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, tkachenkoi@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com +per-file res*/*/*.xml = atsjenk@google.com, hwwang@google.com, lbill@google.com, madym@google.com, vaniadesmonda@google.com, pbdr@google.com, mpodolian@google.com, liranb@google.com, pragyabajoria@google.com, uysalorhan@google.com, gsennton@google.com, mattsziklay@google.com, mdehaini@google.com, peanutbutter@google.com, jeremysim@google.com per-file res*/*/tv_*.xml = bronger@google.com diff --git a/libs/WindowManager/Shell/aconfig/multitasking.aconfig b/libs/WindowManager/Shell/aconfig/multitasking.aconfig index 065644627393..a08f88a5b937 100644 --- a/libs/WindowManager/Shell/aconfig/multitasking.aconfig +++ b/libs/WindowManager/Shell/aconfig/multitasking.aconfig @@ -179,8 +179,8 @@ flag { } flag { - name: "enable_non_default_display_split" + name: "enable_bubble_bar_on_phones" namespace: "multitasking" - description: "Enables split screen on non default displays" - bug: "384999213" + description: "Try out bubble bar on phones" + bug: "394869612" } diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png Binary files differnew file mode 100644 index 000000000000..15198748eea5 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png Binary files differnew file mode 100644 index 000000000000..99673f6400e9 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubbleBar.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png Binary files differnew file mode 100644 index 000000000000..ba4ebab75a7e --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_10_90.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png Binary files differnew file mode 100644 index 000000000000..b3ff64401a96 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_bubble_split_90_10.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png Binary files differnew file mode 100644 index 000000000000..534e320a0596 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png Binary files differnew file mode 100644 index 000000000000..67c9f49171ba --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_10_90.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png Binary files differnew file mode 100644 index 000000000000..a0fb4902a6f3 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_landscape_dragZones_expandedView_split_90_10.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png Binary files differnew file mode 100644 index 000000000000..27b35d447868 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png Binary files differnew file mode 100644 index 000000000000..11528a028a8f --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubbleBar.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png Binary files differnew file mode 100644 index 000000000000..ef9937700c08 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_10_90.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png Binary files differnew file mode 100644 index 000000000000..f0cf08bfcf4e --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_bubble_split_90_10.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png Binary files differnew file mode 100644 index 000000000000..bbaafb39845a --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/foldable_inner/light_portrait_dragZones_expandedView.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png Binary files differnew file mode 100644 index 000000000000..38ebf3f3201a --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubble.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png Binary files differnew file mode 100644 index 000000000000..2e4fd51e3932 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_bubbleBar.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png Binary files differnew file mode 100644 index 000000000000..a1ba9fb50d6a --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_landscape_dragZones_expandedView.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png Binary files differnew file mode 100644 index 000000000000..51bb15e10d30 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubble.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png Binary files differnew file mode 100644 index 000000000000..b643e2a69b2c --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_bubbleBar.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png Binary files differnew file mode 100644 index 000000000000..e6eeab7129be --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/tablet/light_portrait_dragZones_expandedView.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt new file mode 100644 index 000000000000..24f43d347163 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryScreenshotTest.kt @@ -0,0 +1,170 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.content.Context +import android.graphics.Color +import android.graphics.drawable.Drawable +import android.graphics.drawable.GradientDrawable +import android.view.View +import android.view.WindowManager +import android.widget.FrameLayout +import androidx.annotation.ColorInt +import androidx.core.graphics.blue +import androidx.core.graphics.green +import androidx.core.graphics.red +import androidx.core.graphics.toColorInt +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.android.wm.shell.testing.goldenpathmanager.WMShellGoldenPathManager +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import platform.test.runner.parameterized.ParameterizedAndroidJunit4 +import platform.test.runner.parameterized.Parameters +import platform.test.screenshot.DeviceEmulationSpec +import platform.test.screenshot.Displays +import platform.test.screenshot.ViewScreenshotTestRule +import platform.test.screenshot.ViewScreenshotTestRule.Mode +import platform.test.screenshot.getEmulatedDevicePathConfig + +@RunWith(ParameterizedAndroidJunit4::class) +class DragZoneFactoryScreenshotTest(private val param: Param) { + companion object { + @Parameters(name = "{0}") + @JvmStatic + fun getTestSpecs(): List<Param> { + val params = mutableListOf<Param>() + val draggedObjects = + listOf( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + DraggedObject.BubbleBar(BubbleBarLocation.LEFT), + DraggedObject.ExpandedView(BubbleBarLocation.LEFT), + ) + DeviceEmulationSpec.forDisplays(Displays.Tablet, isDarkTheme = false).forEach { tablet + -> + draggedObjects.forEach { draggedObject -> + params.add(Param(tablet, draggedObject, SplitScreenMode.NONE)) + } + } + DeviceEmulationSpec.forDisplays(Displays.FoldableInner, isDarkTheme = false).forEach { + foldable -> + draggedObjects.forEach { draggedObject -> + params.add(Param(foldable, draggedObject, SplitScreenMode.NONE)) + val isBubble = draggedObject is DraggedObject.Bubble + val isExpandedView = draggedObject is DraggedObject.ExpandedView + val addMoreSplitModes = isBubble || (isExpandedView && foldable.isLandscape) + if (addMoreSplitModes) { + params.add(Param(foldable, draggedObject, SplitScreenMode.SPLIT_10_90)) + params.add(Param(foldable, draggedObject, SplitScreenMode.SPLIT_90_10)) + } + } + } + return params + } + } + + class Param( + val emulationSpec: DeviceEmulationSpec, + val draggedObject: DraggedObject, + val splitScreenMode: SplitScreenMode + ) { + private val draggedObjectName = + when (draggedObject) { + is DraggedObject.Bubble -> "bubble" + is DraggedObject.BubbleBar -> "bubbleBar" + is DraggedObject.ExpandedView -> "expandedView" + } + + private val splitScreenModeName = + when (splitScreenMode) { + SplitScreenMode.NONE -> "" + SplitScreenMode.SPLIT_50_50 -> "_split_50_50" + SplitScreenMode.SPLIT_10_90 -> "_split_10_90" + SplitScreenMode.SPLIT_90_10 -> "_split_90_10" + } + + val testName = "$draggedObjectName$splitScreenModeName" + + override fun toString() = "${emulationSpec}_$testName" + } + + @get:Rule + val screenshotRule = + ViewScreenshotTestRule( + param.emulationSpec, + WMShellGoldenPathManager(getEmulatedDevicePathConfig(param.emulationSpec)) + ) + + private val context = getApplicationContext<Context>() + + @Test + fun dragZones() { + screenshotRule.screenshotTest("dragZones_${param.testName}", mode = Mode.MatchSize) { + activity -> + activity.actionBar?.hide() + val dragZoneFactory = createDragZoneFactory() + val dragZones = dragZoneFactory.createSortedDragZones(param.draggedObject) + val container = FrameLayout(context) + dragZones.forEach { zone -> container.addZoneView(zone) } + container + } + } + + private fun createDragZoneFactory(): DragZoneFactory { + val deviceConfig = + DeviceConfig.create(context, context.getSystemService(WindowManager::class.java)!!) + val splitScreenModeChecker = SplitScreenModeChecker { param.splitScreenMode } + val desktopWindowModeChecker = DesktopWindowModeChecker { true } + return DragZoneFactory( + context, + deviceConfig, + splitScreenModeChecker, + desktopWindowModeChecker + ) + } + + private fun FrameLayout.addZoneView(zone: DragZone) { + val view = View(context) + this.addView(view, 0) + view.layoutParams = FrameLayout.LayoutParams(zone.bounds.width(), zone.bounds.height()) + view.background = createZoneDrawable(zone.color) + view.x = zone.bounds.left.toFloat() + view.y = zone.bounds.top.toFloat() + } + + private fun createZoneDrawable(@ColorInt color: Int): Drawable { + val shape = GradientDrawable() + shape.shape = GradientDrawable.RECTANGLE + shape.setColor(Color.argb(128, color.red, color.green, color.blue)) + shape.setStroke(2, color) + return shape + } + + private val DragZone.color: Int + @ColorInt + get() = + when (this) { + is DragZone.Bubble -> "#3F5C8B".toColorInt() + is DragZone.Dismiss -> "#8B3F3F".toColorInt() + is DragZone.Split -> "#89B675".toColorInt() + is DragZone.FullScreen -> "#4ED075".toColorInt() + is DragZone.DesktopWindow -> "#EC928E".toColorInt() + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml index fd578a959e3b..95cd1c72a2af 100644 --- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml +++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml @@ -1,10 +1,19 @@ <manifest xmlns:android="http://schemas.android.com/apk/res/android" package="com.android.wm.shell.multivalenttests"> + <uses-permission android:name="android.permission.MANAGE_ACTIVITY_TASKS"/> + <application android:debuggable="true" android:supportsRtl="true" > <uses-library android:name="android.test.runner" /> <activity android:name="com.android.wm.shell.bubbles.bar.BubbleBarAnimationHelperTest$TestActivity" android:exported="true"/> + + <activity android:name=".bubbles.TestActivity" + android:allowEmbedded="true" + android:documentLaunchMode="always" + android:excludeFromRecents="true" + android:exported="false" + android:resizeableActivity="true" /> </application> <instrumentation diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt index 09a93d501f8e..a32ec221e08a 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt @@ -35,7 +35,6 @@ import com.android.internal.statusbar.IStatusBarService import com.android.wm.shell.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.bubbles.Bubbles.SysuiProxy -import com.android.wm.shell.bubbles.properties.ProdBubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -48,6 +47,7 @@ import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.shared.TransactionPool import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.shared.bubbles.BubbleBarUpdate +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit @@ -61,7 +61,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.mock import org.mockito.kotlin.whenever import java.util.Optional @@ -133,7 +132,7 @@ class BubbleControllerBubbleBarTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(Mockito.mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) shellInit.init() @@ -288,7 +287,7 @@ class BubbleControllerBubbleBarTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - ProdBubbleProperties, + BubbleResizabilityChecker() ) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt new file mode 100644 index 000000000000..cb229c99f42c --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerTest.kt @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Context +import android.content.Intent +import android.content.pm.LauncherApps +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import android.graphics.Rect +import android.graphics.drawable.Icon +import android.os.Handler +import android.os.UserHandle +import android.os.UserManager +import android.view.IWindowManager +import android.view.InsetsSource +import android.view.InsetsState +import android.view.WindowInsets +import android.view.WindowManager +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.logging.testing.UiEventLoggerFake +import com.android.internal.protolog.ProtoLog +import com.android.internal.statusbar.IStatusBarService +import com.android.wm.shell.R +import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.bubbles.Bubbles.BubbleExpandListener +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.storage.BubblePersistentRepository +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayImeController +import com.android.wm.shell.common.DisplayInsetsController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ImeListener +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.common.TestSyncExecutor +import com.android.wm.shell.draganddrop.DragAndDropController +import com.android.wm.shell.shared.TransactionPool +import com.android.wm.shell.sysui.ShellCommandHandler +import com.android.wm.shell.sysui.ShellController +import com.android.wm.shell.sysui.ShellInit +import com.android.wm.shell.taskview.TaskViewRepository +import com.android.wm.shell.taskview.TaskViewTransitions +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import com.google.common.util.concurrent.MoreExecutors.directExecutor +import java.util.Optional +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** Tests for [BubbleController] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleControllerTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + + private lateinit var bubbleController: BubbleController + private lateinit var bubblePositioner: BubblePositioner + private lateinit var uiEventLoggerFake: UiEventLoggerFake + private lateinit var bubbleLogger: BubbleLogger + private lateinit var mainExecutor: TestShellExecutor + private lateinit var bgExecutor: TestShellExecutor + private lateinit var bubbleData: BubbleData + private lateinit var eduController: BubbleEducationController + private lateinit var displayController: DisplayController + private lateinit var displayImeController: DisplayImeController + private lateinit var displayInsetsController: DisplayInsetsController + private lateinit var imeListener: ImeListener + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() + + uiEventLoggerFake = UiEventLoggerFake() + bubbleLogger = BubbleLogger(uiEventLoggerFake) + eduController = BubbleEducationController(context) + + mainExecutor = TestShellExecutor() + bgExecutor = TestShellExecutor() + + // Tests don't have permission to add our window to windowManager, so we mock it :( + val windowManager = mock<WindowManager>() + val realWindowManager = context.getSystemService(WindowManager::class.java)!! + // But we do want the metrics from the real one + whenever(windowManager.currentWindowMetrics) + .thenReturn(realWindowManager.currentWindowMetrics) + whenever(windowManager.defaultDisplay).thenReturn(realWindowManager.defaultDisplay) + + bubblePositioner = BubblePositioner(context, windowManager) + + bubbleData = + BubbleData( + context, + bubbleLogger, + bubblePositioner, + eduController, + mainExecutor, + bgExecutor + ) + displayController = mock<DisplayController>() + displayImeController = mock<DisplayImeController>() + displayInsetsController = mock<DisplayInsetsController>() + + bubbleController = + createBubbleController( + bubbleData, + windowManager, + bubbleLogger, + bubblePositioner, + mainExecutor, + bgExecutor, + ) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) + // Flush so that proxy gets set + mainExecutor.flushAll() + + whenever(displayController.getDisplayLayout(anyInt())) + .thenReturn(DisplayLayout(context, realWindowManager.defaultDisplay)) + val insetsChangedListenerCaptor = argumentCaptor<ImeListener>() + verify(displayInsetsController) + .addInsetsChangedListener(anyInt(), insetsChangedListenerCaptor.capture()) + imeListener = insetsChangedListenerCaptor.lastValue + } + + @After + fun tearDown() { + getInstrumentation().waitForIdleSync() + } + + @Test + fun showOrHideNotesBubble_createsNoteBubble() { + val intent = Intent(context, TestActivity::class.java) + intent.setPackage(context.packageName) + val user = UserHandle.of(0) + val expectedKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user) + + getInstrumentation().runOnMainSync { + bubbleController.showOrHideNotesBubble(intent, user, mock<Icon>()) + } + getInstrumentation().waitForIdleSync() + + assertThat(bubbleController.hasBubbles()).isTrue() + assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)).isNotNull() + assertThat(bubbleData.getAnyBubbleWithKey(expectedKey)!!.isNote).isTrue() + } + + @Test + fun onDeviceLocked_expanded_imeHidden_shouldCollapseImmediately() { + val bubble = createBubble("key") + bubblePositioner.setImeVisible(false, 0) + getInstrumentation().runOnMainSync { + bubbleController.inflateAndAdd( + bubble, + /* suppressFlyout= */ true, + /* showInShade= */ true + ) + } + assertThat(bubbleData.hasBubbles()).isTrue() + + // expand and lock the device + getInstrumentation().runOnMainSync { + bubbleController.expandStackAndSelectBubble(bubble) + assertThat(bubbleData.isExpanded).isTrue() + bubbleController.onStatusBarStateChanged(/* isShade= */ false) + } + // verify that we collapsed immediately, since the IME is hidden + assertThat(bubbleData.isExpanded).isFalse() + } + + @Test + fun onDeviceLocked_expanded_imeVisible_shouldHideImeBeforeCollapsing() { + val bubble = createBubble("key") + getInstrumentation().runOnMainSync { + bubbleController.inflateAndAdd( + bubble, + /* suppressFlyout= */ true, + /* showInShade= */ true + ) + } + assertThat(bubbleData.hasBubbles()).isTrue() + + // expand and show the IME. then lock the device + val imeVisibleInsetsState = createFakeInsetsState(imeVisible = true) + getInstrumentation().runOnMainSync { + bubbleController.expandStackAndSelectBubble(bubble) + assertThat(bubbleData.isExpanded).isTrue() + imeListener.insetsChanged(imeVisibleInsetsState) + assertThat(bubblePositioner.isImeVisible).isTrue() + bubbleController.onStatusBarStateChanged(/* isShade= */ false) + } + // check that we haven't actually started collapsing because we weren't notified yet that + // the IME is hidden + assertThat(bubbleData.isExpanded).isTrue() + // collapsing while the device is locked goes through display ime controller + verify(displayImeController).hideImeForBubblesWhenLocked(anyInt()) + + // notify that the IME was hidden + val imeHiddenInsetsState = createFakeInsetsState(imeVisible = false) + getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeHiddenInsetsState) } + assertThat(bubblePositioner.isImeVisible).isFalse() + // bubbles should be collapsed now + assertThat(bubbleData.isExpanded).isFalse() + } + + @Test + fun onDeviceLocked_whileHidingImeDuringCollapse() { + val bubble = createBubble("key") + val expandListener = FakeBubbleExpandListener() + bubbleController.setExpandListener(expandListener) + + getInstrumentation().runOnMainSync { + bubbleController.inflateAndAdd( + bubble, + /* suppressFlyout= */ true, + /* showInShade= */ true + ) + } + assertThat(bubbleData.hasBubbles()).isTrue() + + // expand + getInstrumentation().runOnMainSync { + bubbleController.expandStackAndSelectBubble(bubble) + assertThat(bubbleData.isExpanded).isTrue() + mainExecutor.flushAll() + } + + assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("key" to true)) + + // show the IME + val imeVisibleInsetsState = createFakeInsetsState(imeVisible = true) + getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeVisibleInsetsState) } + + assertThat(bubblePositioner.isImeVisible).isTrue() + + // collapse the stack + getInstrumentation().runOnMainSync { bubbleController.collapseStack() } + assertThat(bubbleData.isExpanded).isFalse() + // since we started to collapse while the IME was visible, we will wait to be notified that + // the IME is hidden before completing the collapse. check that the expand listener was not + // yet called + assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("key" to true)) + + // lock the device during this state + getInstrumentation().runOnMainSync { + bubbleController.onStatusBarStateChanged(/* isShade= */ false) + } + verify(displayImeController).hideImeForBubblesWhenLocked(anyInt()) + + // notify that the IME is hidden + val imeHiddenInsetsState = createFakeInsetsState(imeVisible = false) + getInstrumentation().runOnMainSync { imeListener.insetsChanged(imeHiddenInsetsState) } + assertThat(bubblePositioner.isImeVisible).isFalse() + // verify the collapse action completed + assertThat(expandListener.bubblesExpandedState).isEqualTo(mapOf("key" to false)) + } + + private fun createBubble(key: String): Bubble { + val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) + val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() + val bubble = + Bubble( + key, + shortcutInfo, + /* desiredHeight= */ 0, + Resources.ID_NULL, + "title", + /* taskId= */ 0, + "locus", + /* isDismissable= */ true, + directExecutor(), + directExecutor() + ) {} + return bubble + } + + private fun createFakeInsetsState(imeVisible: Boolean): InsetsState { + val insetsState = InsetsState() + if (imeVisible) { + insetsState + .getOrCreateSource(InsetsSource.ID_IME, WindowInsets.Type.ime()) + .setFrame(Rect(0, 100, 100, 200)) + .setVisible(true) + } + return insetsState + } + + private fun createBubbleController( + bubbleData: BubbleData, + windowManager: WindowManager, + bubbleLogger: BubbleLogger, + bubblePositioner: BubblePositioner, + mainExecutor: TestShellExecutor, + bgExecutor: TestShellExecutor, + ): BubbleController { + val shellInit = ShellInit(mainExecutor) + val shellCommandHandler = ShellCommandHandler() + val shellController = + ShellController( + context, + shellInit, + shellCommandHandler, + displayInsetsController, + mainExecutor, + ) + val surfaceSynchronizer = { obj: Runnable -> obj.run() } + + val bubbleDataRepository = + BubbleDataRepository( + mock<LauncherApps>(), + mainExecutor, + bgExecutor, + BubblePersistentRepository(context), + ) + + val shellTaskOrganizer = + ShellTaskOrganizer( + mock<ShellInit>(), + ShellCommandHandler(), + null, + Optional.empty(), + Optional.empty(), + TestSyncExecutor() + ) + + val resizeChecker = ResizabilityChecker { _, _, _ -> true } + + val bubbleController = + BubbleController( + context, + shellInit, + shellCommandHandler, + shellController, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleDataRepository, + mock<IStatusBarService>(), + windowManager, + displayInsetsController, + displayImeController, + mock<UserManager>(), + mock<LauncherApps>(), + bubbleLogger, + mock<TaskStackListenerImpl>(), + shellTaskOrganizer, + bubblePositioner, + displayController, + Optional.empty(), + mock<DragAndDropController>(), + mainExecutor, + mock<Handler>(), + bgExecutor, + mock<TaskViewRepository>(), + mock<TaskViewTransitions>(), + mock<Transitions>(), + SyncTransactionQueue(TransactionPool(), mainExecutor), + mock<IWindowManager>(), + resizeChecker, + ) + bubbleController.setInflateSynchronously(true) + bubbleController.onInit() + + return bubbleController + } + + private class FakeBubbleExpandListener : BubbleExpandListener { + val bubblesExpandedState = mutableMapOf<String, Boolean>() + + override fun onBubbleExpandChanged(isExpanding: Boolean, key: String) { + bubblesExpandedState[key] = isExpanding + } + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt index 1d0c5057c77f..ec1add21ebf8 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt @@ -31,6 +31,7 @@ import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.google.common.truth.Truth.assertThat import com.google.common.util.concurrent.MoreExecutors.directExecutor import org.junit.Before @@ -154,19 +155,19 @@ class BubblePositionerTest { /** Test that the default resting position on tablet is middle right. */ @Test - fun testGetDefaultPosition_appBubble_onTablet() { + fun testGetDefaultPosition_noteBubble_onTablet() { positioner.update(defaultDeviceConfig.copy(isLargeScreen = true)) val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) - val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) assertThat(startPosition.x).isEqualTo(allowableStackRegion.right) assertThat(startPosition.y).isEqualTo(defaultYPosition) } @Test - fun testGetRestingPosition_appBubble_onTablet_RTL() { + fun testGetRestingPosition_noteBubble_onTablet_RTL() { positioner.update(defaultDeviceConfig.copy(isLargeScreen = true, isRtl = true)) val allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */) - val startPosition = positioner.getDefaultStartPosition(true /* isAppBubble */) + val startPosition = positioner.getDefaultStartPosition(true /* isNoteBubble */) assertThat(startPosition.x).isEqualTo(allowableStackRegion.left) assertThat(startPosition.y).isEqualTo(defaultYPosition) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt index 9d445f0bb80d..e865111e59dc 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt @@ -50,10 +50,10 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito import org.mockito.kotlin.any import org.mockito.kotlin.mock import org.mockito.kotlin.never +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit @@ -635,7 +635,7 @@ class BubbleStackViewTest { @Test fun removeFromWindow_stopMonitoringSwipeUpGesture() { - bubbleStackView = Mockito.spy(bubbleStackView) + bubbleStackView = spy(bubbleStackView) InstrumentationRegistry.getInstrumentation().runOnMainSync { // No way to add to window in the test environment right now so just pretend bubbleStackView.onDetachedFromWindow() @@ -685,7 +685,6 @@ class BubbleStackViewTest { expandedViewManager, bubbleTaskViewFactory, positioner, - bubbleLogger, bubbleStackView, null, iconFactory, diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt new file mode 100644 index 000000000000..9ebc3d78b3a7 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleTaskViewListenerTest.kt @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.app.Notification +import android.app.PendingIntent +import android.content.ComponentName +import android.content.Context +import android.content.Intent +import android.content.pm.ShortcutInfo +import android.graphics.drawable.Icon +import android.os.UserHandle +import android.service.notification.NotificationListenerService.Ranking +import android.service.notification.StatusBarNotification +import android.view.View +import android.widget.FrameLayout +import androidx.test.core.app.ApplicationProvider +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.R +import com.android.wm.shell.bubbles.Bubbles.BubbleMetadataFlagListener +import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.taskview.TaskView +import com.android.wm.shell.taskview.TaskViewController +import com.android.wm.shell.taskview.TaskViewTaskController +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito +import org.mockito.Mockito.never +import org.mockito.Mockito.reset +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for [BubbleTaskViewListener]. + */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleTaskViewListenerTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + + private var taskViewController = mock<TaskViewController>() + private var listenerCallback = mock<BubbleTaskViewListener.Callback>() + private var expandedViewManager = mock<BubbleExpandedViewManager>() + + private lateinit var bubbleTaskViewListener: BubbleTaskViewListener + private lateinit var taskView: TaskView + private lateinit var bubbleTaskView: BubbleTaskView + private lateinit var parentView: ViewPoster + private lateinit var mainExecutor: TestShellExecutor + private lateinit var bgExecutor: TestShellExecutor + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() + + parentView = ViewPoster(context) + mainExecutor = TestShellExecutor() + bgExecutor = TestShellExecutor() + + taskView = TaskView(context, taskViewController, mock<TaskViewTaskController>()) + bubbleTaskView = BubbleTaskView(taskView, mainExecutor) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + } + + @Test + fun createBubbleTaskViewListener_withCreatedTaskView() { + // Make the bubbleTaskView look like it's been created + val taskId = 123 + bubbleTaskView.listener.onTaskCreated(taskId, mock<ComponentName>()) + reset(listenerCallback) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener) + assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView) + + verify(listenerCallback).onTaskCreated() + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun createBubbleTaskViewListener() { + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + assertThat(bubbleTaskView.delegateListener).isEqualTo(bubbleTaskViewListener) + assertThat(bubbleTaskViewListener.taskView).isEqualTo(bubbleTaskView.taskView) + verify(listenerCallback, never()).onTaskCreated() + } + + @Test + fun onInitialized_pendingIntentChatBubble() { + val target = Intent(context, TestActivity::class.java) + val pendingIntent = PendingIntent.getActivity(context, 0, target, + PendingIntent.FLAG_MUTABLE) + + val b = createChatBubble("key", pendingIntent) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isChat).isTrue() + // Has shortcut info + assertThat(b.shortcutInfo).isNotNull() + // But it didn't use that on bubble metadata + assertThat(b.metadataShortcutId).isNull() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + // ..so it's pending intent-based, and launches that + assertThat(b.isPendingIntentActive).isTrue() + verify(taskViewController).startActivity(any(), eq(pendingIntent), any(), any(), any()) + } + + @Test + fun onInitialized_shortcutChatBubble() { + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("mockShortcutId") + .build() + val b = createChatBubble("key", shortcutInfo) + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isChat).isTrue() + assertThat(b.shortcutInfo).isNotNull() + // Chat bubble using a shortcut + assertThat(b.metadataShortcutId).isNotNull() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + assertThat(b.isPendingIntentActive).isFalse() + verify(taskViewController).startShortcutActivity(any(), eq(shortcutInfo), any(), any()) + } + + @Test + fun onInitialized_appBubble() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + assertThat(b.isPendingIntentActive).isFalse() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onInitialized_preparingTransition() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + taskView = Mockito.spy(taskView) + val preparingTransition = mock<BubbleTransitions.BubbleTransition>() + b.preparingTransition = preparingTransition + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + verify(preparingTransition).surfaceCreated() + } + + @Test + fun onInitialized_destroyed() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onReleased() + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onInitialized_initialized() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + assertThat(b.isApp).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + + reset(taskViewController) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + // Already initialized, so no activity should be started. + verify(taskViewController, never()).startActivity(any(), any(), anyOrNull(), any(), any()) + } + + @Test + fun onTaskCreated() { + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + + val taskId = 123 + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + } + getInstrumentation().waitForIdleSync() + + verify(listenerCallback).onTaskCreated() + verify(expandedViewManager, never()).setNoteBubbleTaskId(any(), any()) + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun onTaskCreated_noteBubble() { + val b = createNoteBubble() + bubbleTaskViewListener.setBubble(b) + assertThat(b.isNote).isTrue() + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(taskViewController).startActivity(any(), any(), anyOrNull(), any(), any()) + + val taskId = 123 + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + } + getInstrumentation().waitForIdleSync() + + verify(listenerCallback).onTaskCreated() + verify(expandedViewManager).setNoteBubbleTaskId(eq(b.key), eq(taskId)) + assertThat(bubbleTaskViewListener.taskId).isEqualTo(taskId) + } + + @Test + fun onTaskVisibilityChanged_true() { + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskVisibilityChanged(1, true) + } + verify(listenerCallback).onContentVisibilityChanged(eq(true)) + } + + @Test + fun onTaskVisibilityChanged_false() { + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskVisibilityChanged(1, false) + } + verify(listenerCallback).onContentVisibilityChanged(eq(false)) + } + + @Test + fun onTaskRemovalStarted() { + val mockTaskView = mock<TaskView>() + bubbleTaskView = BubbleTaskView(mockTaskView, mainExecutor) + + bubbleTaskViewListener = + BubbleTaskViewListener( + context, + bubbleTaskView, + parentView, + expandedViewManager, + listenerCallback + ) + + val b = createAppBubble() + bubbleTaskViewListener.setBubble(b) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onInitialized() + } + getInstrumentation().waitForIdleSync() + verify(mockTaskView).startActivity(any(), anyOrNull(), any(), any()) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskRemovalStarted(1) + } + + verify(expandedViewManager).removeBubble(eq(b.key), eq(Bubbles.DISMISS_TASK_FINISHED)) + verify(mockTaskView).release() + assertThat(parentView.lastRemovedView).isEqualTo(mockTaskView) + assertThat(bubbleTaskViewListener.taskView).isNull() + verify(listenerCallback).onTaskRemovalStarted() + } + + @Test + fun onBackPressedOnTaskRoot_expanded() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(true) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId) + } + verify(listenerCallback).onBackPressed() + } + + @Test + fun onBackPressedOnTaskRoot_notExpanded() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(false) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(taskId) + } + verify(listenerCallback, never()).onBackPressed() + } + + @Test + fun onBackPressedOnTaskRoot_taskIdMissMatch() { + val taskId = 123 + whenever(expandedViewManager.isStackExpanded()).doReturn(true) + + getInstrumentation().runOnMainSync { + bubbleTaskViewListener.onTaskCreated(taskId, mock<ComponentName>()) + bubbleTaskViewListener.onBackPressedOnTaskRoot(42) + } + verify(listenerCallback, never()).onBackPressed() + } + + @Test + fun setBubble_isNew() { + val b = createAppBubble() + val isNew = bubbleTaskViewListener.setBubble(b) + assertThat(isNew).isTrue() + } + + @Test + fun setBubble_launchContentChanged() { + val target = Intent(context, TestActivity::class.java) + val pendingIntent = PendingIntent.getActivity( + context, 0, target, + PendingIntent.FLAG_MUTABLE + ) + + val b = createChatBubble("key", pendingIntent) + var isNew = bubbleTaskViewListener.setBubble(b) + // First time bubble is set, so it is "new" + assertThat(isNew).isTrue() + + val b2 = createChatBubble("key", pendingIntent) + isNew = bubbleTaskViewListener.setBubble(b2) + // Second time bubble is set & it uses same type of launch content, not "new" + assertThat(isNew).isFalse() + + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("mockShortcutId") + .build() + val b3 = createChatBubble("key", shortcutInfo) + // bubble is using different content, so it is "new" + isNew = bubbleTaskViewListener.setBubble(b3) + assertThat(isNew).isTrue() + } + + private fun createAppBubble(): Bubble { + val target = Intent(context, TestActivity::class.java) + target.setPackage(context.packageName) + return Bubble.createAppBubble(target, mock<UserHandle>(), mock<Icon>(), + mainExecutor, bgExecutor) + } + + private fun createNoteBubble(): Bubble { + val target = Intent(context, TestActivity::class.java) + target.setPackage(context.packageName) + return Bubble.createNotesBubble(target, mock<UserHandle>(), mock<Icon>(), + mainExecutor, bgExecutor) + } + + private fun createChatBubble(key: String, shortcutInfo: ShortcutInfo): Bubble { + return Bubble( + key, + shortcutInfo, + 0 /* desiredHeight */, + 0 /* desiredHeightResId */, + "title", + -1 /*taskId */, + null /* locusId */, true /* isdismissabel */, + mainExecutor, bgExecutor, mock<BubbleMetadataFlagListener>() + ) + } + + private fun createChatBubble(key: String, pendingIntent: PendingIntent): Bubble { + val metadata = Notification.BubbleMetadata.Builder( + pendingIntent, + Icon.createWithResource(context, R.drawable.bubble_ic_create_bubble) + ).build() + val shortcutInfo = ShortcutInfo.Builder(context) + .setId("shortcutId") + .build() + val notification: Notification = + Notification.Builder(context, key) + .setSmallIcon(mock<Icon>()) + .setWhen(System.currentTimeMillis()) + .setContentTitle("title") + .setContentText("content") + .setBubbleMetadata(metadata) + .build() + val sbn = mock<StatusBarNotification>() + val ranking = mock<Ranking>() + whenever(sbn.getNotification()).thenReturn(notification) + whenever(sbn.getKey()).thenReturn(key) + whenever(ranking.getConversationShortcutInfo()).thenReturn(shortcutInfo) + val entry = BubbleEntry(sbn, ranking, true, false, false, false) + return Bubble( + entry, mock<BubbleMetadataFlagListener>(), null, mainExecutor, + bgExecutor + ) + } + + /** + * FrameLayout that immediately runs any runnables posted to it and tracks view removals. + */ + class ViewPoster(context: Context) : FrameLayout(context) { + + lateinit var lastRemovedView: View + + override fun post(r: Runnable): Boolean { + r.run() + return true + } + + override fun removeView(v: View) { + super.removeView(v) + lastRemovedView = v + } + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt index f1ba0423b422..4168686e9947 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt @@ -35,7 +35,6 @@ import com.android.internal.protolog.ProtoLog import com.android.internal.statusbar.IStatusBarService import com.android.launcher3.icons.BubbleIconFactory import com.android.wm.shell.ShellTaskOrganizer -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -161,7 +160,7 @@ class BubbleViewInfoTaskTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - mock<BubbleProperties>() + BubbleResizabilityChecker() ) // TODO: (b/371829099) - when optional overflow is no longer flagged we can enable this @@ -328,7 +327,6 @@ class BubbleViewInfoTaskTest { expandedViewManager, bubbleTaskViewFactory, bubblePositioner, - bubbleLogger, bubbleStackView, null /* layerView */, iconFactory, diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt index 3c013d3636e8..adcd835d72be 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleExpandedViewManager.kt @@ -38,7 +38,7 @@ class FakeBubbleExpandedViewManager(var bubbleBar: Boolean = false, var expanded override fun dismissBubble(bubble: Bubble, reason: Int) {} - override fun setAppBubbleTaskId(key: String, taskId: Int) {} + override fun setNoteBubbleTaskId(key: String, taskId: Int) {} override fun isStackExpanded(): Boolean { return expanded diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt index 750178678785..af33a8daee9d 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt @@ -45,10 +45,10 @@ class FakeBubbleFactory { .inflate(R.layout.bubble_bar_expanded_view, null, false /* attachToRoot */) as BubbleBarExpandedView) .apply { + this.bubbleLogger = bubbleLogger initialize( expandedViewManager, bubblePositioner, - bubbleLogger, false, /* isOverflow */ bubbleTaskView, mainExecutor, diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt new file mode 100644 index 000000000000..40e80d02e7b3 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/TestActivity.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.bubbles + +import android.app.Activity +import android.os.Bundle +import android.widget.FrameLayout + +class TestActivity : Activity() { + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(FrameLayout(getApplicationContext())) + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt index af238d033aee..3499ee32e649 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/UiEventSubjectTest.kt @@ -29,7 +29,8 @@ import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock import org.mockito.kotlin.whenever /** Test for [UiEventSubject] */ @@ -130,10 +131,10 @@ class UiEventSubjectTest { } private fun createBubble(appUid: Int, packageName: String, instanceId: InstanceId): Bubble { - return mock(Bubble::class.java).apply { - whenever(getAppUid()).thenReturn(appUid) - whenever(getPackageName()).thenReturn(packageName) - whenever(getInstanceId()).thenReturn(instanceId) + return mock<Bubble>() { + on { getAppUid() } doReturn appUid + on { getPackageName() } doReturn packageName + on { getInstanceId() } doReturn instanceId } } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt index d3cfbd00c4a3..56cee4221dba 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt @@ -42,10 +42,10 @@ import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubbleOverflow import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.bubbles.BubbleTaskView -import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewController import com.android.wm.shell.taskview.TaskViewTaskController @@ -358,7 +358,7 @@ class BubbleBarAnimationHelperTest { private fun createOverflow(): BubbleOverflow { val overflow = BubbleOverflow(context, bubblePositioner) - overflow.initializeForBubbleBar(expandedViewManager, bubblePositioner, bubbleLogger) + overflow.initializeForBubbleBar(expandedViewManager, bubblePositioner) return overflow } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt index 7f65e22736b3..1440873cfdf7 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewTest.kt @@ -39,11 +39,11 @@ import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.bubbles.BubbleTaskView import com.android.wm.shell.bubbles.BubbleTaskViewFactory -import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.RegionSamplingProvider import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat import com.android.wm.shell.common.TestShellExecutor +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.android.wm.shell.shared.handles.RegionSamplingHelper import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewController @@ -114,10 +114,10 @@ class BubbleBarExpandedViewTest { bubbleExpandedView = inflater.inflate( R.layout.bubble_bar_expanded_view, null, false /* attachToRoot */ ) as BubbleBarExpandedView + bubbleExpandedView.bubbleLogger = BubbleLogger(uiEventLoggerFake) bubbleExpandedView.initialize( expandedViewManager, positioner, - BubbleLogger(uiEventLoggerFake), false /* isOverflow */, bubbleTaskView, mainExecutor, @@ -279,7 +279,6 @@ class BubbleBarExpandedViewTest { expandedView.initialize( expandedViewManager, positioner, - BubbleLogger(uiEventLoggerFake), false /* isOverflow */, taskView, mainExecutor, @@ -321,7 +320,6 @@ class BubbleBarExpandedViewTest { expandedView.initialize( expandedViewManager, positioner, - BubbleLogger(uiEventLoggerFake), false /* isOverflow */, taskView, mainExecutor, diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt index a6492476176b..7b5831376dc0 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -43,13 +43,13 @@ import com.android.wm.shell.bubbles.BubbleDataRepository import com.android.wm.shell.bubbles.BubbleExpandedViewManager import com.android.wm.shell.bubbles.BubbleLogger import com.android.wm.shell.bubbles.BubblePositioner +import com.android.wm.shell.bubbles.BubbleResizabilityChecker import com.android.wm.shell.bubbles.Bubbles.SysuiProxy import com.android.wm.shell.bubbles.FakeBubbleExpandedViewManager import com.android.wm.shell.bubbles.FakeBubbleFactory import com.android.wm.shell.bubbles.FakeBubbleTaskViewFactory import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.bubbles.storage.BubblePersistentRepository import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController @@ -73,7 +73,6 @@ import org.junit.Before import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.Mockito.mock import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -127,7 +126,7 @@ class BubbleBarLayerViewTest { mainExecutor, bgExecutor, ) - bubbleController.asBubbles().setSysuiProxy(mock(SysuiProxy::class.java)) + bubbleController.asBubbles().setSysuiProxy(mock<SysuiProxy>()) // Flush so that proxy gets set mainExecutor.flushAll() @@ -200,7 +199,7 @@ class BubbleBarLayerViewTest { mock<Transitions>(), SyncTransactionQueue(TransactionPool(), mainExecutor), mock<IWindowManager>(), - mock<BubbleProperties>(), + BubbleResizabilityChecker() ) } diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt index d4cbe6e10971..1b0e11f7103c 100644 --- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleExpandedViewPinControllerTest.kt @@ -30,13 +30,13 @@ import androidx.test.platform.app.InstrumentationRegistry.getInstrumentation import com.android.internal.protolog.ProtoLog import com.android.wm.shell.R import com.android.wm.shell.bubbles.BubblePositioner -import com.android.wm.shell.bubbles.DeviceConfig import com.android.wm.shell.shared.bubbles.BaseBubblePinController import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_IN_DURATION import com.android.wm.shell.shared.bubbles.BaseBubblePinController.Companion.DROP_TARGET_ALPHA_OUT_DURATION import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.shared.bubbles.BubbleBarLocation.LEFT import com.android.wm.shell.shared.bubbles.BubbleBarLocation.RIGHT +import com.android.wm.shell.shared.bubbles.DeviceConfig import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt new file mode 100644 index 000000000000..50d9f77389c8 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/common/TestSyncExecutor.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +class TestSyncExecutor : ShellExecutor { + override fun execute(runnable: Runnable) { + runnable.run() + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + runnable.run() + } + + override fun removeCallbacks(runnable: Runnable) { + } + + override fun hasCallback(runnable: Runnable): Boolean { + return false + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml index ce242751c172..05c1e094d7ae 100644 --- a/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml +++ b/libs/WindowManager/Shell/res/drawable/decor_handle_dark.xml @@ -13,20 +13,10 @@ ~ See the License for the specific language governing permissions and ~ limitations under the License. --> -<vector - xmlns:android="http://schemas.android.com/apk/res/android" - android:width="128dp" - android:height="4dp" - android:viewportWidth="128" - android:viewportHeight="4" - > - <group> - <clip-path - android:pathData="M2 0H126C127.105 0 128 0.895431 128 2C128 3.10457 127.105 4 126 4H2C0.895431 4 0 3.10457 0 2C0 0.895431 0.895431 0 2 0Z" - /> - <path - android:pathData="M0 0V4H128V0" - android:fillColor="@android:color/black" - /> - </group> -</vector> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + android:shape="rectangle"> + <solid android:color="@android:color/black"/> + <corners android:radius="2dp"/> + <size android:height="4dp"/> +</shape> diff --git a/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml new file mode 100644 index 000000000000..c2a20b977b70 --- /dev/null +++ b/libs/WindowManager/Shell/res/drawable/ic_baseline_expand_more_16.xml @@ -0,0 +1,28 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2023 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 + --> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="16dp" + android:height="16dp" + android:viewportWidth="16" + android:viewportHeight="16" + android:tint="?android:attr/textColorSecondary"> + <path + android:fillColor="#FF000000" + android:pathData="M 8 11.375 L 2 5.375 L 3.4 3.975 L 8 8.575 L 12.6 3.975 L 14 5.375 L 8 11.375 Z" + /> +</vector> + diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml index 1d1cdfa85040..9451fd43b1d8 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_handle.xml @@ -20,7 +20,7 @@ android:id="@+id/desktop_mode_caption" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:gravity="center_horizontal"> + android:gravity="center"> <com.android.wm.shell.windowdecor.HandleImageButton android:id="@+id/caption_handle" diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml index 87c520ca1b51..b898e4b06c14 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml @@ -64,7 +64,7 @@ android:id="@+id/expand_menu_button" android:layout_width="16dp" android:layout_height="16dp" - android:src="@drawable/ic_baseline_expand_more_24" + android:src="@drawable/ic_baseline_expand_more_16" android:background="@null" android:scaleType="fitCenter" android:clickable="false" @@ -101,7 +101,7 @@ android:layout_width="44dp" android:layout_height="40dp" android:layout_gravity="end" - android:layout_marginHorizontal="8dp" + android:layout_marginEnd="8dp" android:clickable="true" android:focusable="true"/> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml index b69563b46e06..477d207a5c7e 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu.xml @@ -41,30 +41,21 @@ android:id="@+id/application_icon" android:layout_width="@dimen/desktop_mode_caption_icon_radius" android:layout_height="@dimen/desktop_mode_caption_icon_radius" - android:layout_marginStart="12dp" + android:layout_marginStart="10dp" android:layout_marginEnd="12dp" android:contentDescription="@string/app_icon_text" android:importantForAccessibility="no"/> - <TextView + <com.android.wm.shell.windowdecor.MarqueedTextView android:id="@+id/application_name" - android:layout_width="0dp" - android:layout_height="wrap_content" tools:text="Gmail" - android:importantForAccessibility="no" - android:textColor="@androidprv:color/materialColorOnSurface" - android:textSize="14sp" - android:textFontWeight="500" - android:lineHeight="20dp" - android:textStyle="normal" - android:layout_weight="1"/> + style="@style/DesktopModeHandleMenuActionButtonTextView"/> <com.android.wm.shell.windowdecor.HandleMenuImageButton android:id="@+id/collapse_menu_button" - android:layout_width="32dp" - android:layout_height="32dp" - android:padding="4dp" - android:layout_marginEnd="14dp" + android:layout_width="16dp" + android:layout_height="16dp" + android:layout_marginEnd="16dp" android:layout_marginStart="14dp" android:contentDescription="@string/collapse_menu_text" android:src="@drawable/ic_baseline_expand_more_24" @@ -86,40 +77,55 @@ <ImageButton android:id="@+id/fullscreen_button" - android:layout_marginEnd="4dp" + android:paddingStart="16dp" + android:paddingEnd="12dp" android:contentDescription="@string/fullscreen_text" android:src="@drawable/desktop_mode_ic_handle_menu_fullscreen" android:tint="@androidprv:color/materialColorOnSurface" - android:layout_weight="1" style="@style/DesktopModeHandleMenuWindowingButton"/> + <Space + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + <ImageButton android:id="@+id/split_screen_button" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp" + android:paddingStart="14dp" + android:paddingEnd="14dp" android:contentDescription="@string/split_screen_text" android:src="@drawable/desktop_mode_ic_handle_menu_splitscreen" android:tint="@androidprv:color/materialColorOnSurface" - android:layout_weight="1" style="@style/DesktopModeHandleMenuWindowingButton"/> + <Space + android:id="@+id/floating_button_space" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + <ImageButton android:id="@+id/floating_button" - android:layout_marginStart="4dp" - android:layout_marginEnd="4dp" + android:paddingStart="14dp" + android:paddingEnd="14dp" android:contentDescription="@string/float_button_text" android:src="@drawable/desktop_mode_ic_handle_menu_floating" android:tint="@androidprv:color/materialColorOnSurface" - android:layout_weight="1" style="@style/DesktopModeHandleMenuWindowingButton"/> + <Space + android:id="@+id/desktop_button_space" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_weight="1"/> + <ImageButton android:id="@+id/desktop_button" - android:layout_marginStart="4dp" + android:paddingStart="12dp" + android:paddingEnd="16dp" android:contentDescription="@string/desktop_text" android:src="@drawable/desktop_mode_ic_handle_menu_desktop" android:tint="@androidprv:color/materialColorOnSurface" - android:layout_weight="1" style="@style/DesktopModeHandleMenuWindowingButton"/> </LinearLayout> @@ -134,37 +140,33 @@ android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> - <Button + <com.android.wm.shell.windowdecor.HandleMenuActionButton android:id="@+id/screenshot_button" android:contentDescription="@string/screenshot_text" android:text="@string/screenshot_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_screenshot" - android:drawableTint="@androidprv:color/materialColorOnSurface" + android:src="@drawable/desktop_mode_ic_handle_menu_screenshot" style="@style/DesktopModeHandleMenuActionButton"/> - <Button + <com.android.wm.shell.windowdecor.HandleMenuActionButton android:id="@+id/new_window_button" android:contentDescription="@string/new_window_text" android:text="@string/new_window_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_new_window" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + android:src="@drawable/desktop_mode_ic_handle_menu_new_window" + style="@style/DesktopModeHandleMenuActionButton"/> - <Button + <com.android.wm.shell.windowdecor.HandleMenuActionButton android:id="@+id/manage_windows_button" android:contentDescription="@string/manage_windows_text" android:text="@string/manage_windows_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_manage_windows" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + android:src="@drawable/desktop_mode_ic_handle_menu_manage_windows" + style="@style/DesktopModeHandleMenuActionButton"/> - <Button + <com.android.wm.shell.windowdecor.HandleMenuActionButton android:id="@+id/change_aspect_ratio_button" android:contentDescription="@string/change_aspect_ratio_text" android:text="@string/change_aspect_ratio_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton" /> + android:src="@drawable/desktop_mode_ic_handle_menu_change_aspect_ratio" + style="@style/DesktopModeHandleMenuActionButton"/> </LinearLayout> <LinearLayout @@ -177,22 +179,22 @@ android:elevation="@dimen/desktop_mode_handle_menu_pill_elevation" android:background="@drawable/desktop_mode_decor_handle_menu_background"> - <Button + <com.android.wm.shell.windowdecor.HandleMenuActionButton android:id="@+id/open_in_app_or_browser_button" - android:layout_weight="1" android:contentDescription="@string/open_in_browser_text" android:text="@string/open_in_browser_text" - android:drawableStart="@drawable/desktop_mode_ic_handle_menu_open_in_browser" - android:drawableTint="@androidprv:color/materialColorOnSurface" - style="@style/DesktopModeHandleMenuActionButton"/> + android:src="@drawable/desktop_mode_ic_handle_menu_open_in_browser" + style="@style/DesktopModeHandleMenuActionButton" + android:layout_width="0dp" + android:layout_weight="1"/> <ImageButton android:id="@+id/open_by_default_button" android:layout_width="20dp" android:layout_height="20dp" android:layout_gravity="end|center_vertical" + android:layout_marginStart="8dp" android:layout_marginEnd="16dp" - android:layout_marginStart="10dp" android:contentDescription="@string/open_by_default_settings_text" android:src="@drawable/desktop_mode_ic_handle_menu_open_by_default_settings" android:tint="@androidprv:color/materialColorOnSurface"/> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml new file mode 100644 index 000000000000..379f4e984b73 --- /dev/null +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_handle_menu_action_button.xml @@ -0,0 +1,38 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> +<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + xmlns:tools="http://schemas.android.com/tools" + android:id="@+id/action_button" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:gravity="start|center_vertical" + android:paddingHorizontal="16dp" + android:clickable="true" + android:focusable="true" + android:orientation="horizontal" + android:background="?android:attr/selectableItemBackground"> + + <ImageView + android:id="@+id/image" + android:contentDescription="@+id/label" + style="@style/DesktopModeHandleMenuActionButtonImage"/> + + <com.android.wm.shell.windowdecor.MarqueedTextView + android:id="@+id/label" + style="@style/DesktopModeHandleMenuActionButtonTextView"/> +</LinearLayout> diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml index 8d7e5fd95957..d50a14cf5dae 100644 --- a/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml +++ b/libs/WindowManager/Shell/res/layout/desktop_mode_window_decor_maximize_menu.xml @@ -18,23 +18,28 @@ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" android:id="@+id/maximize_menu" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:background="@drawable/desktop_mode_maximize_menu_background" android:elevation="1dp"> <LinearLayout android:id="@+id/container" android:layout_width="wrap_content" - android:layout_height="@dimen/desktop_mode_maximize_menu_height" + android:layout_height="wrap_content" android:orientation="horizontal" - android:padding="16dp" + android:paddingHorizontal="12dp" + android:paddingVertical="16dp" + android:measureWithLargestChild="true" android:gravity="center"> <LinearLayout android:id="@+id/maximize_menu_immersive_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -44,21 +49,22 @@ android:stateListAnimator="@null" android:importantForAccessibility="yes" android:contentDescription="@string/desktop_mode_maximize_menu_immersive_button_text" - android:layout_marginEnd="8dp" android:layout_marginBottom="4dp" android:alpha="0"/> <TextView android:id="@+id/maximize_menu_immersive_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_immersive_button_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -66,7 +72,11 @@ android:id="@+id/maximize_menu_size_toggle_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <Button android:layout_width="94dp" @@ -81,15 +91,17 @@ <TextView android:id="@+id/maximize_menu_size_toggle_button_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" + android:lineHeight="16sp" android:gravity="center" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:importantForAccessibility="no" android:text="@string/desktop_mode_maximize_menu_maximize_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> @@ -97,7 +109,11 @@ android:id="@+id/maximize_menu_snap_container" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:orientation="vertical"> + android:layout_weight="1" + android:orientation="vertical" + android:gravity="center_horizontal" + android:layout_marginStart="4dp" + android:layout_marginEnd="4dp"> <LinearLayout android:id="@+id/maximize_menu_snap_menu_layout" android:layout_width="wrap_content" @@ -106,7 +122,6 @@ android:padding="4dp" android:background="@drawable/desktop_mode_maximize_menu_layout_background" android:layout_marginBottom="4dp" - android:layout_marginStart="8dp" android:alpha="0"> <Button android:id="@+id/maximize_menu_snap_left_button" @@ -131,16 +146,17 @@ </LinearLayout> <TextView android:id="@+id/maximize_menu_snap_window_text" - android:layout_width="94dp" - android:layout_height="18dp" + android:layout_width="wrap_content" + android:layout_height="wrap_content" android:textSize="11sp" - android:layout_marginBottom="76dp" - android:layout_gravity="center" + android:lineHeight="16sp" android:gravity="center" android:importantForAccessibility="no" android:fontFamily="google-sans-text" + android:textFontWeight="500" android:text="@string/desktop_mode_maximize_menu_snap_text" android:textColor="@androidprv:color/materialColorOnSurface" + android:singleLine="true" android:alpha="0"/> </LinearLayout> </LinearLayout> @@ -150,6 +166,6 @@ <View android:id="@+id/maximize_menu_overlay" android:layout_width="match_parent" - android:layout_height="@dimen/desktop_mode_maximize_menu_height"/> + android:layout_height="match_parent"/> </FrameLayout> diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index a975682b065e..5444c26e9ec9 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Links 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Links 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Volskerm regs"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Ruil apps om"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Volskerm bo"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Bo 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bo 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Stel terug"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Spring na links"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Spring na regs"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Verander grootte van linkerkantse venster"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Verander grootte van regterkantse venster"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimeer of stel venstergrootte terug"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Gaan na deelskermmodus"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Gaan na werkskermvenstermodus"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Verander grootte van linkerkantse venster"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Verander grootte van regterkantse venster"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimeer of stel venstergrootte terug"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimeer of stel venstergrootte terug"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimeer appvenster"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Maak By Verstek Oop-instellings"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Kies hoe om webskakels vir hierdie app oop te maak"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In die app"</string> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index a6921b992234..f3bc29d95673 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ግራ 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ግራ 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"የቀኝ ሙሉ ማያ ገፅ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"መተግበሪያዎችን ይቀያይሩ"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"የላይ ሙሉ ማያ ገፅ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ከላይ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ከላይ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ወደነበረበት መልስ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ወደ ግራ አሳድግ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ወደ ቀኝ አሳድግ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"የመተግበሪያ መስኮትን ወደ ግራ መጠን ቀይር"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"የመተግበሪያ መስኮትን ወደ ቀኝ መጠን ቀይር"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"የመስኮት መጠንን አሳድግ ወይም ወደነበረበት መልስ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ወደ የተከፈለ ማያ ገፅ ሁነታ ግባ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ወደ የዴስክቶፕ መስኮት ሁነታ ግባ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"መስኮትን ወደ ግራ መጠን ቀይር"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"መስኮትን ወደ ቀኝ መጠን ቀይር"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"የመስኮት መጠንን አሳድግ ወይም ወደነበረበት መልስ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"የመስኮት መጠንን አሳድግ ወይም ወደነበረበት መልስ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"የመተግበሪያ መስኮትን አሳንስ"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"በነባሪ ቅንብሮች ክፈት"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ለዚህ የድር መተግበሪያ አገናኙን እንዴት እንደሚከፍቱ ይምረጡ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"በመተግበሪያው ውስጥ"</string> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index b72d25519e4f..60f27cfdee91 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ضبط حجم النافذة اليسرى ليكون ٥٠%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ضبط حجم النافذة اليسرى ليكون ٣٠%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"عرض النافذة اليمنى بملء الشاشة"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"تبديل التطبيقات"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"عرض النافذة العلوية بملء الشاشة"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ضبط حجم النافذة العلوية ليكون ٧٠%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ضبط حجم النافذة العلوية ليكون ٥٠%"</string> @@ -124,7 +125,7 @@ <string name="float_button_text" msgid="9221657008391364581">"نافذة عائمة"</string> <string name="select_text" msgid="5139083974039906583">"اختيار"</string> <string name="screenshot_text" msgid="1477704010087786671">"لقطة شاشة"</string> - <string name="open_in_browser_text" msgid="9181692926376072904">"فتح في المتصفِّح"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"الفتح في المتصفِّح"</string> <string name="open_in_app_text" msgid="2874590745116268525">"فتح في التطبيق"</string> <string name="new_window_text" msgid="6318648868380652280">"نافذة جديدة"</string> <string name="manage_windows_text" msgid="5567366688493093920">"إدارة النوافذ"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"استعادة"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"المحاذاة إلى اليسار"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"المحاذاة إلى اليمين"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"تغيير حجم نافذة التطبيق بمحاذاتها إلى اليمين"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"تغيير حجم نافذة التطبيق بمحاذاتها إلى اليسار"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"تكبير حجم النافذة أو استعادته"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"تفعيل \"وضع تقسيم الشاشة\""</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"تفعيل وضع عرض المحتوى في النافذة الحالية على سطح المكتب"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"تغيير حجم النافذة بمحاذاتها إلى اليمين"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"تغيير حجم النافذة بمحاذاتها إلى اليسار"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"تكبير حجم النافذة أو استعادته"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"تكبير حجم النافذة أو استعادته"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"تصغير نافذة التطبيق"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"إعدادات الفتح تلقائيًا"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"اختيار طريقة فتح روابط الويب لهذا التطبيق"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"في التطبيق"</string> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 632d1265a1e6..0f433479e130 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"বাওঁফালৰ স্ক্ৰীনখন ৫০% কৰক"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"বাওঁফালৰ স্ক্ৰীনখন ৩০% কৰক"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"সোঁফালৰ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"এপ্সমূহ সলনাসলনি কৰক"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"শীৰ্ষ স্ক্ৰীনখন সম্পূৰ্ণ স্ক্ৰীন কৰক"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ স্ক্ৰীনখন ৭০% কৰক"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ স্ক্ৰীনখন ৫০% কৰক"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"পুনঃস্থাপন কৰক"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাওঁফাললৈ স্নেপ কৰক"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"সোঁফাললৈ স্নেপ কৰক"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"বাওঁফালে এপ্ ৱিণ্ড’ৰ আকাৰ সলনি কৰক"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"সোঁফালে এপ্ ৱিণ্ড’ৰ আকাৰ সলনি কৰক"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ৱিণ্ড’ৰ আকাৰ মেক্সিমাইজ বা পুনঃস্থাপন কৰক"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"বিভাজিত-স্ক্ৰীন ম’ড দিয়ক"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ডেস্কটপ ৱিণ্ড’ইং ম’ড দিয়ক"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"সোঁফাললৈ ৱিণ্ড’ৰ আকাৰ সলনি কৰক"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"বাওঁফাললৈ ৱিণ্ড’ৰ আকাৰ সলনি কৰক"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ৱিণ্ড’ৰ আকাৰ মেক্সিমাইজ বা পুনঃস্থাপন কৰক"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ৱিণ্ড’ৰ আকাৰ মেক্সিমাইজ বা পুনঃস্থাপন কৰক"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"এপ্ ৱিণ্ড’ মিনিমাইজ কৰক"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ডিফ’ল্ট ছেটিং খোলক"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"এই এপ্টোৰ বাবে কিদৰে ৱেব লিংক খুলিব পাৰি সেয়া বাছনি কৰক"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"এপ্টোত"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index cf9f1b251af7..aced354ac826 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sol 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Sol 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Sağ tam ekran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Tətbiqləri dəyişin"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Yuxarı tam ekran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Yuxarı 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yuxarı 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Bərpa edin"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Sola tərəf çəkin"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Sağa tərəf çəkin"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Tətbiq pəncərəsinin ölçüsünü sola dəyişin"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Tətbiq pəncərəsinin ölçüsünü sağa dəyişin"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Pəncərə ölçüsünü artırın və ya bərpa edin"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Bölünmüş ekran rejiminə daxil olun"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Masaüstü pəncərə rejiminə daxil olun"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Pəncərə ölçüsünü sola dəyişin"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Pəncərə ölçüsünü sağa dəyişin"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Pəncərə ölçüsünü artırın və ya bərpa edin"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Pəncərə ölçüsünü artırın və ya bərpa edin"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Tətbiq pəncərəsini kiçildin"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Defolt ayarlarla açın"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Bu tətbiq üçün veb-linklərin necə açılacağını seçin"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Tətbiqdə"</string> diff --git a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml index c2d4d8b0613e..b07c61258f4e 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi ekran 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Levi ekran 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Režim celog ekrana za donji ekran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zamenite mesta aplikacijama"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Režim celog ekrana za gornji ekran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gornji ekran 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji ekran 50%"</string> @@ -124,11 +125,11 @@ <string name="float_button_text" msgid="9221657008391364581">"Plutajuće"</string> <string name="select_text" msgid="5139083974039906583">"Izaberite"</string> <string name="screenshot_text" msgid="1477704010087786671">"Snimak ekrana"</string> - <string name="open_in_browser_text" msgid="9181692926376072904">"Otvorite u pregledaču"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Otvori u pregledaču"</string> <string name="open_in_app_text" msgid="2874590745116268525">"Otvorite u aplikaciji"</string> <string name="new_window_text" msgid="6318648868380652280">"Novi prozor"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Upravljajte prozorima"</string> - <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Promenite razmeru"</string> + <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Promeni razmeru"</string> <string name="close_text" msgid="4986518933445178928">"Zatvorite"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Zatvorite meni"</string> <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Otvorite meni"</string> @@ -137,10 +138,20 @@ <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacija ne može da se premesti ovde"</string> <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Imerzivne"</string> <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Vrati"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Uvećajte"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Uvećaj"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Vratite"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Prikačite levo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Prikačite desno"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Promenite veličinu prozora aplikacije nalevo"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Promenite veličinu prozora aplikacije nadesno"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Uvećajte ili vratite veličinu prozora"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Uđite u režim podeljenog ekrana"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Uđite u režim prozora na računaru"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Promenite veličinu prozora nalevo"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Promenite veličinu prozora nadesno"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Uvećajte ili vratite veličinu prozora"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Uvećajte ili vratite veličinu prozora"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Umanjite prozor aplikacije"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Podešavanje Podrazumevano otvaraj"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Odaberite način otvaranja veb-linkova za ovu aplikaciju"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"U aplikaciji"</string> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index dde2374ea491..4c2950b5afa1 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левы экран – 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левы экран – 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Правы экран – поўнаэкранны рэжым"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Пераключыць праграмы"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхні экран – поўнаэкранны рэжым"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхні экран – 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхні экран – 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Аднавіць"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Размясціць злева"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Размясціць справа"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Змяніць памер акна (злева)"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Змяніць памер акна (справа)"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Разгарнуць акно ці аднавіць яго памер"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Уключыць рэжым падзеленага экрана"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Уключыць рэжым вокнаў працоўнага стала"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Змяніць памер акна і перамясціць да левага краю"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Змяніць памер акна і перамясціць да правага краю"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Разгарнуць акно ці аднавіць яго памер"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Разгарнуць акно ці аднавіць яго памер"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Згарнуць акно праграмы"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Налады параметра \"Адкрываць стандартна\""</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Выберыце, як гэта праграма будзе адкрываць вэб-спасылкі"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"У праграме"</string> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 7e804843dfce..fcc4d83baf75 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ляв екран: 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ляв екран: 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Десен екран: Показване на цял екран"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Размяна на приложенията"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Горен екран: Показване на цял екран"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горен екран: 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горен екран: 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Възстановяване"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Прилепване наляво"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Прилепване надясно"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Преоразмеряване на прозореца на приложението наляво"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Преоразмеряване на прозореца на приложението надясно"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Увеличаване или възстановяване на размера на прозореца"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Активиране на режима за разделен екран"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Активиране на режима за настолни компютри"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Преоразмеряване на прозореца наляво"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Преоразмеряване на прозореца надясно"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Увеличаване или възстановяване на размера на прозореца"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Увеличаване или възстановяване на размера на прозореца"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Намаляване на прозореца на приложението"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Отваряне на настройките по подразбиране"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Изберете как да се отварят уеб връзките за това приложение"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"В приложението"</string> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 4c6e6c1fee2f..b2c435e5ef0e 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"৫০% বাকি আছে"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"৩০% বাকি আছে"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ডান দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"অ্যাপ পাল্টান"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"উপর দিকের অংশ নিয়ে পূর্ণ স্ক্রিন"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"শীর্ষ ৭০%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"শীর্ষ ৫০%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ফিরিয়ে আনুন"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"বাঁদিকে স্ন্যাপ করুন"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ডানদিকে স্ন্যাপ করুন"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"বাঁদিকে অ্যাপ উইন্ডো রিসাইজ করুন"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ডানদিকে অ্যাপ উইন্ডো রিসাইজ করুন"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"উইন্ডো সাইজ বড় বা রিস্টোর করুন"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"স্প্লিট স্ক্রিন মোডে প্রবেশ করুন"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ডেস্কটপ উইন্ডোইং মোডে প্রবেশ করুন"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"বাঁদিকে উইন্ডো রিসাইজ করুন"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ডানদিকে উইন্ডো রিসাইজ করুন"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"উইন্ডো সাইজ বড় বা রিস্টোর করুন"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"উইন্ডো সাইজ বড় বা রিস্টোর করুন"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"অ্যাপ উইন্ডো ছোট করুন"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ডিফল্ট হিসেবে থাকা সেটিংস খুলুন"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"এই অ্যাপের জন্য কীভাবে ওয়েব লিঙ্ক খুলবেন তা বেছে নিন"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"অ্যাপের মধ্যে"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 244149b855f6..8c1619ce925c 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevo 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Lijevo 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desno cijeli ekran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zamjena aplikacija"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Gore cijeli ekran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gore 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gore 50%"</string> @@ -137,10 +138,20 @@ <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ne možete premjestiti aplikaciju ovdje"</string> <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Uvjerljivo"</string> <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Vraćanje"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziranje"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiziraj"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Vraćanje"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pomicanje ulijevo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pomicanje udesno"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Promjena veličine prozora aplikacije lijevo"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Promjena veličine prozora aplikacije desno"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimiziranje ili vraćanje veličine prozora"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Pokretanje načina rada podijeljenog ekrana"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Pokretanje načina rada s prozorima na radnoj površini"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Promjena veličine prozora i poravnanje lijevo"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Promjena veličine prozora i poravnanje desno"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimiziranje ili vraćanje veličine prozora"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimiziranje ili vraćanje veličine prozora"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimiziranje prozora aplikacije"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Otvaranje prema zadanim postavkama"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Odaberite način otvaranja web linkova za ovu aplikaciju"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"U aplikaciji"</string> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index 786ed769e7b7..37802f4c7f94 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pantalla esquerra al 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Pantalla esquerra al 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla dreta completa"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Canvia les aplicacions"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Pantalla superior al 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Pantalla superior al 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaura"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajusta a l\'esquerra"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajusta a la dreta"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Canvia la mida de la finestra de l\'aplicació a l\'esquerra"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Canvia la mida de la finestra de l\'aplicació a la dreta"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximitza o restaura la mida de la finestra"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Entra al mode de pantalla dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Entra al mode d\'enfinestrament a l\'escriptori"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Canvia la mida de la finestra a l\'esquerra"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Canvia la mida de la finestra a la dreta"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximitza o restaura la mida de la finestra"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximitza o restaura la mida de la finestra"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimitza la finestra de l\'aplicació"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Configuració d\'obertura predeterminada"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Tria com vols obrir els enllaços web per a aquesta aplicació"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"A l\'aplicació"</string> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 99e9a8350822..c4514eb4ce8d 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % vlevo"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % vlevo"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pravá část na celou obrazovku"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zaměnit aplikace"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Horní část na celou obrazovku"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % nahoře"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % nahoře"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Obnovit"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Přichytit vlevo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Přichytit vpravo"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Změnit velikost okna aplikace vlevo"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Změnit velikost okna aplikace vpravo"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximalizovat nebo obnovit velikost okna"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Přechod do režimu rozdělené obrazovky"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Přejít do režimu okenního systému pro počítače"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Přichytit okno vlevo"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Přichytit okno vpravo"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximalizovat nebo obnovit velikost okna"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximalizovat nebo obnovit velikost okna"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimalizovat okno aplikace"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Otevírat podle výchozího nastavení"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Určete, jak se v této aplikaci mají otevírat webové odkazy"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"V aplikaci"</string> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 6021a96e8cbe..e662a16ced1e 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Venstre 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Venstre 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Vis højre del i fuld skærm"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Byt apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Vis øverste del i fuld skærm"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Øverste 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Øverste 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Gendan"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fastgør til venstre"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fastgør til højre"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Juster størrelsen på appvinduet til venstre"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Juster størrelsen på appvinduet til højre"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimer eller gendan vinduesstørrelse"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Åbn opdelt skærm"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Åbn tilstanden for vinduer på computeren"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Juster størrelsen på vinduet til venstre"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Juster størrelsen på vinduet til højre"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimer eller gendan vinduesstørrelse"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimer eller gendan vinduesstørrelse"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimer appvindue"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Indstillinger for automatisk åbning"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Vælg, hvordan denne app skal åben weblinks"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"I appen"</string> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 7b296620099b..7b21719bc880 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % links"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % links"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Vollbild rechts"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Apps austauschen"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Vollbild oben"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % oben"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % oben"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Wiederherstellen"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Links andocken"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Rechts andocken"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Größe des linken App-Fensters anpassen"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Größe des rechten App-Fensters anpassen"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Fenstergröße maximieren oder wiederherstellen"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Splitscreen-Modus aktivieren"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Desktop-Fenstermodus aktivieren"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Fenstergröße links anpassen"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Fenstergröße rechts anpassen"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Fenstergröße maximieren oder wiederherstellen"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Fenstergröße maximieren oder wiederherstellen"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"App-Fenster minimieren"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Einstellungen für die Option „Standardmäßig öffnen“"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Festlegen, wie Weblinks für diese App geöffnet werden"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In der App"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index 879347adf406..eb45a31c5d8a 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Αριστερή 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Αριστερή 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Δεξιά πλήρης οθόνη"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Εναλλαγή εφαρμογών"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Πάνω πλήρης οθόνη"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Πάνω 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Πάνω 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Επαναφορά"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Κούμπωμα αριστερά"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Κούμπωμα δεξιά"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Αλλαγή μεγέθους παραθύρου εφαρμογής αριστερά"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Αλλαγή μεγέθους παραθύρου εφαρμογής δεξιά"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Μεγιστοποίηση ή επαναφορά μεγέθους παραθύρου"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Είσοδος στη λειτουργία διαχωρισμού οθόνης"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Είσοδος στη λειτουργία προσαρμογής σε παράθυρο υπολογιστή"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Αλλαγή μεγέθους παραθύρου προς τα αριστερά"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Αλλαγή μεγέθους παραθύρου προς τα δεξιά"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Μεγιστοποίηση ή επαναφορά μεγέθους παραθύρου"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Μεγιστοποίηση ή επαναφορά μεγέθους παραθύρου"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Ελαχιστοποίηση παραθύρου εφαρμογής"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Άνοιγμα ρυθμίσεων από προεπιλογή"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Επιλογή τρόπου ανοίγματος συνδέσμων ιστού για την εφαρμογή"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Στην εφαρμογή"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 358e31476242..8dc27dabfc2c 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Swap apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Resize app window left"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Resize app window right"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximise or restore window size"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Enter split-screen mode"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Enter desktop windowing mode"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Resize window to left"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Resize window to right"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximise or restore window size"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximise or restore window size"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimise app window"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Open by default settings"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choose how to open web links for this app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In the app"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 923f30b9a5ba..20d141e7808c 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Swap Apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Resize app window left"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Resize app window right"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximize or restore window size"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Enter split screen mode"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Enter desktop windowing mode"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Resize window to left"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Resize window to right"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximize or restore window size"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximize or restore window size"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimize app window"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Open by default settings"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choose how to open web links for this app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In the app"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 358e31476242..8dc27dabfc2c 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Swap apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Resize app window left"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Resize app window right"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximise or restore window size"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Enter split-screen mode"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Enter desktop windowing mode"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Resize window to left"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Resize window to right"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximise or restore window size"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximise or restore window size"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimise app window"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Open by default settings"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choose how to open web links for this app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In the app"</string> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 358e31476242..8dc27dabfc2c 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Left 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Left 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Right full screen"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Swap apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Top full screen"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Top 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Top 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Snap left"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Snap right"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Resize app window left"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Resize app window right"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximise or restore window size"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Enter split-screen mode"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Enter desktop windowing mode"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Resize window to left"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Resize window to right"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximise or restore window size"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximise or restore window size"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimise app window"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Open by default settings"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choose how to open web links for this app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In the app"</string> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index 7a2e8cffffcf..24c2bed5e79e 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda: 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Izquierda: 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla derecha completa"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Intercambiar apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Superior: 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior: 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restablecer"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar a la izquierda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar a la derecha"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ajustar el tamaño de la ventana de la app hacia la izquierda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ajustar el tamaño de la ventana de la app hacia la derecha"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar o restablecer el tamaño de la ventana"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Activar el modo de pantalla dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Ingresar al modo ventana de computadora"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ajustar el tamaño de la ventana hacia la izquierda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ajustar el tamaño de la ventana hacia la derecha"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar o restablecer el tamaño de la ventana"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar o restablecer el tamaño de la ventana"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar ventana de la app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Abrir con la configuración predeterminada"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Elige cómo abrir vínculos web para esta app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"En la app"</string> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 2a30bfbd1ba1..dd9635dccfcb 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Izquierda 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Izquierda 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla derecha completa"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Cambiar aplicaciones"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla superior completa"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Superior 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Superior 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Acoplar a la izquierda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Acoplar a la derecha"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Cambiar tamaño de la ventana de la aplicación izquierda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Cambiar tamaño de la ventana de la aplicación derecha"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar o restaurar tamaño de la ventana"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Activar modo Pantalla dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Activar modo Escritorio basado en ventanas"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Cambiar tamaño de la ventana a la izquierda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Cambiar tamaño de la ventana a la derecha"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar o restaurar tamaño de la ventana"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar o restaurar tamaño de la ventana"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar ventana de la aplicación"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Abrir con los ajustes predeterminados"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Elige cómo quieres abrir los enlaces web de esta aplicación"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"En la aplicación"</string> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index 9a15f90ac27e..56b5f0bb0874 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasak: 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vasak: 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Parem täisekraan"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Rakenduste vahetamine"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ülemine täisekraan"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Ülemine: 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ülemine: 50%"</string> @@ -124,7 +125,7 @@ <string name="float_button_text" msgid="9221657008391364581">"Hõljuv"</string> <string name="select_text" msgid="5139083974039906583">"Vali"</string> <string name="screenshot_text" msgid="1477704010087786671">"Ekraanipilt"</string> - <string name="open_in_browser_text" msgid="9181692926376072904">"Avamine brauseris"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Ava brauseris"</string> <string name="open_in_app_text" msgid="2874590745116268525">"Ava rakenduses"</string> <string name="new_window_text" msgid="6318648868380652280">"Uus aken"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Akende haldamine"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Taasta"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Tõmmake vasakule"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Tõmmake paremale"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Rakenduse akna suuruse muutmine vasakul"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Rakenduse akna suuruse muutmine paremal"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Akna suuruse maksimeerimine või taastamine"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Poolitatud ekraani režiimi sisenemine"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Töölaua akende kuvamise režiimi sisenemine"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Akna suuruse muutmine, vasakule"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Akna suuruse muutmine, paremale"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Akna suuruse maksimeerimine või taastamine"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Akna suuruse maksimeerimine või taastamine"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Rakenduse akna minimeerimine"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Avamisviisi vaikeseaded"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Valige, kuidas avada selle rakenduse puhul veebilinke"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Rakenduses"</string> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 7c03b24eaef8..9898af0c394d 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ezarri ezkerraldea % 50en"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ezarri ezkerraldea % 30en"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ezarri eskuinaldea pantaila osoan"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Aldatu aplikazioz"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ezarri goialdea pantaila osoan"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Ezarri goialdea % 70en"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Ezarri goialdea % 50en"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Leheneratu"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ezarri ezkerrean"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ezarri eskuinean"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Aldatu aplikazioaren leihoaren tamaina eta eraman ezkerrera"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Aldatu aplikazioaren leihoaren tamaina eta eraman eskuinera"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizatu edo leheneratu leihoaren tamaina"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Sartu pantaila zatituaren moduan"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Sartu ordenagailuan leihoak erabiltzeko moduan"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Aldatu leihoaren tamaina eta eraman ezkerrera"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Aldatu leihoaren tamaina eta eraman eskuinera"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizatu edo leheneratu leihoaren tamaina"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizatu edo leheneratu leihoaren tamaina"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizatu aplikazioaren leihoa"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Modu lehenetsian irekitzearen ezarpenak"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Aukeratu nola ireki sareko estekak aplikazio honetan"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Aplikazioan"</string> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index f9a3c355773c..22ef61f62e13 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"٪۵۰ چپ"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"٪۳۰ چپ"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"تمامصفحه راست"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"جابهجا کردن برنامهها"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"تمامصفحه بالا"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"٪۷۰ بالا"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"٪۵۰ بالا"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"بازیابی کردن"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"کشیدن بهچپ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"کشیدن بهراست"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"تغییر اندازه پنجره برنامه در چپ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"تغییر اندازه پنجره برنامه در راست"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"بیشینهسازی یا بازیابی اندازه پنجره"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ورود به حالت صفحه تقسیمشده"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"رفتن به حالت پردازش پنجرهای میز کار"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"تغییر اندازه پنجره به چپ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"تغییر اندازه پنجره به راست"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"بیشینهسازی یا بازیابی اندازه پنجره"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"بیشینهسازی یا بازیابی اندازه پنجره"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"کمینهسازی پنجره برنامه"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"تنظیمات باز کردن بهطور پیشفرض"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"انتخاب روش باز کردن پیوندهای وب مربوط به این برنامه"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"در برنامه"</string> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index d89e36aad3d3..b23c833fa453 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vasen 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vasen 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Oikea koko näytölle"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Vaihda sovellusta"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Yläosa koko näytölle"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Yläosa 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Yläosa 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Palauta"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Siirrä vasemmalle"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Siirrä oikealle"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Muuta vasemmanpuoleisen sovellusikkunan kokoa"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Muuta oikeanpuoleisen sovellusikkunan kokoa"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Suurenna ikkuna tai palauta ikkunan koko"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Avaa kahtia jaettu näyttö"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Siirry työpöydän ikkunointitilaan"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Muuta vasemmanpuoleisen ikkunan kokoa"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Muuta vasemmanpuoleisen ikkunan kokoa"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Suurenna ikkuna tai palauta ikkunan koko"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Suurenna ikkuna tai palauta ikkunan koko"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Pienennä sovellusikkuna"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Avaa oletusasetusten mukaan"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Valitse, miten verkkolinkit avataan tässä sovelluksessa"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Sovelluksessa"</string> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index e2730d422013..34b5b0acf753 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % à la gauche"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % à la gauche"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Plein écran à la droite"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Permuter des applis"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Plein écran dans le haut"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % dans le haut"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % dans le haut"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurer"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Épingler à gauche"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Épingler à droite"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionner la fenêtre de l\'appli à gauche"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionner la fenêtre de l\'appli à droite"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Entrer en mode Écran divisé"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Entrer en mode Fenêtrage bureau"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionner la fenêtre vers la gauche"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionner la fenêtre vers la droite"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Réduire la fenêtre de l\'appli"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Ouvrir les paramètres par défaut"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choisissez comment ouvrir les liens Web pour cette appli"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Dans l\'appli"</string> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index a97a48cdcd46..be41bba34772 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Écran de gauche à 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Écran de gauche à 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Écran de droite en plein écran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Échanger les applis"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Écran du haut en plein écran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Écran du haut à 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Écran du haut à 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurer"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ancrer à gauche"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ancrer à droite"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionner la fenêtre de l\'appli vers la gauche"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionner la fenêtre de l\'appli vers la droite"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Passer en mode Écran partagé"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Activer le mode fenêtrage du bureau"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionner la fenêtre vers la gauche"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionner la fenêtre vers la droite"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Agrandir ou restaurer la taille de la fenêtre"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Réduire la fenêtre de l\'application"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Ouvrir les paramètres par défaut"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Choisir comment ouvrir les liens Web pour cette appli"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Dans l\'application"</string> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index 445cc70d4e8d..aa2f6392842b 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50 % á esquerda"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30 % á esquerda"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pantalla completa á dereita"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Cambiar as aplicacións"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Pantalla completa arriba"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70 % arriba"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50 % arriba"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Axustar á esquerda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Axustar á dereita"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Axustar o tamaño da ventá da aplicación á esquerda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Axustar o tamaño da ventá da aplicación á dereita"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar ou restaurar o tamaño da ventá"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Entrar no modo de pantalla dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Entrar no modo de ventás do ordenador"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Axustar o tamaño da ventá á esquerda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Axustar o tamaño da ventá á dereita"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar ou restaurar o tamaño da ventá"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar ou restaurar o tamaño da ventá"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar a ventá da aplicación"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Abrir coa configuración predeterminada"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Escoller como abrir as ligazóns web para esta aplicación"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Na aplicación"</string> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index 6bef1bb6e061..dcd57385809f 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ડાબે 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ડાબે 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"જમણી સ્ક્રીન સ્ક્રીન"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ઍપને સ્વૉપ કરો"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"શીર્ષ પૂર્ણ સ્ક્રીન"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"શીર્ષ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"શીર્ષ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"રિસ્ટોર કરો"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ડાબે સ્નૅપ કરો"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"જમણે સ્નૅપ કરો"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ડાબી બાજુથી ઍપની વિન્ડોનું કદ બદલો"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"જમણી બાજુથી ઍપની વિન્ડોનું કદ બદલો"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"વિન્ડોનું કદ મહત્તમ કરો અથવા રિસ્ટોર કરો"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"સ્ક્રીન-વિભાજન મોડ દાખલ કરો"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ડેસ્કટૉપ વિન્ડો મોડ દાખલ કરો"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ડાબી બાજુ વિન્ડોનું કદ બદલો"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"જમણી બાજુ વિન્ડોનું કદ બદલો"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"વિન્ડોનું કદ મહત્તમ કરો અથવા રિસ્ટોર કરો"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"વિન્ડોનું કદ મહત્તમ કરો અથવા રિસ્ટોર કરો"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ઍપની વિન્ડોને નાની કરો"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"\'ડિફૉલ્ટ તરીકે ખોલો\' સેટિંગ"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"આ ઍપ માટે વેબ લિંક ખોલવાની રીત પસંદ કરો"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ઍપમાં"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 95b3fc0fafd5..4bf2d92c1860 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बाईं स्क्रीन को 50% बनाएं"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"बाईं स्क्रीन को 30% बनाएं"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"दाईं स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ऐप्लिकेशन स्वैप करें"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ऊपर की स्क्रीन को फ़ुल स्क्रीन बनाएं"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ऊपर की स्क्रीन को 70% बनाएं"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ऊपर की स्क्रीन को 50% बनाएं"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"पहले जैसा करें"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बाईं ओर स्नैप करें"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"दाईं ओर स्नैप करें"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ऐप्लिकेशन विंडो का साइज़ बाईं ओर से बदलें"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ऐप्लिकेशन विंडो का साइज़ दाईं ओर से बदलें"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"विंडो को बड़ा करें या उसका साइज़ पहले जैसा करें"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"स्प्लिट स्क्रीन मोड में चालू करें"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"डेस्कटॉप विंडो मोड में जाएं"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"विंडो का साइज़ बाईं ओर से बदलें"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"विंडो का साइज़ दाईं ओर से बढ़ाएं"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"विंडो को बड़ा करें या उसका साइज़ पहले जैसा करें"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"विंडो को बड़ा करें या उसका साइज़ पहले जैसा करें"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ऐप्लिकेशन की विंडो को छोटा करें"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"डिफ़ॉल्ट सेटिंग के हिसाब से खोलें"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"इस ऐप्लिकेशन के लिए वेब लिंक खोलने का तरीका चुनें"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ऐप्लिकेशन में"</string> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index 28bab79042a0..157822c5dc4f 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Lijevi zaslon na 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Lijevi zaslon na 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desni zaslon u cijeli zaslon"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zamijeni aplikacije"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Gornji zaslon u cijeli zaslon"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gornji zaslon na 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gornji zaslon na 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Vrati"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Poravnaj lijevo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Poravnaj desno"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Promijeni veličinu prozora aplikacije ulijevo"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Promijeni veličinu prozora aplikacije udesno"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimiziraj ili vrati veličinu prozora"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Pokreni način podijeljenog zaslona"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Pokreni način prikaza u prozorima na računalu"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Promijeni veličinu prozora ulijevo"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Promijeni veličinu prozora udesno"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimiziraj ili vrati veličinu prozora"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimiziraj ili vrati veličinu prozora"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimiziraj prozor aplikacije"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Otvori prema zadanim postavkama"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Odaberite način otvaranja web-veza za ovu aplikaciju"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"U aplikaciji"</string> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 1afb57d8c80a..546a465c8699 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Bal oldali 50%-ra"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Bal oldali 30%-ra"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Jobb oldali teljes képernyőre"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Váltás az alkalmazások között"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Felső teljes képernyőre"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Felső 70%-ra"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Felső 50%-ra"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Visszaállítás"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Balra igazítás"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Jobbra igazítás"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Alkalmazásablak átméretezése balra"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Alkalmazásablak átméretezése jobbra"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Ablak teljes méretre állítása vagy visszaállítása"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Belépés osztott képernyős módba"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Asztali ablakkezelési mód indítása"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ablak átméretezése balra"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ablak átméretezése jobbra"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Ablak teljes méretre állítása vagy visszaállítása"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Ablak teljes méretre állítása vagy visszaállítása"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Alkalmazásablak kis méretre állítása"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Alapértelmezett beállítások megnyitása"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Az app webes linkjeinek megnyitásához használt módszer"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Az alkalmazásban"</string> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 7266942434c0..39a395f9add1 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ձախ էկրանը՝ 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ձախ էկրանը՝ 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Աջ էկրանը՝ լիաէկրան"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Հավելվածները տեղերով փոխել"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Վերևի էկրանը՝ լիաէկրան"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Վերևի էկրանը՝ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Վերևի էկրանը՝ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Վերականգնել"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ամրացնել ձախ կողմում"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ամրացնել աջ կողմում"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ձգել հավելվածի պատուհանը դեպի ձախ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ձգել հավելվածի պատուհանը դեպի աջ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Ծավալել կամ վերականգնել պատուհանի չափսը"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Մտնել էկրանի տրոհման ռեժիմ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Մտնել համակարգչի ռեժիմ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ձգել պատուհանը դեպի ձախ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ձգել պատուհանը դեպի աջ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Ծավալել կամ վերականգնել պատուհանի չափսը"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Ծավալել կամ վերականգնել պատուհանի չափսը"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Ծալել հավելվածի պատուհանը"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Բացել կարգավորումներն ըստ կանխադրման"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Ընտրեք՝ ինչպես բացել այս հավելվածի վեբ հղումները"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Հավելվածում"</string> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 1197413553db..09ce5257c56e 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kiri 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Layar penuh di kanan"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Ganti Aplikasi"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Layar penuh di atas"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Atas 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Pulihkan"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Maksimalkan ke kiri"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Maksimalkan ke kanan"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ubah ukuran jendela aplikasi ke kiri"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ubah ukuran jendela aplikasi ke kanan"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimalkan atau pulihkan ukuran jendela"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Masuk ke mode layar terpisah"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Masuk ke mode windowing desktop"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ubah ukuran jendela ke kiri"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ubah ukuran jendela ke kanan"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimalkan atau pulihkan ukuran jendela"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimalkan atau pulihkan ukuran jendela"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimalkan jendela aplikasi"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Buka dengan setelan default"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Pilih cara membuka link web untuk aplikasi ini"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Di aplikasi"</string> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index 9646cb375f2f..61c1d0e7759c 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vinstri 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vinstri 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Hægri á öllum skjánum"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Skipta á milli forrita"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Efri á öllum skjánum"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Efri 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Efri 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Endurheimta"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Smella til vinstri"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Smella til hægri"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Breyta stærð forritsglugga til vinstri"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Breyta stærð forritsglugga til hægri"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Hámarka eða endurheimta stærð glugga"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Skipta skjánum"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Opna gluggastillingu í tölvu"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Breyta stærð glugga til vinstri"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Breyta stærð glugga til hægri"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Hámarka eða endurheimta stærð glugga"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Hámarka eða endurheimta stærð glugga"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Lágmarka stærð forritsglugga"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Stillingar sjálfvirkrar opnunar"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Veldu hvernig veftenglar opnast í forritinu"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Í forritinu"</string> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index c3f6b3b49d9f..fab259e03b3b 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Schermata sinistra al 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Schermata sinistra al 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Schermata destra a schermo intero"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Scambia app"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Schermata superiore a schermo intero"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Schermata superiore al 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Schermata superiore al 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Ripristina"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Aggancia a sinistra"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Aggancia a destra"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ridimensiona la finestra dell\'app a sinistra"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ridimensiona la finestra dell\'app a destra"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Ingrandisci o ripristina le dimensioni della finestra"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Attiva la modalità schermo diviso"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Attiva la modalità finestre del desktop"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ridimensiona la finestra a sinistra"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ridimensiona la finestra a destra"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Ingrandisci o ripristina le dimensioni della finestra"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Ingrandisci o ripristina le dimensioni della finestra"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Riduci a icona la finestra dell\'app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Apri in base alle impostazioni predefinite"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Scegli come aprire i link web per questa app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"All\'interno dell\'app"</string> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index cf9c18b43a5e..b164b1131ad2 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"שמאלה 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"שמאלה 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"מסך ימני מלא"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"מעבר בין אפליקציות"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"מסך עליון מלא"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"עליון 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"עליון 50%"</string> @@ -128,7 +129,7 @@ <string name="open_in_app_text" msgid="2874590745116268525">"פתיחה באפליקציה"</string> <string name="new_window_text" msgid="6318648868380652280">"חלון חדש"</string> <string name="manage_windows_text" msgid="5567366688493093920">"ניהול החלונות"</string> - <string name="change_aspect_ratio_text" msgid="9104456064548212806">"שינוי של יחס גובה-רוחב"</string> + <string name="change_aspect_ratio_text" msgid="9104456064548212806">"שינוי יחס הגובה-רוחב"</string> <string name="close_text" msgid="4986518933445178928">"סגירה"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"סגירת התפריט"</string> <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"פתיחת התפריט"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"שחזור"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"הצמדה לשמאל"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"הצמדה לימין"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"שינוי הגודל של חלון האפליקציה שמשמאל"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"שינוי הגודל של חלון האפליקציה שמימין"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"שחזור של גודל החלון או הגדלת החלון"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"כניסה למצב מסך מפוצל"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"כניסה למצב שינוי הגודל של החלונות בממשק המחשב"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"שינוי גודל החלון שמשמאל"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"שינוי גודל החלון שמימין"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"שחזור של גודל החלון או הגדלת החלון"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"שחזור של גודל החלון או הגדלת החלון"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"מזעור החלון של האפליקציה"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"הגדרות לפתיחה כברירת מחדל"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"כאן בוחרים איך לפתוח באפליקציה הזו קישורים לדפי אינטרנט"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"באפליקציה"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index c955ecb4f508..3fe2a515437f 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右全画面"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"アプリを切り替える"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"上部全画面"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"上 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"上 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"復元"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"左にスナップ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"右にスナップ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"アプリ ウィンドウを左側にサイズ変更する"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"アプリ ウィンドウを右側にサイズ変更する"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ウィンドウを最大化する、またはウィンドウを元のサイズに戻す"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"分割画面モードに切り替える"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"デスクトップ ウィンドウ モードに切り替える"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ウィンドウを左側にサイズ変更する"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ウィンドウを右側にサイズ変更する"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ウィンドウを最大化する、またはウィンドウを元のサイズに戻す"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ウィンドウを最大化する、またはウィンドウを元のサイズに戻す"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"アプリ ウィンドウを最小化する"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"デフォルトの設定で開く"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"このアプリのウェブリンクを開く方法を選択"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"アプリ内"</string> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 2c286d2644df..1be19af9b372 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"მარცხენა ეკრანი — 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"მარცხენა ეკრანი — 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"მარჯვენა ნაწილის სრულ ეკრანზე გაშლა"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"აპების გადართვა"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ზედა ნაწილის სრულ ეკრანზე გაშლა"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ზედა ეკრანი — 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ზედა ეკრანი — 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"აღდგენა"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"მარცხნივ გადატანა"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"მარჯვნივ გადატანა"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"აპის მარცხენა ფანჯრის ზომის შეცვლა"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"აპის მარჯვენა ფანჯრის ზომის შეცვლა"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ფანჯრის მაქსიმალურ ზომამდე გაზრდა ან აღდგენა"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"გაყოფილი ეკრანის რეჟიმში შესვლა"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"დესკტოპის ფანჯრის რეჟიმში შესვლა"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ფანჯრის ზომის შეცვლა მარცხნივ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ფანჯრის ზომის შეცვლა მარჯვნივ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ფანჯრის მაქსიმალურ ზომამდე გაზრდა ან აღდგენა"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ფანჯრის მაქსიმალურ ზომამდე გაზრდა ან აღდგენა"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"აპის ფანჯრის ზომის შემცირება"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"პარამეტრების ნაგულისხმევად გახსნა"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ამ აპისთვის ვებ ბმულების გახსნის წესის არჩევა"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"აპში"</string> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 58afb7fdd6c4..5bd85191ec65 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% сол жақта"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% сол жақта"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Оң жағын толық экранға шығару"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Қолданбаларды ауыстыру"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Жоғарғы жағын толық экранға шығару"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% жоғарғы жақта"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% жоғарғы жақта"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Қалпына келтіру"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Солға тіркеу"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Оңға тіркеу"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Қолданба терезесінің өлшемін сол жақтан өзгерту"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Қолданба терезесінің өлшемін оң жақтан өзгерту"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Терезе өлшемін ұлғайту не қалпына келтіру"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Экранды бөлу режиміне өту"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Жұмыс үстелінің терезе режиміне өту"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Терезе өлшемін сол жаққа өзгерту"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Терезе өлшемін оң жаққа өзгерту"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Терезе өлшемін ұлғайту не қалпына келтіру"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Терезе өлшемін ұлғайту не қалпына келтіру"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Қолданба терезесін кішірейту"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Әдепкісінше ашу параметрлері"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Осы қолданбадағы веб-сілтемелерді ашу жолын таңдаңыз"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Қолданбада"</string> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 6abb66dc9ade..f5118972d93f 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ឆ្វេង 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ឆ្វេង 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"អេក្រង់ពេញខាងស្តាំ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ប្ដូរកម្មវិធី"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"អេក្រង់ពេញខាងលើ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ខាងលើ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ខាងលើ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ស្ដារ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ផ្លាស់ទីទៅឆ្វេង"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ផ្លាស់ទីទៅស្ដាំ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ប្ដូរទំហំវិនដូកម្មវិធីទៅឆ្វេង"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ប្ដូរទំហំវិនដូកម្មវិធីទៅស្ដាំ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ស្ដារ ឬបង្កើនទំហំវិនដូជាអតិបរមា"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ចូលទៅមុខងារបំបែកអេក្រង់"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ចូលទៅមុខងារវិនដូកុំព្យូទ័រ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ប្ដូរទំហំវិនដូទៅឆ្វេង"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ប្ដូរទំហំវិនដូទៅស្ដាំ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ស្ដារ ឬបង្កើនទំហំវិនដូជាអតិបរមា"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ស្ដារ ឬបង្កើនទំហំវិនដូជាអតិបរមា"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"បង្រួមវិនដូកម្មវិធី"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ការកំណត់បើកតាមលំនាំដើម"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ជ្រើសរើសរបៀបបើកតំណបណ្ដាញសម្រាប់កម្មវិធីនេះ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"នៅក្នុងកម្មវិធី"</string> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 1da093d666bb..3bd5527a9fe5 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% ಎಡಕ್ಕೆ"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% ಎಡಕ್ಕೆ"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ಬಲ ಫುಲ್ ಸ್ಕ್ರೀನ್"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ಆ್ಯಪ್ಗಳನ್ನು ಸ್ವ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ಮೇಲಿನ ಫುಲ್ ಸ್ಕ್ರೀನ್"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% ಮೇಲಕ್ಕೆ"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% ಮೇಲಕ್ಕೆ"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ಮರುಸ್ಥಾಪಿಸಿ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ಎಡಕ್ಕೆ ಸ್ನ್ಯಾಪ್ ಮಾಡಿ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ಬಲಕ್ಕೆ ಸ್ನ್ಯಾಪ್ ಮಾಡಿ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ಮರುಗಾತ್ರಗೊಳಿಸಿ ಆ್ಯಪ್ ವಿಂಡೋ ಎಡ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ಮರುಗಾತ್ರಗೊಳಿಸಿ ಆ್ಯಪ್ ವಿಂಡೋ ಬಲ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ವಿಂಡೋ ಗಾತ್ರವನ್ನು ಗರಿಷ್ಠಗೊಳಿಸಿ ಅಥವಾ ಮರುಸ್ಥಾಪಿಸಿ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ಸ್ಪ್ಲಿಟ್ ಸ್ಕ್ರೀನ್ ಮೋಡ್ಗೆ ಪ್ರವೇಶಿಸಿ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ಡೆಸ್ಕ್ಟಾಪ್ ವಿಂಡೋಯಿಂಗ್ ಮೋಡ್ಗೆ ಪ್ರವೇಶಿಸಿ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ಮರುಗಾತ್ರಗೊಳಿಸಿ ವಿಂಡೋವನ್ನು ಎಡಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ಮರುಗಾತ್ರಗೊಳಿಸಿ ವಿಂಡೋವನ್ನು ಬಲಕ್ಕೆ ಸರಿಸಿ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ವಿಂಡೋ ಗಾತ್ರವನ್ನು ಗರಿಷ್ಠಗೊಳಿಸಿ ಅಥವಾ ಮರುಸ್ಥಾಪಿಸಿ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ವಿಂಡೋ ಗಾತ್ರವನ್ನು ಗರಿಷ್ಠಗೊಳಿಸಿ ಅಥವಾ ಮರುಸ್ಥಾಪಿಸಿ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ಆ್ಯಪ್ ವಿಂಡೋವನ್ನು ಮಿನಿಮೈಜ್ ಮಾಡಿ"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ಡೀಫಾಲ್ಟ್ ಸೆಟ್ಟಿಂಗ್ಗಳಿಂದ ತೆರೆಯಿರಿ"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ಈ ಆ್ಯಪ್ಗೆ ವೆಬ್ ಲಿಂಕ್ಗಳನ್ನು ಹೇಗೆ ತೆರೆಯಬೇಕು ಎಂಬುದನ್ನು ಆಯ್ಕೆಮಾಡಿ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ಆ್ಯಪ್ನಲ್ಲಿ"</string> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 22f2e0632b46..65add57a9e6b 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"왼쪽 화면 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"왼쪽 화면 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"오른쪽 화면 전체화면"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"앱 전환"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"위쪽 화면 전체화면"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"위쪽 화면 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"위쪽 화면 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"복원"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"왼쪽으로 맞추기"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"오른쪽으로 맞추기"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"앱 창 크기 왼쪽으로 조절"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"앱 창 크기 오른쪽으로 조절"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"창 최대화 또는 크기 복원"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"화면 분할 모드 시작"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"데스크톱 창 모드 시작"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"창 크기 왼쪽으로 조절"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"창 크기 오른쪽으로 조절"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"창 최대화 또는 크기 복원"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"창 최대화 또는 크기 복원"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"앱 창 최소화"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"기본값으로 열기 설정"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"이 앱에서 웹 링크를 여는 방법을 선택하세요"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"앱에서"</string> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index 86529a292cff..96c2226daf58 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Сол жактагы экранды 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Сол жактагы экранды 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Оң жактагы экранды толук экран режимине өткөрүү"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Колдонмолорду алмаштыруу"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Үстүнкү экранды толук экран режимине өткөрүү"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Үстүнкү экранды 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Үстүнкү экранды 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Калыбына келтирүү"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Солго жылдыруу"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Оңго жылдыруу"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Колдонмонун терезесинин өлчөмүн солго өзгөртүү"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Колдонмонун терезесинин өлчөмүн оңго өзгөртүү"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Терезенин өлчөмүн чоңойтуу же калыбына келтирүү"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Экранды бөлүү режимине өтүү"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Иш тактанын терезелери режимине өтүү"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Терезенин өлчөмүн солго өзгөртүү"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Терезенин өлчөмүн оңго өзгөртүү"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Терезенин өлчөмүн чоңойтуу же калыбына келтирүү"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Терезенин өлчөмүн чоңойтуу же калыбына келтирүү"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Колдонмонун терезесин кичирейтүү"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Демейки шартта ачылуучу шилтемелердин параметрлери"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Колдонмодо шилтемелер кантип ачыларын тандаңыз"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Колдонмодо"</string> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index fab0cb245cfd..9337efc92606 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ຊ້າຍ 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ຊ້າຍ 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ເຕັມໜ້າຈໍຂວາ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ສະຫຼັບແອັບ"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ເຕັມໜ້າຈໍເທິງສຸດ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ເທິງສຸດ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ເທິງສຸດ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ກູ້ຄືນ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ແນບຊ້າຍ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ແນບຂວາ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ປັບຂະໜາດໜ້າຈໍແອັບໄປທາງຊ້າຍ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ປັບຂະໜາດໜ້າຈໍແອັບໄປທາງຂວາ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ຂະຫຍາຍ ຫຼື ຄືນຄ່າຂະໜາດໜ້າຈໍ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ເຂົ້າສູ່ໂໝດແບ່ງໜ້າຈໍ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ເຂົ້າສູ່ໂໝດໜ້າຈໍເດັສທັອບ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ປັບຂະໜາດໜ້າຈໍໄປທາງຊ້າຍ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ປັບຂະໜາດໜ້າຈໍໄປທາງຂວາ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ຂະຫຍາຍ ຫຼື ຄືນຄ່າຂະໜາດໜ້າຈໍ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ຂະຫຍາຍ ຫຼື ຄືນຄ່າຂະໜາດໜ້າຈໍ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ຫຍໍ້ໜ້າຈໍແອັບ"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ເປີດຕາມການຕັ້ງຄ່າເລີ່ມຕົ້ນ"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ເລືອກວິທີເປີດລິ້ງເວັບສຳລັບແອັບນີ້"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ໃນແອັບ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index d036e35e9cbf..ede25645c76c 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kairysis ekranas 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kairysis ekranas 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Dešinysis ekranas viso ekrano režimu"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Programų keitimas"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Viršutinis ekranas viso ekrano režimu"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Viršutinis ekranas 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Viršutinis ekranas 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Atkurti"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pritraukti kairėje"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pritraukti dešinėje"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Pakeisti programos lango dydį kairėje"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Pakeisti programos lango dydį dešinėje"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Padidinti arba atkurti lango dydį"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Išskaidyto ekrano režimo įjungimas"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Įjungti darbalaukio pateikimo lange režimą"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Pakeisti lango dydį kairėje"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Pakeisti lango dydį dešinėje"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Padidinti arba atkurti lango dydį"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Padidinti arba atkurti lango dydį"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Sumažinti programos langą"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Atidaryti pagal numatytuosius nustatymus"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Pasirinkite, kaip atidaryti šios programos žiniatinklio nuorodas"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Programoje"</string> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index dc1f7b04c21a..24a969bc8c1b 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Pa kreisi 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Pa kreisi 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Labā daļa pa visu ekrānu"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Apmainīt lietotnes"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Augšdaļa pa visu ekrānu"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Augšdaļa 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Augšdaļa 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Atjaunot"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Piestiprināt pa kreisi"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Piestiprināt pa labi"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Mainīt lietotnes loga lielumu uz kreiso pusi"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Mainīt lietotnes loga lielumu uz labo pusi"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimizēt vai atjaunot loga lielumu"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Ieslēgt ekrāna sadalīšanas režīmu"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Ieslēgt darbvirsmas logu režīmu"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Mainīt loga lielumu uz kreiso pusi"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Mainīt loga lielumu uz labo pusi"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimizēt vai atjaunot loga lielumu"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimizēt vai atjaunot loga lielumu"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizēt lietotnes logu"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Atvērt pēc noklusējuma iestatījumiem"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Izvēlieties, kā atvērt šajā lietotnē norādītās saites"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Lietotnē"</string> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 3da196b52984..f7177acc8681 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левиот 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левиот 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Десниот на цел екран"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Менувајте апликации"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Горниот на цел екран"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горниот 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горниот 50%"</string> @@ -133,7 +134,7 @@ <string name="collapse_menu_text" msgid="7515008122450342029">"Затворете го менито"</string> <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Отвори го менито"</string> <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string> - <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Промени ја големината"</string> + <string name="desktop_mode_maximize_menu_snap_text" msgid="5673738963174074006">"Промени ја гол."</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Апликацијата не може да се премести овде"</string> <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Реалистично"</string> <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Врати"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Врати"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Фотографирај лево"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Фотографирај десно"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Променете ја големината на прозорецот на апликацијата одлево"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Променете ја големината на прозорецот на апликацијата оддесно"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Максимизирајте или вратете ја големината на прозорецот"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Влезете во „Режим на поделен екран“"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Влезете во „Режим со прозорци на работната површина“"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Променете ја големината на прозорецот налево"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Променете ја големината на прозорецот надесно"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Максимизирајте или вратете ја големината на прозорецот"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Максимизирајте или вратете ја големината на прозорецот"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Минимизирајте го прозорецот на апликацијата"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Отвори според стандардните поставки"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Изберете како да се отвораат линковите за апликацијава"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Во апликацијата"</string> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index c2e747c590d8..89215b66ba01 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ഇടത് 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ഇടത് 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"വലത് പൂർണ്ണ സ്ക്രീൻ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ആപ്പുകൾ സ്വാപ്പ് ചെയ്യുക"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"മുകളിൽ പൂർണ്ണ സ്ക്രീൻ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"മുകളിൽ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"മുകളിൽ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"പുനഃസ്ഥാപിക്കുക"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ഇടതുവശത്തേക്ക് സ്നാപ്പ് ചെയ്യുക"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"വലതുവശത്തേക്ക് സ്നാപ്പ് ചെയ്യുക"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ഇടത് ആപ്പ് വിൻഡോ വലുപ്പം മാറ്റുക"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"വലത് ആപ്പ് വിൻഡോ വലുപ്പം മാറ്റുക"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"വിന്ഡോ വലുപ്പം വലുതാക്കുക അല്ലെങ്കിൽ പഴയത് പുനഃസ്ഥാപിക്കുക"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"സ്പ്ലിറ്റ് സ്ക്രീൻ മോഡിൽ പ്രവേശിക്കുക"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ഡെസ്ക്ടോപ്പ് വിൻഡോയിംഗ് മോഡിൽ പ്രവേശിക്കുക"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ഇടത്തേക്ക് ആപ്പ് വിൻഡോ വലുപ്പം മാറ്റുക"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"വലത്തേക്ക് ആപ്പ് വിൻഡോ വലുപ്പം മാറ്റുക"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"വിന്ഡോ വലുപ്പം വലുതാക്കുക അല്ലെങ്കിൽ പഴയത് പുനഃസ്ഥാപിക്കുക"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"വിന്ഡോ വലുപ്പം വലുതാക്കുക അല്ലെങ്കിൽ പഴയത് പുനഃസ്ഥാപിക്കുക"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ആപ്പ് വിന്ഡോ ചെറുതാക്കുക"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ഡിഫോൾട്ട് ക്രമീകരണം ഉപയോഗിച്ച് തുറക്കുക"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ഈ ആപ്പിനായി വെബ് ലിങ്കുകൾ എങ്ങനെ തുറക്കണമെന്ന് തിരഞ്ഞെടുക്കൂ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ആപ്പിൽ"</string> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 045fc2101481..b38026cc5445 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Зүүн 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Зүүн 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Баруун талын бүтэн дэлгэц"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Аппуудыг солих"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Дээд талын бүтэн дэлгэц"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Дээд 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Дээд 50%"</string> @@ -128,7 +129,7 @@ <string name="open_in_app_text" msgid="2874590745116268525">"Аппад нээх"</string> <string name="new_window_text" msgid="6318648868380652280">"Шинэ цонх"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Windows-г удирдах"</string> - <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Харьцааг өөрчлөх"</string> + <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Аспектын харьцааг өөрчлөх"</string> <string name="close_text" msgid="4986518933445178928">"Хаах"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Цэсийг хаах"</string> <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Цэсийг нээх"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Сэргээх"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Зүүн тийш зэрэгцүүлэх"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Баруун тийш зэрэгцүүлэх"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Аппын цонхны хэмжээг зүүн тал руу өөрчлөх"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Аппын цонхны хэмжээг баруун тал руу өөрчлөх"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Цонхны хэмжээг томруулах эсвэл сэргээх"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Дэлгэц хуваах горимд орох"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Дэлгэцийн цонхны горимд орох"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Цонхны хэмжээг зүүн тал руу өөрчлөх"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Цонхны хэмжээг баруун тал руу өөрчлөх"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Цонхны хэмжээг томруулах эсвэл сэргээх"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Цонхны хэмжээг томруулах эсвэл сэргээх"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Аппын цонхыг жижгэрүүлэх"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Өгөгдмөл тохиргоогоор нээх"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Энэ аппад веб холбоосыг хэрхэн нээхийг сонгоно уу"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Аппад"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index 01398d5c4d3a..d9c1d1f45a55 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"डावी 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"डावी 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"उजवी फुल स्क्रीन"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"अॅप्स स्वॅप करा"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"शीर्ष फुल स्क्रीन"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"शीर्ष 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"शीर्ष 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"रिस्टोअर करा"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"डावीकडे स्नॅप करा"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"उजवीकडे स्नॅप करा"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"अॅप विंडोचा डावीकडून आकार बदला"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"अॅप विंडोचा उजवीकडून आकार बदला"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"विंडोचा आकार मोठा करा किंवा रिस्टोअर करा"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"स्प्लिट स्क्रीन मोड एंटर करा"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"डेस्कटॉप विंडोइंग मोड एंटर करा"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"अॅप विंडोचा डावीकडे आकार बदला"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"अॅप विंडोचा उजवीकडे आकार बदला"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"विंडोचा आकार मोठा करा किंवा रिस्टोअर करा"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"विंडोचा आकार मोठा करा किंवा रिस्टोअर करा"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"अॅप विंडो लहान करा"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"बाय डीफॉल्ट सेटिंग्ज उघडा"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"या अॅपसाठीच्या वेब लिंक कशा उघडाव्यात हे निवडा"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ॲपमध्ये"</string> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 3d687dcbd800..a54ef140c9a1 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kiri 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kiri 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Skrin penuh kanan"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Tukar Apl"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Skrin penuh atas"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Atas 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Atas 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Pulihkan"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Autojajar ke kiri"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Autojajar ke kanan"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Butang kiri ubah saiz tetingkap apl"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Butang kanan ubah saiz tetingkap apl"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimumkan atau pulihkan saiz tetingkap"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Masuki mod skrin pisah"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Masuki mod tetingkap desktop"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ubah saiz tetingkap ke sebelah kiri"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ubah saiz tetingkap ke sebelah kanan"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimumkan atau pulihkan saiz tetingkap"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimumkan atau pulihkan saiz tetingkap"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimumkan tetingkap apl"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Buka tetapan secara lalai"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Pilih cara membuka pautan web untuk apl ini"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Pada apl"</string> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 08a935f75355..1f4db6d9b872 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ဘယ်ဘက် မျက်နှာပြင် ၅၀%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ဘယ်ဘက် မျက်နှာပြင် ၃၀%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ညာဘက် မျက်နှာပြင်အပြည့်"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"အက်ပ်ပြောင်းရန်"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"အပေါ်ဘက် မျက်နှာပြင်အပြည့်"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"အပေါ်ဘက် မျက်နှာပြင် ၇၀%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"အပေါ်ဘက် မျက်နှာပြင် ၅၀%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ပြန်ပြောင်းရန်"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ဘယ်တွင် ချဲ့ရန်"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ညာတွင် ချဲ့ရန်"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"အက်ပ်ဝင်းဒိုး ဘယ်ဘက်ကို အရွယ်ပြင်ရန်"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"အက်ပ်ဝင်းဒိုး ညာဘက်ကို အရွယ်ပြင်ရန်"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ဝင်းဒိုးအရွယ်အစားကို ချဲ့ရန် (သို့) ပြန်ပြောင်းရန်"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"မျက်နှာပြင်ခွဲပြခြင်းမုဒ်သို့ ဝင်ရန်"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ဒက်စ်တော့ ဝင်းဒိုးမုဒ်သို့ ဝင်ရန်"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ဝင်းဒိုးကို ဘယ်ဘက်သို့ အရွယ်ပြင်ရန်"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ဝင်းဒိုးကို ညာဘက်သို့ အရွယ်ပြင်ရန်"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ဝင်းဒိုးအရွယ်အစားကို ချဲ့ရန် (သို့) ပြန်ပြောင်းရန်"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ဝင်းဒိုးအရွယ်အစားကို ချဲ့ရန် (သို့) ပြန်ပြောင်းရန်"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"အက်ပ်ဝင်းဒိုးကို ချုံ့ရန်"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"မူရင်းဆက်တင်ဖြင့် ဖွင့်ရန်"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ဤအက်ပ်အတွက် ဝဘ်လင့်ခ်များ မည်သို့ဖွင့်မည်ကို ရွေးပါ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"အက်ပ်တွင်"</string> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 196507866aaf..586a50f74f0d 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Sett størrelsen på den venstre delen av skjermen til 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Sett størrelsen på den venstre delen av skjermen til 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Utvid den høyre delen av skjermen til hele skjermen"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Bytt apper"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Utvid den øverste delen av skjermen til hele skjermen"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Sett størrelsen på den øverste delen av skjermen til 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Sett størrelsen på den øverste delen av skjermen til 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Gjenopprett"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fest til venstre"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fest til høyre"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Endre størrelsen på appvinduet til venstre"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Endre størrelsen på appvinduet til høyre"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimer eller gjenopprett størrelsen på vinduet"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Start modusen for delt skjerm"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Start vindusmodus for skrivebordet"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Endre størrelsen på vinduet til venstre"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Endre størrelsen på vinduet til høyre"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimer eller gjenopprett størrelsen på vinduet"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimer eller gjenopprett størrelsen på vinduet"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimer appvinduet"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Innstillinger for åpning som standard"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Velg hvordan nettlinker skal åpnes for denne appen"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"I appen"</string> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 10e933245e60..f66fb1d30359 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"बायाँ भाग ५०%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"बायाँ भाग ३०%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"दायाँ भाग फुल स्क्रिन"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"एपहरू अदलबदल गर्नुहोस्"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"माथिल्लो भाग फुल स्क्रिन"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"माथिल्लो भाग ७०%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"माथिल्लो भाग ५०%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"रिस्टोर गर्नुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"बायाँतिर स्न्याप गर्नुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"दायाँतिर स्न्याप गर्नुहोस्"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"एपको विन्डोको आकार बदलेर बायाँतिर लैजानुहोस्"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"एपको विन्डोको आकार बदलेर दायाँतिर लैजानुहोस्"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"विन्डोको आकार म्याक्सिमाइज गर्नुहोस् वा रिस्टोर गर्नुहोस्"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"स्प्लिट स्क्रिन मोड प्रयोग गर्नुहोस्"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"डेस्कटप विन्डोइङ मोड प्रयोग गर्नुहोस्"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"विन्डोको आकार बदलेर बायाँतिर लैजानुहोस्"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"विन्डोको आकार बदलेर दायाँतिर लैजानुहोस्"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"विन्डोको आकार म्याक्सिमाइज गर्नुहोस् वा रिस्टोर गर्नुहोस्"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"विन्डोको आकार म्याक्सिमाइज गर्नुहोस् वा रिस्टोर गर्नुहोस्"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"एपको विन्डो मिनिमाइज गर्नुहोस्"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"डिफल्ट सेटिङअनुसार खोल्नुहोस्"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"यो एपका वेब लिंकहरू खोल्ने तरिका छनौट गर्नुहोस्"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"एपमा"</string> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index fc8451522f21..20bc65abab18 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Linkerscherm 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Linkerscherm 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Rechterscherm op volledig scherm"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Apps wisselen"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Bovenste scherm op volledig scherm"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Bovenste scherm 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Bovenste scherm 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Herstellen"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Links uitlijnen"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Rechts uitlijnen"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Formaat van app-venster naar links aanpassen"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Formaat van app-venster naar rechts aanpassen"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Formaat van venster maximaliseren of herstellen"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Modus voor gesplitst scherm openen"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Modus voor desktopvensterfuncties openen"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Formaat van venster naar links aanpassen"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Formaat van venster naar rechts aanpassen"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Formaat van venster maximaliseren of herstellen"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Formaat van venster maximaliseren of herstellen"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"App-venster minimaliseren"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Instellingen voor Standaard openen"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Kies hoe je weblinks voor deze app wilt openen"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"In de app"</string> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index be01593fda39..edb520872d1f 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ବାମ ପଟକୁ 50% କରନ୍ତୁ"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ବାମ ପଟେ 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ଡାହାଣ ପଟକୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ଆପ୍ସ ସ୍ୱାପ କରନ୍ତୁ"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ଉପର ଆଡ଼କୁ ପୂର୍ଣ୍ଣ ସ୍କ୍ରୀନ୍ କରନ୍ତୁ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ଉପର ଆଡ଼କୁ 70% କରନ୍ତୁ"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ଉପର ଆଡ଼କୁ 50% କରନ୍ତୁ"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ରିଷ୍ଟୋର କରନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ବାମରେ ସ୍ନାପ କରନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ଡାହାଣରେ ସ୍ନାପ କରନ୍ତୁ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ଆପ ୱିଣ୍ଡୋ ରିସାଇଜ କରିବା ପାଇଁ ବାମ ବଟନ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ଆପ ୱିଣ୍ଡୋ ରିସାଇଜ କରିବା ପାଇଁ ଡାହାଣ ବଟନ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ୱିଣ୍ଡୋ ସାଇଜକୁ ମେକ୍ସିମାଇଜ କିମ୍ବା ରିଷ୍ଟୋର କରନ୍ତୁ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ସ୍ପ୍ଲିଟ ସ୍କ୍ରିନ ମୋଡରେ ପ୍ରବେଶ କରନ୍ତୁ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ଡେସ୍କଟପ ୱିଣ୍ଡୋଇଂ ମୋଡରେ ପ୍ରବେଶ କରନ୍ତୁ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ବାମପଟକୁ ୱିଣ୍ଡୋ ରିସାଇଜ କରନ୍ତୁ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ଡାହାଣପଟକୁ ୱିଣ୍ଡୋ ରିସାଇଜ କରନ୍ତୁ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ୱିଣ୍ଡୋ ସାଇଜକୁ ମେକ୍ସିମାଇଜ କିମ୍ବା ରିଷ୍ଟୋର କରନ୍ତୁ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ୱିଣ୍ଡୋ ସାଇଜକୁ ମେକ୍ସିମାଇଜ କିମ୍ବା ରିଷ୍ଟୋର କରନ୍ତୁ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ଆପ ୱିଣ୍ଡୋକୁ ମିନିମାଇଜ କରନ୍ତୁ"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ଡିଫଲ୍ଟ ସେଟିଂସକୁ ଖୋଲନ୍ତୁ"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ଏହି ଆପ ପାଇଁ ୱେବ ଲିଙ୍କଗୁଡ଼ିକୁ କିପରି ଖୋଲିବେ, ତାହା ବାଛନ୍ତୁ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ଆପରେ"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index fb4c83e352f7..29de4c45217f 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ਖੱਬੇ 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ਖੱਬੇ 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"ਸੱਜੇ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ਐਪਾਂ ਨੂੰ ਸਵੈਪ ਕਰੋ"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ਉੱਪਰ ਪੂਰੀ ਸਕ੍ਰੀਨ"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ਉੱਪਰ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ਉੱਪਰ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ਖੱਬੇ ਪਾਸੇ ਸਨੈਪ ਕਰੋ"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"ਸੱਜੇ ਪਾਸੇ ਸਨੈਪ ਕਰੋ"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ਐਪ ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਬਦਲ ਕੇ ਖੱਬੇ ਪਾਸੇ ਕਰੋ"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ਐਪ ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਬਦਲ ਕੇ ਸੱਜੇ ਪਾਸੇ ਕਰੋ"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਵਧਾਓ ਜਾਂ ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"ਸਪਲਿਟ ਸਕ੍ਰੀਨ ਮੋਡ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ਡੈਸਕਟਾਪ ਵਿੰਡੋ ਮੋਡ ਵਿੱਚ ਦਾਖਲ ਹੋਵੋ"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਬਦਲ ਕੇ ਖੱਬੇ ਪਾਸੇ ਕਰੋ"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਬਦਲ ਕੇ ਸੱਜੇ ਪਾਸੇ ਕਰੋ"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਵਧਾਓ ਜਾਂ ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ਵਿੰਡੋ ਦਾ ਆਕਾਰ ਵਧਾਓ ਜਾਂ ਮੁੜ-ਬਹਾਲ ਕਰੋ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ਐਪ ਵਿੰਡੋ ਨੂੰ ਛੋਟਾ ਕਰੋ"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਸੈਟਿੰਗਾਂ ਮੁਤਾਬਕ ਖੋਲ੍ਹੋ"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ਇਸ ਐਪ ਲਈ ਵੈੱਬ ਲਿੰਕਾਂ ਨੂੰ ਖੋਲ੍ਹਣ ਦਾ ਤਰੀਕਾ ਚੁਣੋ"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ਐਪ ਵਿੱਚ"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index fa0e7c318f7e..47ee80e6a4e8 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% lewej części ekranu"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% lewej części ekranu"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Prawa część ekranu na pełnym ekranie"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zamień aplikacje"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Górna część ekranu na pełnym ekranie"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% górnej części ekranu"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% górnej części ekranu"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Przywróć"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Przyciągnij do lewej"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Przyciągnij do prawej"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Zmień rozmiar okna aplikacji po lewej"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Zmień rozmiar okna aplikacji po prawej"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Zmaksymalizuj lub przywróć rozmiar okna"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Włącz tryb podzielonego ekranu"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Włącz tryb okien na pulpicie"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Zmień rozmiar okna do lewej"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Zmień rozmiar okna do prawej"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Zmaksymalizuj lub przywróć rozmiar okna"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Zmaksymalizuj lub przywróć rozmiar okna"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Zminimalizuj okno aplikacji"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Ustawienia domyślnego otwierania"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Wybierz, gdzie chcesz otwierać linki z tej aplikacji"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"W aplikacji"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index d9e5f8c77897..0a3ea7011e1e 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Esquerda a 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Lado direito em tela cheia"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Trocar apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Parte superior em tela cheia"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Parte superior a 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionar janela do app para a esquerda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionar janela do app para a direita"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Entrar no modo de tela dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Entrar no modo de janela do computador"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionar janela para a esquerda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionar janela para a direita"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar janela do app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Configurações \"Abrir por padrão\""</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Escolha como abrir links da Web para este app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"No app"</string> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 28dc7b0d228e..c9d196b922db 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"50% no ecrã esquerdo"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"30% no ecrã esquerdo"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ecrã direito inteiro"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Trocar apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ecrã superior inteiro"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"70% no ecrã superior"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"50% no ecrã superior"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Encaixar à esquerda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Encaixar à direita"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionar janela da app para a esquerda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionar janela da app para a direita"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar ou restaurar tamanho da janela"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Aceder ao modo de ecrã dividido"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Aceder ao modo de janelas de computador"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionar janela para a esquerda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionar janela para a direita"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar ou restaurar tamanho da janela"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar ou restaurar tamanho da janela"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar janela da app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Definições de Abrir por predefinição"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Escolha como abrir links da Web para esta app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Na app"</string> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index d9e5f8c77897..0a3ea7011e1e 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Esquerda a 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Esquerda a 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Lado direito em tela cheia"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Trocar apps"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Parte superior em tela cheia"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Parte superior a 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Parte superior a 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restaurar"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Ajustar à esquerda"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Ajustar à direita"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionar janela do app para a esquerda"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionar janela do app para a direita"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Entrar no modo de tela dividida"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Entrar no modo de janela do computador"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionar janela para a esquerda"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionar janela para a direita"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizar ou restaurar o tamanho da janela"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizar janela do app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Configurações \"Abrir por padrão\""</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Escolha como abrir links da Web para este app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"No app"</string> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index b63a8b3b05df..a3313b6496e0 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Partea stângă: 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Partea stângă: 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Partea dreaptă pe ecran complet"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Comută între aplicații"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Partea de sus pe ecran complet"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Partea de sus: 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Partea de sus: 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restabilește"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Trage la stânga"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Trage la dreapta"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Redimensionează fereastra aplicației la stânga"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Redimensionează fereastra aplicației la dreapta"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximizează sau restabilește dimensiunea ferestrei"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Accesează modul ecran împărțit"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Accesează modul de windowing pe desktop"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Redimensionează fereastra la stânga"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Redimensionează fereastra la dreapta"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximizează sau restabilește dimensiunea ferestrei"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximizează sau restabilește dimensiunea ferestrei"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizează fereastra aplicației"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Setări de deschidere în mod prestabilit"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Alege modul de deschidere a linkurilor web pentru aplicație"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"În aplicație"</string> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 709e90eb7fd9..5b20b2bd6499 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Левый на 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Левый на 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Правый во весь экран"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Поменять приложения местами"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхний во весь экран"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхний на 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхний на 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Восстановить"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Привязать слева"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Привязать справа"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Растянуть окно приложения влево"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Растянуть окно приложения вправо"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Развернуть окно или восстановить его размер"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Перейти в режим разделения экрана"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Перейти в режим компьютера"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Растянуть окно влево"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Растянуть окно вправо"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Развернуть окно или восстановить его размер"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Развернуть окно или восстановить его размер"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Свернуть окно приложения"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Настройки, регулирующие, как по умолчанию открываются ссылки"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Выберите, где будут открываться ссылки из этого приложения"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"В приложении"</string> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index da1aa9d71c15..f0ef1d1bc658 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"වම් 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"වම් 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"දකුණු පූර්ණ තිරය"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"යෙදුම් හුවමාරු කරන්න"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ඉහළම පූර්ණ තිරය"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ඉහළම 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ඉහළම 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"ප්රතිසාධනය කරන්න"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"වමට ස්නැප් කරන්න"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"දකුණට ස්නැප් කරන්න"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"යෙදුම් කවුළුව වමට ප්රතිප්රමාණ කරන්න"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"යෙදුම් කවුළුව දකුණට ප්රතිප්රමාණ කරන්න"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"කවුළු ප්රමාණය උපරිම කරන්න හෝ ප්රතිසාධනය කරන්න"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"බෙදුම් තිර මාදිලියට ඇතුළු වන්න"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ඩෙස්ක්ටොප කවුළුකරණ මාදිලියට ඇතුළු වන්න"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"කවුළුව වමට ප්රතිප්රමාණ කරන්න"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"කවුළුව දකුණට ප්රතිප්රමාණ කරන්න"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"කවුළු ප්රමාණය උපරිම කරන්න හෝ ප්රතිසාධනය කරන්න"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"කවුළු ප්රමාණය උපරිම කරන්න හෝ ප්රතිසාධනය කරන්න"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"යෙදුම් කවුළුව අවම කරන්න"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"පෙරනිමි සැකසීම් මඟින් විවෘත කරන්න"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"මෙම යෙදුම සඳහා වෙබ් සබැඳි විවෘත කරන ආකාරය තෝරා ගන්න"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"යෙදුම තුළ"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index aa7799723993..688c217b8d32 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ľavá – 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ľavá – 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Pravá– na celú obrazovku"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Vymeniť aplikácie"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Horná – na celú obrazovku"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Horná – 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Horná – 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Obnoviť"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Prichytiť vľavo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Prichytiť vpravo"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Zmeniť veľkosť okna aplikácie vľavo"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Zmeniť veľkosť okna aplikácie vpravo"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximalizovať alebo obnoviť veľkosť okna"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Spustiť režim rozdelenej obrazovky"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Prejsť na režim okien na pracovnej ploche"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Zmeniť veľkosť okna vľavo"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Zmeniť veľkosť okna vpravo"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximalizovať alebo obnoviť veľkosť okna"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximalizovať alebo obnoviť veľkosť okna"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimalizovať okno aplikácie"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Otvárať podľa predvolených nastavení"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Vyberte, ako sa majú v tejto aplikácii otvárať webové odkazy"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"V aplikácii"</string> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 55452bd0e854..69eb3e311726 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Levi 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Levi 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Desni v celozaslonski način"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Zamenjava aplikacij"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Zgornji v celozaslonski način"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Zgornji 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Zgornji 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Obnovi"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Pripni levo"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Pripni desno"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Sprememba velikosti okna aplikacije na levi"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Sprememba velikosti okna aplikacije na desni"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Povečava ali obnovitev velikosti okna"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Vklop načina razdeljenega zaslona"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Vklop načina prikaza v oknu na namizju"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Sprememba velikosti okna na levi"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Sprememba velikosti okna na desni"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Povečava ali obnovitev velikosti okna"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Povečava ali obnovitev velikosti okna"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Pomanjšava okna aplikacije"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Nastavitve privzetega odpiranja"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Izberite način odpiranja spletnih povezav za to aplikacijo"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"V aplikaciji"</string> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 0492b2f9a51f..fcb0aa6559fa 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Majtas 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Majtas 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Ekrani i plotë djathtas"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Ndërro aplikacionet"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Ekrani i plotë lart"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Lart 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Lart 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Restauro"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Zhvendos majtas"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Zhvendos djathtas"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ndrysho përmasat e dritares së aplikacionit majtas"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ndrysho përmasat e dritares së aplikacionit djathtas"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maksimizo ose restauro madhësinë e dritares"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Hyr në modalitetin e ekranit të ndarë"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Hyr në modalitetin e dritareve në desktop"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ndrysho përmasat e dritares në të majtë"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ndrysho përmasat e dritares në të djathtë"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maksimizo ose restauro madhësinë e dritares"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maksimizo ose restauro madhësinë e dritares"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimizo dritaren e aplikacionit"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Hap sipas cilësimeve të parazgjedhura"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Zgjidh si do t\'i hapësh lidhjet e uebit për këtë aplikacion"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Në aplikacion"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index af8ac6898e83..6a2ffcdf8e89 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Леви екран 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Леви екран 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Режим целог екрана за доњи екран"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Замените места апликацијама"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Режим целог екрана за горњи екран"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Горњи екран 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Горњи екран 50%"</string> @@ -124,11 +125,11 @@ <string name="float_button_text" msgid="9221657008391364581">"Плутајуће"</string> <string name="select_text" msgid="5139083974039906583">"Изаберите"</string> <string name="screenshot_text" msgid="1477704010087786671">"Снимак екрана"</string> - <string name="open_in_browser_text" msgid="9181692926376072904">"Отворите у прегледачу"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"Отвори у прегледачу"</string> <string name="open_in_app_text" msgid="2874590745116268525">"Отворите у апликацији"</string> <string name="new_window_text" msgid="6318648868380652280">"Нови прозор"</string> <string name="manage_windows_text" msgid="5567366688493093920">"Управљајте прозорима"</string> - <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Промените размеру"</string> + <string name="change_aspect_ratio_text" msgid="9104456064548212806">"Промени размеру"</string> <string name="close_text" msgid="4986518933445178928">"Затворите"</string> <string name="collapse_menu_text" msgid="7515008122450342029">"Затворите мени"</string> <string name="desktop_mode_app_header_chip_text" msgid="6366422614991687237">"Отворите мени"</string> @@ -137,10 +138,20 @@ <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Апликација не може да се премести овде"</string> <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Имерзивне"</string> <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Врати"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Увећајте"</string> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Увећај"</string> <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Вратите"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Прикачите лево"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Прикачите десно"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Промените величину прозора апликације налево"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Промените величину прозора апликације надесно"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Увећајте или вратите величину прозора"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Уђите у режим подељеног екрана"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Уђите у режим прозора на рачунару"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Промените величину прозора налево"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Промените величину прозора надесно"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Увећајте или вратите величину прозора"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Увећајте или вратите величину прозора"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Умањите прозор апликације"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Подешавање Подразумевано отварај"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Одаберите начин отварања веб-линкова за ову апликацију"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"У апликацији"</string> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 0c3c18c70040..a9df47650dad 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Vänster 50 %"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Vänster 30 %"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Helskärm på höger skärm"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Byt appar"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Helskärm på övre skärm"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Övre 70 %"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Övre 50 %"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Återställ"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Fäst till vänster"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Fäst till höger"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ändra storlek på appfönstret åt vänster"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ändra storlek på appfönstret åt höger"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Maximera eller återställ fönsterstorleken"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Starta läget för delad skärm"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Starta datorläget"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Ändra storlek på fönstret åt vänster"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Ändra storlek på fönstret åt höger"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Maximera eller återställ fönsterstorleken"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Maximera eller återställ fönsterstorleken"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Minimera appfönstret"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Inställningar för Öppna som standard"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Välj hur webblänkar ska öppnas för den här appen"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"I appen"</string> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 4f0a6ac93b55..a3c9a0d3989c 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kushoto 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kushoto 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Skrini nzima ya kulia"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Badilisha Programu"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Skrini nzima ya juu"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Juu 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Juu 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Rejesha"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Telezesha kushoto"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Telezesha kulia"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Badilisha ukubwa wa dirisha la programu kushoto"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Badilisha ukubwa wa dirisha la programu kulia"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Panua au urejeshe ukubwa wa dirisha"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Ingia katika hali ya skrini iliyogawanywa"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Ingia katika hali ya madirisha ya kompyuta ya mezani"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Badilisha ukubwa wa dirisha kushoto"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Badilisha ukubwa wa dirisha kulia"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Panua au urejeshe ukubwa wa dirisha"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Panua au urejeshe ukubwa wa dirisha"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Punguza dirisha la programu"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Fungua kwa mipangilio chaguomsingi"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Chagua jinsi ya kufungua viungo vya wavuti vya programu hii"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Kwenye programu"</string> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 5fca404d5614..b1b8c7ff2075 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"இடது புறம் 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"இடது புறம் 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"வலது புறம் முழுத் திரை"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ஆப்ஸை மாற்றும்"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"மேற்புறம் முழுத் திரை"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"மேலே 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"மேலே 50%"</string> @@ -124,7 +125,7 @@ <string name="float_button_text" msgid="9221657008391364581">"மிதக்கும் சாளரம்"</string> <string name="select_text" msgid="5139083974039906583">"தேர்ந்தெடுக்கும்"</string> <string name="screenshot_text" msgid="1477704010087786671">"ஸ்கிரீன்ஷாட்"</string> - <string name="open_in_browser_text" msgid="9181692926376072904">"உலாவியில் திறக்கும்"</string> + <string name="open_in_browser_text" msgid="9181692926376072904">"பிரவுசரில் திற"</string> <string name="open_in_app_text" msgid="2874590745116268525">"ஆப்ஸில் திறக்கும்"</string> <string name="new_window_text" msgid="6318648868380652280">"புதிய சாளரம்"</string> <string name="manage_windows_text" msgid="5567366688493093920">"சாளரங்களை நிர்வகிக்கலாம்"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"மீட்டெடுக்கும்"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"இடதுபுறம் நகர்த்தும்"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"வலதுபுறம் நகர்த்தும்"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ஆப்ஸ் சாளரத்தின் இடதுபுறத்தில் அளவை மாற்றும்"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ஆப்ஸ் சாளரத்தின் வலதுபுறத்தில் அளவை மாற்றும்"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"சாளரத்தின் அளவைப் பெரிதாக்கும்/மீட்டெடுக்கும்"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"திரைப் பிரிப்புப் பயன்முறையில் உள்நுழையும்"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"டெஸ்க்டாப் சாளரப் பயன்முறையில் உள்நுழையும்"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"சாளரத்தை இடதுபுறமாக அளவு மாற்றும்"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"சாளரத்தை வலதுபுறமாக அளவு மாற்றும்"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"சாளரத்தின் அளவைப் பெரிதாக்கும்/மீட்டெடுக்கும்"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"சாளரத்தின் அளவைப் பெரிதாக்கும்/மீட்டெடுக்கும்"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ஆப்ஸ் சாளரத்தைச் சிறிதாக்கும்"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"இயல்பாக அமைப்புகளைத் திறக்கும்"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"இந்த ஆப்ஸில் வலை இணைப்புகளைத் திறக்கும் வழிமுறையைத் தேர்வுசெய்யுங்கள்"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ஆப்ஸில்"</string> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index abc4d08cd3ca..932f831c537d 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ఎడమవైపు 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ఎడమవైపు 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"కుడివైపు ఫుల్-స్క్రీన్"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"యాప్లను మార్చండి"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"ఎగువ ఫుల్-స్క్రీన్"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ఎగువ 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ఎగువ 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"రీస్టోర్ చేయండి"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"ఎడమ వైపున స్నాప్ చేయండి"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"కుడి వైపున స్నాప్ చేయండి"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"యాప్ విండో ఎడమ వైపు సైజ్ మార్చండి"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"యాప్ విండో కుడి వైపు సైజ్ మార్చండి"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"విండో సైజ్ను మ్యాగ్జిమైజ్ చేయండి లేదా రీస్టోర్ చేయండి"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"స్ప్లిట్ స్క్రీన్ మోడ్ను ఉపయోగించండి"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"డెస్క్టాప్ విండోయింగ్ మోడ్ను ఎంటర్ చేయండి"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"విండో ఎడమ వైపునకు సైజ్ను మార్చండి"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"విండో కుడి వైపునకు సైజ్ను మార్చండి"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"విండో సైజ్ను మ్యాగ్జిమైజ్ చేయండి లేదా రీస్టోర్ చేయండి"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"విండో సైజ్ను మ్యాగ్జిమైజ్ చేయండి లేదా రీస్టోర్ చేయండి"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"యాప్ విండోను కుదించండి"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"ఆటోమేటిక్ సెట్టింగ్ల ద్వారా తెరవండి"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"ఈ యాప్నకు సంబంధించిన వెబ్ లింక్లను ఎలా తెరవాలో ఎంచుకోండి"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"యాప్లో"</string> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index 7be7373e03a9..e157474d34fa 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"ซ้าย 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"ซ้าย 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"เต็มหน้าจอทางขวา"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"สลับแอป"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"เต็มหน้าจอด้านบน"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"ด้านบน 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"ด้านบน 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"คืนค่า"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"จัดพอดีกับทางซ้าย"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"จัดพอดีกับทางขวา"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"ปรับขนาดหน้าต่างแอปไปทางซ้าย"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ปรับขนาดหน้าต่างแอปไปทางขวา"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ขยายหรือคืนค่าขนาดหน้าต่าง"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"เข้าสู่โหมดแยกหน้าจอ"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"เข้าสู่โหมดหน้าต่างเดสก์ท็อป"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"ปรับขนาดหน้าต่างไปทางซ้าย"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ปรับขนาดหน้าต่างไปทางขวา"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ขยายหรือคืนค่าขนาดหน้าต่าง"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ขยายหรือคืนค่าขนาดหน้าต่าง"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ย่อหน้าต่างแอป"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"เปิดตามการตั้งค่าเริ่มต้น"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"เลือกวิธีเปิดเว็บลิงก์สำหรับแอปนี้"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ในแอป"</string> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 22b0174c0252..7f2970453072 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Gawing 50% ang nasa kaliwa"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Gawing 30% ang nasa kaliwa"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"I-full screen ang nasa kanan"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Pagpalitin ang Mga App"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"I-full screen ang nasa itaas"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Gawing 70% ang nasa itaas"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Gawing 50% ang nasa itaas"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"I-restore"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"I-snap pakaliwa"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"I-snap pakanan"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"I-resize pakaliwa ang window ng app"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"I-resize pakanan ang window ng app"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"I-maximize o i-restore ang laki ng window"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Pumunta sa split screen mode"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Pumunta sa desktop windowing mode"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"I-resize pakaliwa ang window"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"I-resize pakanan ang window"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"I-maximize o i-restore ang laki ng window"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"I-maximize o i-restore ang laki ng window"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"I-minimize ang window ng app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Buksan sa pamamagitan ng mga default na setting"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Piliin kung paano magbukas ng web link para sa app na ito"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Sa app"</string> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 79d64ba1f117..6a5d1abebd25 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Solda %50"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Solda %30"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Sağda tam ekran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Uygulamaların Yerini Değiştir"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Üstte tam ekran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Üstte %70"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Üstte %50"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Geri yükle"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Sola tuttur"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Sağa tuttur"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Uygulama penceresini sola yeniden boyutlandır"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Uygulama penceresini sağa yeniden boyutlandır"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Pencereyi ekranı kaplayacak şekilde büyüt veya önceki boyutuna döndür"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Bölünmüş ekran moduna gir"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Masaüstü pencereleme moduna gir"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Pencereyi sola yeniden boyutlandır"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Pencereyi sağa yeniden boyutlandır"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Pencereyi ekranı kaplayacak şekilde büyüt veya önceki boyutuna döndür"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Pencereyi ekranı kaplayacak şekilde büyüt veya önceki boyutuna döndür"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Uygulama penceresini küçült"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Varsayılan olarak açma ayarları"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Bu uygulama için web bağlantılarının nasıl açılacağını seçin"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Uygulamada"</string> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index aeba9824d3f4..7f4e91d5dfc5 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Ліве вікно на 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Ліве вікно на 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Праве вікно на весь екран"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Поміняти додатки місцями"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Верхнє вікно на весь екран"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Верхнє вікно на 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Верхнє вікно на 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Відновити"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Закріпити ліворуч"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Закріпити праворуч"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Змінити розмір вікна додатка ліворуч"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Змінити розмір вікна додатка праворуч"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Розгорнути вікно або відновити його розмір"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Увімкнути режим розділення екрана"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Увімкнути режим вікон для комп’ютера"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Змінити розмір вікна ліворуч"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Змінити розмір вікна праворуч"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Розгорнути вікно або відновити його розмір"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Розгорнути вікно або відновити його розмір"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Згорнути вікно додатка"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Налаштування \"Відкривати за умовчанням\""</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Виберіть, як відкривати вебпосилання в цьому додатку"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"У додатку"</string> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index cf6fb8926f55..f461d4077087 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"بائیں %50"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"بائیں %30"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"دائیں فل اسکرین"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"ایپس سویپ کریں"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"بالائی فل اسکرین"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"اوپر %70"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"اوپر %50"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"بحال کریں"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"دائیں منتقل کریں"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"بائیں منتقل کریں"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"دائیں طرف ایپ ونڈو کا سائز تبدیل کریں"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"ایپ ونڈو کا سائز بائیں طرف تبدیل کریں"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"ونڈو کا سائز زیادہ سے زیادہ یا بحال کریں"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"سپلٹ اسکرین موڈ میں داخل ہوں"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"ڈیسک ٹاپ ونڈو وضع میں داخل ہوں"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"دائیں طرف ونڈو کا سائز تبدیل کریں"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"ونڈو کا سائز بائیں طرف تبدیل کریں"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"ونڈو کا سائز زیادہ سے زیادہ یا بحال کریں"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"ونڈو کا سائز زیادہ سے زیادہ یا بحال کریں"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"ایپ ونڈو کو چھوٹا کریں"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"بطور ڈیفالٹ ترتیبات کھولیں"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"اس ایپ کے لیے ویب لنکس کھولنے کا طریقہ منتخب کریں"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"ایپ میں"</string> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index c64b84373b17..7c6a2a20aa80 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Chapda 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Chapda 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"O‘ngda to‘liq ekran"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Ilovalarni almashtirish"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Tepada to‘liq ekran"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Tepada 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Tepada 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Tiklash"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Chapga tortish"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Oʻngga tortish"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Ilova chap oynasi oʻlchamini oʻzgartirish"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Ilova oʻng oynasi oʻlchamini oʻzgartirish"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Oyna oʻlchamini kengaytirish yoki asliga qaytarish"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Ajratilgan ekran rejimiga kirish"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Kompyuter rejimiga kirish"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Oyna oʻlchamini chapga oʻzgartirish"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Oyna oʻlchamini oʻngga oʻzgartirish"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Oyna oʻlchamini kengaytirish yoki asliga qaytarish"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Oyna oʻlchamini kengaytirish yoki asliga qaytarish"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Ilova oynasini kichraytirish"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Birlamchi sozlamalar asosida ochish"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Bu ilovalardagi veb havolalar qanday ochilishini tanlang"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Ilovada"</string> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 2a7dae4cbaef..e7cacc345c2b 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Trái 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Trái 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Toàn màn hình bên phải"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Hoán đổi ứng dụng"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Toàn màn hình phía trên"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Trên 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Trên 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Khôi phục"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Di chuyển nhanh sang trái"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Di chuyển nhanh sang phải"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Đổi kích thước và chuyển cửa sổ ứng dụng sang trái"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Đổi kích thước và chuyển cửa sổ ứng dụng sang phải"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Phóng to hoặc khôi phục kích thước cửa sổ"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Mở chế độ chia đôi màn hình"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Mở chế độ cửa sổ trên máy tính"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Đổi kích thước và chuyển cửa sổ sang trái"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Đổi kích thước và chuyển cửa sổ sang phải"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Phóng to hoặc khôi phục kích thước cửa sổ"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Phóng to hoặc khôi phục kích thước cửa sổ"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Thu nhỏ cửa sổ ứng dụng"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Mở các chế độ cài đặt theo mặc định"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Chọn cách mở đường liên kết trang web cho ứng dụng này"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Trong ứng dụng"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index e45fbba6e196..562a0ee09bd6 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左侧 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左侧 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右侧全屏"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"交换应用位置"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"顶部全屏"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"顶部 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"顶部 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"恢复"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"贴靠左侧"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"贴靠右侧"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"调整应用窗口大小并贴靠左侧"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"调整应用窗口大小并贴靠右侧"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"将窗口最大化或恢复大小"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"进入分屏模式"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"进入桌面设备窗口化模式"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"调整窗口大小并贴靠左侧"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"调整窗口大小并贴靠右侧"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"将窗口最大化或恢复大小"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"将窗口最大化或恢复大小"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"将应用窗口最小化"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"默认打开设置"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"选择如何打开此应用中的网页链接"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"在此应用内"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index d5e106394720..eecd9f21be57 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"左邊 50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"左邊 30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"右邊全螢幕"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"切換應用程式"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"頂部全螢幕"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"頂部 70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"頂部 50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"還原"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"貼齊左邊"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"貼齊右邊"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"調整左邊應用程式視窗大小"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"調整右邊應用程式視窗大小"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"將視窗放到最大或者還原視窗大小"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"進入分割螢幕模式"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"進入桌面視窗模式"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"將視窗移去左邊調整大小"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"將視窗移去右邊調整大小"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"將視窗放到最大或者還原視窗大小"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"將視窗放到最大或者還原視窗大小"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"將應用程式視窗縮到最細"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"採用預設設定打開"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"選擇此應用程式開啟網絡連結的方式"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"在應用程式內"</string> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index a0357e12b722..c157c193fa14 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"以 50% 的螢幕空間顯示左側畫面"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"以 30% 的螢幕空間顯示左側畫面"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"以全螢幕顯示右側畫面"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"切換應用程式"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"以全螢幕顯示頂端畫面"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"以 70% 的螢幕空間顯示頂端畫面"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"以 50% 的螢幕空間顯示頂端畫面"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"還原"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"靠左對齊"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"靠右對齊"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"調整應用程式視窗大小並向左貼齊"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"調整應用程式視窗大小並向右貼齊"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"將視窗最大化或還原大小"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"進入分割畫面模式"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"進入電腦視窗化模式"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"調整應用程式視窗大小並向左貼齊"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"調整應用程式視窗大小並向右貼齊"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"將視窗最大化或還原大小"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"將視窗最大化或還原大小"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"將應用程式視窗最小化"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"開啟連結的預設設定"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"選擇如何開啟這個應用程式的網頁連結"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"使用應用程式"</string> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index 810b6c82e09d..a7ba6d21234d 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -43,6 +43,7 @@ <string name="accessibility_action_divider_left_50" msgid="3488317024557521561">"Kwesokunxele ngo-50%"</string> <string name="accessibility_action_divider_left_30" msgid="6023611335723838727">"Kwesokunxele ngo-30%"</string> <string name="accessibility_action_divider_right_full" msgid="3408505054325944903">"Isikrini esigcwele esingakwesokudla"</string> + <string name="accessibility_action_divider_swap" msgid="7026003137401725787">"Shintsha ama-app"</string> <string name="accessibility_action_divider_top_full" msgid="3495871951082107594">"Isikrini esigcwele esiphezulu"</string> <string name="accessibility_action_divider_top_70" msgid="1779164068887875474">"Okuphezulu okungu-70%"</string> <string name="accessibility_action_divider_top_50" msgid="8649582798829048946">"Okuphezulu okungu-50%"</string> @@ -141,6 +142,16 @@ <string name="desktop_mode_maximize_menu_restore_button_text" msgid="4234449220944704387">"Buyisela"</string> <string name="desktop_mode_maximize_menu_snap_left_button_text" msgid="8077452201179893424">"Chofoza kwesobunxele"</string> <string name="desktop_mode_maximize_menu_snap_right_button_text" msgid="7117751068945657304">"Chofoza kwesokudla"</string> + <string name="desktop_mode_a11y_action_snap_left" msgid="2932955411661734668">"Shintsha usayizi we-app yewindi ngakwesokunxele"</string> + <string name="desktop_mode_a11y_action_snap_right" msgid="4577032451624261787">"Shintsha usayizi we-app yewindi ngakwesokudla"</string> + <string name="desktop_mode_a11y_action_maximize_restore" msgid="8026037983417986686">"Khulisa noma buyisela usayizi wewindi"</string> + <string name="app_handle_menu_talkback_split_screen_mode_button_text" msgid="7182959681057464802">"Faka imodi yokuhlukanisa isikrini"</string> + <string name="app_handle_menu_talkback_desktop_mode_button_text" msgid="1230110046930843630">"Faka imodi yokwenza iwindi yedeskithophu"</string> + <string name="maximize_menu_talkback_action_snap_left_text" msgid="500309467459084564">"Shintsha usayizi wewindi ngakwesokunxele"</string> + <string name="maximize_menu_talkback_action_snap_right_text" msgid="7010831426654467163">"Shintsha usayizi wewindi ngakwesokudla"</string> + <string name="maximize_menu_talkback_action_maximize_restore_text" msgid="4942610897847934859">"Khulisa noma buyisela usayizi wewindi"</string> + <string name="maximize_button_talkback_action_maximize_restore_text" msgid="4122441323153198455">"Khulisa noma buyisela usayizi wewindi"</string> + <string name="minimize_button_talkback_action_maximize_restore_text" msgid="8890767445425625935">"Nciphisa iwindi le-app"</string> <string name="open_by_default_settings_text" msgid="2526548548598185500">"Vula amasethingi ngokuzenzakalela"</string> <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Khetha indlela yokuvula amalinki ewebhu ale app"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"Ku-app"</string> diff --git a/libs/WindowManager/Shell/res/values/attrs.xml b/libs/WindowManager/Shell/res/values/attrs.xml index fbb5caa508de..4ba0468a740d 100644 --- a/libs/WindowManager/Shell/res/values/attrs.xml +++ b/libs/WindowManager/Shell/res/values/attrs.xml @@ -23,4 +23,11 @@ <declare-styleable name="MessageState"> <attr name="state_task_focused" format="boolean"/> </declare-styleable> + + <declare-styleable name="HandleMenuActionButton"> + <attr name="android:text" format="string" /> + <attr name="android:textColor" format="color" /> + <attr name="android:src" format="reference" /> + <attr name="android:drawableTint" format="color" /> + </declare-styleable> </resources> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 9e2d23b41556..f5f3f0fe52eb 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -270,6 +270,8 @@ <dimen name="bubble_bar_expanded_view_switch_offset">48dp</dimen> <!-- Minimum width of the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_min_width">200dp</dimen> + <!-- The Bubble Bar drop zone square size. --> + <dimen name="bubble_bar_drop_zone_side_size">200dp</dimen> <!-- Size of the dismiss icon in the bubble bar manage menu. --> <dimen name="bubble_bar_manage_menu_dismiss_icon_size">16dp</dimen> <!-- Padding of the bubble bar manage menu, provides space for menu shadows --> @@ -293,6 +295,10 @@ <dimen name="bubble_bar_dismiss_zone_width">192dp</dimen> <!-- Height of the box around bottom center of the screen where drag only leads to dismiss --> <dimen name="bubble_bar_dismiss_zone_height">242dp</dimen> + <!-- Height of the box at the corner of the screen where drag leads to app moving to bubble --> + <dimen name="bubble_transform_area_width">140dp</dimen> + <!-- Width of the box at the corner of the screen where drag leads to app moving to bubble --> + <dimen name="bubble_transform_area_height">140dp</dimen> <!-- Bottom and end margin for compat buttons. --> <dimen name="compat_button_margin">24dp</dimen> @@ -441,6 +447,17 @@ 80 dp for handle + 20 dp for room to grow on the sides when hovered. --> <dimen name="desktop_mode_fullscreen_decor_caption_width">100dp</dimen> + <!-- Horizontal padding for desktop mode caption in default unhovered untouched state. --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_default">10dp</dimen> + + <!-- Horizontal padding for desktop mode caption when hovered. + 1/2 * (100 dp of total width - 80 dp for handle * 1.2 scaling factor). --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_hovered">2dp</dimen> + + <!-- Horizontal padding for desktop mode caption when touched. + 1/2 * (100 dp of total width - 80 dp for handle * 0.85 scaling factor). --> + <dimen name="desktop_mode_fullscreen_decor_caption_horizontal_padding_touched">16dp</dimen> + <!-- Required empty space to be visible for partially offscreen tasks. --> <dimen name="freeform_required_visible_empty_space_in_header">48dp</dimen> @@ -481,14 +498,6 @@ <!-- The default minimum allowed window height when resizing a window in desktop mode. --> <dimen name="desktop_mode_minimum_window_height">352dp</dimen> - <!-- The width of the maximize menu in desktop mode, depending on the number of options --> - <dimen name="desktop_mode_maximize_menu_width_one_options">126dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_two_options">228dp</dimen> - <dimen name="desktop_mode_maximize_menu_width_three_options">330dp</dimen> - - <!-- The height of the maximize menu in desktop mode. --> - <dimen name="desktop_mode_maximize_menu_height">114dp</dimen> - <!-- The padding of the maximize menu in desktop mode. --> <dimen name="desktop_mode_menu_padding">16dp</dimen> @@ -564,7 +573,7 @@ <dimen name="desktop_mode_handle_menu_corner_radius">26dp</dimen> <!-- The radius of the caption menu icon. --> - <dimen name="desktop_mode_caption_icon_radius">24dp</dimen> + <dimen name="desktop_mode_caption_icon_radius">32dp</dimen> <!-- The radius of the caption menu shadow. --> <dimen name="desktop_mode_handle_menu_shadow_radius">2dp</dimen> diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml index debcba071d9c..c6082b3bd60f 100644 --- a/libs/WindowManager/Shell/res/values/ids.xml +++ b/libs/WindowManager/Shell/res/values/ids.xml @@ -25,6 +25,7 @@ <item type="id" name="action_move_tl_50" /> <item type="id" name="action_move_tl_30" /> <item type="id" name="action_move_rb_full" /> + <item type="id" name="action_swap_apps" /> <!-- For saving PhysicsAnimationLayout animations/animators as view tags. --> <item type="id" name="translation_x_dynamicanimation_tag"/> @@ -46,4 +47,9 @@ <item type="id" name="action_move_bubble_bar_right"/> <item type="id" name="dismiss_view"/> + + <!-- Accessibility actions for desktop windowing. --> + <item type="id" name="action_snap_left"/> + <item type="id" name="action_snap_right"/> + <item type="id" name="action_maximize_restore"/> </resources> diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml index 468c345259d0..1b7daa87064a 100644 --- a/libs/WindowManager/Shell/res/values/strings.xml +++ b/libs/WindowManager/Shell/res/values/strings.xml @@ -90,6 +90,8 @@ <string name="accessibility_action_divider_left_30">Left 30%</string> <!-- Accessibility action for moving docked stack divider to make the right screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_right_full">Right full screen</string> + <!-- Accessibility action for swapping the apps around the divider (double tap action) [CHAR LIMIT=NONE] --> + <string name="accessibility_action_divider_swap">Swap Apps</string> <!-- Accessibility action for moving docked stack divider to make the top screen full screen [CHAR LIMIT=NONE] --> <string name="accessibility_action_divider_top_full">Top full screen</string> @@ -288,7 +290,7 @@ <!-- Accessibility text for the handle fullscreen button [CHAR LIMIT=NONE] --> <string name="fullscreen_text">Fullscreen</string> <!-- Accessibility text for the handle desktop button [CHAR LIMIT=NONE] --> - <string name="desktop_text">Desktop Mode</string> + <string name="desktop_text">Desktop View</string> <!-- Accessibility text for the handle split screen button [CHAR LIMIT=NONE] --> <string name="split_screen_text">Split Screen</string> <!-- Accessibility text for the handle more options button [CHAR LIMIT=NONE] --> @@ -314,7 +316,7 @@ <!-- Accessibility text for the handle menu close menu button [CHAR LIMIT=NONE] --> <string name="collapse_menu_text">Close Menu</string> <!-- Accessibility text for the App Header's App Chip [CHAR LIMIT=NONE] --> - <string name="desktop_mode_app_header_chip_text">Open Menu</string> + <string name="desktop_mode_app_header_chip_text"><xliff:g id="app_name" example="Chrome">%1$s</xliff:g> (Desktop View)</string> <!-- Maximize menu maximize button string. --> <string name="desktop_mode_maximize_menu_maximize_text">Maximize Screen</string> <!-- Maximize menu snap buttons string. --> @@ -333,6 +335,28 @@ <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> <string name="desktop_mode_maximize_menu_snap_right_button_text">Snap right</string> + <!-- Accessibility text for the Maximize Menu's snap left button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_left">Resize app window left</string> + <!-- Accessibility text for the Maximize Menu's snap right button [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_snap_right">Resize app window right</string> + <!-- Accessibility text for the Maximize Menu's snap maximize/restore [CHAR LIMIT=NONE] --> + <string name="desktop_mode_a11y_action_maximize_restore">Maximize or restore window size</string> + + <!-- Accessibility action replacement for caption handle app chip buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_chip_accessibility_announce">Open Menu</string> + <!-- Accessibility action replacement for caption handle menu buttons [CHAR LIMIT=NONE] --> + <string name="app_handle_menu_accessibility_announce">Enter <xliff:g id="windowing_mode" example="Desktop View">%1$s</xliff:g></string> + <!-- Accessibility action replacement for maximize menu enter snap left button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_left_text">Resize window to left</string> + <!-- Accessibility action replacement for maximize menu enter snap right button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_snap_right_text">Resize window to right</string> + <!-- Accessibility action replacement for maximize menu enter maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_menu_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header maximize/restore button [CHAR LIMIT=NONE] --> + <string name="maximize_button_talkback_action_maximize_restore_text">Maximize or restore window size</string> + <!-- Accessibility action replacement for app header minimize button [CHAR LIMIT=NONE] --> + <string name="minimize_button_talkback_action_maximize_restore_text">Minimize app window</string> + <!-- Accessibility text for open by default settings button [CHAR LIMIT=NONE] --> <string name="open_by_default_settings_text">Open by default settings</string> <!-- Subheader for open by default menu string. --> diff --git a/libs/WindowManager/Shell/res/values/strings_tv.xml b/libs/WindowManager/Shell/res/values/strings_tv.xml index 8f806cf56c9b..b50812f36e4b 100644 --- a/libs/WindowManager/Shell/res/values/strings_tv.xml +++ b/libs/WindowManager/Shell/res/values/strings_tv.xml @@ -59,5 +59,7 @@ <!-- Accessibility action: done with moving the PiP [CHAR LIMIT=30] --> <string name="a11y_action_pip_move_done">Done</string> + <string name="font_display_medium" translatable="false">sans-serif</string> + </resources> diff --git a/libs/WindowManager/Shell/res/values/styles.xml b/libs/WindowManager/Shell/res/values/styles.xml index 8a4a7023b8e8..637b47ab3ace 100644 --- a/libs/WindowManager/Shell/res/values/styles.xml +++ b/libs/WindowManager/Shell/res/values/styles.xml @@ -43,20 +43,32 @@ <style name="DesktopModeHandleMenuActionButton"> <item name="android:layout_width">match_parent</item> <item name="android:layout_height">52dp</item> - <item name="android:gravity">start|center_vertical</item> - <item name="android:paddingStart">16dp</item> - <item name="android:paddingEnd">0dp</item> - <item name="android:textSize">14sp</item> - <item name="android:textFontWeight">500</item> <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> - <item name="android:drawablePadding">16dp</item> - <item name="android:background">?android:selectableItemBackground</item> - </style> + <item name="android:drawableTint">@androidprv:color/materialColorOnSurface</item> + </style> + + <style name="DesktopModeHandleMenuActionButtonImage"> + <item name="android:layout_width">20dp</item> + <item name="android:layout_height">20dp</item> + <item name="android:layout_marginEnd">16dp</item> + </style> + + <style name="DesktopModeHandleMenuActionButtonTextView"> + <item name="android:layout_width">0dp</item> + <item name="android:layout_height">wrap_content</item> + <item name="android:layout_weight">1</item> + <item name="android:textSize">14sp</item> + <item name="android:lineHeight">20sp</item> + <item name="android:textFontWeight">500</item> + <item name="android:textColor">@androidprv:color/materialColorOnSurface</item> + <item name="android:ellipsize">marquee</item> + <item name="android:scrollHorizontally">true</item> + <item name="android:singleLine">true</item> + </style> <style name="DesktopModeHandleMenuWindowingButton"> <item name="android:layout_width">48dp</item> <item name="android:layout_height">48dp</item> - <item name="android:padding">14dp</item> <item name="android:scaleType">fitCenter</item> <item name="android:background">?android:selectableItemBackgroundBorderless</item> </style> @@ -75,15 +87,6 @@ <item name="android:background">@color/split_divider_background</item> </style> - <style name="TvPipEduText"> - <item name="android:fontFamily">@*android:string/config_headlineFontFamilyMedium</item> - <item name="android:textAllCaps">true</item> - <item name="android:textSize">10sp</item> - <item name="android:lineSpacingExtra">4sp</item> - <item name="android:lineHeight">16sp</item> - <item name="android:textColor">@color/tv_pip_edu_text</item> - </style> - <style name="LetterboxDialog" parent="@android:style/Theme.DeviceDefault.Dialog.Alert"> <item name="android:layout_width">wrap_content</item> <item name="android:layout_height">wrap_content</item> diff --git a/libs/WindowManager/Shell/res/values/styles_tv.xml b/libs/WindowManager/Shell/res/values/styles_tv.xml new file mode 100644 index 000000000000..a4f5edc7fa35 --- /dev/null +++ b/libs/WindowManager/Shell/res/values/styles_tv.xml @@ -0,0 +1,29 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- Copyright (C) 2025 The Android Open Source Project + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +--> + +<resources xmlns:android="http://schemas.android.com/apk/res/android"> + + <style name="TvPipEduText"> + <item name="android:fontFamily">@string/font_display_medium</item> + <item name="android:textAllCaps">true</item> + <item name="android:textFontWeight">700</item> + <item name="android:textSize">10sp</item> + <item name="android:lineSpacingExtra">4sp</item> + <item name="android:lineHeight">16sp</item> + <item name="android:textColor">@color/tv_pip_edu_text</item> + </style> + +</resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/Android.bp b/libs/WindowManager/Shell/shared/Android.bp index 261c63948a94..af46ca298efe 100644 --- a/libs/WindowManager/Shell/shared/Android.bp +++ b/libs/WindowManager/Shell/shared/Android.bp @@ -74,6 +74,7 @@ java_library { "**/desktopmode/*.kt", ], static_libs: [ + "WindowManager-Shell-shared-AOSP", "com.android.window.flags.window-aconfig-java", "wm_shell-shared-utils", ], diff --git a/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml new file mode 100644 index 000000000000..975d25b25953 --- /dev/null +++ b/libs/WindowManager/Shell/shared/res/color/bubble_drop_target_background_color.xml @@ -0,0 +1,20 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<selector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"> + <item android:alpha="0.35" android:color="@androidprv:color/materialColorPrimaryContainer" /> +</selector> diff --git a/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml new file mode 100644 index 000000000000..89546f9b0807 --- /dev/null +++ b/libs/WindowManager/Shell/shared/res/drawable/bubble_drop_target_background.xml @@ -0,0 +1,25 @@ +<?xml version="1.0" encoding="utf-8"?><!-- + ~ Copyright (C) 2025 The Android Open Source Project + ~ + ~ Licensed under the Apache License, Version 2.0 (the "License"); + ~ you may not use this file except in compliance with the License. + ~ You may obtain a copy of the License at + ~ + ~ http://www.apache.org/licenses/LICENSE-2.0 + ~ + ~ Unless required by applicable law or agreed to in writing, software + ~ distributed under the License is distributed on an "AS IS" BASIS, + ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + ~ See the License for the specific language governing permissions and + ~ limitations under the License. + --> + +<shape xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + android:shape="rectangle"> + <corners android:radius="28dp" /> + <solid android:color="@color/bubble_drop_target_background_color" /> + <stroke + android:width="1dp" + android:color="@androidprv:color/materialColorPrimaryContainer" /> +</shape> diff --git a/libs/WindowManager/Shell/shared/res/values/dimen.xml b/libs/WindowManager/Shell/shared/res/values/dimen.xml index 0b1f76f5ce0e..11a6f32d7454 100644 --- a/libs/WindowManager/Shell/shared/res/values/dimen.xml +++ b/libs/WindowManager/Shell/shared/res/values/dimen.xml @@ -17,4 +17,33 @@ <resources> <dimen name="floating_dismiss_icon_size">32dp</dimen> <dimen name="floating_dismiss_background_size">96dp</dimen> + + <!-- Bubble drag zone dimensions --> + <dimen name="drag_zone_dismiss_fold">140dp</dimen> + <dimen name="drag_zone_dismiss_tablet">200dp</dimen> + <dimen name="drag_zone_bubble_fold">140dp</dimen> + <dimen name="drag_zone_bubble_tablet">200dp</dimen> + <dimen name="drag_zone_full_screen_width">512dp</dimen> + <dimen name="drag_zone_full_screen_height">44dp</dimen> + <dimen name="drag_zone_desktop_window_width">880dp</dimen> + <dimen name="drag_zone_desktop_window_height">300dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_desktop_window_expanded_view_height">350dp</dimen> + <dimen name="drag_zone_split_from_bubble_height">100dp</dimen> + <dimen name="drag_zone_split_from_bubble_width">60dp</dimen> + <dimen name="drag_zone_h_split_from_expanded_view_width">60dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_width">200dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_tablet">285dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_tall">150dp</dimen> + <dimen name="drag_zone_v_split_from_expanded_view_height_fold_short">100dp</dimen> + + <!-- Bubble drop target dimensions --> + <dimen name="drop_target_elevation">1dp</dimen> + <dimen name="drop_target_full_screen_padding">20dp</dimen> + <dimen name="drop_target_desktop_window_padding_small">100dp</dimen> + <dimen name="drop_target_desktop_window_padding_large">130dp</dimen> + <dimen name="drop_target_expanded_view_width">364</dimen> + <dimen name="drop_target_expanded_view_height">578</dimen> + <dimen name="drop_target_expanded_view_padding_bottom">108</dimen> + <dimen name="drop_target_expanded_view_padding_horizontal">24</dimen> </resources>
\ No newline at end of file diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java index 4d00c74155a8..851987269c10 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/TransitionUtil.java @@ -21,6 +21,7 @@ import static android.view.RemoteAnimationTarget.MODE_CHANGING; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; +import static android.view.WindowManager.LayoutParams.LAST_SYSTEM_WINDOW; import static android.view.WindowManager.LayoutParams.TYPE_DOCK_DIVIDER; import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE; @@ -55,9 +56,15 @@ import java.util.function.Predicate; public class TransitionUtil { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = FLAG_FIRST_CUSTOM; + public static final int FLAG_IS_DIM_LAYER = FLAG_FIRST_CUSTOM << 1; /** Flag applied to a transition change to identify it as a desktop wallpaper activity. */ - public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 1; + public static final int FLAG_IS_DESKTOP_WALLPAPER_ACTIVITY = FLAG_FIRST_CUSTOM << 2; + + /** + * Applied to a {@link RemoteAnimationTarget} to identify dim layers for animation in Launcher. + */ + public static final int TYPE_SPLIT_SCREEN_DIM_LAYER = LAST_SYSTEM_WINDOW + 1; /** @return true if the transition was triggered by opening something vs closing something */ public static boolean isOpeningType(@WindowManager.TransitionType int type) { @@ -117,6 +124,11 @@ public class TransitionUtil { return isNonApp(change) && change.hasFlags(FLAG_IS_DIVIDER_BAR); } + /** Returns `true` if `change` is an app's dim layer. */ + public static boolean isDimLayer(TransitionInfo.Change change) { + return isNonApp(change) && change.hasFlags(FLAG_IS_DIM_LAYER); + } + /** Returns `true` if `change` is only re-ordering. */ public static boolean isOrderOnly(TransitionInfo.Change change) { return change.getMode() == TRANSIT_CHANGE @@ -231,6 +243,14 @@ public class TransitionUtil { t.setLayer(leash, Integer.MAX_VALUE); return; } + if (isDimLayer(change)) { + // When a dim layer gets reparented onto the transition root, we need to zero out its + // position so that it's in line with everything else on the transition root. Also, + // we need to set a crop because we don't want it applying MATCH_PARENT on the whole + // root surface. + t.setPosition(leash, 0, 0); + t.setCrop(leash, change.getEndAbsBounds()); + } // Put all the OPEN/SHOW on top if ((change.getFlags() & FLAG_IS_WALLPAPER) != 0) { @@ -284,14 +304,19 @@ public class TransitionUtil { // Copied Transitions setup code (which expects bottom-to-top order, so we swap here) setupLeash(leashSurface, change, info.getChanges().size() - order, info, t); t.reparent(change.getLeash(), leashSurface); - t.setAlpha(change.getLeash(), 1.0f); - t.show(change.getLeash()); + if (!isDimLayer(change)) { + // Most leashes going onto the transition root should have their alpha set here to make + // them visible. But dim layers should be left untouched (their alpha value is their + // actual dim value). + t.setAlpha(change.getLeash(), 1.0f); + } if (!isDividerBar(change)) { // For divider, don't modify its inner leash position when creating the outer leash // for the transition. In case the position being wrong after the transition finished. t.setPosition(change.getLeash(), 0, 0); } t.setLayer(change.getLeash(), 0); + t.show(change.getLeash()); return leashSurface; } @@ -333,6 +358,9 @@ public class TransitionUtil { if (isDividerBar(change)) { return getDividerTarget(change, leash); } + if (isDimLayer(change)) { + return getDimLayerTarget(change, leash); + } int taskId; boolean isNotInRecents; @@ -439,6 +467,17 @@ public class TransitionUtil { TYPE_DOCK_DIVIDER); } + private static RemoteAnimationTarget getDimLayerTarget(TransitionInfo.Change change, + SurfaceControl leash) { + return new RemoteAnimationTarget(-1 /* taskId */, newModeToLegacyMode(change.getMode()), + leash, false /* isTranslucent */, null /* clipRect */, + null /* contentInsets */, Integer.MAX_VALUE /* prefixOrderIndex */, + new android.graphics.Point(0, 0) /* position */, change.getStartAbsBounds(), + change.getStartAbsBounds(), new WindowConfiguration(), true, null /* startLeash */, + null /* startBounds */, null /* taskInfo */, false /* allowEnterPip */, + TYPE_SPLIT_SCREEN_DIM_LAYER); + } + /** * Finds the "correct" root idx for a change. The change's end display is prioritized, then * the start display. If there is no display, it will fallback on the 0th root in the diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java index f45dc3a1e892..e92c1eb81e89 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/Interpolators.java @@ -93,10 +93,21 @@ public class Interpolators { public static final PathInterpolator SLOWDOWN_INTERPOLATOR = new PathInterpolator(0.5f, 1f, 0.5f, 1f); + /** + * An interpolator used for dimming a task as it travels offscreen, or towards a distant dismiss + * point. A sharp rise, followed by a steady middle, and ending with another sharp rise. + */ public static final PathInterpolator DIM_INTERPOLATOR = new PathInterpolator(.23f, .87f, .52f, -0.11f); /** + * An interpolator used for dimming a task very quickly. Roughly approximates one of the "sharp + * rises" of {@link #DIM_INTERPOLATOR}. + */ + public static final PathInterpolator FAST_DIM_INTERPOLATOR = + new PathInterpolator(0.23f, 0.87f, 0.83f, 0.83f); + + /** * Use this interpolator for animating progress values coming from the back callback to get * the predictive-back-typical decelerate motion. * diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt index 9d3b56d22a2f..812b3585840a 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/PhysicsAnimator.kt @@ -985,6 +985,11 @@ class PhysicsAnimator<T> private constructor (target: T) { return animators[target] as PhysicsAnimator<T> } + @JvmStatic + @Suppress("UNCHECKED_CAST") + fun <T: Any> getInstanceIfExists(target: T): PhysicsAnimator<T>? = + animators[target] as PhysicsAnimator<T>? + /** * Set whether all physics animators should log a lot of information about animations. * Useful for debugging! diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt index 91d66eaeb088..d1c34a4ac1cf 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt @@ -22,6 +22,7 @@ import android.graphics.PointF import android.graphics.Rect import android.util.DisplayMetrics import android.util.TypedValue +import android.view.Choreographer import android.view.SurfaceControl import android.view.animation.Interpolator import android.window.TransitionInfo @@ -82,6 +83,7 @@ object WindowAnimator { transaction .setPosition(leash, animPos.x, animPos.y) .setScale(leash, animScale, animScale) + .setFrameTimeline(Choreographer.getInstance().vsyncId) .apply() } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java new file mode 100644 index 000000000000..cfa00bbb7649 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/annotations/ShellDesktopThread.java @@ -0,0 +1,32 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.annotations; + +import java.lang.annotation.Documented; +import java.lang.annotation.Inherited; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; + +import javax.inject.Qualifier; + +/** Annotates a method or qualifies a provider that runs on the Shell desktop thread */ +@Documented +@Inherited +@Qualifier +@Retention(RetentionPolicy.RUNTIME) +public @interface ShellDesktopThread { +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt index bd129a28f049..da3d44df1180 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BaseBubblePinController.kt @@ -95,7 +95,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi /** Signal the controller that dragging interaction has finished. */ fun onDragEnd() { - getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + hideDropTarget() dismissZone = null listener?.onRelease(if (onLeft) LEFT else RIGHT) } @@ -139,7 +139,7 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi return rect } - private fun showDropTarget(location: BubbleBarLocation) { + fun showDropTarget(location: BubbleBarLocation) { val targetView = getDropTargetView() ?: createDropTargetView().apply { alpha = 0f } if (targetView.alpha > 0) { targetView.animateOut { @@ -152,6 +152,10 @@ abstract class BaseBubblePinController(private val screenSizeProvider: () -> Poi } } + fun hideDropTarget() { + getDropTargetView()?.let { view -> view.animateOut { removeDropTargetView(view) } } + } + private fun View.animateIn() { dropTargetAnimator?.cancel() dropTargetAnimator = diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt index 84a22b873aaf..6acd9dbe8b91 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleBarLocation.kt @@ -70,6 +70,8 @@ enum class BubbleBarLocation : Parcelable { UpdateSource.A11Y_ACTION_BAR, UpdateSource.A11Y_ACTION_BUBBLE, UpdateSource.A11Y_ACTION_EXP_VIEW, + UpdateSource.APP_ICON_DRAG, + UpdateSource.DRAG_TASK, ) @Retention(AnnotationRetention.SOURCE) annotation class UpdateSource { @@ -91,6 +93,12 @@ enum class BubbleBarLocation : Parcelable { /** Location changed via a11y action on the expanded view */ const val A11Y_ACTION_EXP_VIEW = 6 + + /** Location changed from dragging the application icon to the bubble bar */ + const val APP_ICON_DRAG = 7 + + /** Location changed from dragging a running task to the bubble bar */ + const val DRAG_TASK = 8 } } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt new file mode 100644 index 000000000000..9bee11a92430 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/BubbleDropTargetBoundsProvider.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect + +/** + * Provide bounds for Bubbles drop targets that are shown when dragging over drag zones + */ +interface BubbleDropTargetBoundsProvider { + /** + * Get bubble bar expanded view visual drop target bounds on screen + */ + fun getBubbleBarExpandedViewDropTargetBounds(onLeft: Boolean): Rect +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt index 929330918174..f479da051e06 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/DeviceConfig.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DeviceConfig.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.bubbles +package com.android.wm.shell.shared.bubbles import android.content.Context import android.content.res.Configuration diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt new file mode 100644 index 000000000000..6eff75c9a479 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZone.kt @@ -0,0 +1,71 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.graphics.Rect + +/** + * Represents an invisible area on the screen that determines what happens to a dragged object if it + * is released in that area. + * + * [bounds] are the bounds of the drag zone. Drag zones have an associated drop target that serves + * as visual feedback hinting what would happen if the object is released. When a dragged object is + * dragged into a drag zone, the associated drop target will be displayed. Not all drag zones have + * drop targets; only those that are made visible by Bubbles do. + */ +sealed interface DragZone { + + /** The bounds of this drag zone. */ + val bounds: Rect + /** The bounds of the drop target associated with this drag zone. */ + val dropTarget: Rect? + + fun contains(x: Int, y: Int) = bounds.contains(x, y) + + /** Represents the bubble drag area on the screen. */ + sealed class Bubble(override val bounds: Rect, override val dropTarget: Rect) : DragZone { + data class Left(override val bounds: Rect, override val dropTarget: Rect) : + Bubble(bounds, dropTarget) + + data class Right(override val bounds: Rect, override val dropTarget: Rect) : + Bubble(bounds, dropTarget) + } + + /** Represents dragging to Desktop Window. */ + data class DesktopWindow(override val bounds: Rect, override val dropTarget: Rect) : DragZone + + /** Represents dragging to Full Screen. */ + data class FullScreen(override val bounds: Rect, override val dropTarget: Rect) : DragZone + + /** Represents dragging to dismiss. */ + data class Dismiss(override val bounds: Rect) : DragZone { + override val dropTarget: Rect? = null + } + + /** Represents dragging to enter Split or replace a Split app. */ + sealed class Split(override val bounds: Rect) : DragZone { + override val dropTarget: Rect? = null + + data class Left(override val bounds: Rect) : Split(bounds) + + data class Right(override val bounds: Rect) : Split(bounds) + + data class Top(override val bounds: Rect) : Split(bounds) + + data class Bottom(override val bounds: Rect) : Split(bounds) + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt new file mode 100644 index 000000000000..1a80b0f29aa9 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DragZoneFactory.kt @@ -0,0 +1,592 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.content.Context +import android.graphics.Rect +import android.util.TypedValue +import androidx.annotation.DimenRes +import com.android.wm.shell.shared.R +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode + +/** A class for creating drag zones for dragging bubble objects or dragging into bubbles. */ +class DragZoneFactory( + private val context: Context, + private val deviceConfig: DeviceConfig, + private val splitScreenModeChecker: SplitScreenModeChecker, + private val desktopWindowModeChecker: DesktopWindowModeChecker, +) { + + private val windowBounds: Rect + get() = deviceConfig.windowBounds + + private var dismissDragZoneSize = 0 + private var bubbleDragZoneTabletSize = 0 + private var bubbleDragZoneFoldableSize = 0 + private var fullScreenDragZoneWidth = 0 + private var fullScreenDragZoneHeight = 0 + private var desktopWindowDragZoneWidth = 0 + private var desktopWindowDragZoneHeight = 0 + private var desktopWindowFromExpandedViewDragZoneWidth = 0 + private var desktopWindowFromExpandedViewDragZoneHeight = 0 + private var splitFromBubbleDragZoneHeight = 0 + private var splitFromBubbleDragZoneWidth = 0 + private var hSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneWidth = 0 + private var vSplitFromExpandedViewDragZoneHeightTablet = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldTall = 0 + private var vSplitFromExpandedViewDragZoneHeightFoldShort = 0 + + private var fullScreenDropTargetPadding = 0 + private var desktopWindowDropTargetPaddingSmall = 0 + private var desktopWindowDropTargetPaddingLarge = 0 + private var expandedViewDropTargetWidth = 0 + private var expandedViewDropTargetHeight = 0 + private var expandedViewDropTargetPaddingBottom = 0 + private var expandedViewDropTargetPaddingHorizontal = 0 + + private val fullScreenDropTarget: Rect + get() = + Rect(windowBounds).apply { + inset(fullScreenDropTargetPadding, fullScreenDropTargetPadding) + } + + private val desktopWindowDropTarget: Rect + get() = + Rect(windowBounds).apply { + if (deviceConfig.isLandscape) { + inset( + /* dx= */ desktopWindowDropTargetPaddingLarge, + /* dy= */ desktopWindowDropTargetPaddingSmall + ) + } else { + inset( + /* dx= */ desktopWindowDropTargetPaddingSmall, + /* dy= */ desktopWindowDropTargetPaddingLarge + ) + } + } + + private val expandedViewDropTargetLeft: Rect + get() = + Rect( + expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - + expandedViewDropTargetPaddingBottom - + expandedViewDropTargetHeight, + expandedViewDropTargetWidth + expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - expandedViewDropTargetPaddingBottom + ) + + private val expandedViewDropTargetRight: Rect + get() = + Rect( + windowBounds.right - + expandedViewDropTargetPaddingHorizontal - + expandedViewDropTargetWidth, + windowBounds.bottom - + expandedViewDropTargetPaddingBottom - + expandedViewDropTargetHeight, + windowBounds.right - expandedViewDropTargetPaddingHorizontal, + windowBounds.bottom - expandedViewDropTargetPaddingBottom + ) + + init { + onConfigurationUpdated() + } + + /** Updates all dimensions after a configuration change. */ + fun onConfigurationUpdated() { + dismissDragZoneSize = + if (deviceConfig.isSmallTablet) { + context.resolveDimension(R.dimen.drag_zone_dismiss_fold) + } else { + context.resolveDimension(R.dimen.drag_zone_dismiss_tablet) + } + bubbleDragZoneTabletSize = context.resolveDimension(R.dimen.drag_zone_bubble_tablet) + bubbleDragZoneFoldableSize = context.resolveDimension(R.dimen.drag_zone_bubble_fold) + fullScreenDragZoneWidth = context.resolveDimension(R.dimen.drag_zone_full_screen_width) + fullScreenDragZoneHeight = context.resolveDimension(R.dimen.drag_zone_full_screen_height) + desktopWindowDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_width) + desktopWindowDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_height) + desktopWindowFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_width) + desktopWindowFromExpandedViewDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_desktop_window_expanded_view_height) + splitFromBubbleDragZoneHeight = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_height) + splitFromBubbleDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_split_from_bubble_width) + hSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_h_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneWidth = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_width) + vSplitFromExpandedViewDragZoneHeightTablet = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_tablet) + vSplitFromExpandedViewDragZoneHeightFoldTall = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_tall) + vSplitFromExpandedViewDragZoneHeightFoldShort = + context.resolveDimension(R.dimen.drag_zone_v_split_from_expanded_view_height_fold_short) + fullScreenDropTargetPadding = + context.resolveDimension(R.dimen.drop_target_full_screen_padding) + desktopWindowDropTargetPaddingSmall = + context.resolveDimension(R.dimen.drop_target_desktop_window_padding_small) + desktopWindowDropTargetPaddingLarge = + context.resolveDimension(R.dimen.drop_target_desktop_window_padding_large) + + // TODO b/393172431: Use the shared xml resources once we can easily access them from + // launcher + expandedViewDropTargetWidth = 364.dpToPx() + expandedViewDropTargetHeight = 578.dpToPx() + expandedViewDropTargetPaddingBottom = 108.dpToPx() + expandedViewDropTargetPaddingHorizontal = 24.dpToPx() + } + + private fun Context.resolveDimension(@DimenRes dimension: Int) = + resources.getDimensionPixelSize(dimension) + + private fun Int.dpToPx() = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + this.toFloat(), + context.resources.displayMetrics + ) + .toInt() + + /** + * Creates the list of drag zones for the dragged object. + * + * Drag zones may have overlap, but the list is sorted by priority where the first drag zone has + * the highest priority so it should be checked first. + */ + fun createSortedDragZones(draggedObject: DraggedObject): List<DragZone> { + val dragZones = mutableListOf<DragZone>() + when (draggedObject) { + is DraggedObject.BubbleBar -> { + dragZones.add(createDismissDragZone()) + dragZones.addAll(createBubbleHalfScreenDragZones()) + } + is DraggedObject.Bubble -> { + dragZones.add(createDismissDragZone()) + dragZones.addAll(createBubbleCornerDragZones()) + dragZones.add(createFullScreenDragZone()) + if (shouldShowDesktopWindowDragZones()) { + dragZones.add(createDesktopWindowDragZoneForBubble()) + } + dragZones.addAll(createSplitScreenDragZonesForBubble()) + } + is DraggedObject.ExpandedView -> { + dragZones.add(createDismissDragZone()) + dragZones.add(createFullScreenDragZone()) + if (shouldShowDesktopWindowDragZones()) { + dragZones.add(createDesktopWindowDragZoneForExpandedView()) + } + if (deviceConfig.isSmallTablet) { + dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnFoldable()) + } else { + dragZones.addAll(createSplitScreenDragZonesForExpandedViewOnTablet()) + } + dragZones.addAll(createBubbleHalfScreenDragZones()) + } + } + return dragZones + } + + private fun createDismissDragZone(): DragZone { + return DragZone.Dismiss( + bounds = + Rect( + windowBounds.right / 2 - dismissDragZoneSize / 2, + windowBounds.bottom - dismissDragZoneSize, + windowBounds.right / 2 + dismissDragZoneSize / 2, + windowBounds.bottom + ) + ) + } + + private fun createBubbleCornerDragZones(): List<DragZone> { + val dragZoneSize = + if (deviceConfig.isSmallTablet) { + bubbleDragZoneFoldableSize + } else { + bubbleDragZoneTabletSize + } + return listOf( + DragZone.Bubble.Left( + bounds = + Rect(0, windowBounds.bottom - dragZoneSize, dragZoneSize, windowBounds.bottom), + dropTarget = expandedViewDropTargetLeft, + ), + DragZone.Bubble.Right( + bounds = + Rect( + windowBounds.right - dragZoneSize, + windowBounds.bottom - dragZoneSize, + windowBounds.right, + windowBounds.bottom, + ), + dropTarget = expandedViewDropTargetRight, + ) + ) + } + + private fun createBubbleHalfScreenDragZones(): List<DragZone> { + return listOf( + DragZone.Bubble.Left( + bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), + dropTarget = expandedViewDropTargetLeft, + ), + DragZone.Bubble.Right( + bounds = + Rect( + windowBounds.right / 2, + 0, + windowBounds.right, + windowBounds.bottom, + ), + dropTarget = expandedViewDropTargetRight, + ) + ) + } + + private fun createFullScreenDragZone(): DragZone { + return DragZone.FullScreen( + bounds = + Rect( + windowBounds.right / 2 - fullScreenDragZoneWidth / 2, + 0, + windowBounds.right / 2 + fullScreenDragZoneWidth / 2, + fullScreenDragZoneHeight + ), + dropTarget = fullScreenDropTarget + ) + } + + private fun shouldShowDesktopWindowDragZones() = + !deviceConfig.isSmallTablet && desktopWindowModeChecker.isSupported() + + private fun createDesktopWindowDragZoneForBubble(): DragZone { + return DragZone.DesktopWindow( + bounds = + if (deviceConfig.isLandscape) { + Rect( + windowBounds.right / 2 - desktopWindowDragZoneWidth / 2, + windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2, + windowBounds.right / 2 + desktopWindowDragZoneWidth / 2, + windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2 + ) + } else { + Rect( + 0, + windowBounds.bottom / 2 - desktopWindowDragZoneHeight / 2, + windowBounds.right, + windowBounds.bottom / 2 + desktopWindowDragZoneHeight / 2 + ) + }, + dropTarget = desktopWindowDropTarget + ) + } + + private fun createDesktopWindowDragZoneForExpandedView(): DragZone { + return DragZone.DesktopWindow( + bounds = + Rect( + windowBounds.right / 2 - desktopWindowFromExpandedViewDragZoneWidth / 2, + windowBounds.bottom / 2 - desktopWindowFromExpandedViewDragZoneHeight / 2, + windowBounds.right / 2 + desktopWindowFromExpandedViewDragZoneWidth / 2, + windowBounds.bottom / 2 + desktopWindowFromExpandedViewDragZoneHeight / 2 + ), + dropTarget = desktopWindowDropTarget + ) + } + + private fun createSplitScreenDragZonesForBubble(): List<DragZone> { + // for foldables in landscape mode or tables in portrait modes we have vertical split drag + // zones. otherwise we have horizontal split drag zones. + val isVerticalSplit = deviceConfig.isSmallTablet == deviceConfig.isLandscape + return if (isVerticalSplit) { + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Top( + bounds = Rect(0, 0, windowBounds.right, windowBounds.bottom / 2), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom / 2, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_90_10 -> { + listOf( + DragZone.Split.Top( + bounds = + Rect( + 0, + 0, + windowBounds.right, + windowBounds.bottom - splitFromBubbleDragZoneHeight + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom - splitFromBubbleDragZoneHeight, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + SplitScreenMode.SPLIT_10_90 -> { + listOf( + DragZone.Split.Top( + bounds = Rect(0, 0, windowBounds.right, splitFromBubbleDragZoneHeight), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + splitFromBubbleDragZoneHeight, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } + } else { + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Left( + bounds = Rect(0, 0, windowBounds.right / 2, windowBounds.bottom), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right / 2, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_90_10 -> + listOf( + DragZone.Split.Left( + bounds = + Rect( + 0, + 0, + windowBounds.right - splitFromBubbleDragZoneWidth, + windowBounds.bottom + ), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right - splitFromBubbleDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + SplitScreenMode.SPLIT_10_90 -> + listOf( + DragZone.Split.Left( + bounds = Rect(0, 0, splitFromBubbleDragZoneWidth, windowBounds.bottom), + ), + DragZone.Split.Right( + bounds = + Rect( + splitFromBubbleDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } + } + + private fun createSplitScreenDragZonesForExpandedViewOnTablet(): List<DragZone> { + return if (deviceConfig.isLandscape) { + createHorizontalSplitDragZonesForExpandedView() + } else { + // for tablets in portrait mode, split drag zones appear below the full screen drag zone + // for the top split zone, and above the dismiss zone. Both are horizontally centered. + val splitZoneLeft = windowBounds.right / 2 - vSplitFromExpandedViewDragZoneWidth / 2 + val splitZoneRight = splitZoneLeft + vSplitFromExpandedViewDragZoneWidth + val bottomSplitZoneBottom = windowBounds.bottom - dismissDragZoneSize + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneRight, + fullScreenDragZoneHeight + vSplitFromExpandedViewDragZoneHeightTablet + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + splitZoneLeft, + bottomSplitZoneBottom - vSplitFromExpandedViewDragZoneHeightTablet, + splitZoneRight, + bottomSplitZoneBottom + ), + ) + ) + } + } + + private fun createSplitScreenDragZonesForExpandedViewOnFoldable(): List<DragZone> { + return if (deviceConfig.isLandscape) { + // vertical split drag zones are aligned with the full screen drag zone width + val splitZoneLeft = windowBounds.right / 2 - fullScreenDragZoneWidth / 2 + when (splitScreenModeChecker.getSplitScreenMode()) { + SplitScreenMode.SPLIT_50_50, + SplitScreenMode.NONE -> + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneLeft + fullScreenDragZoneWidth, + fullScreenDragZoneHeight + + vSplitFromExpandedViewDragZoneHeightFoldTall + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + splitZoneLeft, + windowBounds.bottom / 2, + splitZoneLeft + fullScreenDragZoneWidth, + windowBounds.bottom / 2 + + vSplitFromExpandedViewDragZoneHeightFoldTall + ), + ) + ) + SplitScreenMode.SPLIT_10_90 -> + listOf( + DragZone.Split.Top( + bounds = + Rect( + 0, + 0, + windowBounds.right, + vSplitFromExpandedViewDragZoneHeightFoldShort + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + splitZoneLeft, + vSplitFromExpandedViewDragZoneHeightFoldShort, + splitZoneLeft + fullScreenDragZoneWidth, + vSplitFromExpandedViewDragZoneHeightFoldShort + + vSplitFromExpandedViewDragZoneHeightFoldTall + ), + ) + ) + SplitScreenMode.SPLIT_90_10 -> + listOf( + DragZone.Split.Top( + bounds = + Rect( + splitZoneLeft, + fullScreenDragZoneHeight, + splitZoneLeft + fullScreenDragZoneWidth, + fullScreenDragZoneHeight + + vSplitFromExpandedViewDragZoneHeightFoldTall + ), + ), + DragZone.Split.Bottom( + bounds = + Rect( + 0, + windowBounds.bottom - + vSplitFromExpandedViewDragZoneHeightFoldShort, + windowBounds.right, + windowBounds.bottom + ), + ) + ) + } + } else { + // horizontal split drag zones + createHorizontalSplitDragZonesForExpandedView() + } + } + + private fun createHorizontalSplitDragZonesForExpandedView(): List<DragZone> { + // horizontal split drag zones for expanded view appear on the edges of the screen from the + // top down until the dismiss drag zone height + return listOf( + DragZone.Split.Left( + bounds = + Rect( + 0, + 0, + hSplitFromExpandedViewDragZoneWidth, + windowBounds.bottom - dismissDragZoneSize + ), + ), + DragZone.Split.Right( + bounds = + Rect( + windowBounds.right - hSplitFromExpandedViewDragZoneWidth, + 0, + windowBounds.right, + windowBounds.bottom - dismissDragZoneSize + ), + ) + ) + } + + /** Checks the current split screen mode. */ + fun interface SplitScreenModeChecker { + enum class SplitScreenMode { + NONE, + SPLIT_50_50, + SPLIT_10_90, + SPLIT_90_10 + } + + fun getSplitScreenMode(): SplitScreenMode + } + + /** Checks if desktop window mode is supported. */ + fun interface DesktopWindowModeChecker { + fun isSupported(): Boolean + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt new file mode 100644 index 000000000000..028622798f34 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DraggedObject.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +/** A Bubble object being dragged. */ +sealed interface DraggedObject { + /** The initial location of the object at the start of the drag gesture. */ + val initialLocation: BubbleBarLocation + + data class Bubble(override val initialLocation: BubbleBarLocation) : DraggedObject + data class BubbleBar(override val initialLocation: BubbleBarLocation) : DraggedObject + data class ExpandedView(override val initialLocation: BubbleBarLocation) : DraggedObject +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt new file mode 100644 index 000000000000..2dc183f3f707 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/bubbles/DropTargetManager.kt @@ -0,0 +1,174 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.content.Context +import android.graphics.Rect +import android.view.View +import android.widget.FrameLayout +import androidx.core.animation.Animator +import androidx.core.animation.AnimatorListenerAdapter +import androidx.core.animation.ValueAnimator + +/** + * Manages animating drop targets in response to dragging bubble icons or bubble expanded views + * across different drag zones. + */ +class DropTargetManager( + context: Context, + private val container: FrameLayout, + private val isLayoutRtl: Boolean, + private val dragZoneChangedListener: DragZoneChangedListener, +) { + + private var state: DragState? = null + private val dropTargetView = View(context) + private var animator: ValueAnimator? = null + + private companion object { + const val ANIMATION_DURATION_MS = 250L + } + + /** Must be called when a drag gesture is starting. */ + fun onDragStarted(draggedObject: DraggedObject, dragZones: List<DragZone>) { + val state = DragState(dragZones, draggedObject) + dragZoneChangedListener.onInitialDragZoneSet(state.initialDragZone) + this.state = state + animator?.cancel() + setupDropTarget() + } + + private fun setupDropTarget() { + if (dropTargetView.parent != null) container.removeView(dropTargetView) + container.addView(dropTargetView, 0) + // TODO b/393173014: set elevation and background + dropTargetView.alpha = 0f + dropTargetView.scaleX = 1f + dropTargetView.scaleY = 1f + dropTargetView.translationX = 0f + dropTargetView.translationY = 0f + // the drop target is added with a width and height of 1 pixel. when it gets resized, we use + // set its scale to the width and height of the bounds it should have to avoid layout passes + dropTargetView.layoutParams = FrameLayout.LayoutParams(/* width= */ 1, /* height= */ 1) + } + + /** Called when the user drags to a new location. */ + fun onDragUpdated(x: Int, y: Int) { + val state = state ?: return + val oldDragZone = state.currentDragZone + val newDragZone = state.getMatchingDragZone(x = x, y = y) + state.currentDragZone = newDragZone + if (oldDragZone != newDragZone) { + dragZoneChangedListener.onDragZoneChanged(from = oldDragZone, to = newDragZone) + updateDropTarget() + } + } + + /** Called when the drag ended. */ + fun onDragEnded() { + startFadeAnimation(from = dropTargetView.alpha, to = 0f) { + container.removeView(dropTargetView) + } + state = null + } + + private fun updateDropTarget() { + val currentDragZone = state?.currentDragZone ?: return + val dropTargetBounds = currentDragZone.dropTarget + when { + dropTargetBounds == null -> startFadeAnimation(from = dropTargetView.alpha, to = 0f) + dropTargetView.alpha == 0f -> { + dropTargetView.translationX = dropTargetBounds.exactCenterX() + dropTargetView.translationY = dropTargetBounds.exactCenterY() + dropTargetView.scaleX = dropTargetBounds.width().toFloat() + dropTargetView.scaleY = dropTargetBounds.height().toFloat() + startFadeAnimation(from = 0f, to = 1f) + } + else -> startMorphAnimation(dropTargetBounds) + } + } + + private fun startFadeAnimation(from: Float, to: Float, onEnd: (() -> Unit)? = null) { + animator?.cancel() + val animator = ValueAnimator.ofFloat(from, to).setDuration(ANIMATION_DURATION_MS) + animator.addUpdateListener { _ -> dropTargetView.alpha = animator.animatedValue as Float } + if (onEnd != null) { + animator.doOnEnd(onEnd) + } + this.animator = animator + animator.start() + } + + private fun startMorphAnimation(bounds: Rect) { + animator?.cancel() + val startAlpha = dropTargetView.alpha + val startTx = dropTargetView.translationX + val startTy = dropTargetView.translationY + val startScaleX = dropTargetView.scaleX + val startScaleY = dropTargetView.scaleY + val animator = ValueAnimator.ofFloat(0f, 1f).setDuration(ANIMATION_DURATION_MS) + animator.addUpdateListener { _ -> + val fraction = animator.animatedValue as Float + dropTargetView.alpha = startAlpha + (1 - startAlpha) * fraction + dropTargetView.translationX = startTx + (bounds.exactCenterX() - startTx) * fraction + dropTargetView.translationY = startTy + (bounds.exactCenterY() - startTy) * fraction + dropTargetView.scaleX = + startScaleX + (bounds.width().toFloat() - startScaleX) * fraction + dropTargetView.scaleY = + startScaleY + (bounds.height().toFloat() - startScaleY) * fraction + } + this.animator = animator + animator.start() + } + + /** Stores the current drag state. */ + private inner class DragState( + private val dragZones: List<DragZone>, + draggedObject: DraggedObject + ) { + val initialDragZone = + if (draggedObject.initialLocation.isOnLeft(isLayoutRtl)) { + dragZones.filterIsInstance<DragZone.Bubble.Left>().first() + } else { + dragZones.filterIsInstance<DragZone.Bubble.Right>().first() + } + var currentDragZone: DragZone = initialDragZone + + fun getMatchingDragZone(x: Int, y: Int): DragZone { + return dragZones.firstOrNull { it.contains(x, y) } ?: currentDragZone + } + } + + /** An interface to be notified when drag zones change. */ + interface DragZoneChangedListener { + /** An initial drag zone was set. Called when a drag starts. */ + fun onInitialDragZoneSet(dragZone: DragZone) + + /** Called when the object was dragged to a different drag zone. */ + fun onDragZoneChanged(from: DragZone, to: DragZone) + } + + private fun Animator.doOnEnd(onEnd: () -> Unit) { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + onEnd() + } + } + ) + } +} diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt index 0ea3c2a80fb4..14338a49ee2f 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicy.kt @@ -18,6 +18,10 @@ package com.android.wm.shell.shared.desktopmode import android.app.TaskInfo import android.content.Context +import android.content.pm.ActivityInfo +import android.content.pm.ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED +import android.content.pm.ActivityInfo.OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION +import android.content.pm.ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS import android.window.DesktopModeFlags import com.android.internal.R @@ -25,15 +29,17 @@ import com.android.internal.R * Class to decide whether to apply app compat policies in desktop mode. */ // TODO(b/347289970): Consider replacing with API -class DesktopModeCompatPolicy(context: Context) { +class DesktopModeCompatPolicy(private val context: Context) { private val systemUiPackage: String = context.resources.getString(R.string.config_systemUi) + private val defaultHomePackage: String? + get() = context.getPackageManager().getHomeActivities(ArrayList())?.packageName /** * If the top activity should be exempt from desktop windowing and forced back to fullscreen. - * Currently includes all system ui activities and modal dialogs. However if the top activity is - * not being displayed, regardless of its configuration, we will not exempt it as to remain in - * the desktop windowing environment. + * Currently includes all system ui, default home and transparent stack activities. However if + * the top activity is not being displayed, regardless of its configuration, we will not exempt + * it as to remain in the desktop windowing environment. */ fun isTopActivityExemptFromDesktopWindowing(task: TaskInfo) = isTopActivityExemptFromDesktopWindowing(task.baseActivity?.packageName, @@ -43,10 +49,29 @@ class DesktopModeCompatPolicy(context: Context) { numActivities: Int, isTopActivityNoDisplay: Boolean, isActivityStackTransparent: Boolean) = DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue && ((isSystemUiTask(packageName) + || isPartOfDefaultHomePackageOrNoHomeAvailable(packageName) || isTransparentTask(isActivityStackTransparent, numActivities)) && !isTopActivityNoDisplay) /** + * Whether the caption insets should be excluded from configuration for system to handle. + * + * The treatment is enabled when all the of the following is true: + * * Any flags to forcibly consume caption insets are enabled. + * * Top activity have configuration coupled with insets. + * * Task is not resizeable or [ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS] + * is enabled. + */ + fun shouldExcludeCaptionFromAppBounds(taskInfo: TaskInfo): Boolean = + DesktopModeFlags.EXCLUDE_CAPTION_FROM_APP_BOUNDS.isTrue + && isAnyForceConsumptionFlagsEnabled() + && taskInfo.topActivityInfo?.let { + isInsetsCoupledWithConfiguration(it) && (!taskInfo.isResizeable || it.isChangeEnabled( + OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS + )) + } ?: false + + /** * Returns true if all activities in a tasks stack are transparent. If there are no activities * will return false. */ @@ -57,4 +82,19 @@ class DesktopModeCompatPolicy(context: Context) { isActivityStackTransparent && numActivities > 0 private fun isSystemUiTask(packageName: String?) = packageName == systemUiPackage + + /** + * Returns true if the tasks base activity is part of the default home package, or there is + * currently no default home package available. + */ + private fun isPartOfDefaultHomePackageOrNoHomeAvailable(packageName: String?) = + defaultHomePackage == null || (packageName != null && packageName == defaultHomePackage) + + private fun isAnyForceConsumptionFlagsEnabled(): Boolean = + DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue + || DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue + + private fun isInsetsCoupledWithConfiguration(info: ActivityInfo): Boolean = + !(info.isChangeEnabled(OVERRIDE_ENABLE_INSETS_DECOUPLED_CONFIGURATION) + || info.isChangeEnabled(INSETS_DECOUPLED_CONFIGURATION_ENFORCED)) } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java index 1ee71ca78815..00c446c3da60 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatus.java @@ -18,6 +18,8 @@ package com.android.wm.shell.shared.desktopmode; import static android.hardware.display.DisplayManager.DISPLAY_CATEGORY_ALL_INCLUDING_DISABLED; +import static com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper.enableBubbleToFullscreen; + import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; @@ -25,6 +27,7 @@ import android.hardware.display.DisplayManager; import android.os.SystemProperties; import android.view.Display; import android.view.WindowManager; +import android.window.DesktopExperienceFlags; import android.window.DesktopModeFlags; import com.android.internal.R; @@ -187,7 +190,7 @@ public class DesktopModeStatus { * there should be no pooling. */ public static int getWindowDecorScvhPoolSize(@NonNull Context context) { - if (!Flags.enableDesktopWindowingScvhCacheBugFix()) return 0; + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_SCVH_CACHE.isTrue()) return 0; final int maxTaskLimit = getMaxTaskLimit(context); if (maxTaskLimit > 0) { return maxTaskLimit; @@ -206,37 +209,62 @@ public class DesktopModeStatus { /** * Return {@code true} if the current device supports desktop mode. */ - @VisibleForTesting - public static boolean isDesktopModeSupported(@NonNull Context context) { + private static boolean isDesktopModeSupported(@NonNull Context context) { return context.getResources().getBoolean(R.bool.config_isDesktopModeSupported); } /** + * Return {@code true} if the current device supports the developer option for desktop mode. + */ + private static boolean isDesktopModeDevOptionSupported(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_isDesktopModeDevOptionSupported); + } + + /** + * Return {@code true} if the current device can host desktop sessions on its internal display. + */ + public static boolean canInternalDisplayHostDesktops(@NonNull Context context) { + return context.getResources().getBoolean(R.bool.config_canInternalDisplayHostDesktops); + } + + + /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopModeDevOption(@NonNull Context context) { - return isDeviceEligibleForDesktopMode(context) && Flags.showDesktopWindowingDevOption(); + return isDeviceEligibleForDesktopModeDevOption(context) + && Flags.showDesktopWindowingDevOption(); } /** * Return {@code true} if desktop mode dev option should be shown on current device */ public static boolean canShowDesktopExperienceDevOption(@NonNull Context context) { - return Flags.showDesktopExperienceDevOption(); + return Flags.showDesktopExperienceDevOption() + && isDeviceEligibleForDesktopMode(context); } /** Returns if desktop mode dev option should be enabled if there is no user override. */ - public static boolean shouldDevOptionBeEnabledByDefault() { - return Flags.enableDesktopWindowingMode(); + public static boolean shouldDevOptionBeEnabledByDefault(Context context) { + return isDeviceEligibleForDesktopMode(context) + && Flags.enableDesktopWindowingMode(); } /** * Return {@code true} if desktop mode is enabled and can be entered on the current device. */ public static boolean canEnterDesktopMode(@NonNull Context context) { - if (!isDeviceEligibleForDesktopMode(context)) return false; + return (isDeviceEligibleForDesktopMode(context) + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue()) + || isDesktopModeEnabledByDevOption(context); + } - return DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODE.isTrue(); + /** + * Check if Desktop mode should be enabled because the dev option is shown and enabled. + */ + private static boolean isDesktopModeEnabledByDevOption(@NonNull Context context) { + return DesktopModeFlags.isDesktopModeForcedEnabled() + && canShowDesktopModeDevOption(context); } /** @@ -244,7 +272,7 @@ public class DesktopModeStatus { * frontend implementations). */ public static boolean enableMultipleDesktops(@NonNull Context context) { - return Flags.enableMultipleDesktopsBackend() + return DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue() && Flags.enableMultipleDesktopsFrontend() && canEnterDesktopMode(context); } @@ -254,7 +282,8 @@ public class DesktopModeStatus { * necessarily enabling desktop mode */ public static boolean overridesShowAppHandle(@NonNull Context context) { - return Flags.showAppHandleLargeScreens() && deviceHasLargeScreen(context); + return (Flags.showAppHandleLargeScreens() || enableBubbleToFullscreen()) + && deviceHasLargeScreen(context); } /** @@ -295,10 +324,34 @@ public class DesktopModeStatus { } /** - * Return {@code true} if desktop mode is unrestricted and is supported in the device. + * Return {@code true} if desktop mode is unrestricted and is supported on the device. */ public static boolean isDeviceEligibleForDesktopMode(@NonNull Context context) { - return !enforceDeviceRestrictions() || isDesktopModeSupported(context); + if (!enforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + final boolean desktopModeSupportedByDevOptions = + Flags.enableDesktopModeThroughDevOption() + && isDesktopModeDevOptionSupported(context); + return desktopModeSupported || desktopModeSupportedByDevOptions; + } + + /** + * Return {@code true} if the developer option for desktop mode is unrestricted and is supported + * in the device. + * + * Note that, if {@link #isDeviceEligibleForDesktopMode(Context)} is true, then + * {@link #isDeviceEligibleForDesktopModeDevOption(Context)} is also true. + */ + private static boolean isDeviceEligibleForDesktopModeDevOption(@NonNull Context context) { + if (!enforceDeviceRestrictions()) { + return true; + } + final boolean desktopModeSupported = isDesktopModeSupported(context) + && canInternalDisplayHostDesktops(context); + return desktopModeSupported || isDesktopModeDevOptionSupported(context); } /** diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java index 4127adc1f901..12938db07ece 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/draganddrop/DragAndDropConstants.java @@ -24,4 +24,9 @@ public class DragAndDropConstants { * ignore drag events. */ public static final String EXTRA_DISALLOW_HIT_REGION = "DISALLOW_HIT_REGION"; + + /** + * An Intent extra that Launcher can use to specify the {@link android.content.pm.ShortcutInfo} + */ + public static final String EXTRA_SHORTCUT_INFO = "EXTRA_SHORTCUT_INFO"; } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java index 62ca5c687a2a..b1bc6e81e1bd 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/pip/PipContentOverlay.java @@ -28,6 +28,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.HardwareBuffer; import android.util.TypedValue; import android.view.SurfaceControl; import android.window.TaskSnapshot; @@ -225,12 +226,17 @@ public abstract class PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); - tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setBuffer(mLeash, buffer); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); + // Cleanup the bitmap and buffer after setting up the leash + mBitmap.recycle(); + mBitmap = null; + buffer.close(); } @Override @@ -253,14 +259,6 @@ public abstract class PipContentOverlay { .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } - @Override - public void detach(SurfaceControl.Transaction tx) { - super.detach(tx); - if (mBitmap != null && !mBitmap.isRecycled()) { - mBitmap.recycle(); - } - } - private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); @@ -282,7 +280,9 @@ public abstract class PipContentOverlay { mOverlayHalfSize + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); + Bitmap oldBitmap = mBitmap; mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + oldBitmap.recycle(); } } } diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java index b48296f5f76a..759e711100c3 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/split/SplitScreenConstants.java @@ -262,6 +262,7 @@ public class SplitScreenConstants { /** Flag applied to a transition change to identify it as a divider bar for animation. */ public static final int FLAG_IS_DIVIDER_BAR = TransitionUtil.FLAG_IS_DIVIDER_BAR; + public static final int FLAG_IS_DIM_LAYER = TransitionUtil.FLAG_IS_DIM_LAYER; public static final String splitPositionToString(@SplitPosition int pos) { switch (pos) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java index b4ef9f0fc2ac..55ed5fa4b56f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/activityembedding/ActivityEmbeddingController.java @@ -168,7 +168,8 @@ public class ActivityEmbeddingController implements Transitions.TransitionHandle @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { mAnimationRunner.cancelAnimationFromMerge(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java index 8dabd54a01ff..d1c7f7d7dcad 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/BackAnimationController.java @@ -1463,7 +1463,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mClosePrepareTransition == transition) { mClosePrepareTransition = null; @@ -1476,7 +1478,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (info.getType() == TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION && !mCloseTransitionRequested && info.getChanges().isEmpty() && mApps == null) { finishCallback.onTransitionFinished(null); - t.apply(); + startT.apply(); applyFinishOpenTransition(); return; } @@ -1489,7 +1491,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } // Handle the commit transition if this handler is running the open transition. finishCallback.onTransitionFinished(null); - t.apply(); + startT.apply(); if (mCloseTransitionRequested) { if (mApps == null || mApps.length == 0) { // animation was done diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java index c1dadada505a..5bd8d86f1144 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java @@ -357,9 +357,9 @@ public class BadgedImageView extends ConstraintLayout { void showBadge() { Bitmap appBadgeBitmap = mBubble.getAppBadge(); - final boolean isAppLaunchIntent = (mBubble instanceof Bubble) - && ((Bubble) mBubble).isAppLaunchIntent(); - if (appBadgeBitmap == null || isAppLaunchIntent) { + final boolean showAppBadge = (mBubble instanceof Bubble) + && ((Bubble) mBubble).showAppBadge(); + if (appBadgeBitmap == null || !showAppBadge) { mAppIcon.setVisibility(GONE); return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java index c40a276cb7bd..d9489287ff42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java @@ -54,9 +54,9 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; +import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; -import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleInfo; import com.android.wm.shell.shared.bubbles.ParcelableFlyoutMessage; import com.android.wm.shell.taskview.TaskView; @@ -72,11 +72,25 @@ import java.util.concurrent.Executor; public class Bubble implements BubbleViewProvider { private static final String TAG = "Bubble"; - /** A string suffix used in app bubbles' {@link #mKey}. */ + /** A string prefix used in app bubbles' {@link #mKey}. */ public static final String KEY_APP_BUBBLE = "key_app_bubble"; - /** Whether the bubble is an app bubble. */ - private final boolean mIsAppBubble; + /** A string prefix used in note bubbles' {@link #mKey}. */ + public static final String KEY_NOTE_BUBBLE = "key_note_bubble"; + + /** The possible types a bubble may be. */ + public enum BubbleType { + /** Chat is from a notification. */ + TYPE_CHAT, + /** Notes are from the note taking API. */ + TYPE_NOTE, + /** Shortcuts from bubble anything, based on {@link ShortcutInfo}. */ + TYPE_SHORTCUT, + /** Apps are from bubble anything. */ + TYPE_APP, + } + + private final BubbleType mType; private final String mKey; @Nullable @@ -186,10 +200,10 @@ public class Bubble implements BubbleViewProvider { * that bubble being added back to the stack anyways. */ @Nullable - private PendingIntent mIntent; - private boolean mIntentActive; + private PendingIntent mPendingIntent; + private boolean mPendingIntentActive; @Nullable - private PendingIntent.CancelListener mIntentCancelListener; + private PendingIntent.CancelListener mPendingIntentCancelListener; /** * Sent when the bubble & notification are no longer visible to the user (i.e. no @@ -199,12 +213,10 @@ public class Bubble implements BubbleViewProvider { private PendingIntent mDeleteIntent; /** - * Used only for a special bubble in the stack that has {@link #mIsAppBubble} set to true. - * There can only be one of these bubbles in the stack and this intent will be populated for - * that bubble. + * Used for app & note bubbles. */ @Nullable - private Intent mAppIntent; + private Intent mIntent; /** * Set while preparing a transition for animation. Several steps are needed before animation @@ -217,7 +229,6 @@ public class Bubble implements BubbleViewProvider { * Create a bubble with limited information based on given {@link ShortcutInfo}. * Note: Currently this is only being used when the bubble is persisted to disk. */ - @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final String key, @NonNull final ShortcutInfo shortcutInfo, final int desiredHeight, final int desiredHeightResId, @Nullable final String title, int taskId, @Nullable final String locus, boolean isDismissable, @@ -244,14 +255,15 @@ public class Bubble implements BubbleViewProvider { mBgExecutor = bgExecutor; mTaskId = taskId; mBubbleMetadataFlagListener = listener; - mIsAppBubble = false; + // TODO (b/394085999) read/write type to xml + mType = BubbleType.TYPE_CHAT; } private Bubble( Intent intent, UserHandle user, @Nullable Icon icon, - boolean isAppBubble, + BubbleType type, String key, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { @@ -260,17 +272,40 @@ public class Bubble implements BubbleViewProvider { mFlags = 0; mUser = user; mIcon = icon; - mIsAppBubble = isAppBubble; + mType = type; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; - mAppIntent = intent; + mIntent = intent; mDesiredHeight = Integer.MAX_VALUE; mPackageName = intent.getPackage(); } + private Bubble( + PendingIntent intent, + UserHandle user, + String key, + @ShellMainThread Executor mainExecutor, + @ShellBackgroundThread Executor bgExecutor) { + mGroupKey = null; + mLocusId = null; + mFlags = 0; + mUser = user; + mIcon = null; + mType = BubbleType.TYPE_APP; + mKey = key; + mShowBubbleUpdateDot = false; + mMainExecutor = mainExecutor; + mBgExecutor = bgExecutor; + mTaskId = INVALID_TASK_ID; + mPendingIntent = intent; + mIntent = null; + mDesiredHeight = Integer.MAX_VALUE; + mPackageName = ComponentUtils.getPackageName(intent); + } + private Bubble(ShortcutInfo info, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { mGroupKey = null; @@ -278,13 +313,13 @@ public class Bubble implements BubbleViewProvider { mFlags = 0; mUser = info.getUserHandle(); mIcon = info.getIcon(); - mIsAppBubble = false; + mType = BubbleType.TYPE_SHORTCUT; mKey = getBubbleKeyForShortcut(info); mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = INVALID_TASK_ID; - mAppIntent = null; + mIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = info.getPackage(); mShortcutInfo = info; @@ -302,17 +337,36 @@ public class Bubble implements BubbleViewProvider { mFlags = 0; mUser = user; mIcon = icon; - mIsAppBubble = true; + mType = BubbleType.TYPE_APP; mKey = key; mShowBubbleUpdateDot = false; mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mTaskId = task.taskId; - mAppIntent = null; + mIntent = null; mDesiredHeight = Integer.MAX_VALUE; mPackageName = task.baseActivity.getPackageName(); } + /** Creates a note taking bubble. */ + public static Bubble createNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(intent, + user, + icon, + BubbleType.TYPE_NOTE, + getNoteBubbleKeyForApp(intent.getPackage(), user), + mainExecutor, bgExecutor); + } + + /** Creates an app bubble. */ + public static Bubble createAppBubble(PendingIntent intent, UserHandle user, + @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { + return new Bubble(intent, + user, + /* key= */ getAppBubbleKeyForApp(ComponentUtils.getPackageName(intent), user), + mainExecutor, bgExecutor); + } /** Creates an app bubble. */ public static Bubble createAppBubble(Intent intent, UserHandle user, @Nullable Icon icon, @@ -320,8 +374,8 @@ public class Bubble implements BubbleViewProvider { return new Bubble(intent, user, icon, - /* isAppBubble= */ true, - /* key= */ getAppBubbleKeyForApp(intent.getPackage(), user), + BubbleType.TYPE_APP, + getAppBubbleKeyForApp(intent.getPackage(), user), mainExecutor, bgExecutor); } @@ -353,6 +407,16 @@ public class Bubble implements BubbleViewProvider { } /** + * Returns the key for a note bubble from an app with package name, {@code packageName} on an + * Android user, {@code user}. + */ + public static String getNoteBubbleKeyForApp(String packageName, UserHandle user) { + Objects.requireNonNull(packageName); + Objects.requireNonNull(user); + return KEY_NOTE_BUBBLE + ":" + user.getIdentifier() + ":" + packageName; + } + + /** * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the * {@code shortcutInfo} id. */ @@ -369,19 +433,22 @@ public class Bubble implements BubbleViewProvider { return KEY_APP_BUBBLE + ":" + taskInfo.taskId; } + /** + * Creates a chat bubble based on a notification (contents of {@link BubbleEntry}. + */ @VisibleForTesting(visibility = PRIVATE) public Bubble(@NonNull final BubbleEntry entry, final Bubbles.BubbleMetadataFlagListener listener, final Bubbles.PendingIntentCanceledListener intentCancelListener, @ShellMainThread Executor mainExecutor, @ShellBackgroundThread Executor bgExecutor) { - mIsAppBubble = false; + mType = BubbleType.TYPE_CHAT; mKey = entry.getKey(); mGroupKey = entry.getGroupKey(); mLocusId = entry.getLocusId(); mBubbleMetadataFlagListener = listener; - mIntentCancelListener = intent -> { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + mPendingIntentCancelListener = intent -> { + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } mainExecutor.execute(() -> { intentCancelListener.onPendingIntentCanceled(this); @@ -404,7 +471,7 @@ public class Bubble implements BubbleViewProvider { getTitle(), getAppName(), isImportantConversation(), - !isAppLaunchIntent(), + showAppBadge(), getParcelableFlyoutMessage()); } @@ -567,10 +634,10 @@ public class Bubble implements BubbleViewProvider { if (cleanupTaskView) { cleanupTaskView(); } - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } - mIntentActive = false; + mPendingIntentActive = false; } /** Cleans-up the taskview associated with this bubble (possibly removing the task from wm) */ @@ -641,7 +708,6 @@ public class Bubble implements BubbleViewProvider { * @param expandedViewManager the bubble expanded view manager. * @param taskViewFactory the task view factory used to create the task view for the bubble. * @param positioner the bubble positioner. - * @param bubbleLogger log bubble metrics. * @param stackView the view the bubble is added to, iff showing as floating. * @param layerView the layer the bubble is added to, iff showing in the bubble bar. * @param iconFactory the icon factory used to create images for the bubble. @@ -651,7 +717,6 @@ public class Bubble implements BubbleViewProvider { BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, - BubbleLogger bubbleLogger, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory iconFactory, @@ -666,7 +731,6 @@ public class Bubble implements BubbleViewProvider { expandedViewManager, taskViewFactory, positioner, - bubbleLogger, stackView, layerView, iconFactory, @@ -688,7 +752,6 @@ public class Bubble implements BubbleViewProvider { expandedViewManager, taskViewFactory, positioner, - bubbleLogger, stackView, layerView, iconFactory, @@ -840,19 +903,19 @@ public class Bubble implements BubbleViewProvider { mDesiredHeightResId = entry.getBubbleMetadata().getDesiredHeightResId(); mIcon = entry.getBubbleMetadata().getIcon(); - if (!mIntentActive || mIntent == null) { - if (mIntent != null) { - mIntent.unregisterCancelListener(mIntentCancelListener); + if (!mPendingIntentActive || mPendingIntent == null) { + if (mPendingIntent != null) { + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); } - mIntent = entry.getBubbleMetadata().getIntent(); - if (mIntent != null) { - mIntent.registerCancelListener(mIntentCancelListener); + mPendingIntent = entry.getBubbleMetadata().getIntent(); + if (mPendingIntent != null) { + mPendingIntent.registerCancelListener(mPendingIntentCancelListener); } - } else if (mIntent != null && entry.getBubbleMetadata().getIntent() == null) { + } else if (mPendingIntent != null && entry.getBubbleMetadata().getIntent() == null) { // Was an intent bubble now it's a shortcut bubble... still unregister the listener - mIntent.unregisterCancelListener(mIntentCancelListener); - mIntentActive = false; - mIntent = null; + mPendingIntent.unregisterCancelListener(mPendingIntentCancelListener); + mPendingIntentActive = false; + mPendingIntent = null; } mDeleteIntent = entry.getBubbleMetadata().getDeleteIntent(); } @@ -892,12 +955,15 @@ public class Bubble implements BubbleViewProvider { * Sets if the intent used for this bubble is currently active (i.e. populating an * expanded view, expanded or not). */ - void setIntentActive() { - mIntentActive = true; + void setPendingIntentActive() { + mPendingIntentActive = true; } - boolean isIntentActive() { - return mIntentActive; + /** + * Whether the pending intent of this bubble is active (i.e. has been sent). + */ + boolean isPendingIntentActive() { + return mPendingIntentActive; } public InstanceId getInstanceId() { @@ -970,13 +1036,6 @@ public class Bubble implements BubbleViewProvider { } /** - * Whether this bubble is conversation - */ - public boolean isConversation() { - return null != mShortcutInfo; - } - - /** * Sets whether this notification should be suppressed in the shade. */ @VisibleForTesting @@ -1084,48 +1143,70 @@ public class Bubble implements BubbleViewProvider { } } + /** + * Returns the pending intent used to populate the bubble. + */ @Nullable - PendingIntent getBubbleIntent() { - return mIntent; + PendingIntent getPendingIntent() { + return mPendingIntent; } /** - * Whether this bubble represents the full app, i.e. the intent used is the launch - * intent for an app. In this case we don't show a badge on the icon. + * Whether an app badge should be shown for this bubble. */ - public boolean isAppLaunchIntent() { - if (BubbleAnythingFlagHelper.enableCreateAnyBubble() && mAppIntent != null) { - return mAppIntent.hasCategory("android.intent.category.LAUNCHER"); - } - return false; + public boolean showAppBadge() { + return isChat() || isShortcut() || isNote(); } + /** + * Returns the pending intent to send when a bubble is dismissed (set via the notification API). + */ @Nullable PendingIntent getDeleteIntent() { return mDeleteIntent; } + /** + * Returns the intent used to populate the bubble. + */ @Nullable - @VisibleForTesting - public Intent getAppBubbleIntent() { - return mAppIntent; + public Intent getIntent() { + return mIntent; } /** - * Sets the intent for a bubble that is an app bubble (one for which {@link #mIsAppBubble} is - * true). - * - * @param appIntent The intent to set for the app bubble. + * Sets the intent used to populate the bubble. + */ + void setIntent(Intent intent) { + mIntent = intent; + } + + /** + * Returns whether this bubble is a conversation from the notification API. + */ + public boolean isChat() { + return mType == BubbleType.TYPE_CHAT; + } + + /** + * Returns whether this bubble is a note from the note taking API. + */ + public boolean isNote() { + return mType == BubbleType.TYPE_NOTE; + } + + /** + * Returns whether this bubble is a shortcut. */ - void setAppBubbleIntent(Intent appIntent) { - mAppIntent = appIntent; + public boolean isShortcut() { + return mType == BubbleType.TYPE_SHORTCUT; } /** - * Returns whether this bubble is from an app versus a notification. + * Returns whether this bubble is an app. */ - public boolean isAppBubble() { - return mIsAppBubble; + public boolean isApp() { + return mType == BubbleType.TYPE_APP; } /** Creates open app settings intent */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java index 5cd04b11bbfd..58b46d202599 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java @@ -22,6 +22,7 @@ import static android.service.notification.NotificationListenerService.REASON_CA import static android.view.View.INVISIBLE; import static android.view.View.VISIBLE; import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS; +import static android.view.WindowManager.TRANSIT_CHANGE; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_BUBBLES; import static com.android.wm.shell.bubbles.BubbleDebugConfig.TAG_WITH_CLASS_NAME; @@ -46,10 +47,10 @@ import android.app.NotificationChannel; import android.app.PendingIntent; import android.app.TaskInfo; import android.content.BroadcastReceiver; +import android.content.ClipDescription; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; -import android.content.pm.ActivityInfo; import android.content.pm.LauncherApps; import android.content.pm.PackageManager; import android.content.pm.ShortcutInfo; @@ -94,8 +95,8 @@ import com.android.launcher3.icons.BubbleIconFactory; import com.android.wm.shell.Flags; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.bar.BubbleBarLayerView; -import com.android.wm.shell.bubbles.properties.BubbleProperties; import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -117,6 +118,9 @@ import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; +import com.android.wm.shell.shared.bubbles.DeviceConfig; +import com.android.wm.shell.shared.draganddrop.DragAndDropConstants; import com.android.wm.shell.sysui.ConfigurationChangeListener; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; @@ -149,7 +153,8 @@ import java.util.function.IntConsumer; * The controller manages addition, removal, and visible state of bubbles on screen. */ public class BubbleController implements ConfigurationChangeListener, - RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider { + RemoteCallable<BubbleController>, Bubbles.SysuiProxy.Provider, + BubbleBarDragListener { private static final String TAG = TAG_WITH_CLASS_NAME ? "BubbleController" : TAG_BUBBLES; @@ -205,9 +210,9 @@ public class BubbleController implements ConfigurationChangeListener, private final ShellController mShellController; private final ShellCommandHandler mShellCommandHandler; private final IWindowManager mWmService; - private final BubbleProperties mBubbleProperties; private final BubbleTaskViewFactory mBubbleTaskViewFactory; private final BubbleExpandedViewManager mExpandedViewManager; + private final ResizabilityChecker mResizabilityChecker; // Used to post to main UI thread private final ShellExecutor mMainExecutor; @@ -323,7 +328,7 @@ public class BubbleController implements ConfigurationChangeListener, Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService, - BubbleProperties bubbleProperties) { + ResizabilityChecker resizabilityChecker) { mContext = context; mShellCommandHandler = shellCommandHandler; mShellController = shellController; @@ -372,7 +377,6 @@ public class BubbleController implements ConfigurationChangeListener, mDragAndDropController = dragAndDropController; mSyncQueue = syncQueue; mWmService = wmService; - mBubbleProperties = bubbleProperties; shellInit.addInitCallback(this::onInit, this); mBubbleTaskViewFactory = new BubbleTaskViewFactory() { @Override @@ -385,6 +389,7 @@ public class BubbleController implements ConfigurationChangeListener, } }; mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this); + mResizabilityChecker = resizabilityChecker; } private void registerOneHandedState(OneHandedController oneHanded) { @@ -417,12 +422,12 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setListener(mBubbleDataListener); mBubbleData.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); mDataRepository.setSuppressionChangedListener(this::onBubbleMetadataFlagChanged); - mBubbleData.setPendingIntentCancelledListener(bubble -> { - if (bubble.getBubbleIntent() == null) { + if (bubble.getPendingIntent() == null) { return; } - if (bubble.isIntentActive() || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { + if (bubble.isPendingIntentActive() + || mBubbleData.hasBubbleInStackWithKey(bubble.getKey())) { bubble.setPendingIntentCanceled(); return; } @@ -590,8 +595,7 @@ public class BubbleController implements ConfigurationChangeListener, * <p>If bubble bar is supported, bubble views will be updated to switch to bar mode. */ public void registerBubbleStateListener(Bubbles.BubbleStateListener listener) { - mBubbleProperties.refresh(); - if (canShowAsBubbleBar() && listener != null) { + if (Flags.enableBubbleBar() && mBubblePositioner.isLargeScreen() && listener != null) { // Only set the listener if we can show the bubble bar. mBubbleStateListener = listener; setUpBubbleViewsForMode(); @@ -608,7 +612,6 @@ public class BubbleController implements ConfigurationChangeListener, * will be updated accordingly. */ public void unregisterBubbleStateListener() { - mBubbleProperties.refresh(); if (mBubbleStateListener != null) { mBubbleStateListener = null; setUpBubbleViewsForMode(); @@ -640,6 +643,14 @@ public class BubbleController implements ConfigurationChangeListener, mOnImeHidden = onImeHidden; mBubblePositioner.setImeVisible(false /* visible */, 0 /* height */); int displayId = mWindowManager.getDefaultDisplay().getDisplayId(); + // if the device is locked we can't use the status bar service to hide the IME because + // the IME state is frozen and it will lead to internal IME state going out of sync. This + // will make the IME visible when the device is unlocked. Instead we use + // DisplayImeController directly to make sure the state is correct when the device unlocks. + if (isDeviceLocked()) { + mDisplayImeController.hideImeForBubblesWhenLocked(displayId); + return; + } try { mBarService.hideCurrentInputMethodForBubbles(displayId); } catch (RemoteException e) { @@ -679,8 +690,20 @@ public class BubbleController implements ConfigurationChangeListener, ? mNotifEntryToExpandOnShadeUnlock.getKey() : "null")); mIsStatusBarShade = isShade; if (!mIsStatusBarShade && didChange) { - // Only collapse stack on change - collapseStack(); + if (mBubbleData.isExpanded()) { + // If the IME is visible, hide it first and then collapse. + if (mBubblePositioner.isImeVisible()) { + hideCurrentInputMethod(this::collapseStack); + } else { + collapseStack(); + } + } else if (mOnImeHidden != null) { + // a request to collapse started before we're notified that the device is locking. + // we're currently waiting for the IME to collapse, before mOnImeHidden can be + // executed, which may not happen since the screen may already be off. hide the IME + // immediately now that we're locked and pass the same runnable so it can complete. + hideCurrentInputMethod(mOnImeHidden); + } } if (mNotifEntryToExpandOnShadeUnlock != null) { @@ -746,14 +769,11 @@ public class BubbleController implements ConfigurationChangeListener, } } - /** Whether bubbles are showing in the bubble bar. */ + /** Whether bubbles would be shown with the bubble bar UI. */ public boolean isShowingAsBubbleBar() { - return canShowAsBubbleBar() && mBubbleStateListener != null; - } - - /** Whether the current configuration supports showing as bubble bar. */ - private boolean canShowAsBubbleBar() { - return mBubbleProperties.isBubbleBarEnabled() && mBubblePositioner.isLargeScreen(); + return Flags.enableBubbleBar() + && mBubblePositioner.isLargeScreen() + && mBubbleStateListener != null; } /** @@ -762,7 +782,7 @@ public class BubbleController implements ConfigurationChangeListener, */ @Nullable public BubbleBarLocation getBubbleBarLocation() { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { return mBubblePositioner.getBubbleBarLocation(); } return null; @@ -773,16 +793,22 @@ public class BubbleController implements ConfigurationChangeListener, */ public void setBubbleBarLocation(BubbleBarLocation bubbleBarLocation, @BubbleBarLocation.UpdateSource int source) { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { + updateExpandedViewForBubbleBarLocation(bubbleBarLocation, source); + BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); + bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; + mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); + } + } + + private void updateExpandedViewForBubbleBarLocation(BubbleBarLocation bubbleBarLocation, + @BubbleBarLocation.UpdateSource int source) { + if (isShowingAsBubbleBar()) { BubbleBarLocation previousLocation = mBubblePositioner.getBubbleBarLocation(); mBubblePositioner.setBubbleBarLocation(bubbleBarLocation); if (mLayerView != null && !mLayerView.isExpandedViewDragged()) { mLayerView.updateExpandedView(); } - BubbleBarUpdate bubbleBarUpdate = new BubbleBarUpdate(); - bubbleBarUpdate.bubbleBarLocation = bubbleBarLocation; - mBubbleStateListener.onBubbleStateChange(bubbleBarUpdate); - logBubbleBarLocationIfChanged(bubbleBarLocation, previousLocation, source); } } @@ -815,6 +841,14 @@ public class BubbleController implements ConfigurationChangeListener, case BubbleBarLocation.UpdateSource.A11Y_ACTION_EXP_VIEW: // TODO(b/349845968): move logging from BubbleBarLayerView to here break; + case BubbleBarLocation.UpdateSource.APP_ICON_DRAG: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP); + break; + case BubbleBarLocation.UpdateSource.DRAG_TASK: + mLogger.log(onLeft ? BubbleLogger.Event.BUBBLE_BAR_MOVED_LEFT_DRAG_TASK + : BubbleLogger.Event.BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK); + break; } } @@ -825,11 +859,71 @@ public class BubbleController implements ConfigurationChangeListener, * {@link #setBubbleBarLocation(BubbleBarLocation, int)}. */ public void animateBubbleBarLocation(BubbleBarLocation bubbleBarLocation) { - if (canShowAsBubbleBar()) { + if (isShowingAsBubbleBar()) { mBubbleStateListener.animateBubbleBarLocation(bubbleBarLocation); } } + @Override + public void onDragItemOverBubbleBarDragZone(@Nullable BubbleBarLocation bubbleBarLocation) { + if (bubbleBarLocation == null) return; + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + mBubbleStateListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + ensureBubbleViewsAndWindowCreated(); + if (mLayerView != null) { + mLayerView.showBubbleBarExtendedViewDropTarget(bubbleBarLocation); + } + } + } + + @Override + public void onItemDraggedOutsideBubbleBarDropZone() { + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + mBubbleStateListener.onItemDraggedOutsideBubbleBarDropZone(); + hideBubbleBarExpandedViewDropTarget(); + } + } + + @Override + public void onItemDroppedOverBubbleBarDragZone(@NonNull BubbleBarLocation location, + Intent itemIntent) { + hideBubbleBarExpandedViewDropTarget(); + ShortcutInfo shortcutInfo = (ShortcutInfo) itemIntent + .getExtra(DragAndDropConstants.EXTRA_SHORTCUT_INFO); + if (shortcutInfo != null) { + expandStackAndSelectBubble(shortcutInfo, location); + return; + } + UserHandle user = (UserHandle) itemIntent.getExtra(Intent.EXTRA_USER); + PendingIntent pendingIntent = (PendingIntent) itemIntent + .getExtra(ClipDescription.EXTRA_PENDING_INTENT); + if (pendingIntent != null && user != null) { + expandStackAndSelectBubble(pendingIntent, user, location); + } + } + + @Override + public Map<BubbleBarLocation, Rect> getBubbleBarDropZones(int l, int t, int r, int b) { + Map<BubbleBarLocation, Rect> result = new HashMap<>(); + if (isShowingAsBubbleBar() && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + // TODO(b/393172431) : Utilise DragZoneFactory once it is ready + final int bubbleBarDropZoneSideSize = getContext().getResources().getDimensionPixelSize( + R.dimen.bubble_bar_drop_zone_side_size); + int top = t - bubbleBarDropZoneSideSize; + result.put(BubbleBarLocation.LEFT, + new Rect(l, top, l + bubbleBarDropZoneSideSize, b)); + result.put(BubbleBarLocation.RIGHT, + new Rect(r - bubbleBarDropZoneSideSize, top, r, b)); + } + return result; + } + + private void hideBubbleBarExpandedViewDropTarget() { + if (mLayerView != null) { + mLayerView.hideBubbleBarExpandedViewDropTarget(); + } + } + /** Whether this userId belongs to the current user. */ private boolean isCurrentProfile(int userId) { return userId == UserHandle.USER_ALL @@ -868,6 +962,11 @@ public class BubbleController implements ConfigurationChangeListener, return mBubblePositioner; } + /** Provides bounds for drag zone drop targets */ + public BubbleDropTargetBoundsProvider getBubbleDropTargetBoundsProvider() { + return mBubblePositioner; + } + BubbleIconFactory getIconFactory() { return mBubbleIconFactory; } @@ -951,7 +1050,7 @@ public class BubbleController implements ConfigurationChangeListener, registerBroadcastReceiver(); if (isShowingAsBubbleBar()) { mBubbleData.getOverflow().initializeForBubbleBar( - mExpandedViewManager, mBubblePositioner, mLogger); + mExpandedViewManager, mBubblePositioner); } else { mBubbleData.getOverflow().initialize( mExpandedViewManager, mStackView, mBubblePositioner); @@ -1159,7 +1258,6 @@ public class BubbleController implements ConfigurationChangeListener, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, - mLogger, mStackView, mLayerView, mBubbleIconFactory, @@ -1171,7 +1269,6 @@ public class BubbleController implements ConfigurationChangeListener, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, - mLogger, mStackView, mLayerView, mBubbleIconFactory, @@ -1198,6 +1295,11 @@ public class BubbleController implements ConfigurationChangeListener, mContext.getResources().getDimensionPixelSize( com.android.internal.R.dimen.importance_ring_stroke_width)); mStackView.onDisplaySizeChanged(); + // TODO b/392893178: Merge the unfold and the task view transition so that we don't + // have to post a delayed runnable to the looper to update the bounds + if (mStackView.isExpanded()) { + mStackView.postDelayed(() -> mStackView.updateExpandedView(), 500); + } } if (newConfig.fontScale != mFontScale) { mFontScale = newConfig.fontScale; @@ -1431,16 +1533,24 @@ public class BubbleController implements ConfigurationChangeListener, * Expands and selects a bubble created or found via the provided shortcut info. * * @param info the shortcut info for the bubble. + * @param bubbleBarLocation optional location in case bubble bar should be repositioned. */ - public void expandStackAndSelectBubble(ShortcutInfo info) { + public void expandStackAndSelectBubble(ShortcutInfo info, + @Nullable BubbleBarLocation bubbleBarLocation) { if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + } Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info); if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); } else { b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); - inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, + updateLocation); } } @@ -1462,23 +1572,62 @@ public class BubbleController implements ConfigurationChangeListener, } /** + * Expands and selects a bubble created or found for this app. + * + * @param pendingIntent the intent for the bubble. + * @param bubbleBarLocation optional location in case bubble bar should be repositioned. + */ + public void expandStackAndSelectBubble(PendingIntent pendingIntent, UserHandle user, + @Nullable BubbleBarLocation bubbleBarLocation) { + if (!BubbleAnythingFlagHelper.enableCreateAnyBubble()) return; + BubbleBarLocation updateLocation = isShowingAsBubbleBar() ? bubbleBarLocation : null; + if (updateLocation != null) { + updateExpandedViewForBubbleBarLocation(updateLocation, + BubbleBarLocation.UpdateSource.APP_ICON_DRAG); + } + Bubble b = mBubbleData.getOrCreateBubble(pendingIntent, user); + ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - pendingIntent=%s", + pendingIntent); + if (b.isInflated()) { + mBubbleData.setSelectedBubbleAndExpandStack(b, updateLocation); + } else { + b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); + inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false, updateLocation); + } + } + + /** * Expands and selects a bubble created from a running task in a different mode. * * @param taskInfo the task. + * @param dragData optional information about the task when it is being dragged into a bubble */ - public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo) { + public void expandStackAndSelectBubble(ActivityManager.RunningTaskInfo taskInfo, + @Nullable BubbleTransitions.DragData dragData) { if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) return; Bubble b = mBubbleData.getOrCreateBubble(taskInfo); // Removes from overflow ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", taskInfo.taskId); + BubbleBarLocation location = null; + if (dragData != null) { + location = + dragData.isReleasedOnLeft() ? BubbleBarLocation.LEFT : BubbleBarLocation.RIGHT; + } if (b.isInflated()) { - mBubbleData.setSelectedBubbleAndExpandStack(b); + mBubbleData.setSelectedBubbleAndExpandStack(b, location); + if (dragData != null && dragData.getPendingWct() != null) { + mTransitions.startTransition(TRANSIT_CHANGE, + dragData.getPendingWct(), /* handler= */ null); + } } else { + if (location != null) { + setBubbleBarLocation(location, BubbleBarLocation.UpdateSource.DRAG_TASK); + } b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE); // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); mBubbleTransitions.startConvertToBubble(b, taskInfo, mExpandedViewManager, - mBubbleTaskViewFactory, mBubblePositioner, mLogger, mStackView, mLayerView, - mBubbleIconFactory, mInflateSynchronously); + mBubbleTaskViewFactory, mBubblePositioner, mStackView, mLayerView, + mBubbleIconFactory, dragData, mInflateSynchronously); } } @@ -1540,78 +1689,80 @@ public class BubbleController implements ConfigurationChangeListener, /** * This method has different behavior depending on: - * - if an app bubble exists - * - if an app bubble is expanded + * - if a notes bubble exists + * - if a notes bubble is expanded * - * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * If no notes bubble exists, this will add and expand a bubble with the provided intent. The * intent must be explicit (i.e. include a package name or fully qualified component class name) * and the activity for it should be resizable. * - * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is - * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is + * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling * this method will expand it. * * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses * the bubble or bubble stack. * - * Some notes: - * - Only one app bubble is supported at a time, regardless of users. Multi-users support is - * tracked in b/273533235. - * - Calling this method with a different intent than the existing app bubble will do nothing + * Some details: + * - Calling this method with a different intent than the existing bubble will do nothing * * @param intent the intent to display in the bubble expanded view. * @param user the {@link UserHandle} of the user to start this activity for. * @param icon the {@link Icon} to use for the bubble view. */ - public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + public void showOrHideNotesBubble(Intent intent, UserHandle user, @Nullable Icon icon) { if (intent == null || intent.getPackage() == null) { - Log.w(TAG, "App bubble failed to show, invalid intent: " + intent + Log.w(TAG, "Notes bubble failed to show, invalid intent: " + intent + ((intent != null) ? " with package: " + intent.getPackage() : " ")); return; } - String appBubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(), user); + String noteBubbleKey = Bubble.getNoteBubbleKeyForApp(intent.getPackage(), user); PackageManager packageManager = getPackageManagerForUser(mContext, user.getIdentifier()); - if (!isResizableActivity(intent, packageManager, appBubbleKey)) return; // logs errors + if (!mResizabilityChecker.isResizableActivity(intent, packageManager, noteBubbleKey)) { + // resize check logs any errors + return; + } - Bubble existingAppBubble = mBubbleData.getBubbleInStackWithKey(appBubbleKey); + Bubble existingNotebubble = mBubbleData.getBubbleInStackWithKey(noteBubbleKey); ProtoLog.d(WM_SHELL_BUBBLES, - "showOrHideAppBubble, key=%s existingAppBubble=%s stackVisibility=%s " + "showOrHideNotesBubble, key=%s existingAppBubble=%s stackVisibility=%s " + "statusBarShade=%s", - appBubbleKey, existingAppBubble, + noteBubbleKey, existingNotebubble, (mStackView != null ? mStackView.getVisibility() : "null"), mIsStatusBarShade); - if (existingAppBubble != null) { + if (existingNotebubble != null) { BubbleViewProvider selectedBubble = mBubbleData.getSelectedBubble(); if (isStackExpanded()) { - if (selectedBubble != null && appBubbleKey.equals(selectedBubble.getKey())) { - ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", appBubbleKey); - // App bubble is expanded, lets collapse + if (selectedBubble != null && noteBubbleKey.equals(selectedBubble.getKey())) { + ProtoLog.d(WM_SHELL_BUBBLES, "collapseStack for %s", noteBubbleKey); + // Notes bubble is expanded, lets collapse collapseStack(); } else { - ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", appBubbleKey); - // App bubble is not selected, select it - mBubbleData.setSelectedBubble(existingAppBubble); + ProtoLog.d(WM_SHELL_BUBBLES, "setSelected for %s", noteBubbleKey); + // Notes bubble is not selected, select it + mBubbleData.setSelectedBubble(existingNotebubble); } } else { - ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", appBubbleKey); - // App bubble is not selected, select it & expand - mBubbleData.setSelectedBubbleAndExpandStack(existingAppBubble); + ProtoLog.d(WM_SHELL_BUBBLES, "setSelectedBubbleAndExpandStack %s", noteBubbleKey); + // Notes bubble is not selected, select it & expand + mBubbleData.setSelectedBubbleAndExpandStack(existingNotebubble); } } else { // Check if it exists in the overflow - Bubble b = mBubbleData.getOverflowBubbleWithKey(appBubbleKey); + Bubble b = mBubbleData.getOverflowBubbleWithKey(noteBubbleKey); if (b != null) { // It's in the overflow, so remove it & reinflate - mBubbleData.dismissBubbleWithKey(appBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); + mBubbleData.dismissBubbleWithKey(noteBubbleKey, Bubbles.DISMISS_NOTIF_CANCEL); // Update the bubble entry in the overflow with the latest intent. - b.setAppBubbleIntent(intent); + b.setIntent(intent); } else { - // App bubble does not exist, lets add and expand it - b = Bubble.createAppBubble(intent, user, icon, mMainExecutor, mBackgroundExecutor); + // Notes bubble does not exist, lets add and expand it + b = Bubble.createNotesBubble(intent, user, icon, mMainExecutor, + mBackgroundExecutor); } - ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", appBubbleKey); + ProtoLog.d(WM_SHELL_BUBBLES, "inflateAndAdd %s", noteBubbleKey); b.setShouldAutoExpand(true); inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false); } @@ -1659,9 +1810,9 @@ public class BubbleController implements ConfigurationChangeListener, } } - /** Sets the app bubble's taskId which is cached for SysUI. */ - public void setAppBubbleTaskId(String key, int taskId) { - mImpl.mCachedState.setAppBubbleTaskId(key, taskId); + /** Sets the note bubble's taskId which is cached for SysUI. */ + public void setNoteBubbleTaskId(String key, int taskId) { + mImpl.mCachedState.setNoteBubbleTaskId(key, taskId); } /** @@ -1686,7 +1837,6 @@ public class BubbleController implements ConfigurationChangeListener, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, - mLogger, mStackView, mLayerView, mBubbleIconFactory, @@ -1750,7 +1900,6 @@ public class BubbleController implements ConfigurationChangeListener, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, - mLogger, mStackView, mLayerView, mBubbleIconFactory, @@ -1823,16 +1972,26 @@ public class BubbleController implements ConfigurationChangeListener, @VisibleForTesting public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + inflateAndAdd(bubble, suppressFlyout, showInShade, /* bubbleBarLocation= */ null); + } + + /** + * Inflates and adds a bubble. Updates Bubble Bar location if bubbles + * are shown in the Bubble Bar and the location is not null. + */ + @VisibleForTesting + public void inflateAndAdd(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { // Lazy init stack view when a bubble is created ensureBubbleViewsAndWindowCreated(); bubble.setInflateSynchronously(mInflateSynchronously); bubble.inflate( - b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade), + b -> mBubbleData.notificationEntryUpdated(b, suppressFlyout, showInShade, + bubbleBarLocation), mContext, mExpandedViewManager, mBubbleTaskViewFactory, mBubblePositioner, - mLogger, mStackView, mLayerView, mBubbleIconFactory, @@ -2162,7 +2321,8 @@ public class BubbleController implements ConfigurationChangeListener, ProtoLog.d(WM_SHELL_BUBBLES, "mBubbleDataListener#applyUpdate:" + " added=%s removed=%b updated=%s orderChanged=%b expansionChanged=%b" + " expanded=%b selectionChanged=%b selected=%s" - + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b", + + " suppressed=%s unsupressed=%s shouldShowEducation=%b showOverflowChanged=%b" + + " bubbleBarLocation=%s", update.addedBubble != null ? update.addedBubble.getKey() : "null", !update.removedBubbles.isEmpty(), update.updatedBubble != null ? update.updatedBubble.getKey() : "null", @@ -2171,7 +2331,9 @@ public class BubbleController implements ConfigurationChangeListener, update.selectedBubble != null ? update.selectedBubble.getKey() : "null", update.suppressedBubble != null ? update.suppressedBubble.getKey() : "null", update.unsuppressedBubble != null ? update.unsuppressedBubble.getKey() : "null", - update.shouldShowEducation, update.showOverflowChanged); + update.shouldShowEducation, update.showOverflowChanged, + update.mBubbleBarLocation != null ? update.mBubbleBarLocation.toString() + : "null"); ensureBubbleViewsAndWindowCreated(); @@ -2483,6 +2645,10 @@ public class BubbleController implements ConfigurationChangeListener, mBubbleData.setSelectedBubbleAndExpandStack(bubbleToSelect); } + private boolean isDeviceLocked() { + return !mIsStatusBarShade; + } + /** * Description of current bubble state. */ @@ -2515,7 +2681,7 @@ public class BubbleController implements ConfigurationChangeListener, * @param context the context to use. * @param entry the entry to bubble. */ - static boolean canLaunchInTaskView(Context context, BubbleEntry entry) { + boolean canLaunchInTaskView(Context context, BubbleEntry entry) { if (BubbleAnythingFlagHelper.enableCreateAnyBubble()) return true; PendingIntent intent = entry.getBubbleMetadata() != null ? entry.getBubbleMetadata().getIntent() @@ -2530,26 +2696,8 @@ public class BubbleController implements ConfigurationChangeListener, } PackageManager packageManager = getPackageManagerForUser( context, entry.getStatusBarNotification().getUser().getIdentifier()); - return isResizableActivity(intent.getIntent(), packageManager, entry.getKey()); - } - - static boolean isResizableActivity(Intent intent, PackageManager packageManager, String key) { - if (intent == null) { - Log.w(TAG, "Unable to send as bubble: " + key + " null intent"); - return false; - } - ActivityInfo info = intent.resolveActivityInfo(packageManager, 0); - if (info == null) { - Log.w(TAG, "Unable to send as bubble: " + key - + " couldn't find activity info for intent: " + intent); - return false; - } - if (!ActivityInfo.isResizeableMode(info.resizeMode)) { - Log.w(TAG, "Unable to send as bubble: " + key - + " activity is not resizable for intent: " + intent); - return false; - } - return true; + return mResizabilityChecker.isResizableActivity(intent.getIntent(), packageManager, + entry.getKey()); } static PackageManager getPackageManagerForUser(Context context, int userId) { @@ -2690,7 +2838,8 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void showShortcutBubble(ShortcutInfo info) { - mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info)); + mMainExecutor.execute(() -> mController + .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null)); } @Override @@ -2781,7 +2930,7 @@ public class BubbleController implements ConfigurationChangeListener, private HashMap<String, String> mSuppressedGroupToNotifKeys = new HashMap<>(); private HashMap<String, Bubble> mShortcutIdToBubble = new HashMap<>(); - private HashMap<String, Integer> mAppBubbleTaskIds = new HashMap(); + private HashMap<String, Integer> mNoteBubbleTaskIds = new HashMap(); private ArrayList<Bubble> mTmpBubbles = new ArrayList<>(); @@ -2813,20 +2962,20 @@ public class BubbleController implements ConfigurationChangeListener, mSuppressedBubbleKeys.clear(); mShortcutIdToBubble.clear(); - mAppBubbleTaskIds.clear(); + mNoteBubbleTaskIds.clear(); for (Bubble b : mTmpBubbles) { mShortcutIdToBubble.put(b.getShortcutId(), b); updateBubbleSuppressedState(b); - if (b.isAppBubble()) { - mAppBubbleTaskIds.put(b.getKey(), b.getTaskId()); + if (b.isNote()) { + mNoteBubbleTaskIds.put(b.getKey(), b.getTaskId()); } } } - /** Sets the app bubble's taskId which is cached for SysUI. */ - synchronized void setAppBubbleTaskId(String key, int taskId) { - mAppBubbleTaskIds.put(key, taskId); + /** Sets the note bubble's taskId which is cached for SysUI. */ + synchronized void setNoteBubbleTaskId(String key, int taskId) { + mNoteBubbleTaskIds.put(key, taskId); } /** @@ -2878,7 +3027,7 @@ public class BubbleController implements ConfigurationChangeListener, pw.println(" suppressing: " + key); } - pw.println("mAppBubbleTaskIds: " + mAppBubbleTaskIds.values()); + pw.println("mNoteBubbleTaskIds: " + mNoteBubbleTaskIds.values()); } } @@ -2916,9 +3065,10 @@ public class BubbleController implements ConfigurationChangeListener, @Override public void expandStackAndSelectBubble(ShortcutInfo info) { - mMainExecutor.execute(() -> { - BubbleController.this.expandStackAndSelectBubble(info); - }); + mMainExecutor.execute(() -> + BubbleController.this + .expandStackAndSelectBubble(info, /* bubbleBarLocation = */ null) + ); } @Override @@ -2929,14 +3079,14 @@ public class BubbleController implements ConfigurationChangeListener, } @Override - public void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon) { + public void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon) { mMainExecutor.execute( - () -> BubbleController.this.showOrHideAppBubble(intent, user, icon)); + () -> BubbleController.this.showOrHideNotesBubble(intent, user, icon)); } @Override - public boolean isAppBubbleTaskId(int taskId) { - return mCachedState.mAppBubbleTaskIds.values().contains(taskId); + public boolean isNoteBubbleTaskId(int taskId) { + return mCachedState.mNoteBubbleTaskIds.values().contains(taskId); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java index 96d0f6d5654e..abcdb7e70cec 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java @@ -43,6 +43,7 @@ import com.android.wm.shell.R; import com.android.wm.shell.bubbles.Bubbles.DismissReason; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.bubbles.BubbleBarUpdate; import com.android.wm.shell.shared.bubbles.RemovedBubble; @@ -91,6 +92,8 @@ public class BubbleData { @Nullable Bubble suppressedBubble; @Nullable Bubble unsuppressedBubble; @Nullable String suppressedSummaryGroup; + @Nullable + BubbleBarLocation mBubbleBarLocation; // Pair with Bubble and @DismissReason Integer final List<Pair<Bubble, Integer>> removedBubbles = new ArrayList<>(); @@ -116,6 +119,7 @@ public class BubbleData { || unsuppressedBubble != null || suppressedSummaryChanged || suppressedSummaryGroup != null + || mBubbleBarLocation != null || showOverflowChanged; } @@ -169,6 +173,7 @@ public class BubbleData { } bubbleBarUpdate.showOverflowChanged = showOverflowChanged; bubbleBarUpdate.showOverflow = !overflowBubbles.isEmpty(); + bubbleBarUpdate.bubbleBarLocation = mBubbleBarLocation; return bubbleBarUpdate; } @@ -396,8 +401,23 @@ public class BubbleData { * {@link #setExpanded(boolean)} immediately after, which will generate 2 separate updates. */ public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble) { + setSelectedBubbleAndExpandStack(bubble, /* bubbleBarLocation = */ null); + } + + /** + * Sets the selected bubble and expands it. Also updates bubble bar location if the + * bubbleBarLocation is not {@code null} + * + * <p>This dispatches a single state update for 3 changes and should be used instead of + * calling {@link BubbleController#setBubbleBarLocation(BubbleBarLocation, int)} followed by + * {@link #setSelectedBubbleAndExpandStack(BubbleViewProvider)} immediately after, which will + * generate 2 separate updates. + */ + public void setSelectedBubbleAndExpandStack(BubbleViewProvider bubble, + @Nullable BubbleBarLocation bubbleBarLocation) { setSelectedBubbleInternal(bubble); setExpandedInternal(true); + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } @@ -471,6 +491,16 @@ public class BubbleData { return bubbleToReturn; } + Bubble getOrCreateBubble(PendingIntent pendingIntent, UserHandle user) { + String bubbleKey = Bubble.getAppBubbleKeyForApp(pendingIntent.getCreatorPackage(), user); + Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey); + if (bubbleToReturn == null) { + bubbleToReturn = Bubble.createAppBubble(pendingIntent, user, mMainExecutor, + mBgExecutor); + } + return bubbleToReturn; + } + Bubble getOrCreateBubble(TaskInfo taskInfo) { UserHandle user = UserHandle.of(mCurrentUserId); String bubbleKey = Bubble.getAppBubbleKeyForTask(taskInfo); @@ -503,13 +533,25 @@ public class BubbleData { } /** + * Calls {@link #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation)} passing + * {@code null} for bubbleBarLocation. + * + * @see #notificationEntryUpdated(Bubble, boolean, boolean, BubbleBarLocation) + */ + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + notificationEntryUpdated(bubble, suppressFlyout, showInShade, /* bubbleBarLocation = */ + null); + } + + /** * When this method is called it is expected that all info in the bubble has completed loading. * @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager, * BubbleTaskViewFactory, BubblePositioner, BubbleLogger, BubbleStackView, * com.android.wm.shell.bubbles.bar.BubbleBarLayerView, * com.android.launcher3.icons.BubbleIconFactory, boolean) */ - void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade) { + void notificationEntryUpdated(Bubble bubble, boolean suppressFlyout, boolean showInShade, + @Nullable BubbleBarLocation bubbleBarLocation) { mPendingBubbles.remove(bubble.getKey()); // No longer pending once we're here Bubble prevBubble = getBubbleInStackWithKey(bubble.getKey()); suppressFlyout |= !bubble.isTextChanged(); @@ -557,6 +599,7 @@ public class BubbleData { doSuppress(bubble); } } + mStateChange.mBubbleBarLocation = bubbleBarLocation; dispatchPendingChanges(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt index bd4708259b50..ed23986d0f64 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleEducationController.kt @@ -83,4 +83,4 @@ class BubbleEducationController(private val context: Context) { /** Convenience extension method to check if the bubble is a conversation bubble */ private val BubbleViewProvider.isConversationBubble: Boolean - get() = if (this is Bubble) isConversation else false + get() = if (this is Bubble) isChat else false diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java index e98d53e85b94..3f607a9c52ef 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java @@ -227,18 +227,18 @@ public class BubbleExpandedView extends LinearLayout { MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() - || (mBubble.getShortcutInfo() != null + || (mBubble.isShortcut() && BubbleAnythingFlagHelper.enableCreateAnyBubble())); - if (mBubble.isAppBubble()) { + // TODO - currently based on type, really it's what the "launch item" is. + if (mBubble.isApp() || mBubble.isNote()) { Context context = mContext.createContextAsUser( mBubble.getUser(), Context.CONTEXT_RESTRICTED); PendingIntent pi = PendingIntent.getActivity( context, /* requestCode= */ 0, - mBubble.getAppBubbleIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + mBubble.getIntent().addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, /* options= */ null); mTaskView.startActivity(pi, /* fillInIntent= */ null, options, @@ -252,7 +252,7 @@ public class BubbleExpandedView extends LinearLayout { } else { options.setLaunchedFromBubble(true); if (mBubble != null) { - mBubble.setIntentActive(); + mBubble.setPendingIntentActive(); } final Intent fillInIntent = new Intent(); // Apply flags to make behaviour match documentLaunchMode=always. @@ -285,9 +285,9 @@ public class BubbleExpandedView extends LinearLayout { // The taskId is saved to use for removeTask, preventing appearance in recent tasks. mTaskId = taskId; - if (mBubble != null && mBubble.isAppBubble()) { + if (mBubble != null && mBubble.isNote()) { // Let the controller know sooner what the taskId is. - mManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); + mManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); } // With the task org, the taskAppeared callback will only happen once the task has @@ -689,11 +689,6 @@ public class BubbleExpandedView extends LinearLayout { } } - /** Sets the alpha for the pointer. */ - public void setPointerAlpha(float alpha) { - mPointerView.setAlpha(alpha); - } - /** * Get alpha from underlying {@code TaskView} if this view is for a bubble. * Or get alpha for the overflow view if this view is for overflow. @@ -796,24 +791,6 @@ public class BubbleExpandedView extends LinearLayout { onContainerClipUpdate(); } - /** - * Sets the clipping for the view. - */ - public void setTaskViewClip(Rect rect) { - mLeftClip = rect.left; - mTopClip = rect.top; - mRightClip = rect.right; - mBottomClip = rect.bottom; - onContainerClipUpdate(); - } - - /** - * Returns a rect representing the clipping for the view. - */ - public Rect getTaskViewClip() { - return new Rect(mLeftClip, mTopClip, mRightClip, mBottom); - } - private void onContainerClipUpdate() { if (mTopClip == 0 && mBottomClip == 0 && mRightClip == 0 && mLeftClip == 0) { if (mIsClipping) { @@ -943,7 +920,7 @@ public class BubbleExpandedView extends LinearLayout { }); if (isNew) { - mPendingIntent = mBubble.getBubbleIntent(); + mPendingIntent = mBubble.getPendingIntent(); if ((mPendingIntent != null || mBubble.hasMetadataShortcutId()) && mTaskView != null) { setContentVisibility(false); @@ -970,7 +947,7 @@ public class BubbleExpandedView extends LinearLayout { */ private boolean didBackingContentChange(Bubble newBubble) { boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getBubbleIntent() != null; + boolean newIsIntentBased = newBubble.getPendingIntent() != null; return prevWasIntentBased != newIsIntentBased; } @@ -1124,13 +1101,6 @@ public class BubbleExpandedView extends LinearLayout { } /** - * Return width of the current pointer - */ - public int getPointerWidth() { - return mPointerWidth; - } - - /** * Position of the manage button displayed in the expanded view. Used for placing user * education about the manage button. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt index a02623138f1e..6be49ddc549a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt @@ -28,7 +28,7 @@ interface BubbleExpandedViewManager { fun promoteBubbleFromOverflow(bubble: Bubble) fun removeBubble(key: String, reason: Int) fun dismissBubble(bubble: Bubble, reason: Int) - fun setAppBubbleTaskId(key: String, taskId: Int) + fun setNoteBubbleTaskId(key: String, taskId: Int) fun isStackExpanded(): Boolean fun isShowingAsBubbleBar(): Boolean fun hideCurrentInputMethod() @@ -73,8 +73,8 @@ interface BubbleExpandedViewManager { controller.dismissBubble(bubble, reason) } - override fun setAppBubbleTaskId(key: String, taskId: Int) { - controller.setAppBubbleTaskId(key, taskId) + override fun setNoteBubbleTaskId(key: String, taskId: Int) { + controller.setNoteBubbleTaskId(key, taskId) } override fun isStackExpanded(): Boolean = controller.isStackExpanded diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java index 347df330c4b3..a0c473173bf1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleLogger.java @@ -20,6 +20,8 @@ import com.android.internal.logging.UiEvent; import com.android.internal.logging.UiEventLogger; import com.android.internal.util.FrameworkStatsLog; +import javax.inject.Inject; + /** * Implementation of UiEventLogger for logging bubble UI events. * @@ -145,8 +147,20 @@ public class BubbleLogger { @UiEvent(doc = "bubble promoted from overflow back to bubble bar") BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR(1949), + @UiEvent(doc = "application icon is dropped in the BubbleBar left drop zone") + BUBBLE_BAR_MOVED_LEFT_APP_ICON_DROP(2082), + + @UiEvent(doc = "application icon is dropped in the BubbleBar right drop zone") + BUBBLE_BAR_MOVED_RIGHT_APP_ICON_DROP(2083), + @UiEvent(doc = "while bubble bar is expanded, switch to another/existing bubble") - BUBBLE_BAR_BUBBLE_SWITCHED(1977) + BUBBLE_BAR_BUBBLE_SWITCHED(1977), + + @UiEvent(doc = "bubble bar moved to the left edge of the screen by dragging a task") + BUBBLE_BAR_MOVED_LEFT_DRAG_TASK(2146), + + @UiEvent(doc = "bubble bar moved to the right edge of the screen by dragging a task") + BUBBLE_BAR_MOVED_RIGHT_DRAG_TASK(2147), // endregion ; @@ -163,6 +177,7 @@ public class BubbleLogger { } } + @Inject public BubbleLogger(UiEventLogger uiEventLogger) { mUiEventLogger = uiEventLogger; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt index 086c91985ae3..d94f8440d3c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleOverflow.kt @@ -73,13 +73,11 @@ class BubbleOverflow(private val context: Context, private val positioner: Bubbl fun initializeForBubbleBar( expandedViewManager: BubbleExpandedViewManager, positioner: BubblePositioner, - bubbleLogger: BubbleLogger, ) { createBubbleBarExpandedView() .initialize( expandedViewManager, positioner, - bubbleLogger, /* isOverflow= */ true, /* bubbleTaskView= */ null, /* mainExecutor= */ null, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java index a725e04d3f8a..5273a7cf2432 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java @@ -27,18 +27,21 @@ import android.graphics.RectF; import android.view.Surface; import android.view.WindowManager; +import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconNormalizer; import com.android.wm.shell.R; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; +import com.android.wm.shell.shared.bubbles.DeviceConfig; /** * Keeps track of display size, configuration, and specific bubble sizes. One place for all * placement and positioning calculations to refer to. */ -public class BubblePositioner { +public class BubblePositioner implements BubbleDropTargetBoundsProvider { /** The screen edge the bubble stack is pinned to */ public enum StackPinnedEdge { @@ -99,6 +102,7 @@ public class BubblePositioner { private int mManageButtonHeight; private int mOverflowHeight; private int mMinimumFlyoutWidthLargeScreen; + private int mBubbleBarExpandedViewDropTargetPadding; private PointF mRestingStackPosition; @@ -163,6 +167,8 @@ public class BubblePositioner { res.getDimensionPixelSize(R.dimen.bubble_bar_expanded_view_width), mPositionRect.width() - 2 * mExpandedViewPadding ); + mBubbleBarExpandedViewDropTargetPadding = res.getDimensionPixelSize( + R.dimen.bubble_bar_expanded_view_drop_target_padding); if (mShowingInBubbleBar) { mExpandedViewLargeScreenWidth = mExpandedViewBubbleBarWidth; @@ -758,20 +764,20 @@ public class BubblePositioner { * is being shown, for a normal bubble. */ public PointF getDefaultStartPosition() { - return getDefaultStartPosition(false /* isAppBubble */); + return getDefaultStartPosition(false /* isNoteBubble */); } /** * The stack position to use if we don't have a saved location or if user education * is being shown. * - * @param isAppBubble whether this start position is for an app bubble or not. + * @param isNoteBubble whether this start position is for a note bubble or not. */ - public PointF getDefaultStartPosition(boolean isAppBubble) { + public PointF getDefaultStartPosition(boolean isNoteBubble) { // Normal bubbles start on the left if we're in LTR, right otherwise. // TODO (b/294284894): update language around "app bubble" here // App bubbles start on the right in RTL, left otherwise. - final boolean startOnLeft = isAppBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); + final boolean startOnLeft = isNoteBubble ? mDeviceConfig.isRtl() : !mDeviceConfig.isRtl(); return getStartPosition(startOnLeft ? StackPinnedEdge.LEFT : StackPinnedEdge.RIGHT); } @@ -964,4 +970,14 @@ public class BubblePositioner { int top = getExpandedViewBottomForBubbleBar() - height; out.offsetTo(left, top); } + + @NonNull + @Override + public Rect getBubbleBarExpandedViewDropTargetBounds(boolean onLeft) { + Rect bounds = new Rect(); + getBubbleBarExpandedViewBounds(onLeft, false, bounds); + bounds.inset(mBubbleBarExpandedViewDropTargetPadding, + mBubbleBarExpandedViewDropTargetPadding); + return bounds; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt new file mode 100644 index 000000000000..6ca08215152f --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleResizabilityChecker.kt @@ -0,0 +1,58 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Intent +import android.content.pm.ActivityInfo +import android.content.pm.PackageManager +import android.util.Log + +/** + * Checks if an intent is resizable to display in a bubble. + */ +class BubbleResizabilityChecker : ResizabilityChecker { + + override fun isResizableActivity( + intent: Intent?, + packageManager: PackageManager, key: String + ): Boolean { + if (intent == null) { + Log.w(TAG, "Unable to send as bubble: $key null intent") + return false + } + val info = intent.resolveActivityInfo(packageManager, 0) + if (info == null) { + Log.w( + TAG, ("Unable to send as bubble: " + key + + " couldn't find activity info for intent: " + intent) + ) + return false + } + if (!ActivityInfo.isResizeableMode(info.resizeMode)) { + Log.w( + TAG, ("Unable to send as bubble: " + key + + " activity is not resizable for intent: " + intent) + ) + return false + } + return true + } + + companion object { + private const val TAG = "BubbleResizeChecker" + } +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java index f1f49eda75b6..92724178cf84 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleStackView.java @@ -91,6 +91,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.animation.PhysicsAnimator; +import com.android.wm.shell.shared.bubbles.DeviceConfig; import com.android.wm.shell.shared.bubbles.DismissView; import com.android.wm.shell.shared.bubbles.RelativeTouchListener; import com.android.wm.shell.shared.magnetictarget.MagnetizedObject; @@ -1384,16 +1385,16 @@ public class BubbleStackView extends FrameLayout /** * Whether the selected bubble is conversation bubble */ - private boolean isConversationBubble() { + private boolean isChat() { BubbleViewProvider bubble = mBubbleData.getSelectedBubble(); - return bubble instanceof Bubble && ((Bubble) bubble).isConversation(); + return bubble instanceof Bubble && ((Bubble) bubble).isChat(); } /** * Whether the educational view should show for the expanded view "manage" menu. */ private boolean shouldShowManageEdu() { - if (!isConversationBubble()) { + if (!isChat()) { // We only show user education for conversation bubbles right now return false; } @@ -1440,7 +1441,7 @@ public class BubbleStackView extends FrameLayout * Whether education view should show for the collapsed stack. */ private boolean shouldShowStackEdu() { - if (!isConversationBubble()) { + if (!isChat()) { // We only show user education for conversation bubbles right now return false; } @@ -1975,12 +1976,11 @@ public class BubbleStackView extends FrameLayout return; } - if (firstBubble && bubble.isAppBubble() && !mPositioner.hasUserModifiedDefaultPosition()) { - // TODO (b/294284894): update language around "app bubble" here - // If it's an app bubble and we don't have a previous resting position, update the - // controllers to use the default position for the app bubble (it'd be different from + if (firstBubble && bubble.isNote() && !mPositioner.hasUserModifiedDefaultPosition()) { + // If it's an note bubble and we don't have a previous resting position, update the + // controllers to use the default position for the note bubble (it'd be different from // the position initialized with the controllers originally). - PointF startPosition = mPositioner.getDefaultStartPosition(true /* isAppBubble */); + PointF startPosition = mPositioner.getDefaultStartPosition(true /* isNoteBubble */); mStackOnLeftOrWillBe = mPositioner.isStackOnLeft(startPosition); mStackAnimationController.setStackPosition(startPosition); mExpandedAnimationController.setCollapsePoint(startPosition); @@ -3322,20 +3322,16 @@ public class BubbleStackView extends FrameLayout // name and icon. if (show) { final Bubble bubble = mBubbleData.getBubbleInStackWithKey(mExpandedBubble.getKey()); - if (bubble != null && !bubble.isAppBubble()) { - // Setup options for non app bubbles + if (bubble != null && bubble.isChat()) { + // Setup options for chat bubbles mManageDontBubbleView.setVisibility(VISIBLE); mManageSettingsIcon.setImageBitmap(bubble.getRawAppBadge()); mManageSettingsText.setText(getResources().getString( R.string.bubbles_app_settings, bubble.getAppName())); mManageSettingsView.setVisibility(VISIBLE); } else { - // Setup options for app bubbles - // App bubbles have no conversations - // so we don't show the option to not bubble conversation + // Not a chat bubble, so don't show conversation / notification settings mManageDontBubbleView.setVisibility(GONE); - // App bubbles are not notification based - // so we don't show the option to go to notification settings mManageSettingsView.setVisibility(GONE); } } @@ -3552,7 +3548,7 @@ public class BubbleStackView extends FrameLayout } } - private void updateExpandedView() { + void updateExpandedView() { boolean isOverflowExpanded = mExpandedBubble != null && BubbleOverflow.KEY.equals(mExpandedBubble.getKey()); int[] paddings = mPositioner.getExpandedViewContainerPadding( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java deleted file mode 100644 index a6b858500dcb..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java +++ /dev/null @@ -1,282 +0,0 @@ -/* - * Copyright (C) 2023 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.bubbles; - -import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; -import static android.app.ActivityTaskManager.INVALID_TASK_ID; -import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; -import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; - -import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; - -import android.app.ActivityOptions; -import android.app.ActivityTaskManager; -import android.app.PendingIntent; -import android.content.ComponentName; -import android.content.Context; -import android.content.Intent; -import android.graphics.Rect; -import android.util.Log; -import android.view.View; -import android.view.ViewGroup; - -import androidx.annotation.Nullable; - -import com.android.internal.protolog.ProtoLog; -import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; -import com.android.wm.shell.taskview.TaskView; - -/** - * Handles creating and updating the {@link TaskView} associated with a {@link Bubble}. - */ -public class BubbleTaskViewHelper { - - private static final String TAG = BubbleTaskViewHelper.class.getSimpleName(); - - /** - * Listener for users of {@link BubbleTaskViewHelper} to use to be notified of events - * on the task. - */ - public interface Listener { - - /** Called when the task is first created. */ - void onTaskCreated(); - - /** Called when the visibility of the task changes. */ - void onContentVisibilityChanged(boolean visible); - - /** Called when back is pressed on the task root. */ - void onBackPressed(); - - /** Called when task removal has started. */ - void onTaskRemovalStarted(); - } - - private final Context mContext; - private final BubbleExpandedViewManager mExpandedViewManager; - private final BubbleTaskViewHelper.Listener mListener; - private final View mParentView; - - @Nullable - private Bubble mBubble; - @Nullable - private PendingIntent mPendingIntent; - @Nullable - private TaskView mTaskView; - private int mTaskId = INVALID_TASK_ID; - - private final TaskView.Listener mTaskViewListener = new TaskView.Listener() { - private boolean mInitialized = false; - private boolean mDestroyed = false; - - @Override - public void onInitialized() { - ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s", - mDestroyed, mInitialized, getBubbleKey()); - - if (mDestroyed || mInitialized) { - return; - } - - // Custom options so there is no activity transition animation - ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, - 0 /* enterResId */, 0 /* exitResId */); - - Rect launchBounds = new Rect(); - mTaskView.getBoundsOnScreen(launchBounds); - - // TODO: I notice inconsistencies in lifecycle - // Post to keep the lifecycle normal - mParentView.post(() -> { - ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s", - getBubbleKey()); - try { - options.setTaskAlwaysOnTop(true); - options.setPendingIntentBackgroundActivityStartMode( - MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); - final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() - || (mBubble.getShortcutInfo() != null - && BubbleAnythingFlagHelper.enableCreateAnyBubble())); - if (mBubble.getPreparingTransition() != null) { - mBubble.getPreparingTransition().surfaceCreated(); - } else if (mBubble.isAppBubble()) { - Context context = - mContext.createContextAsUser( - mBubble.getUser(), Context.CONTEXT_RESTRICTED); - PendingIntent pi = PendingIntent.getActivity( - context, - /* requestCode= */ 0, - mBubble.getAppBubbleIntent() - .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), - PendingIntent.FLAG_IMMUTABLE | PendingIntent.FLAG_UPDATE_CURRENT, - /* options= */ null); - mTaskView.startActivity(pi, /* fillInIntent= */ null, options, - launchBounds); - } else if (isShortcutBubble) { - options.setLaunchedFromBubble(true); - options.setApplyActivityFlagsForBubbles(true); - mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), - options, launchBounds); - } else { - options.setLaunchedFromBubble(true); - if (mBubble != null) { - mBubble.setIntentActive(); - } - final Intent fillInIntent = new Intent(); - // Apply flags to make behaviour match documentLaunchMode=always. - fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); - fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); - mTaskView.startActivity(mPendingIntent, fillInIntent, options, - launchBounds); - } - } catch (RuntimeException e) { - // If there's a runtime exception here then there's something - // wrong with the intent, we can't really recover / try to populate - // the bubble again so we'll just remove it. - Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() - + ", " + e.getMessage() + "; removing bubble"); - mExpandedViewManager.removeBubble( - getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); - } - mInitialized = true; - }); - } - - @Override - public void onReleased() { - mDestroyed = true; - } - - @Override - public void onTaskCreated(int taskId, ComponentName name) { - ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s", - taskId, getBubbleKey()); - // The taskId is saved to use for removeTask, preventing appearance in recent tasks. - mTaskId = taskId; - - if (mBubble != null && mBubble.isAppBubble()) { - // Let the controller know sooner what the taskId is. - mExpandedViewManager.setAppBubbleTaskId(mBubble.getKey(), mTaskId); - } - - // With the task org, the taskAppeared callback will only happen once the task has - // already drawn - mListener.onTaskCreated(); - } - - @Override - public void onTaskVisibilityChanged(int taskId, boolean visible) { - mListener.onContentVisibilityChanged(visible); - } - - @Override - public void onTaskRemovalStarted(int taskId) { - ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s", - taskId, getBubbleKey()); - if (mBubble != null) { - mExpandedViewManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); - } - if (mTaskView != null) { - mTaskView.release(); - ((ViewGroup) mParentView).removeView(mTaskView); - mTaskView = null; - } - mListener.onTaskRemovalStarted(); - } - - @Override - public void onBackPressedOnTaskRoot(int taskId) { - if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) { - mListener.onBackPressed(); - } - } - }; - - public BubbleTaskViewHelper(Context context, - BubbleExpandedViewManager expandedViewManager, - BubbleTaskViewHelper.Listener listener, - BubbleTaskView bubbleTaskView, - View parent) { - mContext = context; - mExpandedViewManager = expandedViewManager; - mListener = listener; - mParentView = parent; - mTaskView = bubbleTaskView.getTaskView(); - bubbleTaskView.setDelegateListener(mTaskViewListener); - if (bubbleTaskView.isCreated()) { - mTaskId = bubbleTaskView.getTaskId(); - mListener.onTaskCreated(); - } - } - - /** - * Sets the bubble or updates the bubble used to populate the view. - * - * @return true if the bubble is new, false if it was an update to the same bubble. - */ - public boolean update(Bubble bubble) { - boolean isNew = mBubble == null || didBackingContentChange(bubble); - mBubble = bubble; - if (isNew) { - mPendingIntent = mBubble.getBubbleIntent(); - return true; - } - return false; - } - - /** Returns the bubble key associated with this view. */ - @Nullable - public String getBubbleKey() { - return mBubble != null ? mBubble.getKey() : null; - } - - /** Returns the TaskView associated with this view. */ - @Nullable - public TaskView getTaskView() { - return mTaskView; - } - - /** - * Returns the task id associated with the task in this view. If the task doesn't exist then - * {@link ActivityTaskManager#INVALID_TASK_ID}. - */ - public int getTaskId() { - return mTaskId; - } - - /** Returns whether the bubble set on the helper is valid to populate the task view. */ - public boolean isValidBubble() { - return mBubble != null && (mPendingIntent != null || mBubble.hasMetadataShortcutId()); - } - - // TODO (b/274980695): Is this still relevant? - /** - * Bubbles are backed by a pending intent or a shortcut, once the activity is - * started we never change it / restart it on notification updates -- unless the bubble's - * backing data switches. - * - * This indicates if the new bubble is backed by a different data source than what was - * previously shown here (e.g. previously a pending intent & now a shortcut). - * - * @param newBubble the bubble this view is being updated with. - * @return true if the backing content has changed. - */ - private boolean didBackingContentChange(Bubble newBubble) { - boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; - boolean newIsIntentBased = newBubble.getBubbleIntent() != null; - return prevWasIntentBased != newIsIntentBased; - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java new file mode 100644 index 000000000000..a38debb702dc --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewListener.java @@ -0,0 +1,279 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles; + +import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS; +import static android.app.ActivityTaskManager.INVALID_TASK_ID; +import static android.content.Intent.FLAG_ACTIVITY_MULTIPLE_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NEW_DOCUMENT; + +import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BUBBLES; + +import android.app.ActivityOptions; +import android.app.ActivityTaskManager; +import android.app.PendingIntent; +import android.content.ComponentName; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.util.Log; +import android.view.View; +import android.view.ViewGroup; + +import androidx.annotation.Nullable; + +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.taskview.TaskView; + +/** + * A listener that works with task views for bubbles, manages launching the appropriate + * content into the task view from the bubble and sends updates of task view events back to + * the parent view via {@link BubbleTaskViewListener.Callback}. + */ +public class BubbleTaskViewListener implements TaskView.Listener { + private static final String TAG = BubbleTaskViewListener.class.getSimpleName(); + + /** + * Callback to let the view parent of TaskView to be notified of different events. + */ + public interface Callback { + + /** Called when the task is first created. */ + void onTaskCreated(); + + /** Called when the visibility of the task changes. */ + void onContentVisibilityChanged(boolean visible); + + /** Called when back is pressed on the task root. */ + void onBackPressed(); + + /** Called when task removal has started. */ + void onTaskRemovalStarted(); + } + + private final Context mContext; + private final BubbleExpandedViewManager mExpandedViewManager; + private final BubbleTaskViewListener.Callback mCallback; + private final View mParentView; + + private Bubble mBubble; + @Nullable + private PendingIntent mPendingIntent; + private int mTaskId = INVALID_TASK_ID; + private TaskView mTaskView; + + private boolean mInitialized = false; + private boolean mDestroyed = false; + + public BubbleTaskViewListener(Context context, BubbleTaskView bubbleTaskView, View parentView, + BubbleExpandedViewManager manager, BubbleTaskViewListener.Callback callback) { + mContext = context; + mTaskView = bubbleTaskView.getTaskView(); + mParentView = parentView; + mExpandedViewManager = manager; + mCallback = callback; + bubbleTaskView.setDelegateListener(this); + if (bubbleTaskView.isCreated()) { + mTaskId = bubbleTaskView.getTaskId(); + callback.onTaskCreated(); + } + } + + @Override + public void onInitialized() { + ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: destroyed=%b initialized=%b bubble=%s", + mDestroyed, mInitialized, getBubbleKey()); + + if (mDestroyed || mInitialized) { + return; + } + + // Custom options so there is no activity transition animation + ActivityOptions options = ActivityOptions.makeCustomAnimation(mContext, + 0 /* enterResId */, 0 /* exitResId */); + + Rect launchBounds = new Rect(); + mTaskView.getBoundsOnScreen(launchBounds); + + // TODO: I notice inconsistencies in lifecycle + // Post to keep the lifecycle normal + // TODO - currently based on type, really it's what the "launch item" is. + mParentView.post(() -> { + ProtoLog.d(WM_SHELL_BUBBLES, "onInitialized: calling startActivity, bubble=%s", + getBubbleKey()); + try { + options.setTaskAlwaysOnTop(true); + options.setPendingIntentBackgroundActivityStartMode( + MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS); + final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId() + || (mBubble.isShortcut() + && BubbleAnythingFlagHelper.enableCreateAnyBubble())); + if (mBubble.getPreparingTransition() != null) { + mBubble.getPreparingTransition().surfaceCreated(); + } else if (mBubble.isApp() || mBubble.isNote()) { + Context context = + mContext.createContextAsUser( + mBubble.getUser(), Context.CONTEXT_RESTRICTED); + Intent fillInIntent = null; + // First try get pending intent from the bubble + PendingIntent pi = mBubble.getPendingIntent(); + if (pi == null) { + // If null - create new one + pi = PendingIntent.getActivity( + context, + /* requestCode= */ 0, + mBubble.getIntent() + .addFlags(FLAG_ACTIVITY_MULTIPLE_TASK), + PendingIntent.FLAG_IMMUTABLE + | PendingIntent.FLAG_UPDATE_CURRENT, + /* options= */ null); + } else { + fillInIntent = new Intent(pi.getIntent()); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + } + mTaskView.startActivity(pi, fillInIntent, options, launchBounds); + } else if (isShortcutBubble) { + options.setLaunchedFromBubble(true); + options.setApplyActivityFlagsForBubbles(true); + mTaskView.startShortcutActivity(mBubble.getShortcutInfo(), + options, launchBounds); + } else { + options.setLaunchedFromBubble(true); + if (mBubble != null) { + mBubble.setPendingIntentActive(); + } + final Intent fillInIntent = new Intent(); + // Apply flags to make behaviour match documentLaunchMode=always. + fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT); + fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK); + mTaskView.startActivity(mPendingIntent, fillInIntent, options, + launchBounds); + } + } catch (RuntimeException e) { + // If there's a runtime exception here then there's something + // wrong with the intent, we can't really recover / try to populate + // the bubble again so we'll just remove it. + Log.w(TAG, "Exception while displaying bubble: " + getBubbleKey() + + ", " + e.getMessage() + "; removing bubble"); + mExpandedViewManager.removeBubble( + getBubbleKey(), Bubbles.DISMISS_INVALID_INTENT); + } + mInitialized = true; + }); + } + + @Override + public void onReleased() { + mDestroyed = true; + } + + @Override + public void onTaskCreated(int taskId, ComponentName name) { + ProtoLog.d(WM_SHELL_BUBBLES, "onTaskCreated: taskId=%d bubble=%s", + taskId, getBubbleKey()); + // The taskId is saved to use for removeTask, preventing appearance in recent tasks. + mTaskId = taskId; + + if (mBubble != null && mBubble.isNote()) { + // Let the controller know sooner what the taskId is. + mExpandedViewManager.setNoteBubbleTaskId(mBubble.getKey(), mTaskId); + } + + // With the task org, the taskAppeared callback will only happen once the task has + // already drawn + mCallback.onTaskCreated(); + } + + @Override + public void onTaskVisibilityChanged(int taskId, boolean visible) { + mCallback.onContentVisibilityChanged(visible); + } + + @Override + public void onTaskRemovalStarted(int taskId) { + ProtoLog.d(WM_SHELL_BUBBLES, "onTaskRemovalStarted: taskId=%d bubble=%s", + taskId, getBubbleKey()); + if (mBubble != null) { + mExpandedViewManager.removeBubble(mBubble.getKey(), Bubbles.DISMISS_TASK_FINISHED); + } + if (mTaskView != null) { + mTaskView.release(); + ((ViewGroup) mParentView).removeView(mTaskView); + mTaskView = null; + } + mCallback.onTaskRemovalStarted(); + } + + @Override + public void onBackPressedOnTaskRoot(int taskId) { + if (mTaskId == taskId && mExpandedViewManager.isStackExpanded()) { + mCallback.onBackPressed(); + } + } + + /** + * Sets the bubble or updates the bubble used to populate the view. + * + * @return true if the bubble is new or if the launch content of the bubble changed from the + * previous bubble. + */ + public boolean setBubble(Bubble bubble) { + boolean isNew = mBubble == null || didBackingContentChange(bubble); + mBubble = bubble; + if (isNew) { + mPendingIntent = mBubble.getPendingIntent(); + } + return isNew; + } + + /** Returns the TaskView associated with this view. */ + @Nullable + public TaskView getTaskView() { + return mTaskView; + } + + /** + * Returns the task id associated with the task in this view. If the task doesn't exist then + * {@link ActivityTaskManager#INVALID_TASK_ID}. + */ + public int getTaskId() { + return mTaskId; + } + + private String getBubbleKey() { + return mBubble != null ? mBubble.getKey() : ""; + } + + // TODO (b/274980695): Is this still relevant? + /** + * Bubbles are backed by a pending intent or a shortcut, once the activity is + * started we never change it / restart it on notification updates -- unless the bubble's + * backing data switches. + * + * This indicates if the new bubble is backed by a different data source than what was + * previously shown here (e.g. previously a pending intent & now a shortcut). + * + * @param newBubble the bubble this view is being updated with. + * @return true if the backing content has changed. + */ + private boolean didBackingContentChange(Bubble newBubble) { + boolean prevWasIntentBased = mBubble != null && mPendingIntent != null; + boolean newIsIntentBased = newBubble.getPendingIntent() != null; + return prevWasIntentBased != newIsIntentBased; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java index 29fb1a23017c..a676f41baafe 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTransitions.java @@ -22,6 +22,8 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.View.INVISIBLE; import static android.view.WindowManager.TRANSIT_CHANGE; +import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE; + import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; @@ -90,13 +92,12 @@ public class BubbleTransitions { */ public BubbleTransition startConvertToBubble(Bubble bubble, TaskInfo taskInfo, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, - BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, + BubblePositioner positioner, BubbleStackView stackView, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, - boolean inflateSync) { - ConvertToBubble convert = new ConvertToBubble(bubble, taskInfo, mContext, - expandedViewManager, factory, positioner, logger, stackView, layerView, iconFactory, - inflateSync); - return convert; + DragData dragData, boolean inflateSync) { + return new ConvertToBubble(bubble, taskInfo, mContext, + expandedViewManager, factory, positioner, stackView, layerView, iconFactory, + dragData, inflateSync); } /** @@ -150,43 +151,92 @@ public class BubbleTransitions { } /** + * Information about the task when it is being dragged to a bubble + */ + public static class DragData { + private final Rect mBounds; + private final WindowContainerTransaction mPendingWct; + private final boolean mReleasedOnLeft; + + /** + * @param bounds bounds of the dragged task when the drag was released + * @param wct pending operations to be applied when finishing the drag + * @param releasedOnLeft true if the bubble was released in the left drop target + */ + public DragData(@Nullable Rect bounds, @Nullable WindowContainerTransaction wct, + boolean releasedOnLeft) { + mBounds = bounds; + mPendingWct = wct; + mReleasedOnLeft = releasedOnLeft; + } + + /** + * @return bounds of the dragged task when the drag was released + */ + @Nullable + public Rect getBounds() { + return mBounds; + } + + /** + * @return pending operations to be applied when finishing the drag + */ + @Nullable + public WindowContainerTransaction getPendingWct() { + return mPendingWct; + } + + /** + * @return true if the bubble was released in the left drop target + */ + public boolean isReleasedOnLeft() { + return mReleasedOnLeft; + } + } + + /** * BubbleTransition that coordinates the process of a non-bubble task becoming a bubble. The * steps are as follows: * * 1. Start inflating the bubble view * 2. Once inflated (but not-yet visible), tell WM to do the shell-transition. - * 3. Transition becomes ready, so notify Launcher - * 4. Launcher responds with showExpandedView which calls continueExpand() to make view visible - * 5. Surface is created which kicks off actual animation + * 3. When the transition becomes ready, notify Launcher in parallel + * 4. Wait for surface to be created + * 5. Once surface is ready, animate the task to a bubble * - * So, constructor -> onInflated -> startAnimation -> continueExpand -> surfaceCreated. + * While the animation is pending, we keep a reference to the pending transition in the bubble. + * This allows us to check in other parts of the code that this bubble will be shown via the + * transition animation. * - * continueExpand and surfaceCreated are set-up to happen in either order, though, to support - * UX/timing adjustments. + * startAnimation, continueExpand and surfaceCreated are set-up to happen in either order, + * to support UX/timing adjustments. */ @VisibleForTesting class ConvertToBubble implements Transitions.TransitionHandler, BubbleTransition { final BubbleBarLayerView mLayerView; Bubble mBubble; + @Nullable DragData mDragData; IBinder mTransition; Transitions.TransitionFinishCallback mFinishCb; WindowContainerTransaction mFinishWct = null; final Rect mStartBounds = new Rect(); SurfaceControl mSnapshot = null; TaskInfo mTaskInfo; - boolean mFinishedExpand = false; BubbleViewProvider mPriorBubble = null; + private final TransitionProgress mTransitionProgress = new TransitionProgress(); private SurfaceControl.Transaction mFinishT; private SurfaceControl mTaskLeash; ConvertToBubble(Bubble bubble, TaskInfo taskInfo, Context context, BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory factory, - BubblePositioner positioner, BubbleLogger logger, BubbleStackView stackView, - BubbleBarLayerView layerView, BubbleIconFactory iconFactory, boolean inflateSync) { + BubblePositioner positioner, BubbleStackView stackView, + BubbleBarLayerView layerView, BubbleIconFactory iconFactory, + @Nullable DragData dragData, boolean inflateSync) { mBubble = bubble; mTaskInfo = taskInfo; mLayerView = layerView; + mDragData = dragData; mBubble.setInflateSynchronously(inflateSync); mBubble.setPreparingTransition(this); mBubble.inflate( @@ -195,7 +245,6 @@ public class BubbleTransitions { expandedViewManager, factory, positioner, - logger, stackView, layerView, iconFactory, @@ -210,6 +259,9 @@ public class BubbleTransitions { final Rect launchBounds = new Rect(); mLayerView.getExpandedViewRestBounds(launchBounds); WindowContainerTransaction wct = new WindowContainerTransaction(); + if (mDragData != null && mDragData.getPendingWct() != null) { + wct.merge(mDragData.getPendingWct(), true); + } if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_MULTI_WINDOW) { if (mTaskInfo.getParentTaskId() != INVALID_TASK_ID) { wct.reparent(mTaskInfo.token, null, true); @@ -228,7 +280,7 @@ public class BubbleTransitions { state.mVisible = true; } mTaskViewTransitions.enqueueExternal(tv.getController(), () -> { - mTransition = mTransitions.startTransition(TRANSIT_CHANGE, wct, this); + mTransition = mTransitions.startTransition(TRANSIT_CONVERT_TO_BUBBLE, wct, this); return mTransition; }); } @@ -248,7 +300,9 @@ public class BubbleTransitions { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { } @@ -292,6 +346,11 @@ public class BubbleTransitions { } mFinishCb = finishCallback; + if (mDragData != null && mDragData.getBounds() != null) { + // Override start bounds with the dragged task bounds + mStartBounds.set(mDragData.getBounds()); + } + // Now update state (and talk to launcher) in parallel with snapshot stuff mBubbleData.notificationEntryUpdated(mBubble, /* suppressFlyout= */ true, /* showInShade= */ false); @@ -303,15 +362,22 @@ public class BubbleTransitions { mStartBounds.left - info.getRoot(0).getOffset().x, mStartBounds.top - info.getRoot(0).getOffset().y); startTransaction.setLayer(mSnapshot, Integer.MAX_VALUE); + + BubbleBarExpandedView bbev = mBubble.getBubbleBarExpandedView(); + if (bbev != null) { + // Corners get reset during the animation. Add them back + startTransaction.setCornerRadius(mSnapshot, bbev.getRestingCornerRadius()); + } + startTransaction.apply(); mTaskViewTransitions.onExternalDone(transition); + mTransitionProgress.setTransitionReady(); + startExpandAnim(); return true; } - @Override - public void continueExpand() { - mFinishedExpand = true; + private void startExpandAnim() { final boolean animate = mLayerView.canExpandView(mBubble); if (animate) { mPriorBubble = mLayerView.prepareConvertedView(mBubble); @@ -322,19 +388,25 @@ public class BubbleTransitions { mLayerView.removeView(priorView); mPriorBubble = null; } - if (!animate || mBubble.getTaskView().getSurfaceControl() != null) { + if (!animate || mTransitionProgress.isReadyToAnimate()) { playAnimation(animate); } } @Override + public void continueExpand() { + mTransitionProgress.setReadyToExpand(); + } + + @Override public void surfaceCreated() { + mTransitionProgress.setSurfaceReady(); mMainExecutor.execute(() -> { final TaskViewTaskController tvc = mBubble.getTaskView().getController(); final TaskViewRepository.TaskViewState state = mRepository.byTaskView(tvc); if (state == null) return; state.mVisible = true; - if (mFinishedExpand) { + if (mTransitionProgress.isReadyToAnimate()) { playAnimation(true /* animate */); } }); @@ -350,9 +422,6 @@ public class BubbleTransitions { mFinishWct = null; } - // Preparation is complete. - mBubble.setPreparingTransition(null); - if (animate) { mLayerView.animateConvert(startT, mStartBounds, mSnapshot, mTaskLeash, () -> { mFinishCb.onTransitionFinished(mFinishWct); @@ -364,6 +433,42 @@ public class BubbleTransitions { mFinishCb = null; } } + + /** + * Keeps track of internal state of different steps of this BubbleTransition. + */ + private class TransitionProgress { + private boolean mTransitionReady; + private boolean mReadyToExpand; + private boolean mSurfaceReady; + + void setTransitionReady() { + mTransitionReady = true; + onUpdate(); + } + + void setReadyToExpand() { + mReadyToExpand = true; + onUpdate(); + } + + void setSurfaceReady() { + mSurfaceReady = true; + onUpdate(); + } + + boolean isReadyToAnimate() { + // Animation only depends on transition and surface state + return mTransitionReady && mSurfaceReady; + } + + private void onUpdate() { + if (mTransitionReady && mReadyToExpand && mSurfaceReady) { + // Clear the transition from bubble when all the steps are ready + mBubble.setPreparingTransition(null); + } + } + } } /** @@ -423,7 +528,9 @@ public class BubbleTransitions { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java index 96b6043059d2..d78f459c6f5f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTask.java @@ -73,7 +73,6 @@ public class BubbleViewInfoTask { private final WeakReference<BubbleExpandedViewManager> mExpandedViewManager; private final WeakReference<BubbleTaskViewFactory> mTaskViewFactory; private final WeakReference<BubblePositioner> mPositioner; - private final WeakReference<BubbleLogger> mBubbleLogger; private final WeakReference<BubbleStackView> mStackView; private final WeakReference<BubbleBarLayerView> mLayerView; private final BubbleIconFactory mIconFactory; @@ -95,7 +94,6 @@ public class BubbleViewInfoTask { BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, - BubbleLogger bubbleLogger, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory factory, @@ -108,7 +106,6 @@ public class BubbleViewInfoTask { mExpandedViewManager = new WeakReference<>(expandedViewManager); mTaskViewFactory = new WeakReference<>(taskViewFactory); mPositioner = new WeakReference<>(positioner); - mBubbleLogger = new WeakReference<>(bubbleLogger); mStackView = new WeakReference<>(stackView); mLayerView = new WeakReference<>(layerView); mIconFactory = factory; @@ -224,7 +221,7 @@ public class BubbleViewInfoTask { ProtoLog.v(WM_SHELL_BUBBLES, "Task initializing bubble bar expanded view key=%s", mBubble.getKey()); viewInfo.bubbleBarExpandedView.initialize(mExpandedViewManager.get(), - mPositioner.get(), mBubbleLogger.get(), false /* isOverflow */, + mPositioner.get(), false /* isOverflow */, viewInfo.taskView, mMainExecutor, mBgExecutor, new RegionSamplingProvider() { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java index c1da94cc470f..06e02a1a4cf8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskLegacy.java @@ -78,7 +78,6 @@ public class BubbleViewInfoTaskLegacy extends private WeakReference<BubbleExpandedViewManager> mExpandedViewManager; private WeakReference<BubbleTaskViewFactory> mTaskViewFactory; private WeakReference<BubblePositioner> mPositioner; - private WeakReference<BubbleLogger> mBubbleLogger; private WeakReference<BubbleStackView> mStackView; private WeakReference<BubbleBarLayerView> mLayerView; private BubbleIconFactory mIconFactory; @@ -96,7 +95,6 @@ public class BubbleViewInfoTaskLegacy extends BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, - BubbleLogger bubbleLogger, @Nullable BubbleStackView stackView, @Nullable BubbleBarLayerView layerView, BubbleIconFactory factory, @@ -109,7 +107,6 @@ public class BubbleViewInfoTaskLegacy extends mExpandedViewManager = new WeakReference<>(expandedViewManager); mTaskViewFactory = new WeakReference<>(taskViewFactory); mPositioner = new WeakReference<>(positioner); - mBubbleLogger = new WeakReference<>(bubbleLogger); mStackView = new WeakReference<>(stackView); mLayerView = new WeakReference<>(layerView); mIconFactory = factory; @@ -127,9 +124,8 @@ public class BubbleViewInfoTaskLegacy extends } if (mLayerView.get() != null) { return BubbleViewInfo.populateForBubbleBar(mContext.get(), mExpandedViewManager.get(), - mTaskViewFactory.get(), mPositioner.get(), mBubbleLogger.get(), - mLayerView.get(), mIconFactory, mBubble, mSkipInflation, mMainExecutor, - mBackgroundExecutor); + mTaskViewFactory.get(), mPositioner.get(), mLayerView.get(), mIconFactory, + mBubble, mSkipInflation, mMainExecutor, mBackgroundExecutor); } else { return BubbleViewInfo.populate(mContext.get(), mExpandedViewManager.get(), mTaskViewFactory.get(), mPositioner.get(), mStackView.get(), mIconFactory, @@ -191,7 +187,6 @@ public class BubbleViewInfoTaskLegacy extends BubbleExpandedViewManager expandedViewManager, BubbleTaskViewFactory taskViewFactory, BubblePositioner positioner, - BubbleLogger bubbleLogger, BubbleBarLayerView layerView, BubbleIconFactory iconFactory, Bubble b, @@ -205,7 +200,7 @@ public class BubbleViewInfoTaskLegacy extends LayoutInflater inflater = LayoutInflater.from(c); info.bubbleBarExpandedView = (BubbleBarExpandedView) inflater.inflate( R.layout.bubble_bar_expanded_view, layerView, false /* attachToRoot */); - info.bubbleBarExpandedView.initialize(expandedViewManager, positioner, bubbleLogger, + info.bubbleBarExpandedView.initialize(expandedViewManager, positioner, false /* isOverflow */, bubbleTaskView, mainExecutor, backgroundExecutor, new RegionSamplingProvider() { @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java index 4297fac0f6a8..44ae74479949 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java @@ -135,33 +135,31 @@ public interface Bubbles { /** * This method has different behavior depending on: - * - if an app bubble exists - * - if an app bubble is expanded + * - if a notes bubble exists + * - if a notes bubble is expanded * - * If no app bubble exists, this will add and expand a bubble with the provided intent. The + * If no notes bubble exists, this will add and expand a bubble with the provided intent. The * intent must be explicit (i.e. include a package name or fully qualified component class name) * and the activity for it should be resizable. * - * If an app bubble exists, this will toggle the visibility of it, i.e. if the app bubble is - * expanded, calling this method will collapse it. If the app bubble is not expanded, calling + * If a notes bubble exists, this will toggle the visibility of it, i.e. if the notes bubble is + * expanded, calling this method will collapse it. If the notes bubble is not expanded, calling * this method will expand it. * * These bubbles are <b>not</b> backed by a notification and remain until the user dismisses * the bubble or bubble stack. * - * Some notes: - * - Only one app bubble is supported at a time, regardless of users. Multi-users support is - * tracked in b/273533235. - * - Calling this method with a different intent than the existing app bubble will do nothing + * Some details: + * - Calling this method with a different intent than the existing bubble will do nothing * * @param intent the intent to display in the bubble expanded view. - * @param user the {@link UserHandle} of the user to start this activity for. - * @param icon the {@link Icon} to use for the bubble view. + * @param user the {@link UserHandle} of the user to start this activity for. + * @param icon the {@link Icon} to use for the bubble view. */ - void showOrHideAppBubble(Intent intent, UserHandle user, @Nullable Icon icon); + void showOrHideNoteBubble(Intent intent, UserHandle user, @Nullable Icon icon); /** @return true if the specified {@code taskId} corresponds to app bubble's taskId. */ - boolean isAppBubbleTaskId(int taskId); + boolean isNoteBubbleTaskId(int taskId); /** ` * @return a {@link SynchronousScreenCaptureListener} after performing a screenshot that may diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt new file mode 100644 index 000000000000..6b3a72f567b9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/ResizabilityChecker.kt @@ -0,0 +1,35 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles + +import android.content.Intent +import android.content.pm.PackageManager + +/** + * Interface to check whether the activity backed by a specific intent is resizable. + */ +fun interface ResizabilityChecker { + + /** + * Returns whether the provided intent represents a resizable activity. + * + * @param intent the intent to check + * @param packageManager the package manager to use to do the look up + * @param key a key representing thing being checked (used for error logging) + */ + fun isResizableActivity(intent: Intent?, packageManager: PackageManager, key: String): Boolean +}
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt new file mode 100644 index 000000000000..3ff80b5ab8ac --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarDragListener.kt @@ -0,0 +1,40 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.bubbles.bar + +import android.content.Intent +import android.graphics.Rect +import com.android.wm.shell.shared.bubbles.BubbleBarLocation + +/** Controller that takes care of the bubble bar drag events. */ +interface BubbleBarDragListener { + + /** Called when the drag event is over the bubble bar drop zone. */ + fun onDragItemOverBubbleBarDragZone(location: BubbleBarLocation) + + /** Called when the drag event leaves the bubble bar drop zone. */ + fun onItemDraggedOutsideBubbleBarDropZone() + + /** Called when the drop event happens over the bubble bar drop zone. */ + fun onItemDroppedOverBubbleBarDragZone(location: BubbleBarLocation, itemIntent: Intent) + + /** + * Returns mapping of the bubble bar locations to the corresponding + * [rect][android.graphics.Rect] zone. + */ + fun getBubbleBarDropZones(l: Int, t: Int, r: Int, b: Int): Map<BubbleBarLocation, Rect> +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java index ac5b9c9866ed..d93dbc3c15d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java @@ -43,9 +43,10 @@ import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflowContainerView; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleTaskView; -import com.android.wm.shell.bubbles.BubbleTaskViewHelper; +import com.android.wm.shell.bubbles.BubbleTaskViewListener; import com.android.wm.shell.bubbles.Bubbles; import com.android.wm.shell.bubbles.RegionSamplingProvider; +import com.android.wm.shell.dagger.HasWMComponent; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.shared.handles.RegionSamplingHelper; import com.android.wm.shell.taskview.TaskView; @@ -53,8 +54,10 @@ import com.android.wm.shell.taskview.TaskView; import java.util.concurrent.Executor; import java.util.function.Supplier; +import javax.inject.Inject; + /** Expanded view of a bubble when it's part of the bubble bar. */ -public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewHelper.Listener { +public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskViewListener.Callback { /** * The expanded view listener notifying the {@link BubbleBarLayerView} about the internal * actions and events @@ -107,9 +110,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView private Bubble mBubble; private BubbleExpandedViewManager mManager; private BubblePositioner mPositioner; - private BubbleLogger mBubbleLogger; private boolean mIsOverflow; - private BubbleTaskViewHelper mBubbleTaskViewHelper; + private BubbleTaskViewListener mBubbleTaskViewListener; private BubbleBarMenuViewController mMenuViewController; @Nullable private Supplier<Rect> mLayerBoundsSupplier; @@ -177,6 +179,12 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView VISIBLE } + // Ideally this would be package private, but we have to set this in a fake for test and we + // don't yet have dagger set up for tests, so have to set manually + @VisibleForTesting + @Inject + public BubbleLogger bubbleLogger; + public BubbleBarExpandedView(Context context) { this(context, null); } @@ -198,6 +206,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView protected void onFinishInflate() { super.onFinishInflate(); Context context = getContext(); + if (context instanceof HasWMComponent) { + ((HasWMComponent) context).getWMComponent().inject(this); + } setElevation(getResources().getDimensionPixelSize(R.dimen.bubble_elevation)); mCaptionHeight = context.getResources().getDimensionPixelSize( R.dimen.bubble_bar_expanded_view_caption_height); @@ -218,7 +229,6 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Initializes the view, must be called before doing anything else. */ public void initialize(BubbleExpandedViewManager expandedViewManager, BubblePositioner positioner, - BubbleLogger bubbleLogger, boolean isOverflow, @Nullable BubbleTaskView bubbleTaskView, @Nullable Executor mainExecutor, @@ -226,7 +236,6 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView @Nullable RegionSamplingProvider regionSamplingProvider) { mManager = expandedViewManager; mPositioner = positioner; - mBubbleLogger = bubbleLogger; mIsOverflow = isOverflow; mMainExecutor = mainExecutor; mBackgroundExecutor = backgroundExecutor; @@ -241,9 +250,10 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView mHandleView.setVisibility(View.GONE); } else { mTaskView = bubbleTaskView.getTaskView(); - mBubbleTaskViewHelper = new BubbleTaskViewHelper(mContext, expandedViewManager, - /* listener= */ this, bubbleTaskView, - /* viewParent= */ this); + mBubbleTaskViewListener = new BubbleTaskViewListener(mContext, bubbleTaskView, + /* viewParent= */ this, + expandedViewManager, + /* callback= */ this); // if the task view is already attached to a parent we need to remove it if (mTaskView.getParent() != null) { @@ -290,20 +300,20 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView if (mListener != null) { mListener.onUnBubbleConversation(bubble.getKey()); } - mBubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_OPT_OUT); + bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_OPT_OUT); } @Override public void onOpenAppSettings(Bubble bubble) { mManager.collapseStack(); mContext.startActivityAsUser(bubble.getSettingsIntent(mContext), bubble.getUser()); - mBubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_GO_TO_SETTINGS); + bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_APP_MENU_GO_TO_SETTINGS); } @Override public void onDismissBubble(Bubble bubble) { mManager.dismissBubble(bubble, Bubbles.DISMISS_USER_GESTURE); - mBubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU); + bubbleLogger.log(bubble, BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_APP_MENU); } @Override @@ -530,13 +540,15 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView /** Updates the bubble shown in the expanded view. */ public void update(Bubble bubble) { mBubble = bubble; - mBubbleTaskViewHelper.update(bubble); + mBubbleTaskViewListener.setBubble(bubble); mMenuViewController.updateMenu(bubble); } /** The task id of the activity shown in the task view, if it exists. */ public int getTaskId() { - return mBubbleTaskViewHelper != null ? mBubbleTaskViewHelper.getTaskId() : INVALID_TASK_ID; + return mBubbleTaskViewListener != null + ? mBubbleTaskViewListener.getTaskId() + : INVALID_TASK_ID; } /** Sets layer bounds supplier used for obscured touchable region of task view */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java index f3f8d6f96a42..e3b0872df593 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java @@ -45,11 +45,11 @@ import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubbleOverflow; import com.android.wm.shell.bubbles.BubblePositioner; import com.android.wm.shell.bubbles.BubbleViewProvider; -import com.android.wm.shell.bubbles.DeviceConfig; import com.android.wm.shell.bubbles.DismissViewUtils; import com.android.wm.shell.bubbles.bar.BubbleBarExpandedViewDragController.DragListener; import com.android.wm.shell.shared.bubbles.BaseBubblePinController; import com.android.wm.shell.shared.bubbles.BubbleBarLocation; +import com.android.wm.shell.shared.bubbles.DeviceConfig; import com.android.wm.shell.shared.bubbles.DismissView; import kotlin.Unit; @@ -128,6 +128,16 @@ public class BubbleBarLayerView extends FrameLayout setOnClickListener(view -> hideModalOrCollapse()); } + /** Hides the expanded view drop target. */ + public void hideBubbleBarExpandedViewDropTarget() { + mBubbleExpandedViewPinController.hideDropTarget(); + } + + /** Shows the expanded view drop target at the requested {@link BubbleBarLocation location} */ + public void showBubbleBarExtendedViewDropTarget(@NonNull BubbleBarLocation bubbleBarLocation) { + mBubbleExpandedViewPinController.showDropTarget(bubbleBarLocation); + } + @Override protected void onAttachedToWindow() { super.onAttachedToWindow(); @@ -514,8 +524,8 @@ public class BubbleBarLayerView extends FrameLayout * Skips logging if it is {@link BubbleOverflow}. */ private void logBubbleEvent(BubbleLogger.Event event) { - if (mExpandedBubble != null && mExpandedBubble instanceof Bubble bubble) { - mBubbleLogger.log(bubble, event); + if (mExpandedBubble != null && mExpandedBubble instanceof Bubble) { + mBubbleLogger.log((Bubble) mExpandedBubble, event); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java index 5f437d4af40f..b7761ec75782 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuViewController.java @@ -222,7 +222,7 @@ class BubbleBarMenuViewController { Resources resources = mContext.getResources(); int tintColor = mContext.getColor(com.android.internal.R.color.materialColorOnSurface); - if (bubble.isConversation()) { + if (bubble.isChat()) { // Don't bubble conversation action menuActions.add(new BubbleBarMenuView.MenuAction( Icon.createWithResource(mContext, R.drawable.bubble_ic_stop_bubble), diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt deleted file mode 100644 index 4206d9320b7d..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/BubbleProperties.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 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.bubbles.properties - -/** - * An interface for exposing bubble properties via flags which can be controlled easily in tests. - */ -interface BubbleProperties { - /** - * Whether bubble bar is enabled. - * - * When this is `true`, depending on additional factors, such as screen size and taskbar state, - * bubbles will be displayed in the bubble bar instead of floating. - * - * When this is `false`, bubbles will be floating. - */ - val isBubbleBarEnabled: Boolean - - /** Refreshes the current value of [isBubbleBarEnabled]. */ - fun refresh() -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt deleted file mode 100644 index 33b61b164988..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/properties/ProdBubbleProperties.kt +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright (C) 2023 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.bubbles.properties - -import android.os.SystemProperties -import com.android.wm.shell.Flags - -/** Provides bubble properties in production. */ -object ProdBubbleProperties : BubbleProperties { - - private var _isBubbleBarEnabled = Flags.enableBubbleBar() || - SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) - - override val isBubbleBarEnabled - get() = _isBubbleBarEnabled - - override fun refresh() { - _isBubbleBarEnabled = Flags.enableBubbleBar() || - SystemProperties.getBoolean("persist.wm.debug.bubble_bar", false) - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java index e69d60ddd6c6..97184c859d4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayController.java @@ -39,6 +39,7 @@ import androidx.annotation.BinderThread; import com.android.window.flags.Flags; import com.android.wm.shell.common.DisplayChangeController.OnDisplayChangingListener; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; import java.util.ArrayList; @@ -66,6 +67,7 @@ public class DisplayController { private final SparseArray<DisplayRecord> mDisplays = new SparseArray<>(); private final ArrayList<OnDisplaysChangedListener> mDisplayChangedListeners = new ArrayList<>(); private final Map<Integer, RectF> mUnpopulatedDisplayBounds = new HashMap<>(); + private DisplayTopology mDisplayTopology; public DisplayController(Context context, IWindowManager wmService, ShellInit shellInit, ShellExecutor mainExecutor, DisplayManager displayManager) { @@ -91,7 +93,8 @@ public class DisplayController { onDisplayAdded(displayIds[i]); } - if (Flags.enableConnectedDisplaysWindowDrag()) { + if (Flags.enableConnectedDisplaysWindowDrag() + && DesktopModeStatus.canEnterDesktopMode(mContext)) { mDisplayManager.registerTopologyListener(mMainExecutor, this::onDisplayTopologyChanged); onDisplayTopologyChanged(mDisplayManager.getDisplayTopology()); @@ -155,6 +158,7 @@ public class DisplayController { for (int i = 0; i < mDisplays.size(); ++i) { listener.onDisplayAdded(mDisplays.keyAt(i)); } + listener.onTopologyChanged(mDisplayTopology); } } @@ -243,6 +247,7 @@ public class DisplayController { if (topology == null) { return; } + mDisplayTopology = topology; SparseArray<RectF> absoluteBounds = topology.getAbsoluteBounds(); mUnpopulatedDisplayBounds.clear(); for (int i = 0; i < absoluteBounds.size(); ++i) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java index 94e629a6887f..8377a35a9e7d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java @@ -224,6 +224,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged } } + /** Hides the IME for Bubbles when the device is locked. */ + public void hideImeForBubblesWhenLocked(int displayId) { + PerDisplay pd = mImePerDisplay.get(displayId); + pd.setImeInputTargetRequestedVisibility(false, pd.getImeSourceControl().getImeStatsToken()); + } + /** An implementation of {@link IDisplayWindowInsetsController} for a given display id. */ public class PerDisplay implements DisplayInsetsController.OnInsetsChangedListener { final int mDisplayId; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt new file mode 100644 index 000000000000..41382047945b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/InputChannelSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.view.InputChannel +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<InputChannel>]. This can be used in place of kotlin default + * parameters values [builder = ::InputChannel] which requires the [@JvmOverloads] annotation to + * make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [InputChannel]s. + */ +@WMSingleton +class InputChannelSupplier @Inject constructor() : Supplier<InputChannel> { + override fun get(): InputChannel = InputChannel() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt index 0577f9e625ca..16938647001b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/UserProfileContexts.kt @@ -25,6 +25,7 @@ import android.util.SparseArray import com.android.wm.shell.sysui.ShellController import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.sysui.UserChangeListener +import androidx.core.util.size /** Creates and manages contexts for all the profiles of the current user. */ class UserProfileContexts( @@ -35,6 +36,8 @@ class UserProfileContexts( // Contexts for all the profiles of the current user. private val currentProfilesContext = SparseArray<Context>() + private val shellUserId = baseContext.userId + lateinit var userContext: Context private set @@ -49,6 +52,9 @@ class UserProfileContexts( currentProfilesContext.clear() this@UserProfileContexts.userContext = userContext currentProfilesContext.put(newUserId, userContext) + if (newUserId != shellUserId) { + currentProfilesContext.put(shellUserId, baseContext) + } } override fun onUserProfilesChanged(profiles: List<UserInfo>) { @@ -69,9 +75,9 @@ class UserProfileContexts( currentProfilesContext.put(profile.id, profileContext) } val profilesToRemove = buildList<Int> { - for (i in 0..<currentProfilesContext.size()) { + for (i in 0..<currentProfilesContext.size) { val userId = currentProfilesContext.keyAt(i) - if (profiles.none { it.id == userId }) { + if (userId != shellUserId && profiles.none { it.id == userId }) { add(userId) } } @@ -80,4 +86,12 @@ class UserProfileContexts( } operator fun get(userId: Int): Context? = currentProfilesContext.get(userId) + + fun getOrCreate(userId: Int): Context { + val context = currentProfilesContext[userId] + if (context != null) return context + return baseContext.createContextAsUser(UserHandle.of(userId), /* flags= */ 0).also { + currentProfilesContext[userId] = it + } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt new file mode 100644 index 000000000000..a1d700af5569 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowContainerTransactionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.window.WindowContainerTransaction +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<WindowContainerTransaction>]. This can be used in place of kotlin default + * parameters values [builder = ::WindowContainerTransaction] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [WindowContainerTransaction]s. + */ +@WMSingleton +class WindowContainerTransactionSupplier @Inject constructor( +) : Supplier<WindowContainerTransaction> { + override fun get(): WindowContainerTransaction = WindowContainerTransaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt new file mode 100644 index 000000000000..2c66e97f03e1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/WindowSessionSupplier.kt @@ -0,0 +1,35 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.view.IWindowSession +import android.view.WindowManagerGlobal +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<IWindowSession>]. This can be used in place of kotlin default + * parameters values [builder = WindowManagerGlobal::getWindowSession] which requires the + * [@JvmOverloads] annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default [Supplier] for + * [IWindowSession]s. + */ +@WMSingleton +class WindowSessionSupplier @Inject constructor() : Supplier<IWindowSession> { + override fun get(): IWindowSession = WindowManagerGlobal.getWindowSession() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java new file mode 100644 index 000000000000..c6afc313b239 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/pip/PipDesktopState.java @@ -0,0 +1,159 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.pip; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import android.app.ActivityManager; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.window.flags.Flags; +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.desktopmode.DesktopRepository; +import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; +import com.android.wm.shell.pip2.phone.PipTransition; + +import java.util.Optional; + +/** Helper class for PiP on Desktop Mode. */ +public class PipDesktopState { + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; + private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + + public PipDesktopState(PipDisplayLayoutState pipDisplayLayoutState, + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + mPipDisplayLayoutState = pipDisplayLayoutState; + mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; + mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + } + + /** + * Returns whether PiP in Desktop Windowing is enabled by checking the following: + * - Desktop Windowing in PiP flag is enabled + * - DesktopWallpaperActivityTokenProvider is injected + * - DesktopUserRepositories is injected + */ + public boolean isDesktopWindowingPipEnabled() { + return Flags.enableDesktopWindowingPip() + && mDesktopWallpaperActivityTokenProviderOptional.isPresent() + && mDesktopUserRepositoriesOptional.isPresent(); + } + + /** Returns whether PiP in Connected Displays is enabled by checking the flag. */ + public boolean isConnectedDisplaysPipEnabled() { + return Flags.enableConnectedDisplaysPip(); + } + + /** Returns whether the display with the PiP task is in freeform windowing mode. */ + private boolean isDisplayInFreeform() { + final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( + mPipDisplayLayoutState.getDisplayId()); + if (tdaInfo != null) { + return tdaInfo.configuration.windowConfiguration.getWindowingMode() + == WINDOWING_MODE_FREEFORM; + } + return false; + } + + /** Returns whether PiP is active in a display that is in active Desktop Mode session. */ + public boolean isPipInDesktopMode() { + // Early return if PiP in Desktop Windowing is not supported. + if (!isDesktopWindowingPipEnabled()) { + return false; + } + final int displayId = mPipDisplayLayoutState.getDisplayId(); + return getDesktopRepository().getVisibleTaskCount(displayId) > 0 + || getDesktopWallpaperActivityTokenProvider().isWallpaperActivityVisible(displayId) + || isDisplayInFreeform(); + } + + /** Returns whether {@param pipTask} would be entering in a Desktop Mode session. */ + public boolean isPipEnteringInDesktopMode(ActivityManager.RunningTaskInfo pipTask) { + // Early return if PiP in Desktop Windowing is not supported. + if (!isDesktopWindowingPipEnabled()) { + return false; + } + final DesktopRepository desktopRepository = getDesktopRepository(); + return desktopRepository.getVisibleTaskCount(pipTask.getDisplayId()) > 0 + || desktopRepository.isMinimizedPipPresentInDisplay(pipTask.getDisplayId()); + } + + /** + * Invoked when an EXIT_PiP transition is detected in {@link PipTransition}. + * Returns whether the PiP exiting should also trigger the active Desktop Mode session to exit. + */ + public boolean shouldExitPipExitDesktopMode() { + // Early return if PiP in Desktop Windowing is not supported. + if (!isDesktopWindowingPipEnabled()) { + return false; + } + final int displayId = mPipDisplayLayoutState.getDisplayId(); + return getDesktopRepository().getVisibleTaskCount(displayId) == 0 + && getDesktopWallpaperActivityTokenProvider().isWallpaperActivityVisible(displayId); + } + + /** + * Returns a {@link WindowContainerTransaction} that reorders the {@link WindowContainerToken} + * of the DesktopWallpaperActivity for the display with the given {@param displayId}. + */ + public WindowContainerTransaction getWallpaperActivityTokenWct(int displayId) { + return new WindowContainerTransaction().reorder( + getDesktopWallpaperActivityTokenProvider().getToken(displayId), /* onTop= */ false); + } + + /** + * The windowing mode to restore to when resizing out of PIP direction. + * Defaults to undefined and can be overridden to restore to an alternate windowing mode. + */ + public int getOutPipWindowingMode() { + // If we are exiting PiP while the device is in Desktop mode (the task should expand to + // freeform windowing mode): + // 1) If the display windowing mode is freeform, set windowing mode to UNDEFINED so it will + // resolve the windowing mode to the display's windowing mode. + // 2) If the display windowing mode is not FREEFORM, set windowing mode to FREEFORM. + if (isPipInDesktopMode()) { + if (isDisplayInFreeform()) { + return WINDOWING_MODE_UNDEFINED; + } else { + return WINDOWING_MODE_FREEFORM; + } + } + + // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. + return WINDOWING_MODE_UNDEFINED; + } + + private DesktopRepository getDesktopRepository() { + return mDesktopUserRepositoriesOptional.get().getCurrent(); + } + + private DesktopWallpaperActivityTokenProvider getDesktopWallpaperActivityTokenProvider() { + return mDesktopWallpaperActivityTokenProviderOptional.get(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java new file mode 100644 index 000000000000..fb2a324375b6 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/CenterParallaxSpec.java @@ -0,0 +1,39 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_ALIGN_CENTER} is the desired + * parallax effect. + */ +public class CenterParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = (retreatingSurface.width() - retreatingContent.width()) / 2; + } else { + retreatingOut.y = (retreatingSurface.height() - retreatingContent.height()) / 2; + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java new file mode 100644 index 000000000000..39ecbb379d7d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DismissingParallaxSpec.java @@ -0,0 +1,75 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; + +import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; +import android.view.WindowManager; + +/** + * Calculation class, used when + * {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_DISMISSING} is the desired parallax + * effect. + */ +public class DismissingParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + if (dimmingSide == DOCKED_INVALID) { + return; + } + + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + int totalDismissingDistance = 0; + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getDismissStartTarget().getPosition() + - snapAlgorithm.getFirstSplitTarget().getPosition(); + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + totalDismissingDistance = snapAlgorithm.getLastSplitTarget().getPosition() + - snapAlgorithm.getDismissEndTarget().getPosition(); + } + + float parallaxFraction = + calculateParallaxDismissingFraction(progressTowardScreenEdge, dimmingSide); + if (isLeftRightSplit) { + retreatingOut.x = (int) (parallaxFraction * totalDismissingDistance); + } else { + retreatingOut.y = (int) (parallaxFraction * totalDismissingDistance); + } + } + + /** + * @return for a specified {@code fraction}, this returns an adjusted value that simulates a + * slowing down parallax effect + */ + private float calculateParallaxDismissingFraction(float fraction, int dockSide) { + float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; + + // Less parallax at the top, just because. + if (dockSide == WindowManager.DOCKED_TOP) { + result /= 2f; + } + return result; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java index 2f5afcaa907b..5b2dd97a338f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerSnapAlgorithm.java @@ -465,5 +465,9 @@ public class DividerSnapAlgorithm { this.snapPosition = snapPosition; this.distanceMultiplier = distanceMultiplier; } + + public int getPosition() { + return position; + } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java index 2c418d34f09a..06044ccc1c61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/DividerView.java @@ -125,11 +125,13 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { } }; - private final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { + final AccessibilityDelegate mHandleDelegate = new AccessibilityDelegate() { @Override public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) { super.onInitializeAccessibilityNodeInfo(host, info); final DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; + info.addAction(new AccessibilityAction(R.id.action_swap_apps, + mContext.getString(R.string.accessibility_action_divider_swap))); if (mSplitLayout.isLeftRightSplit()) { info.addAction(new AccessibilityAction(R.id.action_move_tl_full, mContext.getString(R.string.accessibility_action_divider_left_full))); @@ -172,6 +174,11 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { @Override public boolean performAccessibilityAction(@NonNull View host, int action, @Nullable Bundle args) { + if (action == R.id.action_swap_apps) { + mSplitLayout.onDoubleTappedDivider(); + return true; + } + DividerSnapAlgorithm.SnapTarget nextTarget = null; DividerSnapAlgorithm snapAlgorithm = mSplitLayout.mDividerSnapAlgorithm; if (action == R.id.action_move_tl_full) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java new file mode 100644 index 000000000000..9fa162164e0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/FlexParallaxSpec.java @@ -0,0 +1,172 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; +import static com.android.wm.shell.shared.animation.Interpolators.FAST_DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_FLEX} + * is the desired parallax effect. + */ +public class FlexParallaxSpec implements ParallaxSpec { + final Rect mTempRect = new Rect(); + + @Override + public int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getMiddleTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** + * Calculates the amount of dim to apply to a task surface moving offscreen in flexible split. + * In flexible split, there are two dimming "behaviors". + * 1) "slow dim": when moving the divider from the middle of the screen to a target at 10% or + * 90%, we dim the app slightly as it moves partially offscreen. + * 2) "fast dim": when moving the divider from a side snap target further toward the screen + * edge, we dim the app rapidly as it approaches the dismiss point. + * @return 0f = no dim applied. 1f = full black. + */ + public float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + int startDismissPos = snapAlgorithm.getDismissStartTarget().getPosition(); + int firstTargetPos = snapAlgorithm.getFirstSplitTarget().getPosition(); + int middleTargetPos = snapAlgorithm.getMiddleTarget().getPosition(); + int lastTargetPos = snapAlgorithm.getLastSplitTarget().getPosition(); + int endDismissPos = snapAlgorithm.getDismissEndTarget().getPosition(); + float progress; + + if (startDismissPos <= position && position < firstTargetPos) { + // Divider is on the left/top (between 0% and 10% of screen), "fast dim" as it moves + // toward the screen edge + progress = (float) (firstTargetPos - position) / (firstTargetPos - startDismissPos); + return fastDim(progress); + } else if (firstTargetPos <= position && position < middleTargetPos) { + // Divider is between 10% and 50%, "slow dim" as it moves toward the left/top target + progress = (float) (middleTargetPos - position) / (middleTargetPos - firstTargetPos); + return slowDim(progress); + } else if (middleTargetPos <= position && position < lastTargetPos) { + // Divider is between 50% and 90%, "slow dim" as it moves toward the right/bottom target + progress = (float) (position - middleTargetPos) / (lastTargetPos - middleTargetPos); + return slowDim(progress); + } else if (lastTargetPos <= position && position <= endDismissPos) { + // Divider is on the right/bottom (between 90% and 100% of screen), "fast dim" as it + // moves toward screen edge + progress = (float) (position - lastTargetPos) / (endDismissPos - lastTargetPos); + return fastDim(progress); + } + return 0f; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at zero and ramps + * up to the default amount of dimming for an offscreen app, + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM}. + */ + private float slowDim(float progress) { + return DIM_INTERPOLATOR.getInterpolation(progress) * DEFAULT_OFFSCREEN_DIM; + } + + /** + * Used by {@link #getDimValue} to determine the amount to dim an app. Starts at + * {@link ResizingEffectPolicy#DEFAULT_OFFSCREEN_DIM} and ramps up to 100% dim (full black). + */ + private float fastDim(float progress) { + return DEFAULT_OFFSCREEN_DIM + (FAST_DIM_INTERPOLATOR.getInterpolation(progress) + * (1 - DEFAULT_OFFSCREEN_DIM)); + } + + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // Whether an app is getting pushed offscreen by the divider. + boolean isRetreatingOffscreen = !displayBounds.contains(retreatingSurface); + // Whether an app was getting pulled onscreen at the beginning of the drag. + boolean advancingSideStartedOffscreen = !displayBounds.contains(advancingContent); + + // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) + if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { + // On the left side, we use parallax to simulate the contents sticking to the + // divider. This is because surfaces naturally expand to the bottom and right, + // so when a surface's area expands, the contents stick to the left. This is + // correct behavior on the right-side surface, but not the left. + if (topLeftShrink) { + if (isLeftRightSplit) { + retreatingOut.x = retreatingSurface.width() - retreatingContent.width(); + } else { + retreatingOut.y = retreatingSurface.height() - retreatingContent.height(); + } + } + // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) + } else { + mTempRect.set(retreatingSurface); + Point rootOffset = new Point(); + // 10:90 -> 50:50, 10:90, or dismiss right + if (advancingSideStartedOffscreen) { + // We have to handle a complicated case here to keep the parallax smooth. + // When the divider crosses the 50% mark, the retreating-side app surface + // will start expanding offscreen. This is expected and unavoidable, but + // makes the parallax look disjointed. In order to preserve the illusion, + // we add another offset (rootOffset) to simulate the surface staying + // onscreen. + if (mTempRect.intersect(displayBounds)) { + if (retreatingSurface.left < displayBounds.left) { + rootOffset.x = displayBounds.left - retreatingSurface.left; + } + if (retreatingSurface.top < displayBounds.top) { + rootOffset.y = displayBounds.top - retreatingSurface.top; + } + } + + // On the left side, we again have to simulate the contents sticking to the + // divider. + if (!topLeftShrink) { + if (isLeftRightSplit) { + advancingOut.x = advancingSurface.width() - advancingContent.width(); + } else { + advancingOut.y = advancingSurface.height() - advancingContent.height(); + } + } + } + + // In all these cases, the shrinking app also receives a center parallax. + if (isLeftRightSplit) { + retreatingOut.x = rootOffset.x + + ((mTempRect.width() - retreatingContent.width()) / 2); + } else { + retreatingOut.y = rootOffset.y + + ((mTempRect.height() - retreatingContent.height()) / 2); + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java new file mode 100644 index 000000000000..043b2880f28b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/NoParallaxSpec.java @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Calculation class, used when {@link com.android.wm.shell.common.split.SplitLayout#PARALLAX_NONE} + * is the desired parallax effect. + */ +public class NoParallaxSpec implements ParallaxSpec { + @Override + public void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink) { + // no-op + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java new file mode 100644 index 000000000000..84d849b3c1f9 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ParallaxSpec.java @@ -0,0 +1,62 @@ +/* + * 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.common.split; + +import static android.view.WindowManager.DOCKED_BOTTOM; +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; +import static android.view.WindowManager.DOCKED_TOP; + +import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; + +import android.graphics.Point; +import android.graphics.Rect; + +/** + * Default interface for a set of calculation classes, used for calculating various parallax and + * dimming effects in split screen. + */ +public interface ParallaxSpec { + /** Returns an int indicating which side of the screen is being dimmed (if any). */ + default int getDimmingSide(int position, DividerSnapAlgorithm snapAlgorithm, + boolean isLeftRightSplit) { + if (position < snapAlgorithm.getFirstSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; + } else if (position > snapAlgorithm.getLastSplitTarget().getPosition()) { + return isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; + } + return DOCKED_INVALID; + } + + /** Returns the dim amount that we'll apply to the app surface. 0f = no dim, 1f = full black */ + default float getDimValue(int position, DividerSnapAlgorithm snapAlgorithm) { + float progressTowardScreenEdge = + Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); + return DIM_INTERPOLATOR.getInterpolation(progressTowardScreenEdge); + } + + /** + * Calculates the amount to offset app surfaces to create nice parallax effects. Writes to + * {@link ResizingEffectPolicy#mRetreatingSideParallax} and + * {@link ResizingEffectPolicy#mAdvancingSideParallax}. + */ + void getParallax(Point retreatingOut, Point advancingOut, int position, + DividerSnapAlgorithm snapAlgorithm, boolean isLeftRightSplit, Rect displayBounds, + Rect retreatingSurface, Rect retreatingContent, Rect advancingSurface, + Rect advancingContent, int dimmingSide, boolean topLeftShrink); +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java index 3f76fd0220ff..e2e1f9698a90 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/ResizingEffectPolicy.java @@ -26,27 +26,32 @@ import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTE import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_DISMISSING; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_NONE; -import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; -import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import android.graphics.Point; import android.graphics.Rect; import android.view.SurfaceControl; -import android.view.WindowManager; /** * This class governs how and when parallax and dimming effects are applied to task surfaces, * usually when the divider is being moved around by the user (or during an animation). */ class ResizingEffectPolicy { + /** The default amount to dim an app that is partially offscreen. */ + public static float DEFAULT_OFFSCREEN_DIM = 0.32f; + private final SplitLayout mSplitLayout; /** The parallax algorithm we are currently using. */ private final int mParallaxType; + /** + * A convenience class, corresponding to {@link #mParallaxType}, that performs all the + * calculations for parallax and dimming values. + */ + private final ParallaxSpec mParallaxSpec; int mShrinkSide = DOCKED_INVALID; // The current dismissing side. - int mDismissingSide = DOCKED_INVALID; + int mDimmingSide = DOCKED_INVALID; /** * A {@link Point} that stores a single x and y value, representing the parallax translation @@ -62,7 +67,7 @@ class ResizingEffectPolicy { final Point mAdvancingSideParallax = new Point(); // The dimming value to hint the dismissing side and progress. - float mDismissingDimValue = 0.0f; + float mDimValue = 0.0f; /** * Content bounds for the app that the divider is moving toward. This is the content that is @@ -95,35 +100,38 @@ class ResizingEffectPolicy { ResizingEffectPolicy(int parallaxType, SplitLayout splitLayout) { mParallaxType = parallaxType; mSplitLayout = splitLayout; + switch (mParallaxType) { + case PARALLAX_DISMISSING: + mParallaxSpec = new DismissingParallaxSpec(); + break; + case PARALLAX_ALIGN_CENTER: + mParallaxSpec = new CenterParallaxSpec(); + break; + case PARALLAX_FLEX: + mParallaxSpec = new FlexParallaxSpec(); + break; + case PARALLAX_NONE: + default: + mParallaxSpec = new NoParallaxSpec(); + break; + } } /** - * Calculates the desired parallax values and stores them in {@link #mRetreatingSideParallax} - * and {@link #mAdvancingSideParallax}. These values will be then be applied in - * {@link #adjustRootSurface}. - * - * @param position The divider's position on the screen (x-coordinate in left-right split, - * y-coordinate in top-bottom split). + * Calculates the desired parallax and dimming values for a task surface and stores them in + * {@link #mRetreatingSideParallax}, {@link #mAdvancingSideParallax}, and + * {@link #mDimValue} These values will be then be applied in + * {@link #adjustRootSurface} and {@link #adjustDimSurface} respectively. */ void applyDividerPosition( int position, boolean isLeftRightSplit, DividerSnapAlgorithm snapAlgorithm) { - mDismissingSide = DOCKED_INVALID; + mDimmingSide = DOCKED_INVALID; mRetreatingSideParallax.set(0, 0); mAdvancingSideParallax.set(0, 0); - mDismissingDimValue = 0; + mDimValue = 0; Rect displayBounds = mSplitLayout.getRootBounds(); - int totalDismissingDistance = 0; - if (position < snapAlgorithm.getFirstSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP; - totalDismissingDistance = snapAlgorithm.getDismissStartTarget().position - - snapAlgorithm.getFirstSplitTarget().position; - } else if (position > snapAlgorithm.getLastSplitTarget().position) { - mDismissingSide = isLeftRightSplit ? DOCKED_RIGHT : DOCKED_BOTTOM; - totalDismissingDistance = snapAlgorithm.getLastSplitTarget().position - - snapAlgorithm.getDismissEndTarget().position; - } - + // Figure out which side is shrinking, and assign retreating/advancing bounds final boolean topLeftShrink = isLeftRightSplit ? position < mSplitLayout.getTopLeftContentBounds().right : position < mSplitLayout.getTopLeftContentBounds().bottom; @@ -141,106 +149,20 @@ class ResizingEffectPolicy { mAdvancingSurface.set(mSplitLayout.getTopLeftBounds()); } - if (mDismissingSide != DOCKED_INVALID) { - float fraction = - Math.max(0, Math.min(snapAlgorithm.calculateDismissingFraction(position), 1f)); - mDismissingDimValue = DIM_INTERPOLATOR.getInterpolation(fraction); - if (mParallaxType == PARALLAX_DISMISSING) { - fraction = calculateParallaxDismissingFraction(fraction, mDismissingSide); - if (isLeftRightSplit) { - mRetreatingSideParallax.x = (int) (fraction * totalDismissingDistance); - } else { - mRetreatingSideParallax.y = (int) (fraction * totalDismissingDistance); - } - } - } - - if (mParallaxType == PARALLAX_ALIGN_CENTER) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - (mRetreatingSurface.width() - mRetreatingContent.width()) / 2; - } else { - mRetreatingSideParallax.y = - (mRetreatingSurface.height() - mRetreatingContent.height()) / 2; - } - } else if (mParallaxType == PARALLAX_FLEX) { - // Whether an app is getting pushed offscreen by the divider. - boolean isRetreatingOffscreen = !displayBounds.contains(mRetreatingSurface); - // Whether an app was getting pulled onscreen at the beginning of the drag. - boolean advancingSideStartedOffscreen = !displayBounds.contains(mAdvancingContent); + // Figure out if we should be dimming one side + mDimmingSide = mParallaxSpec.getDimmingSide(position, snapAlgorithm, isLeftRightSplit); - // The simpler case when an app gets pushed offscreen (e.g. 50:50 -> 90:10) - if (isRetreatingOffscreen && !advancingSideStartedOffscreen) { - // On the left side, we use parallax to simulate the contents sticking to the - // divider. This is because surfaces naturally expand to the bottom and right, - // so when a surface's area expands, the contents stick to the left. This is - // correct behavior on the right-side surface, but not the left. - if (topLeftShrink) { - if (isLeftRightSplit) { - mRetreatingSideParallax.x = - mRetreatingSurface.width() - mRetreatingContent.width(); - } else { - mRetreatingSideParallax.y = - mRetreatingSurface.height() - mRetreatingContent.height(); - } - } - // All other cases (e.g. 10:90 -> 50:50, 10:90 -> 90:10, 10:90 -> dismiss) - } else { - mTempRect.set(mRetreatingSurface); - Point rootOffset = new Point(); - // 10:90 -> 50:50, 10:90, or dismiss right - if (advancingSideStartedOffscreen) { - // We have to handle a complicated case here to keep the parallax smooth. - // When the divider crosses the 50% mark, the retreating-side app surface - // will start expanding offscreen. This is expected and unavoidable, but - // makes the parallax look disjointed. In order to preserve the illusion, - // we add another offset (rootOffset) to simulate the surface staying - // onscreen. - mTempRect.intersect(displayBounds); - if (mRetreatingSurface.left < displayBounds.left) { - rootOffset.x = displayBounds.left - mRetreatingSurface.left; - } - if (mRetreatingSurface.top < displayBounds.top) { - rootOffset.y = displayBounds.top - mRetreatingSurface.top; - } - - // On the left side, we again have to simulate the contents sticking to the - // divider. - if (!topLeftShrink) { - if (isLeftRightSplit) { - mAdvancingSideParallax.x = - mAdvancingSurface.width() - mAdvancingContent.width(); - } else { - mAdvancingSideParallax.y = - mAdvancingSurface.height() - mAdvancingContent.height(); - } - } - } - - // In all these cases, the shrinking app also receives a center parallax. - if (isLeftRightSplit) { - mRetreatingSideParallax.x = rootOffset.x - + ((mTempRect.width() - mRetreatingContent.width()) / 2); - } else { - mRetreatingSideParallax.y = rootOffset.y - + ((mTempRect.height() - mRetreatingContent.height()) / 2); - } - } + // If so, calculate dimming + if (mDimmingSide != DOCKED_INVALID) { + mDimValue = mParallaxSpec.getDimValue(position, snapAlgorithm); } - } - /** - * @return for a specified {@code fraction}, this returns an adjusted value that simulates a - * slowing down parallax effect - */ - private float calculateParallaxDismissingFraction(float fraction, int dockSide) { - float result = SLOWDOWN_INTERPOLATOR.getInterpolation(fraction) / 3.5f; - - // Less parallax at the top, just because. - if (dockSide == WindowManager.DOCKED_TOP) { - result /= 2f; - } - return result; + // Calculate parallax and modify mRetreatingSideParallax and mAdvancingSideParallax, for use + // in adjustRootSurface(). + mParallaxSpec.getParallax(mRetreatingSideParallax, mAdvancingSideParallax, position, + snapAlgorithm, isLeftRightSplit, displayBounds, mRetreatingSurface, + mRetreatingContent, mAdvancingSurface, mAdvancingContent, mDimmingSide, + topLeftShrink); } /** Applies the calculated parallax and dimming values to task surfaces. */ @@ -250,7 +172,7 @@ class ResizingEffectPolicy { SurfaceControl advancingLeash = null; if (mParallaxType == PARALLAX_DISMISSING) { - switch (mDismissingSide) { + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: retreatingLeash = leash1; @@ -303,14 +225,17 @@ class ResizingEffectPolicy { void adjustDimSurface(SurfaceControl.Transaction t, SurfaceControl dimLayer1, SurfaceControl dimLayer2) { SurfaceControl targetDimLayer; - switch (mDismissingSide) { + SurfaceControl oppositeDimLayer; + switch (mDimmingSide) { case DOCKED_TOP: case DOCKED_LEFT: targetDimLayer = dimLayer1; + oppositeDimLayer = dimLayer2; break; case DOCKED_BOTTOM: case DOCKED_RIGHT: targetDimLayer = dimLayer2; + oppositeDimLayer = dimLayer1; break; case DOCKED_INVALID: default: @@ -318,7 +243,9 @@ class ResizingEffectPolicy { t.setAlpha(dimLayer2, 0).hide(dimLayer2); return; } - t.setAlpha(targetDimLayer, mDismissingDimValue) - .setVisibility(targetDimLayer, mDismissingDimValue > 0.001f); + t.setAlpha(targetDimLayer, mDimValue) + .setVisibility(targetDimLayer, mDimValue > 0.001f); + t.setAlpha(oppositeDimLayer, 0f) + .setVisibility(oppositeDimLayer, false); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java index cd5c135691d7..708e26cc5546 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitLayout.java @@ -128,6 +128,8 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // The touch layer is on a stage root, and is sibling with things like the app activity itself // and the app veil. We want it to be above all those. public static final int RESTING_TOUCH_LAYER = Integer.MAX_VALUE; + // The dim layer is also on the stage root, and stays under the touch layer. + public static final int RESTING_DIM_LAYER = RESTING_TOUCH_LAYER - 1; // Animation specs for the swap animation private static final int SWAP_ANIMATION_TOTAL_DURATION = 500; @@ -394,11 +396,19 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Returns the divider position as a fraction from 0 to 1. */ public float getDividerPositionAsFraction() { - return Math.min(1f, Math.max(0f, mIsLeftRightSplit - ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) - / getBottomRightBounds().right - : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) - / getBottomRightBounds().bottom)); + if (Flags.enableFlexibleTwoAppSplit()) { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (getTopLeftBounds().right + getBottomRightBounds().left) / 2f + / getDisplayWidth() + : (getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f + / getDisplayHeight())); + } else { + return Math.min(1f, Math.max(0f, mIsLeftRightSplit + ? (float) ((getTopLeftBounds().right + getBottomRightBounds().left) / 2f) + / getBottomRightBounds().right + : (float) ((getTopLeftBounds().bottom + getBottomRightBounds().top) / 2f) + / getBottomRightBounds().bottom)); + } } private void updateInvisibleRect() { @@ -1193,6 +1203,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // Resets layer of divider bar to make sure it is always on top. t.setLayer(dividerLeash, RESTING_DIVIDER_LAYER); } + if (dimLayer1 != null) { + t.setLayer(dimLayer1, RESTING_DIM_LAYER); + } + if (dimLayer2 != null) { + t.setLayer(dimLayer2, RESTING_DIM_LAYER); + } copyTopLeftRefBounds(mTempRect); t.setPosition(leash1, mTempRect.left, mTempRect.top) .setWindowCrop(leash1, mTempRect.width(), mTempRect.height()); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java index d1d133d16ae4..ad0e7fc187e9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitState.java @@ -57,4 +57,9 @@ public class SplitState { public List<RectF> getLayout(@SplitScreenState int state) { return mSplitSpec.getSpec(state); } + + /** Returns the layout associated with the current split state. */ + public List<RectF> getCurrentLayout() { + return getLayout(mState); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt new file mode 100644 index 000000000000..0b6c06ac5649 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.view.SurfaceControl +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<SurfaceControl.Builder>]. This can be used in place of kotlin default + * parameters values [builder = ::SurfaceControl.Builder] which requires the [@JvmOverloads] + * annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default builder for + * [SurfaceControl]s. + */ +@WMSingleton +class SurfaceBuilderSupplier @Inject constructor() : Supplier<SurfaceControl.Builder> { + override fun get(): SurfaceControl.Builder = SurfaceControl.Builder() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt new file mode 100644 index 000000000000..2d9899b4fccf --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/transition/TransactionSupplier.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.view.SurfaceControl +import com.android.wm.shell.dagger.WMSingleton +import java.util.function.Supplier +import javax.inject.Inject + +/** + * An Injectable [Supplier<SurfaceControl.Transaction>]. This can be used in place of kotlin default + * parameters values [builder = ::SurfaceControl.Transaction] which requires the [@JvmOverloads] + * annotation to make this available in Java. + * This can be used every time a component needs the dependency to the default builder for + * [SurfaceControl.Transaction]s. + */ +@WMSingleton +class TransactionSupplier @Inject constructor() : Supplier<SurfaceControl.Transaction> { + override fun get(): SurfaceControl.Transaction = SurfaceControl.Transaction() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java index 1323fe0fa9ca..201870fe0181 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUIController.java @@ -37,9 +37,9 @@ import android.view.Display; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.accessibility.AccessibilityManager; +import android.window.DesktopModeFlags; import com.android.internal.annotations.VisibleForTesting; -import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayController.OnDisplaysChangedListener; @@ -71,7 +71,6 @@ import java.util.Optional; import java.util.Set; import java.util.function.Consumer; import java.util.function.Function; -import java.util.function.IntPredicate; import java.util.function.Predicate; /** @@ -874,6 +873,7 @@ public class CompatUIController implements OnDisplaysChangedListener, } boolean isDesktopModeShowing = mDesktopUserRepositories.get().getCurrent() .getVisibleTaskCount(taskInfo.displayId) > 0; - return Flags.skipCompatUiEducationInDesktopMode() && isDesktopModeShowing; + return DesktopModeFlags.ENABLE_DESKTOP_SKIP_COMPAT_UI_EDUCATION_IN_DESKTOP_MODE_BUGFIX + .isTrue() && isDesktopModeShowing; } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt new file mode 100644 index 000000000000..f7afbb5bdaef --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureListener.kt @@ -0,0 +1,65 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.view.GestureDetector.OnContextClickListener +import android.view.GestureDetector.OnDoubleTapListener +import android.view.GestureDetector.OnGestureListener +import android.view.MotionEvent + +/** + * Interface which unions all the interfaces related to gestures. + */ +interface LetterboxGestureListener : OnGestureListener, OnDoubleTapListener, OnContextClickListener + +/** + * Convenience class which provide an overrideable implementation of + * {@link LetterboxGestureListener}. + */ +object LetterboxGestureDelegate : LetterboxGestureListener { + override fun onDown(e: MotionEvent): Boolean = false + + override fun onShowPress(e: MotionEvent) { + } + + override fun onSingleTapUp(e: MotionEvent): Boolean = false + + override fun onScroll( + e1: MotionEvent?, + e2: MotionEvent, + distanceX: Float, + distanceY: Float + ): Boolean = false + + override fun onLongPress(e: MotionEvent) { + } + + override fun onFling( + e1: MotionEvent?, + e2: MotionEvent, + velocityX: Float, + velocityY: Float + ): Boolean = false + + override fun onSingleTapConfirmed(e: MotionEvent): Boolean = false + + override fun onDoubleTap(e: MotionEvent): Boolean = false + + override fun onDoubleTapEvent(e: MotionEvent): Boolean = false + + override fun onContextClick(e: MotionEvent): Boolean = false +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt new file mode 100644 index 000000000000..afd8e1519d24 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputController.kt @@ -0,0 +1,113 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Rect +import android.graphics.Region +import android.os.Handler +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxUtils.Maps.runOnItem +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT +import java.util.function.Supplier +import javax.inject.Inject + +/** + * [LetterboxController] implementation responsible for handling the spy [SurfaceControl] we use + * to detect letterbox events. + */ +@WMSingleton +class LetterboxInputController @Inject constructor( + private val context: Context, + private val handler: Handler, + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + private val listenerSupplier: Supplier<LetterboxGestureListener>, + private val windowSessionSupplier: WindowSessionSupplier, + private val inputChannelSupplier: InputChannelSupplier +) : LetterboxController { + + companion object { + @JvmStatic + private val TAG = "LetterboxInputController" + } + + private val inputDetectorMap = mutableMapOf<LetterboxKey, LetterboxInputDetector>() + + override fun createLetterboxSurface( + key: LetterboxKey, + transaction: Transaction, + parentLeash: SurfaceControl + ) { + inputDetectorMap.runOnItem(key, onMissed = { k, m -> + m[k] = + LetterboxInputDetector( + context, + handler, + listenerSupplier.get(), + inputSurfaceBuilder, + windowSessionSupplier, + inputChannelSupplier + ).apply { + start(transaction, parentLeash, key) + } + }) + } + + override fun destroyLetterboxSurface( + key: LetterboxKey, + transaction: Transaction + ) { + with(inputDetectorMap) { + runOnItem(key, onFound = { item -> + item.stop(transaction) + }) + remove(key) + } + } + + override fun updateLetterboxSurfaceVisibility( + key: LetterboxKey, + transaction: Transaction, + visible: Boolean + ) { + with(inputDetectorMap) { + runOnItem(key, onFound = { item -> + item.updateVisibility(transaction, visible) + }) + } + } + + override fun updateLetterboxSurfaceBounds( + key: LetterboxKey, + transaction: Transaction, + taskBounds: Rect, + activityBounds: Rect + ) { + inputDetectorMap.runOnItem(key, onFound = { item -> + item.updateTouchableRegion(transaction, Region(taskBounds)) + }) + } + + override fun dump() { + ProtoLog.v(WM_SHELL_APP_COMPAT, "%s: %s", TAG, "${inputDetectorMap.keys}") + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt new file mode 100644 index 000000000000..812cc0161aae --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputDetector.kt @@ -0,0 +1,230 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Region +import android.os.Binder +import android.os.Handler +import android.os.IBinder +import android.os.RemoteException +import android.view.GestureDetector +import android.view.IWindowSession +import android.view.InputChannel +import android.view.InputEvent +import android.view.InputEventReceiver +import android.view.MotionEvent +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import android.view.WindowManager +import android.window.InputTransferToken +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT + +/** + * This is responsible for detecting events on a given [SurfaceControl]. + */ +class LetterboxInputDetector( + private val context: Context, + private val handler: Handler, + private val listener: LetterboxGestureListener, + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + private val windowSessionSupplier: WindowSessionSupplier, + private val inputChannelSupplier: InputChannelSupplier +) { + + companion object { + @JvmStatic + private val TAG = "LetterboxInputDetector" + } + + private var state: InputDetectorState? = null + + fun start(tx: Transaction, source: SurfaceControl, key: LetterboxKey) { + if (!isRunning()) { + val tmpState = + InputDetectorState( + context, + handler, + source, + key.displayId, + listener, + inputSurfaceBuilder, + windowSessionSupplier.get(), + inputChannelSupplier + ) + if (tmpState.start(tx)) { + state = tmpState + } else { + ProtoLog.v( + WM_SHELL_APP_COMPAT, + "%s not started for %s on %s", + TAG, + "$source", + "$key" + ) + } + } + } + + fun updateTouchableRegion(tx: Transaction, region: Region) { + if (isRunning()) { + state?.setTouchableRegion(tx, region) + } + } + + fun isRunning() = state != null + + fun updateVisibility(tx: Transaction, visible: Boolean) { + if (isRunning()) { + state?.updateVisibility(tx, visible) + } + } + + fun stop(tx: Transaction) { + if (isRunning()) { + state!!.stop(tx) + state = null + } + } + + /** + * The state for a {@link SurfaceControl} for a given displayId. + */ + private class InputDetectorState( + val context: Context, + val handler: Handler, + val source: SurfaceControl, + val displayId: Int, + val listener: LetterboxGestureListener, + val inputSurfaceBuilder: LetterboxInputSurfaceBuilder, + val windowSession: IWindowSession, + inputChannelSupplier: InputChannelSupplier + ) { + + private val inputToken: IBinder + private val inputChannel: InputChannel + private var receiver: EventReceiver? = null + private var inputSurface: SurfaceControl? = null + + init { + inputToken = Binder() + inputChannel = inputChannelSupplier.get() + } + + fun start(tx: Transaction): Boolean { + val inputTransferToken = InputTransferToken() + try { + inputSurface = + inputSurfaceBuilder.createInputSurface( + tx, + source, + "Sink for $source", + "$TAG creation" + ) + windowSession.grantInputChannel( + displayId, + inputSurface, + inputToken, + null, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY, + WindowManager.LayoutParams.INPUT_FEATURE_SPY, + WindowManager.LayoutParams.TYPE_INPUT_CONSUMER, + null, + inputTransferToken, + "$TAG of $source", + inputChannel + ) + + receiver = EventReceiver(context, inputChannel, handler, listener) + return true + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + return false + } + + fun setTouchableRegion(tx: Transaction, region: Region) { + try { + tx.setWindowCrop(inputSurface, region.bounds.width(), region.bounds.height()) + + windowSession.updateInputChannel( + inputChannel.token, + displayId, + inputSurface, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY, + WindowManager.LayoutParams.INPUT_FEATURE_SPY, + region + ) + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + } + + fun updateVisibility(tx: Transaction, visible: Boolean) { + inputSurface?.let { + tx.setVisibility(it, visible) + } + } + + fun stop(tx: Transaction) { + receiver?.dispose() + receiver = null + inputChannel.dispose() + windowSession.removeToken(inputToken) + inputSurface?.let { s -> + tx.remove(s) + } + } + + // Removes the provided token + private fun IWindowSession.removeToken(token: IBinder) { + try { + remove(token) + } catch (e: RemoteException) { + e.rethrowFromSystemServer() + } + } + } + + /** + * Reads from the provided {@link InputChannel} and identifies a specific event. + */ + private class EventReceiver( + context: Context, + inputChannel: InputChannel, + uiHandler: Handler, + listener: LetterboxGestureListener + ) : InputEventReceiver(inputChannel, uiHandler.looper) { + private val eventDetector: GestureDetector + + init { + eventDetector = GestureDetector( + context, listener, + uiHandler + ) + } + + override fun onInputEvent(event: InputEvent) { + finishInputEvent(event, eventDetector.onTouchEvent(event as MotionEvent)) + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt new file mode 100644 index 000000000000..fd8d86576115 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxInputSurfaceBuilder.kt @@ -0,0 +1,58 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.view.SurfaceControl +import android.view.SurfaceControl.Transaction +import com.android.wm.shell.common.transition.SurfaceBuilderSupplier +import com.android.wm.shell.dagger.WMSingleton +import javax.inject.Inject + +/** + * Component responsible for the actual creation of the Letterbox surfaces. + */ +@WMSingleton +class LetterboxInputSurfaceBuilder @Inject constructor( + private val surfaceBuilderSupplier: SurfaceBuilderSupplier +) { + + companion object { + /* + * Letterbox spy surfaces need to stay above the activity layer which is 0. + */ + // TODO(b/378673153): Consider adding this to [TaskConstants]. + @JvmStatic + private val TASK_CHILD_LAYER_LETTERBOX_SPY = 1000 + } + + fun createInputSurface( + tx: Transaction, + parentLeash: SurfaceControl, + surfaceName: String, + callSite: String + ) = surfaceBuilderSupplier.get() + .setName(surfaceName) + .setContainerLayer() + .setParent(parentLeash) + .setCallsite(callSite) + .build().apply { + tx.setLayer(this, TASK_CHILD_LAYER_LETTERBOX_SPY) + .setTrustedOverlay(this, true) + .show(this) + .apply() + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt new file mode 100644 index 000000000000..bdffcf51e7d4 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListener.kt @@ -0,0 +1,66 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.view.GestureDetector +import android.view.MotionEvent +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY + +/** + * [GestureDetector.SimpleOnGestureListener] implementation which receives events from the + * Letterbox Input surface, understands the type of event and filter them based on the current + * letterbox position. + */ +class ReachabilityGestureListener( + private val taskId: Int, + private val token: WindowContainerToken?, + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) : GestureDetector.SimpleOnGestureListener() { + + // The current letterbox bounds. Double tap events are ignored when happening in these bounds. + private val activityBounds = Rect() + + override fun onDoubleTap(e: MotionEvent): Boolean { + val x = e.rawX.toInt() + val y = e.rawY.toInt() + if (!activityBounds.contains(x, y)) { + val wct = wctSupplier.get().apply { + setReachabilityOffset(token!!, taskId, x, y) + } + transitions.startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + return true + } + return false + } + + /** + * Updates the bounds for the letterboxed activity. + */ + fun updateActivityBounds(newActivityBounds: Rect) { + activityBounds.set(newActivityBounds) + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt new file mode 100644 index 000000000000..5e9fe09bc840 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactory.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.window.WindowContainerToken +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.dagger.WMSingleton +import com.android.wm.shell.transition.Transitions +import javax.inject.Inject + +/** + * A Factory for [ReachabilityGestureListener]. + */ +@WMSingleton +class ReachabilityGestureListenerFactory @Inject constructor( + private val transitions: Transitions, + private val animationHandler: Transitions.TransitionHandler, + private val wctSupplier: WindowContainerTransactionSupplier +) { + /** + * @return a [ReachabilityGestureListener] implementation to listen to double tap events and + * creating the related [WindowContainerTransaction] to handle the transition. + */ + fun createReachabilityGestureListener( + taskId: Int, + token: WindowContainerToken? + ): ReachabilityGestureListener = + ReachabilityGestureListener(taskId, token, transitions, animationHandler, wctSupplier) +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS index 752d2fd721a5..8ab53eaa5eea 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS @@ -1,2 +1,3 @@ # WM Shell sub-module dagger owners -jorgegil@google.com
\ No newline at end of file +jorgegil@google.com +madym@google.com
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java index aebd94fc173a..34d840eed3f0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/TvWMShellModule.java @@ -92,7 +92,7 @@ public class TvWMShellModule { MultiInstanceHelper multiInstanceHelper, SplitState splitState, @ShellMainThread ShellExecutor mainExecutor, - Handler mainHandler, + @ShellMainThread Handler mainHandler, SystemWindows systemWindows) { return new TvSplitScreenController(context, shellInit, shellCommandHandler, shellController, shellTaskOrganizer, syncQueue, rootTDAOrganizer, displayController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java index 151dc438702d..ed5bebbb6a29 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMComponent.java @@ -23,6 +23,7 @@ import androidx.annotation.Nullable; import com.android.wm.shell.appzoomout.AppZoomOut; import com.android.wm.shell.back.BackAnimation; import com.android.wm.shell.bubbles.Bubbles; +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView; import com.android.wm.shell.desktopmode.DesktopMode; import com.android.wm.shell.displayareahelper.DisplayAreaHelper; import com.android.wm.shell.keyguard.KeyguardTransitions; @@ -72,6 +73,8 @@ public interface WMComponent { getShell().onInit(); } + // Interfaces provided to SysUI + @WMSingleton ShellInterface getShell(); @@ -116,4 +119,9 @@ public interface WMComponent { @WMSingleton Optional<AppZoomOut> getAppZoomOut(); + + // Injector methods to support field injection + + /** Injector method for {@link BubbleBarExpandedView}. */ + void inject(BubbleBarExpandedView bubbleBarExpandedView); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java index e0a829df79ad..0e6481b1c0ac 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellBaseModule.java @@ -31,6 +31,7 @@ import android.os.SystemProperties; import android.provider.Settings; import android.view.IWindowManager; import android.view.accessibility.AccessibilityManager; +import android.window.DesktopModeFlags; import android.window.SystemPerformanceHinter; import com.android.internal.logging.UiEventLogger; @@ -315,7 +316,7 @@ public abstract class WMShellBaseModule { @WMSingleton @Provides static CompatUIStatusManager provideCompatUIStatusManager(@NonNull Context context) { - if (Flags.enableCompatUiVisibilityStatus()) { + if (DesktopModeFlags.ENABLE_DESKTOP_COMPAT_UI_VISIBILITY_STATUS.isTrue()) { return new CompatUIStatusManager( newState -> Settings.Secure.putInt(context.getContentResolver(), COMPAT_UI_EDUCATION_SHOWING, newState), @@ -580,12 +581,15 @@ public abstract class WMShellBaseModule { ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Optional<RecentTasksController> recentTasksOptional, - Optional<WindowDecorViewModel> windowDecorViewModelOptional) { + Optional<WindowDecorViewModel> windowDecorViewModelOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { if (fullscreenTaskListener.isPresent()) { return fullscreenTaskListener.get(); } else { return new FullscreenTaskListener(shellInit, shellTaskOrganizer, syncQueue, - recentTasksOptional, windowDecorViewModelOptional); + recentTasksOptional, windowDecorViewModelOptional, + desktopWallpaperActivityTokenProviderOptional); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java index d7ddbdeaa6da..ee3e39e71558 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellConcurrencyModule.java @@ -37,6 +37,7 @@ import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.annotations.ExternalMainThread; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.annotations.ShellSplashscreenThread; @@ -193,13 +194,26 @@ public abstract class WMShellConcurrencyModule { } /** + * Provides a Shell desktop thread Executor + */ + @WMSingleton + @Provides + @ShellDesktopThread + public static ShellExecutor provideDesktopModeMiscExecutor() { + HandlerThread shellDesktopThread = new HandlerThread("wmshell.desktop", + THREAD_PRIORITY_TOP_APP_BOOST); + shellDesktopThread.start(); + return new HandlerExecutor(shellDesktopThread.getThreadHandler()); + } + + /** * Provides a Shell background thread Handler for low priority background tasks. */ @WMSingleton @Provides @ShellBackgroundThread public static Handler provideSharedBackgroundHandler() { - HandlerThread shellBackgroundThread = new HandlerThread("wmshell.background", + final HandlerThread shellBackgroundThread = new HandlerThread("wmshell.background", THREAD_PRIORITY_BACKGROUND); shellBackgroundThread.start(); return shellBackgroundThread.getThreadHandler(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java index c81838f56a74..2fd8c27d5970 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java @@ -59,7 +59,8 @@ import com.android.wm.shell.bubbles.BubbleDataRepository; import com.android.wm.shell.bubbles.BubbleEducationController; import com.android.wm.shell.bubbles.BubbleLogger; import com.android.wm.shell.bubbles.BubblePositioner; -import com.android.wm.shell.bubbles.properties.ProdBubbleProperties; +import com.android.wm.shell.bubbles.BubbleResizabilityChecker; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.bubbles.storage.BubblePersistentRepository; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayImeController; @@ -80,9 +81,9 @@ import com.android.wm.shell.dagger.pip.PipModule; import com.android.wm.shell.desktopmode.CloseDesktopTaskTransitionHandler; import com.android.wm.shell.desktopmode.DefaultDragToDesktopTransitionHandler; import com.android.wm.shell.desktopmode.DesktopActivityOrientationChangeHandler; -import com.android.wm.shell.desktopmode.DesktopBackNavigationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopDisplayEventHandler; import com.android.wm.shell.desktopmode.DesktopImmersiveController; +import com.android.wm.shell.desktopmode.DesktopMinimizationTransitionHandler; import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeDragAndDropTransitionHandler; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; @@ -111,6 +112,7 @@ import com.android.wm.shell.desktopmode.education.AppToWebEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.education.data.AppToWebEducationDatastoreRepository; import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer; +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver; import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; @@ -132,6 +134,7 @@ import com.android.wm.shell.recents.RecentsTransitionHandler; import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.shared.annotations.ShellAnimationThread; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; @@ -293,7 +296,7 @@ public abstract class WMShellModule { transitions, syncQueue, wmService, - ProdBubbleProperties.INSTANCE); + new BubbleResizabilityChecker()); } // @@ -748,6 +751,7 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler, + @ShellDesktopThread ShellExecutor desktopExecutor, Optional<DesktopTasksLimiter> desktopTasksLimiter, Optional<RecentTasksController> recentTasksController, InteractionJankMonitor interactionJankMonitor, @@ -755,11 +759,11 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopModeUiEventLogger desktopModeUiEventLogger, - DesktopTilingDecorViewModel desktopTilingDecorViewModel, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, Optional<BubbleController> bubbleController, OverviewToDesktopTransitionObserver overviewToDesktopTransitionObserver, DesksOrganizer desksOrganizer, + DesksTransitionObserver desksTransitionObserver, UserProfileContexts userProfileContexts, DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopTasksController( @@ -786,17 +790,18 @@ public abstract class WMShellModule { recentsTransitionHandler, multiInstanceHelper, mainExecutor, + desktopExecutor, desktopTasksLimiter, recentTasksController.orElse(null), interactionJankMonitor, mainHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, bubbleController, overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionObserver, userProfileContexts, desktopModeCompatPolicy); } @@ -910,12 +915,16 @@ public abstract class WMShellModule { Context context, Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, - InteractionJankMonitor interactionJankMonitor) { + @DynamicOverride DesktopUserRepositories desktopUserRepositories, + InteractionJankMonitor interactionJankMonitor, + Optional<BubbleController> bubbleController) { return ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS_BUGFIX.isTrue() ? new SpringDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor) + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor, bubbleController) : new DefaultDragToDesktopTransitionHandler( - context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor); + context, transitions, rootTaskDisplayAreaOrganizer, desktopUserRepositories, + interactionJankMonitor, bubbleController); } @WMSingleton @@ -979,7 +988,8 @@ public abstract class WMShellModule { DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel ) { if (!DesktopModeStatus.canEnterDesktopModeOrShowAppHandle(context)) { return Optional.empty(); @@ -995,7 +1005,8 @@ public abstract class WMShellModule { desktopTasksLimiter, appHandleEducationController, appToWebEducationController, windowDecorCaptionHandleRepository, activityOrientationChangeHandler, focusTransitionObserver, desktopModeEventLogger, desktopModeUiEventLogger, - taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy)); + taskResourceLoader, recentsTransitionHandler, desktopModeCompatPolicy, + desktopTilingDecorViewModel)); } @WMSingleton @@ -1069,11 +1080,11 @@ public abstract class WMShellModule { @WMSingleton @Provides - static DesktopBackNavigationTransitionHandler provideDesktopBackNavigationTransitionHandler( + static DesktopMinimizationTransitionHandler provideDesktopMinimizationTransitionHandler( @ShellMainThread ShellExecutor mainExecutor, @ShellAnimationThread ShellExecutor animExecutor, DisplayController displayController) { - return new DesktopBackNavigationTransitionHandler(mainExecutor, animExecutor, + return new DesktopMinimizationTransitionHandler(mainExecutor, animExecutor, displayController); } @@ -1134,6 +1145,7 @@ public abstract class WMShellModule { Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, Optional<BackAnimationController> backAnimationController, DesktopWallpaperActivityTokenProvider desktopWallpaperActivityTokenProvider, + @NonNull DesksTransitionObserver desksTransitionObserver, ShellInit shellInit) { return desktopUserRepositories.flatMap( repository -> @@ -1146,11 +1158,21 @@ public abstract class WMShellModule { desktopMixedTransitionHandler.get(), backAnimationController.get(), desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit))); } @WMSingleton @Provides + static DesksTransitionObserver provideDesksTransitionObserver( + @NonNull @DynamicOverride DesktopUserRepositories desktopUserRepositories, + @NonNull DesksOrganizer desksOrganizer + ) { + return new DesksTransitionObserver(desktopUserRepositories, desksOrganizer); + } + + @WMSingleton + @Provides static Optional<DesktopMixedTransitionHandler> provideDesktopMixedTransitionHandler( Context context, Transitions transitions, @@ -1158,7 +1180,7 @@ public abstract class WMShellModule { FreeformTaskTransitionHandler freeformTaskTransitionHandler, CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, Optional<DesktopImmersiveController> desktopImmersiveController, - DesktopBackNavigationTransitionHandler desktopBackNavigationTransitionHandler, + DesktopMinimizationTransitionHandler desktopMinimizationTransitionHandler, InteractionJankMonitor interactionJankMonitor, @ShellMainThread Handler handler, ShellInit shellInit, @@ -1176,7 +1198,7 @@ public abstract class WMShellModule { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController.get(), - desktopBackNavigationTransitionHandler, + desktopMinimizationTransitionHandler, interactionJankMonitor, handler, shellInit, @@ -1256,10 +1278,10 @@ public abstract class WMShellModule { @WMSingleton @Provides static DesktopWindowingEducationTooltipController - provideDesktopWindowingEducationTooltipController( - Context context, - AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, - DisplayController displayController) { + provideDesktopWindowingEducationTooltipController( + Context context, + AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, + DisplayController displayController) { return new DesktopWindowingEducationTooltipController( context, additionalSystemViewContainerFactory, displayController); } @@ -1288,7 +1310,8 @@ public abstract class WMShellModule { WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopWindowingEducationTooltipController desktopWindowingEducationTooltipController, @ShellMainThread CoroutineScope applicationScope, - @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher) { + @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher, + DesktopModeUiEventLogger desktopModeUiEventLogger) { return new AppHandleEducationController( context, appHandleEducationFilter, @@ -1296,7 +1319,8 @@ public abstract class WMShellModule { windowDecorCaptionHandleRepository, desktopWindowingEducationTooltipController, applicationScope, - backgroundDispatcher); + backgroundDispatcher, + desktopModeUiEventLogger); } @WMSingleton @@ -1398,6 +1422,7 @@ public abstract class WMShellModule { IconProvider iconProvider, GlobalDragListener globalDragListener, Transitions transitions, + Lazy<BubbleController> bubbleControllerLazy, @ShellMainThread ShellExecutor mainExecutor) { return new DragAndDropController( context, @@ -1410,6 +1435,12 @@ public abstract class WMShellModule { iconProvider, globalDragListener, transitions, + new Lazy<>() { + @Override + public BubbleBarDragListener get() { + return bubbleControllerLazy.get(); + } + }, mainExecutor); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java index 413300612f7d..f8b18f29c797 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/pip/Pip2Module.java @@ -31,6 +31,7 @@ import com.android.wm.shell.common.TaskStackListenerImpl; import com.android.wm.shell.common.pip.PipAppOpsListener; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMediaController; import com.android.wm.shell.common.pip.PipPerfHintController; @@ -52,6 +53,7 @@ import com.android.wm.shell.pip2.phone.PipTransition; import com.android.wm.shell.pip2.phone.PipTransitionState; import com.android.wm.shell.pip2.phone.PipUiStateChangeController; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellCommandHandler; import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; @@ -84,14 +86,13 @@ public abstract class Pip2Module { @NonNull PipDisplayLayoutState pipDisplayLayoutState, @NonNull PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, - Optional<DesktopWallpaperActivityTokenProvider> - desktopWallpaperActivityTokenProviderOptional) { + Optional<SplitScreenController> splitScreenControllerOptional, + PipDesktopState pipDesktopState) { return new PipTransition(context, shellInit, shellTaskOrganizer, transitions, pipBoundsState, null, pipBoundsAlgorithm, pipTaskListener, pipScheduler, pipStackListenerController, pipDisplayLayoutState, - pipUiStateChangeController, displayController, desktopUserRepositoriesOptional, - desktopWallpaperActivityTokenProviderOptional); + pipUiStateChangeController, displayController, splitScreenControllerOptional, + pipDesktopState); } @WMSingleton @@ -142,13 +143,10 @@ public abstract class Pip2Module { PipBoundsState pipBoundsState, @ShellMainThread ShellExecutor mainExecutor, PipTransitionState pipTransitionState, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, - Optional<DesktopWallpaperActivityTokenProvider> - desktopWallpaperActivityTokenProviderOptional, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + Optional<SplitScreenController> splitScreenControllerOptional, + PipDesktopState pipDesktopState) { return new PipScheduler(context, pipBoundsState, mainExecutor, pipTransitionState, - desktopUserRepositoriesOptional, desktopWallpaperActivityTokenProviderOptional, - rootTaskDisplayAreaOrganizer); + splitScreenControllerOptional, pipDesktopState); } @WMSingleton @@ -180,6 +178,7 @@ public abstract class Pip2Module { @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, @NonNull PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -188,8 +187,8 @@ public abstract class Pip2Module { Optional<PipPerfHintController> pipPerfHintControllerOptional) { return new PipTouchHandler(context, shellInit, shellCommandHandler, menuPhoneController, pipBoundsAlgorithm, pipBoundsState, pipTransitionState, pipScheduler, - sizeSpecSource, pipDisplayLayoutState, displayController, pipMotionHelper, - floatingContentCoordinator, pipUiEventLogger, mainExecutor, + sizeSpecSource, pipDisplayLayoutState, pipDesktopState, displayController, + pipMotionHelper, floatingContentCoordinator, pipUiEventLogger, mainExecutor, pipPerfHintControllerOptional); } @@ -233,4 +232,17 @@ public abstract class Pip2Module { return new PipTaskListener(context, shellTaskOrganizer, pipTransitionState, pipScheduler, pipBoundsState, pipBoundsAlgorithm, mainExecutor); } + + @WMSingleton + @Provides + static PipDesktopState providePipDesktopState( + PipDisplayLayoutState pipDisplayLayoutState, + Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer + ) { + return new PipDesktopState(pipDisplayLayoutState, desktopUserRepositoriesOptional, + desktopWallpaperActivityTokenProviderOptional, rootTaskDisplayAreaOrganizer); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt index 6f455df6cfec..c38558d7bde9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopDisplayEventHandler.kt @@ -26,6 +26,7 @@ import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERN import android.view.Display.DEFAULT_DISPLAY import android.view.IWindowManager import android.view.WindowManager.TRANSIT_CHANGE +import android.window.DesktopExperienceFlags import android.window.WindowContainerTransaction import com.android.internal.protolog.ProtoLog import com.android.window.flags.Flags @@ -62,7 +63,7 @@ class DesktopDisplayEventHandler( private fun onInit() { displayController.addDisplayWindowListener(this) - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue()) { desktopTasksController.onDeskRemovedListener = this } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt index a4620d5a4dfe..c3da1548bb8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopImmersiveController.kt @@ -23,6 +23,7 @@ import android.os.IBinder import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.animation.DecelerateInterpolator +import android.window.DesktopModeFlags import android.window.DesktopModeFlags.ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS import android.window.TransitionInfo import android.window.TransitionRequestInfo @@ -152,7 +153,7 @@ class DesktopImmersiveController( displayId: Int, reason: ExitReason, ) { - if (!Flags.enableFullyImmersiveInDesktop()) return + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return val result = exitImmersiveIfApplicable(wct, displayId, excludeTaskId = null, reason) result.asExit()?.runOnTransitionStart?.invoke(transition) } @@ -171,7 +172,7 @@ class DesktopImmersiveController( excludeTaskId: Int? = null, reason: ExitReason, ): ExitResult { - if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return ExitResult.NoExit val immersiveTask = desktopUserRepositories.current.getTaskInFullImmersiveState(displayId) ?: return ExitResult.NoExit @@ -213,7 +214,7 @@ class DesktopImmersiveController( taskInfo: RunningTaskInfo, reason: ExitReason, ): ExitResult { - if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return ExitResult.NoExit if (desktopUserRepositories.current.isTaskInFullImmersiveState(taskInfo.taskId)) { // A full immersive task is being minimized, make sure the immersive state is broken // (i.e. resize back to max bounds). @@ -396,7 +397,7 @@ class DesktopImmersiveController( taskId = taskId, immersive = pendingTransition.direction == Direction.ENTER, ) - if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { + if (DesktopModeFlags.ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE.isTrue) { when (pendingTransition.direction) { Direction.EXIT -> { desktopRepository.removeBoundsBeforeFullImmersive(taskId) @@ -457,7 +458,7 @@ class DesktopImmersiveController( val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: error("Expected non-null display layout for displayId: ${taskInfo.displayId}") - return if (Flags.enableRestoreToPreviousSizeFromDesktopImmersive()) { + return if (DesktopModeFlags.ENABLE_RESTORE_TO_PREVIOUS_SIZE_FROM_DESKTOP_IMMERSIVE.isTrue) { desktopUserRepositories.current.removeBoundsBeforeFullImmersive(taskInfo.taskId) ?: if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue()) { calculateInitialBounds(displayLayout, taskInfo) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMinimizationTransitionHandler.kt index 56c50ff484d4..728638d9bbd3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopBackNavigationTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMinimizationTransitionHandler.kt @@ -31,10 +31,13 @@ import com.android.wm.shell.shared.animation.MinimizeAnimator.create import com.android.wm.shell.transition.Transitions /** - * The [Transitions.TransitionHandler] that handles transitions for tasks that are closing or going - * to back as part of back navigation. This handler is used only for animating transitions. + * The [Transitions.TransitionHandler] that handles transitions for tasks that are: + * - Closing or going to back as part of back navigation + * - Going to back as part of minimization button usage. + * + * Note that this handler is used only for animating transitions. */ -class DesktopBackNavigationTransitionHandler( +class DesktopMinimizationTransitionHandler( private val mainExecutor: ShellExecutor, private val animExecutor: ShellExecutor, private val displayController: DisplayController, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt index 3beee2e57410..1f7edb413908 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopMixedTransitionHandler.kt @@ -31,10 +31,8 @@ import android.window.TransitionInfo.Change import android.window.TransitionRequestInfo import android.window.WindowContainerTransaction import androidx.annotation.VisibleForTesting -import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE import com.android.internal.jank.InteractionJankMonitor import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.freeform.FreeformTaskTransitionHandler import com.android.wm.shell.freeform.FreeformTaskTransitionStarter @@ -54,7 +52,7 @@ class DesktopMixedTransitionHandler( private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, private val desktopImmersiveController: DesktopImmersiveController, - private val desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler, + private val desktopMinimizationTransitionHandler: DesktopMinimizationTransitionHandler, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, shellInit: ShellInit, @@ -73,9 +71,31 @@ class DesktopMixedTransitionHandler( wct: WindowContainerTransaction?, ) = freeformTaskTransitionHandler.startWindowingModeTransition(targetWindowingMode, wct) - /** Delegates starting minimized mode transition to [FreeformTaskTransitionHandler]. */ - override fun startMinimizedModeTransition(wct: WindowContainerTransaction?): IBinder = - freeformTaskTransitionHandler.startMinimizedModeTransition(wct) + /** + * Starts a minimize transition for [taskId], with [isLastTask] which is true if the task going + * to be minimized is the last visible task. + */ + override fun startMinimizedModeTransition( + wct: WindowContainerTransaction?, + taskId: Int, + isLastTask: Boolean, + ): IBinder { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue) { + return freeformTaskTransitionHandler.startMinimizedModeTransition( + wct, + taskId, + isLastTask, + ) + } + requireNotNull(wct) + return transitions + .startTransition(Transitions.TRANSIT_MINIMIZE, wct, /* handler= */ this) + .also { transition -> + pendingMixedTransitions.add( + PendingMixedTransition.Minimize(transition, taskId, isLastTask) + ) + } + } /** Delegates starting PiP transition to [FreeformTaskTransitionHandler]. */ override fun startPipTransition(wct: WindowContainerTransaction?): IBinder = @@ -106,7 +126,7 @@ class DesktopMixedTransitionHandler( exitingImmersiveTask: Int? = null, ): IBinder { if ( - !Flags.enableFullyImmersiveInDesktop() && + !DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX.isTrue ) { return transitions.startTransition(transitionType, wct, /* handler= */ null) @@ -204,7 +224,6 @@ class DesktopMixedTransitionHandler( return dispatchCloseLastDesktopTaskAnimation( transition, info, - closeChange, startTransaction, finishTransaction, finishCallback, @@ -301,7 +320,15 @@ class DesktopMixedTransitionHandler( finishTransaction: SurfaceControl.Transaction, finishCallback: TransitionFinishCallback, ): Boolean { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue) return false + val shouldAnimate = + if (info.type == Transitions.TRANSIT_MINIMIZE) { + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX.isTrue + } else { + DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue + } + if (!shouldAnimate) { + return false + } val minimizeChange = findTaskChange(info, pending.minimizingTask) if (minimizeChange == null) { @@ -319,8 +346,8 @@ class DesktopMixedTransitionHandler( ) } - // Animate minimizing desktop task transition with [DesktopBackNavigationTransitionHandler]. - return desktopBackNavigationTransitionHandler.startAnimation( + // Animate minimizing desktop task transition with [DesktopMinimizationTransitionHandler]. + return desktopMinimizationTransitionHandler.startAnimation( transition, info, startTransaction, @@ -345,18 +372,10 @@ class DesktopMixedTransitionHandler( private fun dispatchCloseLastDesktopTaskAnimation( transition: IBinder, info: TransitionInfo, - change: TransitionInfo.Change, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, finishCallback: TransitionFinishCallback, ): Boolean { - // Starting the jank trace if closing the last window in desktop mode. - interactionJankMonitor.begin( - change.leash, - context, - handler, - CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, - ) // Dispatch the last desktop task closing animation. return dispatchToLeftoverHandler( transition = transition, @@ -364,10 +383,6 @@ class DesktopMixedTransitionHandler( startTransaction = startTransaction, finishTransaction = finishTransaction, finishCallback = finishCallback, - doOnFinishCallback = { - // Finish the jank trace when closing the last window in desktop mode. - interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE) - }, ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt index 164d04bbde65..0cc8a6a5c1a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeShellCommandHandler.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.desktopmode -import com.android.window.flags.Flags +import android.window.DesktopExperienceFlags import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.UNKNOWN import com.android.wm.shell.sysui.ShellCommandHandler import java.io.PrintWriter @@ -56,8 +56,8 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - if (!Flags.enableMultipleDesktopsBackend()) { - return controller.moveTaskToDesktop(taskId, transitionSource = UNKNOWN) + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + return controller.moveTaskToDefaultDeskAndActivate(taskId, transitionSource = UNKNOWN) } if (args.size < 3) { pw.println("Error: desk id should be provided as arguments") @@ -70,8 +70,9 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } + controller.moveTaskToDesk(taskId = taskId, deskId = deskId, transitionSource = UNKNOWN) pw.println("Not implemented.") - return false + return true } private fun runMoveToNextDisplay(args: Array<String>, pw: PrintWriter): Boolean { @@ -94,7 +95,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -115,7 +116,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runActivateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -131,12 +132,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.activateDesk(deskId) + return true } private fun runRemoveDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -152,12 +153,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: desk id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.removeDesk(deskId) + return true } private fun runRemoveAllDesks(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -166,7 +167,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskToFront(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -187,7 +188,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runMoveTaskOutOfDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -203,12 +204,12 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl pw.println("Error: task id should be an integer") return false } - pw.println("Not implemented.") - return false + controller.moveToFullscreen(taskId, transitionSource = UNKNOWN) + return true } private fun runCanCreateDesk(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -224,7 +225,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } private fun runGetActiveDeskId(args: Array<String>, pw: PrintWriter): Boolean { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("Not supported.") return false } @@ -245,7 +246,7 @@ class DesktopModeShellCommandHandler(private val controller: DesktopTasksControl } override fun printShellCommandHelp(pw: PrintWriter, prefix: String) { - if (!Flags.enableMultipleDesktopsBackend()) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { pw.println("$prefix moveTaskToDesk <taskId> ") pw.println("$prefix Move a task with given id to desktop mode.") pw.println("$prefix moveToNextDisplay <taskId> ") diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt index b96b9d2adddf..b9cb32d8a14f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUiEventLogger.kt @@ -149,7 +149,25 @@ class DesktopModeUiEventLogger( @UiEvent(doc = "Enter multi-instance by using the New Window button") DESKTOP_WINDOW_MULTI_INSTANCE_NEW_WINDOW_CLICK(2069), @UiEvent(doc = "Enter multi-instance by clicking an icon in the Manage Windows menu") - DESKTOP_WINDOW_MULTI_INSTANCE_MANAGE_WINDOWS_ICON_CLICK(2070); + DESKTOP_WINDOW_MULTI_INSTANCE_MANAGE_WINDOWS_ICON_CLICK(2070), + @UiEvent(doc = "Education tooltip on the app handle is shown") + APP_HANDLE_EDUCATION_TOOLTIP_SHOWN(2097), + @UiEvent(doc = "Education tooltip on the app handle is clicked") + APP_HANDLE_EDUCATION_TOOLTIP_CLICKED(2098), + @UiEvent(doc = "Education tooltip on the app handle is dismissed by the user") + APP_HANDLE_EDUCATION_TOOLTIP_DISMISSED(2099), + @UiEvent(doc = "Enter desktop mode education tooltip on the app handle menu is shown") + ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN(2100), + @UiEvent(doc = "Enter desktop mode education tooltip on the app handle menu is clicked") + ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED(2101), + @UiEvent(doc = "Enter desktop mode education tooltip is dismissed by the user") + ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2102), + @UiEvent(doc = "Exit desktop mode education tooltip on the app header menu is shown") + EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN(2103), + @UiEvent(doc = "Exit desktop mode education tooltip on the app header menu is clicked") + EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED(2104), + @UiEvent(doc = "Exit desktop mode education tooltip is dismissed by the user") + EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED(2105); override fun getId(): Int = mId } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt index 90191345147c..c5ee3137e5ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeUtils.kt @@ -18,8 +18,10 @@ package com.android.wm.shell.desktopmode +import android.annotation.DimenRes import android.app.ActivityManager.RunningTaskInfo import android.app.TaskInfo +import android.content.Context import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED import android.content.pm.ActivityInfo.isFixedOrientationLandscape import android.content.pm.ActivityInfo.isFixedOrientationPortrait @@ -28,8 +30,10 @@ import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.graphics.Rect import android.os.SystemProperties import android.util.Size +import com.android.wm.shell.R import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayLayout +import kotlin.math.ceil val DESKTOP_MODE_INITIAL_BOUNDS_SCALE: Float = SystemProperties.getInt("persist.wm.debug.desktop_mode_initial_bounds_scale", 75) / 100f @@ -53,10 +57,12 @@ fun calculateDefaultDesktopTaskBounds(displayLayout: DisplayLayout): Rect { * aspect ratio, orientation and resizability to calculate an area this is compatible with the * applications previous configuration. */ +@JvmOverloads fun calculateInitialBounds( displayLayout: DisplayLayout, taskInfo: RunningTaskInfo, scale: Float = DESKTOP_MODE_INITIAL_BOUNDS_SCALE, + captionInsets: Int = 0, ): Rect { val screenBounds = Rect(0, 0, displayLayout.width(), displayLayout.height()) val appAspectRatio = calculateAspectRatio(taskInfo) @@ -92,7 +98,7 @@ fun calculateInitialBounds( } else { // If activity is unresizeable, regardless of orientation, calculate maximum // size (within the ideal size) maintaining original aspect ratio. - maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio, captionInsets) } } ORIENTATION_PORTRAIT -> { @@ -119,11 +125,17 @@ fun calculateInitialBounds( taskInfo, Size(customPortraitWidthForLandscapeApp, idealSize.height), appAspectRatio, + captionInsets, ) } else { // For portrait unresizeable activities, calculate maximum size (within the // ideal size) maintaining original aspect ratio. - maximizeSizeGivenAspectRatio(taskInfo, idealSize, appAspectRatio) + maximizeSizeGivenAspectRatio( + taskInfo, + idealSize, + appAspectRatio, + captionInsets, + ) } } } @@ -148,11 +160,16 @@ fun calculateMaximizeBounds(displayLayout: DisplayLayout, taskInfo: RunningTaskI } else { // if non-resizable then calculate max bounds according to aspect ratio val activityAspectRatio = calculateAspectRatio(taskInfo) + val captionInsets = + taskInfo.configuration.windowConfiguration.appBounds?.let { + it.top - taskInfo.configuration.windowConfiguration.bounds.top + } ?: 0 val newSize = maximizeSizeGivenAspectRatio( taskInfo, Size(stableBounds.width(), stableBounds.height()), activityAspectRatio, + captionInsets, ) return centerInArea(newSize, stableBounds, stableBounds.left, stableBounds.top) } @@ -166,38 +183,44 @@ fun maximizeSizeGivenAspectRatio( taskInfo: RunningTaskInfo, targetArea: Size, aspectRatio: Float, + captionInsets: Int = 0, ): Size { - val targetHeight = targetArea.height + val targetHeight = targetArea.height - captionInsets val targetWidth = targetArea.width val finalHeight: Int val finalWidth: Int // Get orientation either through top activity or task's orientation if (taskInfo.hasPortraitTopActivity()) { - val tempWidth = (targetHeight / aspectRatio).toInt() + val tempWidth = ceil(targetHeight / aspectRatio).toInt() if (tempWidth <= targetWidth) { finalHeight = targetHeight finalWidth = tempWidth } else { finalWidth = targetWidth - finalHeight = (finalWidth * aspectRatio).toInt() + finalHeight = ceil(finalWidth * aspectRatio).toInt() } } else { - val tempWidth = (targetHeight * aspectRatio).toInt() + val tempWidth = ceil(targetHeight * aspectRatio).toInt() if (tempWidth <= targetWidth) { finalHeight = targetHeight finalWidth = tempWidth } else { finalWidth = targetWidth - finalHeight = (finalWidth / aspectRatio).toInt() + finalHeight = ceil(finalWidth / aspectRatio).toInt() } } - return Size(finalWidth, finalHeight) + return Size(finalWidth, finalHeight + captionInsets) } /** Calculates the aspect ratio of an activity from its fullscreen bounds. */ fun calculateAspectRatio(taskInfo: RunningTaskInfo): Float { - if (taskInfo.appCompatTaskInfo.topActivityAppBounds.isEmpty) return 1f - val appBounds = taskInfo.appCompatTaskInfo.topActivityAppBounds + val appBounds = + if (taskInfo.appCompatTaskInfo.topActivityAppBounds.isEmpty) { + taskInfo.configuration.windowConfiguration.appBounds + ?: taskInfo.configuration.windowConfiguration.bounds + } else { + taskInfo.appCompatTaskInfo.topActivityAppBounds + } return maxOf(appBounds.height(), appBounds.width()) / minOf(appBounds.height(), appBounds.width()).toFloat() } @@ -233,6 +256,13 @@ fun isTaskBoundsEqual(taskBounds: Rect, stableBounds: Rect): Boolean { return taskBounds == stableBounds } +/** Returns the app header height in desktop mode in pixels. */ +fun getAppHeaderHeight(context: Context): Int = + context.resources.getDimensionPixelSize(getAppHeaderHeightId()) + +/** Returns the resource id of the app header height in desktop mode. */ +@DimenRes fun getAppHeaderHeightId(): Int = R.dimen.desktop_mode_freeform_decor_caption_height + /** * Calculates the desired initial bounds for applications in desktop windowing. This is done as a * scale of the screen bounds. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java index 32ee319a053b..99f052832a51 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeVisualIndicator.java @@ -18,9 +18,6 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; -import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR; @@ -28,37 +25,30 @@ import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.Indica import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR; import static com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR; -import android.animation.Animator; -import android.animation.AnimatorListenerAdapter; -import android.animation.RectEvaluator; -import android.animation.ValueAnimator; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.ActivityManager; import android.content.Context; -import android.content.res.Resources; -import android.graphics.PixelFormat; import android.graphics.PointF; import android.graphics.Rect; import android.graphics.Region; -import android.graphics.drawable.LayerDrawable; -import android.util.DisplayMetrics; import android.view.SurfaceControl; -import android.view.SurfaceControlViewHost; -import android.view.View; -import android.view.WindowManager; -import android.view.WindowlessWindowManager; -import android.view.animation.DecelerateInterpolator; +import android.window.DesktopModeFlags; import androidx.annotation.VisibleForTesting; import com.android.internal.policy.SystemBarUtils; -import com.android.window.flags.Flags; import com.android.wm.shell.R; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.shared.annotations.ShellDesktopThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; /** * Animated visual indicator for Desktop Mode windowing transitions. @@ -74,7 +64,11 @@ public class DesktopModeVisualIndicator { /** Indicates impending transition into split select on the left side */ TO_SPLIT_LEFT_INDICATOR, /** Indicates impending transition into split select on the right side */ - TO_SPLIT_RIGHT_INDICATOR + TO_SPLIT_RIGHT_INDICATOR, + /** Indicates impending transition into bubble on the left side */ + TO_BUBBLE_LEFT_INDICATOR, + /** Indicates impending transition into bubble on the right side */ + TO_BUBBLE_RIGHT_INDICATOR } /** @@ -108,34 +102,54 @@ public class DesktopModeVisualIndicator { } } + private final VisualIndicatorViewContainer mVisualIndicatorViewContainer; + private final Context mContext; private final DisplayController mDisplayController; - private final RootTaskDisplayAreaOrganizer mRootTdaOrganizer; private final ActivityManager.RunningTaskInfo mTaskInfo; - private final SurfaceControl mTaskSurface; - private SurfaceControl mLeash; - - private final SyncTransactionQueue mSyncQueue; - private SurfaceControlViewHost mViewHost; - private View mView; private IndicatorType mCurrentType; - private DragStartState mDragStartState; - private boolean mIsReleased; + private final DragStartState mDragStartState; - public DesktopModeVisualIndicator(SyncTransactionQueue syncQueue, + public DesktopModeVisualIndicator(@ShellDesktopThread ShellExecutor desktopExecutor, + @ShellMainThread ShellExecutor mainExecutor, + SyncTransactionQueue syncQueue, ActivityManager.RunningTaskInfo taskInfo, DisplayController displayController, Context context, SurfaceControl taskSurface, RootTaskDisplayAreaOrganizer taskDisplayAreaOrganizer, - DragStartState dragStartState) { - mSyncQueue = syncQueue; + DragStartState dragStartState, + @Nullable BubbleDropTargetBoundsProvider bubbleBoundsProvider) { + SurfaceControl.Builder builder = new SurfaceControl.Builder(); + taskDisplayAreaOrganizer.attachToDisplayArea(taskInfo.displayId, builder); + mVisualIndicatorViewContainer = new VisualIndicatorViewContainer( + DesktopModeFlags.ENABLE_DESKTOP_INDICATOR_IN_SEPARATE_THREAD_BUGFIX.isTrue() + ? desktopExecutor : mainExecutor, + mainExecutor, builder, syncQueue, bubbleBoundsProvider); mTaskInfo = taskInfo; mDisplayController = displayController; mContext = context; - mTaskSurface = taskSurface; - mRootTdaOrganizer = taskDisplayAreaOrganizer; mCurrentType = NO_INDICATOR; mDragStartState = dragStartState; + mVisualIndicatorViewContainer.createView( + mContext, + mDisplayController.getDisplay(mTaskInfo.displayId), + mDisplayController.getDisplayLayout(mTaskInfo.displayId), + mTaskInfo, + taskSurface + ); + } + + /** Start the fade out animation, running the callback on the main thread once it is done. */ + public void fadeOutIndicator( + @NonNull Runnable callback) { + mVisualIndicatorViewContainer.fadeOutIndicator( + mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, callback + ); + } + + /** Release the visual indicator view and its viewhost. */ + public void releaseVisualIndicator() { + mVisualIndicatorViewContainer.releaseVisualIndicator(); } /** @@ -149,12 +163,19 @@ public class DesktopModeVisualIndicator { // left, and split right for the right edge. This is universal across all drag event types. if (inputCoordinates.x < 0) return TO_SPLIT_LEFT_INDICATOR; if (inputCoordinates.x > layout.width()) return TO_SPLIT_RIGHT_INDICATOR; - // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. - // In drags not originating on a freeform caption, we should default to a TO_DESKTOP - // indicator. - IndicatorType result = mDragStartState == DragStartState.FROM_FREEFORM - ? NO_INDICATOR - : TO_DESKTOP_INDICATOR; + IndicatorType result; + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen() + && !DesktopModeStatus.canEnterDesktopMode(mContext)) { + // If desktop is not available, default to "no indicator" + result = NO_INDICATOR; + } else { + // If we are in freeform, we don't want a visible indicator in the "freeform" drag zone. + // In drags not originating on a freeform caption, we should default to a TO_DESKTOP + // indicator. + result = mDragStartState == DragStartState.FROM_FREEFORM + ? NO_INDICATOR + : TO_DESKTOP_INDICATOR; + } final int transitionAreaWidth = mContext.getResources().getDimensionPixelSize( com.android.wm.shell.R.dimen.desktop_mode_transition_region_thickness); // Because drags in freeform use task position for indicator calculation, we need to @@ -166,21 +187,40 @@ public class DesktopModeVisualIndicator { captionHeight); final Region splitRightRegion = calculateSplitRightRegion(layout, transitionAreaWidth, captionHeight); - if (fullscreenRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + final int x = (int) inputCoordinates.x; + final int y = (int) inputCoordinates.y; + if (fullscreenRegion.contains(x, y)) { result = TO_FULLSCREEN_INDICATOR; } - if (splitLeftRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + if (splitLeftRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_LEFT_INDICATOR; } - if (splitRightRegion.contains((int) inputCoordinates.x, (int) inputCoordinates.y)) { + if (splitRightRegion.contains(x, y)) { result = IndicatorType.TO_SPLIT_RIGHT_INDICATOR; } + if (BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { + if (calculateBubbleLeftRegion(layout).contains(x, y)) { + result = IndicatorType.TO_BUBBLE_LEFT_INDICATOR; + } else if (calculateBubbleRightRegion(layout).contains(x, y)) { + result = IndicatorType.TO_BUBBLE_RIGHT_INDICATOR; + } + } if (mDragStartState != DragStartState.DRAGGED_INTENT) { - transitionIndicator(result); + mVisualIndicatorViewContainer.transitionIndicator( + mTaskInfo, mDisplayController, mCurrentType, result + ); + mCurrentType = result; } return result; } + /** + * Returns the [DragStartState] of the visual indicator. + */ + DragStartState getDragStartState() { + return mDragStartState; + } + @VisibleForTesting Region calculateFullscreenRegion(DisplayLayout layout, int captionHeight) { final Region region = new Region(); @@ -238,313 +278,27 @@ public class DesktopModeVisualIndicator { return region; } - /** - * Create a fullscreen indicator with no animation - */ - private void createView() { - if (mIsReleased) return; - final SurfaceControl.Transaction t = new SurfaceControl.Transaction(); - final Resources resources = mContext.getResources(); - final DisplayMetrics metrics = resources.getDisplayMetrics(); - final int screenWidth; - final int screenHeight; - if (Flags.enableBugFixesForSecondaryDisplay()) { - final DisplayLayout displayLayout = - mDisplayController.getDisplayLayout(mTaskInfo.displayId); - screenWidth = displayLayout.width(); - screenHeight = displayLayout.height(); - } else { - screenWidth = metrics.widthPixels; - screenHeight = metrics.heightPixels; - } - mView = new View(mContext); - final SurfaceControl.Builder builder = new SurfaceControl.Builder(); - mRootTdaOrganizer.attachToDisplayArea(mTaskInfo.displayId, builder); - mLeash = builder - .setName("Desktop Mode Visual Indicator") - .setContainerLayer() - .setCallsite("DesktopModeVisualIndicator.createView") - .build(); - t.show(mLeash); - final WindowManager.LayoutParams lp = - new WindowManager.LayoutParams(screenWidth, screenHeight, TYPE_APPLICATION, - FLAG_NOT_FOCUSABLE, PixelFormat.TRANSPARENT); - lp.setTitle("Desktop Mode Visual Indicator"); - lp.setTrustedOverlay(); - lp.inputFeatures |= INPUT_FEATURE_NO_INPUT_CHANNEL; - final WindowlessWindowManager windowManager = new WindowlessWindowManager( - mTaskInfo.configuration, mLeash, - /* hostInputToken= */ null); - mViewHost = new SurfaceControlViewHost(mContext, - mDisplayController.getDisplay(mTaskInfo.displayId), windowManager, - "DesktopModeVisualIndicator"); - mViewHost.setView(mView, lp); - // We want this indicator to be behind the dragged task, but in front of all others. - t.setRelativeLayer(mLeash, mTaskSurface, -1); - - mSyncQueue.runInSync(transaction -> { - transaction.merge(t); - t.close(); - }); - } - - /** - * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. - */ - private void fadeInIndicator(IndicatorType type) { - mView.setBackgroundResource(R.drawable.desktop_windowing_transition_background); - final VisualIndicatorAnimator animator = VisualIndicatorAnimator - .fadeBoundsIn(mView, type, - mDisplayController.getDisplayLayout(mTaskInfo.displayId)); - animator.start(); - mCurrentType = type; - } - - /** - * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. - * - * @param finishCallback called when animation ends or gets cancelled - */ - void fadeOutIndicator(@Nullable Runnable finishCallback) { - if (mCurrentType == NO_INDICATOR) { - // In rare cases, fade out can be requested before the indicator has determined its - // initial type and started animating in. In this case, no animator is needed. - finishCallback.run(); - return; - } - final VisualIndicatorAnimator animator = VisualIndicatorAnimator - .fadeBoundsOut(mView, mCurrentType, - mDisplayController.getDisplayLayout(mTaskInfo.displayId)); - animator.start(); - if (finishCallback != null) { - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - finishCallback.run(); - } - }); - } - mCurrentType = NO_INDICATOR; - } - - /** - * Takes existing indicator and animates it to bounds reflecting a new indicator type. - */ - private void transitionIndicator(IndicatorType newType) { - if (mCurrentType == newType) return; - if (mView == null) { - createView(); - } - if (mCurrentType == NO_INDICATOR) { - fadeInIndicator(newType); - } else if (newType == NO_INDICATOR) { - fadeOutIndicator(/* finishCallback= */ null); - } else { - final VisualIndicatorAnimator animator = VisualIndicatorAnimator.animateIndicatorType( - mView, mDisplayController.getDisplayLayout(mTaskInfo.displayId), mCurrentType, - newType); - mCurrentType = newType; - animator.start(); - } + @VisibleForTesting + Region calculateBubbleLeftRegion(DisplayLayout layout) { + int regionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_width); + int regionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_height); + return new Region(0, layout.height() - regionHeight, regionWidth, layout.height()); } - /** - * Release the indicator and its components when it is no longer needed. - */ - public void releaseVisualIndicator(SurfaceControl.Transaction t) { - mIsReleased = true; - if (mViewHost == null) return; - if (mViewHost != null) { - mViewHost.release(); - mViewHost = null; - } - - if (mLeash != null) { - t.remove(mLeash); - mLeash = null; - } + @VisibleForTesting + Region calculateBubbleRightRegion(DisplayLayout layout) { + int regionWidth = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_width); + int regionHeight = mContext.getResources().getDimensionPixelSize( + com.android.wm.shell.R.dimen.bubble_transform_area_height); + return new Region(layout.width() - regionWidth, layout.height() - regionHeight, + layout.width(), layout.height()); } - /** - * Animator for Desktop Mode transitions which supports bounds and alpha animation. - */ - private static class VisualIndicatorAnimator extends ValueAnimator { - private static final int FULLSCREEN_INDICATOR_DURATION = 200; - private static final float FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f; - private static final float INDICATOR_FINAL_OPACITY = 0.35f; - private static final int MAXIMUM_OPACITY = 255; - - /** - * Determines how this animator will interact with the view's alpha: - * Fade in, fade out, or no change to alpha - */ - private enum AlphaAnimType { - ALPHA_FADE_IN_ANIM, ALPHA_FADE_OUT_ANIM, ALPHA_NO_CHANGE_ANIM - } - - private final View mView; - private final Rect mStartBounds; - private final Rect mEndBounds; - private final RectEvaluator mRectEvaluator; - - private VisualIndicatorAnimator(View view, Rect startBounds, - Rect endBounds) { - mView = view; - mStartBounds = new Rect(startBounds); - mEndBounds = endBounds; - setFloatValues(0, 1); - mRectEvaluator = new RectEvaluator(new Rect()); - } - - private static VisualIndicatorAnimator fadeBoundsIn( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { - final Rect endBounds = getIndicatorBounds(displayLayout, type); - final Rect startBounds = getMinBounds(endBounds); - view.getBackground().setBounds(startBounds); - - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM); - return animator; - } - - private static VisualIndicatorAnimator fadeBoundsOut( - @NonNull View view, IndicatorType type, @NonNull DisplayLayout displayLayout) { - final Rect startBounds = getIndicatorBounds(displayLayout, type); - final Rect endBounds = getMinBounds(startBounds); - view.getBackground().setBounds(startBounds); - - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM); - return animator; - } - - /** - * Create animator for visual indicator changing type (i.e., fullscreen to freeform, - * freeform to split, etc.) - * - * @param view the view for this indicator - * @param displayLayout information about the display the transitioning task is currently on - * @param origType the original indicator type - * @param newType the new indicator type - */ - private static VisualIndicatorAnimator animateIndicatorType(@NonNull View view, - @NonNull DisplayLayout displayLayout, IndicatorType origType, - IndicatorType newType) { - final Rect startBounds = getIndicatorBounds(displayLayout, origType); - final Rect endBounds = getIndicatorBounds(displayLayout, newType); - final VisualIndicatorAnimator animator = new VisualIndicatorAnimator( - view, startBounds, endBounds); - animator.setInterpolator(new DecelerateInterpolator()); - setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM); - return animator; - } - - /** Calculates the bounds the indicator should have when fully faded in. */ - private static Rect getIndicatorBounds(DisplayLayout layout, IndicatorType type) { - final Rect desktopStableBounds = new Rect(); - layout.getStableBounds(desktopStableBounds); - final int padding = desktopStableBounds.top; - switch (type) { - case TO_FULLSCREEN_INDICATOR: - desktopStableBounds.top += padding; - desktopStableBounds.bottom -= padding; - desktopStableBounds.left += padding; - desktopStableBounds.right -= padding; - return desktopStableBounds; - case TO_DESKTOP_INDICATOR: - final float adjustmentPercentage = 1f - - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE; - return new Rect((int) (adjustmentPercentage * desktopStableBounds.width() / 2), - (int) (adjustmentPercentage * desktopStableBounds.height() / 2), - (int) (desktopStableBounds.width() - - (adjustmentPercentage * desktopStableBounds.width() / 2)), - (int) (desktopStableBounds.height() - - (adjustmentPercentage * desktopStableBounds.height() / 2))); - case TO_SPLIT_LEFT_INDICATOR: - return new Rect(padding, padding, - desktopStableBounds.width() / 2 - padding, - desktopStableBounds.height()); - case TO_SPLIT_RIGHT_INDICATOR: - return new Rect(desktopStableBounds.width() / 2 + padding, padding, - desktopStableBounds.width() - padding, - desktopStableBounds.height()); - default: - throw new IllegalArgumentException("Invalid indicator type provided."); - } - } - - /** - * Add necessary listener for animation of indicator - */ - private static void setupIndicatorAnimation(@NonNull VisualIndicatorAnimator animator, - AlphaAnimType animType) { - animator.addUpdateListener(a -> { - if (animator.mView != null) { - animator.updateBounds(a.getAnimatedFraction(), animator.mView); - if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) { - animator.updateIndicatorAlpha(a.getAnimatedFraction(), animator.mView); - } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) { - animator.updateIndicatorAlpha(1 - a.getAnimatedFraction(), animator.mView); - } - } else { - animator.cancel(); - } - }); - animator.addListener(new AnimatorListenerAdapter() { - @Override - public void onAnimationEnd(Animator animation) { - animator.mView.getBackground().setBounds(animator.mEndBounds); - } - }); - animator.setDuration(FULLSCREEN_INDICATOR_DURATION); - } - - /** - * Update bounds of view based on current animation fraction. - * Use of delta is to animate bounds independently, in case we need to - * run multiple animations simultaneously. - * - * @param fraction fraction to use, compared against previous fraction - * @param view the view to update - */ - private void updateBounds(float fraction, View view) { - if (mStartBounds.equals(mEndBounds)) { - return; - } - final Rect currentBounds = mRectEvaluator.evaluate(fraction, mStartBounds, mEndBounds); - view.getBackground().setBounds(currentBounds); - } - - /** - * Fade in the fullscreen indicator - * - * @param fraction current animation fraction - */ - private void updateIndicatorAlpha(float fraction, View view) { - final LayerDrawable drawable = (LayerDrawable) view.getBackground(); - drawable.findDrawableByLayerId(R.id.indicator_stroke) - .setAlpha((int) (MAXIMUM_OPACITY * fraction)); - drawable.findDrawableByLayerId(R.id.indicator_solid) - .setAlpha((int) (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY)); - } - - /** - * Return the minimum bounds of a visual indicator, to be used at the end of fading out - * and the start of fading in. - */ - private static Rect getMinBounds(Rect maxBounds) { - return new Rect((int) (maxBounds.left - + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())), - (int) (maxBounds.top - + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())), - (int) (maxBounds.right - - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())), - (int) (maxBounds.bottom - - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height()))); - } + @VisibleForTesting + Rect getIndicatorBounds() { + return mVisualIndicatorViewContainer.getIndicatorBounds(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt index 4ff1a5f1be31..eba1be517147 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopRepository.kt @@ -22,6 +22,7 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import androidx.core.util.forEach import androidx.core.util.valueIterator @@ -137,7 +138,7 @@ class DesktopRepository( private var desktopGestureExclusionExecutor: Executor? = null private val desktopData: DesktopData = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { MultiDesktopData() } else { SingleDesktopData() @@ -174,6 +175,9 @@ class DesktopRepository( /** Returns the number of desks in the given display. */ fun getNumberOfDesks(displayId: Int) = desktopData.getNumberOfDesks(displayId) + /** Returns the display the given desk is in. */ + fun getDisplayForDesk(deskId: Int) = desktopData.getDisplayForDesk(deskId) + /** Adds [regionListener] to inform about changes to exclusion regions for all Desktop tasks. */ fun setExclusionRegionListener(regionListener: Consumer<Region>, executor: Executor) { desktopGestureExclusionListener = regionListener @@ -207,6 +211,14 @@ class DesktopRepository( desktopData.createDesk(displayId, deskId) } + /** Returns the ids of the existing desks in the given display. */ + @VisibleForTesting + fun getDeskIds(displayId: Int): Set<Int> = + desktopData.desksSequence(displayId).map { desk -> desk.deskId }.toSet() + + /** Returns the id of the default desk in the given display. */ + fun getDefaultDeskId(displayId: Int): Int? = getDefaultDesk(displayId)?.deskId + /** Returns the default desk in the given display. */ private fun getDefaultDesk(displayId: Int): Desk? = desktopData.getDefaultDesk(displayId) @@ -215,31 +227,51 @@ class DesktopRepository( desktopData.setActiveDesk(displayId = displayId, deskId = deskId) } + /** Sets the given desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) { + desktopData.setDeskInactive(deskId) + } + + /** Returns the id of the active desk in the given display, if any. */ + @VisibleForTesting + fun getActiveDeskId(displayId: Int): Int? = desktopData.getActiveDesk(displayId)?.deskId + + /** Returns the id of the desk to which this task belongs. */ + fun getDeskIdForTask(taskId: Int): Int? = + desktopData.desksSequence().find { desk -> desk.activeTasks.contains(taskId) }?.deskId + /** * Adds task with [taskId] to the list of freeform tasks on [displayId]'s active desk. * * TODO: b/389960283 - add explicit [deskId] argument. */ fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) { - addOrMoveFreeformTaskToTop(displayId, taskId) - addActiveTask(displayId, taskId) - updateTask(displayId, taskId, isVisible) + val activeDesk = + checkNotNull(desktopData.getDefaultDesk(displayId)) { + "Expected desk in display: $displayId" + } + addTaskToDesk(displayId = displayId, deskId = activeDesk.deskId, taskId = taskId, isVisible) } - /** - * Adds task with [taskId] to the list of active tasks on [displayId]'s active desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - private fun addActiveTask(displayId: Int, taskId: Int) { - val activeDesk = desktopData.getDefaultDesk(displayId) - checkNotNull(activeDesk) { "Expected desk in display: $displayId" } + fun addTaskToDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + addOrMoveTaskToTopOfDesk(displayId = displayId, deskId = deskId, taskId = taskId) + addActiveTaskToDesk(displayId = displayId, deskId = deskId, taskId = taskId) + updateTaskInDesk( + displayId = displayId, + deskId = deskId, + taskId = taskId, + isVisible = isVisible, + ) + } - // Removes task if it is active on another desk excluding [activeDesk]. - removeActiveTask(taskId, excludedDeskId = activeDesk.deskId) + private fun addActiveTaskToDesk(displayId: Int, deskId: Int, taskId: Int) { + val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } - if (activeDesk.activeTasks.add(taskId)) { - logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, activeDesk.deskId) + // Removes task if it is active on another desk excluding this desk. + removeActiveTask(taskId, excludedDeskId = deskId) + + if (desk.activeTasks.add(taskId)) { + logD("Adds active task=%d displayId=%d deskId=%d", taskId, displayId, deskId) updateActiveTasksListeners(displayId) } } @@ -248,20 +280,40 @@ class DesktopRepository( @VisibleForTesting fun removeActiveTask(taskId: Int, excludedDeskId: Int? = null) { val affectedDisplays = mutableSetOf<Int>() - desktopData.forAllDesks { displayId, desk -> - if (desk.deskId != excludedDeskId && desk.activeTasks.remove(taskId)) { - logD( - "Removed active task=%d displayId=%d deskId=%d", - taskId, - displayId, - desk.deskId, - ) - affectedDisplays.add(displayId) + desktopData + .desksSequence() + .filter { desk -> desk.displayId != excludedDeskId } + .forEach { desk -> + val removed = removeActiveTaskFromDesk(desk.deskId, taskId, notifyListeners = false) + if (removed) { + logD( + "Removed active task=%d displayId=%d deskId=%d", + taskId, + desk.displayId, + desk.deskId, + ) + affectedDisplays.add(desk.displayId) + } } - } affectedDisplays.forEach { displayId -> updateActiveTasksListeners(displayId) } } + private fun removeActiveTaskFromDesk( + deskId: Int, + taskId: Int, + notifyListeners: Boolean = true, + ): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + if (desk.activeTasks.remove(taskId)) { + logD("Removed active task=%d from deskId=%d", taskId, desk.deskId) + if (notifyListeners) { + updateActiveTasksListeners(desk.displayId) + } + return true + } + return false + } + /** * Adds given task to the closing task list for [displayId]'s active desk. * @@ -300,10 +352,22 @@ class DesktopRepository( fun isActiveTask(taskId: Int) = desksSequence().any { taskId in it.activeTasks } + @VisibleForTesting + fun isActiveTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.activeTasks + } + fun isClosingTask(taskId: Int) = desksSequence().any { taskId in it.closingTasks } fun isVisibleTask(taskId: Int) = desksSequence().any { taskId in it.visibleTasks } + @VisibleForTesting + fun isVisibleTaskInDesk(taskId: Int, deskId: Int): Boolean { + val desk = desktopData.getDesk(deskId) ?: return false + return taskId in desk.visibleTasks + } + fun isMinimizedTask(taskId: Int) = desksSequence().any { taskId in it.minimizedTasks } /** @@ -390,15 +454,22 @@ class DesktopRepository( emptySet() } - /** Removes task from visible tasks of all displays except [excludedDisplayId]. */ - private fun removeVisibleTask(taskId: Int, excludedDisplayId: Int? = null) { + /** Removes task from visible tasks of all desks except [excludedDeskId]. */ + private fun removeVisibleTask(taskId: Int, excludedDeskId: Int? = null) { desktopData.forAllDesks { displayId, desk -> - if (displayId != excludedDisplayId && desk.visibleTasks.remove(taskId)) { - notifyVisibleTaskListeners(displayId, desk.visibleTasks.size) + if (desk.deskId != excludedDeskId) { + removeVisibleTaskFromDesk(deskId = desk.deskId, taskId = taskId) } } } + private fun removeVisibleTaskFromDesk(deskId: Int, taskId: Int) { + val desk = desktopData.getDesk(deskId) ?: return + if (desk.visibleTasks.remove(taskId)) { + notifyVisibleTaskListeners(desk.displayId, desk.visibleTasks.size) + } + } + /** * Updates visibility of a freeform task with [taskId] on [displayId] and notifies listeners. * @@ -408,30 +479,58 @@ class DesktopRepository( * TODO: b/389960283 - add explicit [deskId] argument. */ fun updateTask(displayId: Int, taskId: Int, isVisible: Boolean) { - logD("updateTask taskId=%d, displayId=%d, isVisible=%b", taskId, displayId, isVisible) + val validDisplayId = + if (displayId == INVALID_DISPLAY) { + // When a task vanishes it doesn't have a displayId. Find the display of the task. + getDisplayIdForTask(taskId) + } else { + displayId + } + if (validDisplayId == null) { + logW("No display id found for task: taskId=%d", taskId) + return + } + val desk = + checkNotNull(desktopData.getDefaultDesk(validDisplayId)) { + "Expected a desk in display: $validDisplayId" + } + updateTaskInDesk( + displayId = validDisplayId, + deskId = desk.deskId, + taskId = taskId, + isVisible, + ) + } + + private fun updateTaskInDesk(displayId: Int, deskId: Int, taskId: Int, isVisible: Boolean) { + check(displayId != INVALID_DISPLAY) { "Display must be valid" } + logD( + "updateTaskInDesk taskId=%d, deskId=%d, displayId=%d, isVisible=%b", + taskId, + deskId, + displayId, + isVisible, + ) if (isVisible) { - // If task is visible, remove it from any other display besides [displayId]. - removeVisibleTask(taskId, excludedDisplayId = displayId) - } else if (displayId == INVALID_DISPLAY) { - // Task has vanished. Check which display to remove the task from. - removeVisibleTask(taskId) - return + // If task is visible, remove it from any other desk besides [deskId]. + removeVisibleTask(taskId, excludedDeskId = deskId) } - val prevCount = getVisibleTaskCount(displayId) + val desk = checkNotNull(desktopData.getDesk(deskId)) { "Did not find desk: $deskId" } + val prevCount = getVisibleTaskCountInDesk(deskId) if (isVisible) { - desktopData.getDefaultDesk(displayId)?.visibleTasks?.add(taskId) - ?: error("Expected non-null desk in display $displayId") + desk.visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { - desktopData.getActiveDesk(displayId)?.visibleTasks?.remove(taskId) + desk.visibleTasks.remove(taskId) } - val newCount = getVisibleTaskCount(displayId) + val newCount = getVisibleTaskCount(deskId) if (prevCount != newCount) { logD( - "Update task visibility taskId=%d visible=%b displayId=%d", + "Update task visibility taskId=%d visible=%b deskId=%d displayId=%d", taskId, isVisible, + deskId, displayId, ) logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount) @@ -526,15 +625,26 @@ class DesktopRepository( /** * Set whether the given task is the full-immersive task in this display's active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [setTaskInFullImmersiveStateInDesk] with + * an explicit desk id instead of using this function and defaulting to the active one. */ fun setTaskInFullImmersiveState(displayId: Int, taskId: Int, immersive: Boolean) { - val desktopData = desktopData.getActiveDesk(displayId) ?: return + val activeDesk = desktopData.getActiveDesk(displayId) ?: return + setTaskInFullImmersiveStateInDesk( + deskId = activeDesk.deskId, + taskId = taskId, + immersive = immersive, + ) + } + + /** Sets whether the given task is the full-immersive task in the given desk. */ + fun setTaskInFullImmersiveStateInDesk(deskId: Int, taskId: Int, immersive: Boolean) { + val desk = desktopData.getDesk(deskId) ?: return if (immersive) { - desktopData.fullImmersiveTaskId = taskId + desk.fullImmersiveTaskId = taskId } else { - if (desktopData.fullImmersiveTaskId == taskId) { - desktopData.fullImmersiveTaskId = null + if (desk.fullImmersiveTaskId == taskId) { + desk.fullImmersiveTaskId = null } } } @@ -591,33 +701,32 @@ class DesktopRepository( /** * Gets number of visible freeform tasks on given [displayId]'s active desk. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - migrate callers to [getVisibleTaskCountInDesk]. */ fun getVisibleTaskCount(displayId: Int): Int = (desktopData.getActiveDesk(displayId)?.visibleTasks?.size ?: 0).also { logD("getVisibleTaskCount=$it") } + /** Gets the number of visible tasks on the given desk. */ + fun getVisibleTaskCountInDesk(deskId: Int): Int = + desktopData.getDesk(deskId)?.visibleTasks?.size ?: 0 + /** * Adds task (or moves if it already exists) to the top of the ordered list. * * Unminimizes the task if it is minimized. - * - * TODO: b/389960283 - add explicit [deskId] argument. */ - private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { - val desk = getDefaultDesk(displayId) ?: error("Expected a desk in display: $displayId") - logD( - "Add or move task to top: display=%d taskId=%d deskId=%d", - taskId, - displayId, - desk.deskId, - ) + private fun addOrMoveTaskToTopOfDesk(displayId: Int, deskId: Int, taskId: Int) { + val desk = desktopData.getDesk(deskId) ?: error("Could not find desk: $deskId") + logD("addOrMoveTaskToTopOfDesk: display=%d deskId=%d taskId=%d", displayId, deskId, taskId) desktopData.forAllDesks { _, desk1 -> desk1.freeformTasksInZOrder.remove(taskId) } desk.freeformTasksInZOrder.add(0, taskId) + // TODO: double check minimization logic. // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: can probably just update the desk. updatePersistentRepository(displayId) } } @@ -625,7 +734,8 @@ class DesktopRepository( /** * Minimizes the task for [taskId] and [displayId]'s active display. * - * TODO: b/389960283 - add explicit [deskId] argument. + * TODO: b/389960283 - consider forcing callers to use [minimizeTaskInDesk] with an explicit + * desk id instead of using this function and defaulting to the active one. */ fun minimizeTask(displayId: Int, taskId: Int) { if (displayId == INVALID_DISPLAY) { @@ -633,39 +743,49 @@ class DesktopRepository( // mark it as minimized. getDisplayIdForTask(taskId)?.let { minimizeTask(it, taskId) } ?: logW("Minimize task: No display id found for task: taskId=%d", taskId) - } else { - logD("Minimize Task: display=%d, task=%d", displayId, taskId) - desktopData.getActiveDesk(displayId)?.minimizedTasks?.add(taskId) - ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) + return + } + val deskId = desktopData.getActiveDesk(displayId)?.deskId + if (deskId == null) { + logD("Minimize task: No active desk found for task: taskId=%d", taskId) + return } - updateTask(displayId, taskId, isVisible = false) + minimizeTaskInDesk(displayId, deskId, taskId) + } + + /** Minimizes the task in its desk. */ + @VisibleForTesting + fun minimizeTaskInDesk(displayId: Int, deskId: Int, taskId: Int) { + logD("Minimize Task: displayId=%d deskId=%d, task=%d", displayId, deskId, taskId) + desktopData.getDesk(deskId)?.minimizedTasks?.add(taskId) + ?: logD("Minimize task: No active desk found for task: taskId=%d", taskId) + updateTaskInDesk(displayId, deskId, taskId, isVisible = false) if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + updatePersistentRepositoryForDesk(deskId) } } /** * Unminimizes the task for [taskId] and [displayId]. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [unminimizeTaskFromDesk] instead. */ fun unminimizeTask(displayId: Int, taskId: Int) { logD("Unminimize Task: display=%d, task=%d", displayId, taskId) - var removed = false - desktopData.forAllDesks(displayId) { desk -> - if (desk.minimizedTasks.remove(taskId)) { - removed = true - } - } - if (!removed) { - logW("Unminimize Task: display=%d, task=%d, no task data", displayId, taskId) + desktopData.forAllDesks(displayId) { desk -> unminimizeTaskFromDesk(desk.deskId, taskId) } + } + + private fun unminimizeTaskFromDesk(deskId: Int, taskId: Int) { + logD("Unminimize Task: deskId=%d, taskId=%d", deskId, taskId) + if (desktopData.getDesk(deskId)?.minimizedTasks?.remove(taskId) != true) { + logW("Unminimize Task: deskId=%d, taskId=%d, no task data", deskId, taskId) } } private fun getDisplayIdForTask(taskId: Int): Int? { var displayForTask: Int? = null desktopData.forAllDesks { displayId, desk -> - if (taskId in desk.freeformTasksInZOrder) { + if (taskId in desk.activeTasks) { displayForTask = displayId } } @@ -679,7 +799,7 @@ class DesktopRepository( * Removes [taskId] from the respective display. If [INVALID_DISPLAY], the original display id * will be looked up from the task id. * - * TODO: b/389960283 - consider adding an explicit [deskId] argument. + * TODO: b/389960283 - consider using [removeTaskFromDesk] instead. */ fun removeTask(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d", taskId) @@ -695,38 +815,43 @@ class DesktopRepository( private fun removeTaskFromDisplay(displayId: Int, taskId: Int) { logD("Removes freeform task: taskId=%d, displayId=%d", taskId, displayId) desktopData.forAllDesks(displayId) { desk -> - if (desk.freeformTasksInZOrder.remove(taskId)) { - logD( - "Remaining freeform tasks in desk: %d, tasks: %s", - desk.deskId, - desk.freeformTasksInZOrder.toDumpString(), - ) - } + removeTaskFromDesk(deskId = desk.deskId, taskId = taskId) } + } + + /** Removes the given task from the given desk. */ + fun removeTaskFromDesk(deskId: Int, taskId: Int) { + logD("removeTaskFromDesk: deskId=%d, taskId=%d", deskId, taskId) + // TODO: b/362720497 - consider not clearing bounds on any removal, such as when moving + // it between desks. It might be better to allow restoring to the previous bounds as long + // as they're valid (probably valid if in the same display). boundsBeforeMaximizeByTaskId.remove(taskId) boundsBeforeFullImmersiveByTaskId.remove(taskId) - // Remove task from unminimized task if it is minimized. - unminimizeTask(displayId, taskId) + val desk = desktopData.getDesk(deskId) ?: return + if (desk.freeformTasksInZOrder.remove(taskId)) { + logD( + "Remaining freeform tasks in desk: %d, tasks: %s", + desk.deskId, + desk.freeformTasksInZOrder.toDumpString(), + ) + } + unminimizeTaskFromDesk(deskId, taskId) // Mark task as not in immersive if it was immersive. - setTaskInFullImmersiveState(displayId = displayId, taskId = taskId, immersive = false) - removeActiveTask(taskId) - removeVisibleTask(taskId) - if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { - updatePersistentRepository(displayId) + setTaskInFullImmersiveStateInDesk(deskId = deskId, taskId = taskId, immersive = false) + removeActiveTaskFromDesk(deskId = deskId, taskId = taskId) + removeVisibleTaskFromDesk(deskId = deskId, taskId = taskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue) { + updatePersistentRepositoryForDesk(desk.deskId) } } - /** - * Removes the active desk for the given [displayId] and returns the active tasks on that desk. - * - * TODO: b/389960283 - add explicit [deskId] argument. - */ - fun removeDesk(displayId: Int): ArraySet<Int> { - val desk = desktopData.getActiveDesk(displayId) - if (desk == null) { - logW("Could not find desk to remove: displayId=%d", displayId) - return ArraySet() - } + /** Removes the given desk and returns the active tasks in that desk. */ + fun removeDesk(deskId: Int): Set<Int> { + val desk = + desktopData.getDesk(deskId) + ?: return emptySet<Int>().also { + logW("Could not find desk to remove: deskId=%d", deskId) + } val activeTasks = ArraySet(desk.activeTasks) desktopData.remove(desk.deskId) return activeTasks @@ -786,24 +911,29 @@ class DesktopRepository( private fun updatePersistentRepository(displayId: Int) { val desks = desktopData.desksSequence(displayId).map { desk -> desk.deepCopy() }.toList() mainCoroutineScope.launch { - desks.forEach { desk -> - try { - persistentRepository.addOrUpdateDesktop( - // Use display id as desk id for now since only once desk per display - // is supported. - userId = userId, - desktopId = desk.deskId, - visibleTasks = desk.visibleTasks, - minimizedTasks = desk.minimizedTasks, - freeformTasksInZOrder = desk.freeformTasksInZOrder, - ) - } catch (exception: Exception) { - logE( - "An exception occurred while updating the persistent repository \n%s", - exception.stackTrace, - ) - } - } + desks.forEach { desk -> updatePersistentRepositoryForDesk(desk) } + } + } + + private fun updatePersistentRepositoryForDesk(deskId: Int) { + val desk = desktopData.getDesk(deskId)?.deepCopy() ?: return + mainCoroutineScope.launch { updatePersistentRepositoryForDesk(desk) } + } + + private suspend fun updatePersistentRepositoryForDesk(desk: Desk) { + try { + persistentRepository.addOrUpdateDesktop( + userId = userId, + desktopId = desk.deskId, + visibleTasks = desk.visibleTasks, + minimizedTasks = desk.minimizedTasks, + freeformTasksInZOrder = desk.freeformTasksInZOrder, + ) + } catch (exception: Exception) { + logE( + "An exception occurred while updating the persistent repository \n%s", + exception.stackTrace, + ) } } @@ -820,21 +950,27 @@ class DesktopRepository( desktopData .desksSequence() .groupBy { it.displayId } - .forEach { (displayId, desks) -> + .map { (displayId, desks) -> + Triple(displayId, desktopData.getActiveDesk(displayId)?.deskId, desks) + } + .forEach { (displayId, activeDeskId, desks) -> pw.println("${prefix}Display #$displayId:") + pw.println("${innerPrefix}activeDesk=$activeDeskId") + pw.println("${innerPrefix}desks:") + val desksPrefix = "$innerPrefix " desks.forEach { desk -> - pw.println("${innerPrefix}Desk #${desk.deskId}:") - pw.print("$innerPrefix activeTasks=") + pw.println("${desksPrefix}Desk #${desk.deskId}:") + pw.print("$desksPrefix activeTasks=") pw.println(desk.activeTasks.toDumpString()) - pw.print("$innerPrefix visibleTasks=") + pw.print("$desksPrefix visibleTasks=") pw.println(desk.visibleTasks.toDumpString()) - pw.print("$innerPrefix freeformTasksInZOrder=") + pw.print("$desksPrefix freeformTasksInZOrder=") pw.println(desk.freeformTasksInZOrder.toDumpString()) - pw.print("$innerPrefix minimizedTasks=") + pw.print("$desksPrefix minimizedTasks=") pw.println(desk.minimizedTasks.toDumpString()) - pw.print("$innerPrefix fullImmersiveTaskId=") + pw.print("$desksPrefix fullImmersiveTaskId=") pw.println(desk.fullImmersiveTaskId) - pw.print("$innerPrefix topTransparentFullscreenTaskId=") + pw.print("$desksPrefix topTransparentFullscreenTaskId=") pw.println(desk.topTransparentFullscreenTaskId) } } @@ -864,6 +1000,9 @@ class DesktopRepository( /** Sets the given desk as the active desk in the given display. */ fun setActiveDesk(displayId: Int, deskId: Int) + /** Sets the desk as inactive if it was active. */ + fun setDeskInactive(deskId: Int) + /** * Returns the default desk in the given display. Useful when the system wants to activate a * desk but doesn't care about which one it activates (e.g. when putting a window into a @@ -944,6 +1083,11 @@ class DesktopRepository( // existence of visible desktop windows, among other factors. } + override fun setDeskInactive(deskId: Int) { + // No-op, in single-desk setups, which desktop is "active" is determined by the + // existence of visible desktop windows, among other factors. + } + override fun getDefaultDesk(displayId: Int): Desk = getDesk(deskId = displayId) override fun getAllActiveDesks(): Set<Desk> = @@ -1012,6 +1156,14 @@ class DesktopRepository( display.activeDeskId = desk.deskId } + override fun setDeskInactive(deskId: Int) { + desktopDisplays.forEach { id, display -> + if (display.activeDeskId == deskId) { + display.activeDeskId = null + } + } + } + override fun getDefaultDesk(displayId: Int): Desk? { val display = desktopDisplays[displayId] ?: return null return display.orderedDesks.find { it.deskId == display.activeDeskId } @@ -1066,7 +1218,7 @@ class DesktopRepository( } override fun getDisplayForDesk(deskId: Int): Int = - getAllActiveDesks().find { it.deskId == deskId }?.displayId + desksSequence().find { it.deskId == deskId }?.displayId ?: error("Display for desk=$deskId not found") } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt index 4d87b2189115..e831d5eecdc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListener.kt @@ -42,6 +42,12 @@ class DesktopTaskChangeListener(private val desktopUserRepositories: DesktopUser desktopUserRepositories.getProfile(taskInfo.userId) if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + // TODO: b/394281403 - with multiple desks, it's possible to have a non-freeform task + // inside a desk, so this should be decoupled from windowing mode. + // Also, changes in/out of desks are handled by the [DesksTransitionObserver], which has + // more specific information about the desk involved in the transition, which might be + // more accurate than assuming it's always the default/active desk in the display, as this + // method does. // Case 1: Freeform task is changed in Desktop Mode. if (isFreeformTask(taskInfo)) { if (taskInfo.isVisible) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt index 29f61ef8b13f..f7fe694be8e2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksController.kt @@ -41,7 +41,7 @@ import android.os.Handler import android.os.IBinder import android.os.SystemProperties import android.os.UserHandle -import android.util.Size +import android.util.Slog import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.MotionEvent @@ -54,6 +54,7 @@ import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_PIP import android.view.WindowManager.TRANSIT_TO_FRONT import android.widget.Toast +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE import android.window.DesktopModeFlags.ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER @@ -103,7 +104,9 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.multidesks.OnDeskRemovedListener import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter @@ -115,6 +118,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener.RecentsTransi import com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ExternalThread +import com.android.wm.shell.shared.annotations.ShellDesktopThread import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -134,7 +138,6 @@ import com.android.wm.shell.sysui.UserChangeListener import com.android.wm.shell.transition.OneShotRemoteHandler import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TransitionFinishCallback -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration import com.android.wm.shell.windowdecor.DragPositioningCallbackUtility import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener @@ -142,12 +145,23 @@ import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.requestingImmersive -import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import java.io.PrintWriter import java.util.Optional import java.util.concurrent.Executor import java.util.concurrent.TimeUnit import java.util.function.Consumer +import kotlin.jvm.optionals.getOrNull + +/** + * A callback to be invoked when a transition is started via |Transitions.startTransition| with the + * transition binder token that it produces. + * + * Useful when multiple components are appending WCT operations to a single transition that is + * started outside of their control, and each of them wants to track the transition lifecycle + * independently by cross-referencing the transition token with future ready-transitions. + */ +typealias RunOnTransitStart = (IBinder) -> Unit /** Handles moving tasks in and out of desktop */ class DesktopTasksController( @@ -174,17 +188,18 @@ class DesktopTasksController( private val recentsTransitionHandler: RecentsTransitionHandler, private val multiInstanceHelper: MultiInstanceHelper, @ShellMainThread private val mainExecutor: ShellExecutor, + @ShellDesktopThread private val desktopExecutor: ShellExecutor, private val desktopTasksLimiter: Optional<DesktopTasksLimiter>, private val recentTasksController: RecentTasksController?, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, private val desktopModeEventLogger: DesktopModeEventLogger, private val desktopModeUiEventLogger: DesktopModeUiEventLogger, - private val desktopTilingDecorViewModel: DesktopTilingDecorViewModel, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, private val bubbleController: Optional<BubbleController>, private val overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver, private val desksOrganizer: DesksOrganizer, + private val desksTransitionObserver: DesksTransitionObserver, private val userProfileContexts: UserProfileContexts, private val desktopModeCompatPolicy: DesktopModeCompatPolicy, ) : @@ -199,26 +214,21 @@ class DesktopTasksController( private var userId: Int private val desktopModeShellCommandHandler: DesktopModeShellCommandHandler = DesktopModeShellCommandHandler(this) - private val mOnAnimationFinishedCallback = - Consumer<SurfaceControl.Transaction> { t: SurfaceControl.Transaction -> - visualIndicator?.releaseVisualIndicator(t) - visualIndicator = null - } + + private val mOnAnimationFinishedCallback = { releaseVisualIndicator() } + private lateinit var snapEventHandler: SnapEventHandler private val dragToDesktopStateListener = object : DragToDesktopStateListener { - override fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) + override fun onCommitToDesktopAnimationStart() { + removeVisualIndicator() } - override fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) { - removeVisualIndicator(tx) + override fun onCancelToDesktopAnimationEnd() { + removeVisualIndicator() } - private fun removeVisualIndicator(tx: SurfaceControl.Transaction) { - visualIndicator?.fadeOutIndicator { - visualIndicator?.releaseVisualIndicator(tx) - visualIndicator = null - } + private fun removeVisualIndicator() { + visualIndicator?.fadeOutIndicator { releaseVisualIndicator() } } } @@ -271,7 +281,7 @@ class DesktopTasksController( RecentsTransitionStateListener.stateToString(state), ) recentsTransitionState = state - desktopTilingDecorViewModel.onOverviewAnimationStateChange( + snapEventHandler.onOverviewAnimationStateChange( RecentsTransitionStateListener.isAnimating(state) ) } @@ -302,6 +312,11 @@ class DesktopTasksController( dragToDesktopTransitionHandler.setSplitScreenController(controller) } + /** Setter to handle snap events */ + fun setSnapEventHandler(handler: SnapEventHandler) { + snapEventHandler = handler + } + /** Returns the transition type for the given remote transition. */ private fun transitionType(remoteTransition: RemoteTransition?): Int { if (remoteTransition == null) { @@ -312,24 +327,10 @@ class DesktopTasksController( } /** Show all tasks, that are part of the desktop, on top of launcher */ + @Deprecated("Use activateDesk() instead.", ReplaceWith("activateDesk()")) fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition? = null) { logV("showDesktopApps") - val wct = WindowContainerTransaction() - bringDesktopAppsToFront(displayId, wct) - - val transitionType = transitionType(remoteTransition) - val handler = - remoteTransition?.let { - OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) - } - transitions.startTransition(transitionType, wct, handler).also { t -> - handler?.setTransition(t) - } - - // launch from recent DesktopTaskView - desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( - FREEFORM_ANIMATION_DURATION - ) + activateDefaultDeskInDisplay(displayId, remoteTransition) } /** Gets number of visible freeform tasks in [displayId]. */ @@ -354,10 +355,22 @@ class DesktopTasksController( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() ) { + logV( + "isDesktopModeShowing: hasVisibleTasks=%s hasTopTransparentFullscreenTask=%s hasMinimizedPip=%s", + hasVisibleTasks, + hasTopTransparentFullscreenTask, + hasMinimizedPip, + ) return hasVisibleTasks || hasTopTransparentFullscreenTask || hasMinimizedPip } else if (Flags.enableDesktopWindowingPip()) { + logV( + "isDesktopModeShowing: hasVisibleTasks=%s hasMinimizedPip=%s", + hasVisibleTasks, + hasMinimizedPip, + ) return hasVisibleTasks || hasMinimizedPip } + logV("isDesktopModeShowing: hasVisibleTasks=%s", hasVisibleTasks) return hasVisibleTasks } @@ -368,15 +381,15 @@ class DesktopTasksController( 0 -> return // Full screen case 1 -> - moveRunningTaskToDesktop( - allFocusedTasks.single(), + moveTaskToDefaultDeskAndActivate( + allFocusedTasks.single().taskId, transitionSource = transitionSource, ) // Split-screen case where there are two focused tasks, then we find the child // task to move to desktop. 2 -> - moveRunningTaskToDesktop( - getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]), + moveTaskToDefaultDeskAndActivate( + getSplitFocusedTask(allFocusedTasks[0], allFocusedTasks[1]).taskId, transitionSource = transitionSource, ) else -> @@ -427,7 +440,7 @@ class DesktopTasksController( /** Creates a new desk in the given display. */ fun createDesk(displayId: Int) { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { desksOrganizer.createDesk(displayId) { deskId -> taskRepository.addDesk(displayId = displayId, deskId = deskId) } @@ -439,15 +452,54 @@ class DesktopTasksController( /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ @JvmOverloads - fun moveTaskToDesktop( + fun moveTaskToDefaultDeskAndActivate( + taskId: Int, + wct: WindowContainerTransaction = WindowContainerTransaction(), + transitionSource: DesktopModeTransitionSource, + remoteTransition: RemoteTransition? = null, + callback: IMoveToDesktopCallback? = null, + ): Boolean { + val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) + val backgroundTask = recentTasksController?.findTaskInBackground(taskId) + if (runningTask == null && backgroundTask == null) { + logW("moveTaskToDefaultDeskAndActivate taskId=%d not found", taskId) + return false + } + // TODO(342378842): Instead of using default display, support multiple displays + val displayId = runningTask?.displayId ?: DEFAULT_DISPLAY + val deskId = getDefaultDeskId(displayId) + return moveTaskToDesk( + taskId = taskId, + deskId = deskId, + wct = wct, + transitionSource = transitionSource, + remoteTransition = remoteTransition, + ) + } + + /** Moves task to desktop mode if task is running, else launches it in desktop mode. */ + fun moveTaskToDesk( taskId: Int, + deskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, ): Boolean { val runningTask = shellTaskOrganizer.getRunningTaskInfo(taskId) - if (runningTask == null) { + if (runningTask != null) { + return moveRunningTaskToDesk( + task = runningTask, + deskId = deskId, + wct = wct, + transitionSource = transitionSource, + remoteTransition = remoteTransition, + callback = callback, + ) + } + val backgroundTask = recentTasksController?.findTaskInBackground(taskId) + if (backgroundTask != null) { + // TODO: b/391484662 - add support for |deskId|. return moveBackgroundTaskToDesktop( taskId, wct, @@ -456,8 +508,8 @@ class DesktopTasksController( callback, ) } - moveRunningTaskToDesktop(runningTask, wct, transitionSource, remoteTransition, callback) - return true + logW("moveTaskToDesk taskId=%d not found", taskId) + return false } private fun moveBackgroundTaskToDesktop( @@ -511,31 +563,41 @@ class DesktopTasksController( } /** Moves a running task to desktop. */ - fun moveRunningTaskToDesktop( + private fun moveRunningTaskToDesk( task: RunningTaskInfo, + deskId: Int, wct: WindowContainerTransaction = WindowContainerTransaction(), transitionSource: DesktopModeTransitionSource, remoteTransition: RemoteTransition? = null, callback: IMoveToDesktopCallback? = null, - ) { + ): Boolean { if (desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(task)) { logW("Cannot enter desktop for taskId %d, ineligible top activity found", task.taskId) - return + return false } - logV("moveRunningTaskToDesktop taskId=%d", task.taskId) + val displayId = taskRepository.getDisplayForDesk(deskId) + logV( + "moveRunningTaskToDesk taskId=%d deskId=%d displayId=%d", + task.taskId, + deskId, + displayId, + ) exitSplitIfApplicable(wct, task) val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, - displayId = task.displayId, + displayId = displayId, excludeTaskId = task.taskId, reason = DesktopImmersiveController.ExitReason.TASK_LAUNCH, ) - // Bring other apps to front first val taskIdToMinimize = - bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) - addMoveToDesktopChanges(wct, task) + prepareMoveTaskToDeskAndActivate( + wct = wct, + displayId = displayId, + deskId = deskId, + task = task, + ) val transition: IBinder if (remoteTransition != null) { @@ -554,6 +616,61 @@ class DesktopTasksController( addPendingMinimizeTransition(transition, it, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksTransitionObserver.addPendingTransition( + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = displayId, + deskId = deskId, + enterTaskId = task.taskId, + ) + ) + } else { + taskRepository.setActiveDesk(displayId = displayId, deskId = deskId) + } + return true + } + + /** + * Applies the necessary changes and operations to [wct] to move a task into a desk and + * activating that desk. This includes showing pre-existing tasks of that desk behind the new + * task (but minimizing one of them if needed) and showing Home and the desktop wallpaper. + * + * @return the id of the task that is being minimized, if any. + */ + private fun prepareMoveTaskToDeskAndActivate( + wct: WindowContainerTransaction, + displayId: Int, + deskId: Int, + task: RunningTaskInfo, + ): Int? { + val taskIdToMinimize = + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + // Activate the desk first. + prepareForDeskActivation(displayId, wct) + desksOrganizer.activateDesk(wct, deskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: b/362720497 - do non-running tasks need to be restarted with + // |wct#startTask|? + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding(displayId) + ) + // TODO: b/362720497 - activating a desk with the intention to move a new task to + // it means we may need to minimize something in the activating desk. Do so here + // similar to how it's done in #bringDesktopAppsToFrontBeforeShowingNewTask + // instead of returning null. + null + } else { + // Bring other apps to front first. + bringDesktopAppsToFrontBeforeShowingNewTask(displayId, wct, task.taskId) + } + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + prepareMoveTaskToDesk(wct, task, deskId) + } else { + addMoveToDesktopChanges(wct, task) + } + return taskIdToMinimize } private fun invokeCallbackToOverview(transition: IBinder, callback: IMoveToDesktopCallback?) { @@ -595,21 +712,31 @@ class DesktopTasksController( * [startDragToDesktop]. */ private fun finalizeDragToDesktop(taskInfo: RunningTaskInfo) { + val deskId = getDefaultDeskId(taskInfo.displayId) ProtoLog.v( WM_SHELL_DESKTOP_MODE, - "DesktopTasksController: finalizeDragToDesktop taskId=%d", + "DesktopTasksController: finalizeDragToDesktop taskId=%d deskId=%d", taskInfo.taskId, + deskId, ) val wct = WindowContainerTransaction() exitSplitIfApplicable(wct, taskInfo) - if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - moveHomeTask(wct, toTop = true, taskInfo.displayId) - } else { - moveHomeTask(wct, toTop = true) + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + // |moveHomeTask| is also called in |bringDesktopAppsToFrontBeforeShowingNewTask|, so + // this shouldn't be necessary at all. + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { + moveHomeTask(taskInfo.displayId, wct) + } else { + moveHomeTask(context.displayId, wct) + } } val taskIdToMinimize = - bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) - addMoveToDesktopChanges(wct, taskInfo) + prepareMoveTaskToDeskAndActivate( + wct = wct, + displayId = taskInfo.displayId, + deskId = deskId, + task = taskInfo, + ) val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, @@ -626,6 +753,18 @@ class DesktopTasksController( addPendingMinimizeTransition(it, taskId, MinimizeReason.TASK_LIMIT) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksTransitionObserver.addPendingTransition( + DeskTransition.ActiveDeskWithTask( + token = transition, + displayId = taskInfo.displayId, + deskId = deskId, + enterTaskId = taskInfo.taskId, + ) + ) + } else { + taskRepository.setActiveDesk(displayId = taskInfo.displayId, deskId = deskId) + } } } @@ -655,22 +794,44 @@ class DesktopTasksController( wct: WindowContainerTransaction, displayId: Int, taskInfo: RunningTaskInfo, - ): ((IBinder) -> Unit)? { + ): ((IBinder) -> Unit) { val taskId = taskInfo.taskId - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId) - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(displayId, taskId) + val shouldExitDesktop = + willExitDesktop( + triggerTaskId = taskInfo.taskId, + displayId = displayId, + forceToFullscreen = false, + ) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = shouldExitDesktop, + shouldEndUpAtHome = true, + ) + taskRepository.addClosingTask(displayId, taskId) taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding(displayId, taskId) ) - return desktopImmersiveController - .exitImmersiveIfApplicable( - wct = wct, - taskInfo = taskInfo, - reason = DesktopImmersiveController.ExitReason.CLOSED, - ) - .asExit() - ?.runOnTransitionStart + + val immersiveRunnable = + desktopImmersiveController + .exitImmersiveIfApplicable( + wct = wct, + taskInfo = taskInfo, + reason = DesktopImmersiveController.ExitReason.CLOSED, + ) + .asExit() + ?.runOnTransitionStart + return { transitionToken -> + immersiveRunnable?.invoke(transitionToken) + desktopExitRunnable?.invoke(transitionToken) + } } fun minimizeTask(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { @@ -704,9 +865,20 @@ class DesktopTasksController( private fun minimizeTaskInner(taskInfo: RunningTaskInfo, minimizeReason: MinimizeReason) { val taskId = taskInfo.taskId + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) val displayId = taskInfo.displayId val wct = WindowContainerTransaction() - performDesktopExitCleanupIfNeeded(taskId, displayId, wct, forceToFullscreen = false) + + snapEventHandler.removeTaskIfTiled(displayId, taskId) + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = true) + val willExitDesktop = willExitDesktop(taskId, displayId, forceToFullscreen = false) + val desktopExitRunnable = + performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = displayId, + willExitDesktop = willExitDesktop, + ) // Notify immersive handler as it might need to exit immersive state. val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( @@ -716,7 +888,9 @@ class DesktopTasksController( ) wct.reorder(taskInfo.token, false) - val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct) + val isLastTask = taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId) + val transition: IBinder = + freeformTaskTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask) desktopTasksLimiter.ifPresent { it.addPendingMinimizeChange( transition = transition, @@ -726,12 +900,13 @@ class DesktopTasksController( ) } exitResult.asExit()?.runOnTransitionStart?.invoke(transition) + desktopExitRunnable?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ fun moveToFullscreen(taskId: Int, transitionSource: DesktopModeTransitionSource) { shellTaskOrganizer.getRunningTaskInfo(taskId)?.let { task -> - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, taskId) moveToFullscreenWithAnimation(task, task.positionInParent, transitionSource) } } @@ -739,7 +914,7 @@ class DesktopTasksController( /** Enter fullscreen by moving the focused freeform task in given `displayId` to fullscreen. */ fun enterFullscreen(displayId: Int, transitionSource: DesktopModeTransitionSource) { getFocusedFreeformTask(displayId)?.let { - desktopTilingDecorViewModel.removeTaskIfTiled(displayId, it.taskId) + snapEventHandler.removeTaskIfTiled(displayId, it.taskId) moveToFullscreenWithAnimation(it, it.positionInParent, transitionSource) } } @@ -773,14 +948,23 @@ class DesktopTasksController( ) { logV("moveToFullscreenWithAnimation taskId=%d", task.taskId) val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + val willExitDesktop = willExitDesktop(task.taskId, task.displayId, forceToFullscreen = true) + val deactivationRunnable = addMoveToFullscreenChanges(wct, task, willExitDesktop) - exitDesktopTaskTransitionHandler.startTransition( - transitionSource, - wct, - position, - mOnAnimationFinishedCallback, - ) + // We are moving a freeform task to fullscreen, put the home task under the fullscreen task. + if (!forceEnterDesktop(task.displayId)) { + moveHomeTask(task.displayId, wct) + wct.reorder(task.token, /* onTop= */ true) + } + + val transition = + exitDesktopTaskTransitionHandler.startTransition( + transitionSource, + wct, + position, + mOnAnimationFinishedCallback, + ) + deactivationRunnable?.invoke(transition) // handles case where we are moving to full screen without closing all DW tasks. if (!taskRepository.isOnlyVisibleNonClosingTask(task.taskId)) { @@ -852,7 +1036,7 @@ class DesktopTasksController( logV("moveTaskToFront taskId=%s", taskInfo.taskId) // If a task is tiled, another task should be brought to foreground with it so let // tiling controller handle the request. - if (desktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo)) { + if (snapEventHandler.moveTaskToFrontIfTiled(taskInfo)) { return } val wct = WindowContainerTransaction() @@ -970,11 +1154,13 @@ class DesktopTasksController( cascadeWindow(bounds, displayLayout, displayId) } val pendingIntent = - PendingIntent.getActivity( + PendingIntent.getActivityAsUser( context, /* requestCode= */ 0, intent, PendingIntent.FLAG_IMMUTABLE, + /* options= */ null, + UserHandle.of(userId), ) val ops = ActivityOptions.fromBundle(options).apply { @@ -1007,6 +1193,23 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() + + // check if the task is part of splitscreen + if ( + Flags.enableNonDefaultDisplaySplit() && + Flags.enableMoveToNextDisplayShortcut() && + splitScreenController.isTaskInSplitScreen(task.taskId) + ) { + val stageCoordinatorRootTaskToken = + splitScreenController.multiDisplayProvider.getDisplayRootForDisplayId( + DEFAULT_DISPLAY + ) + + wct.reparent(stageCoordinatorRootTaskToken, displayAreaInfo.token, true /* onTop */) + transitions.startTransition(TRANSIT_CHANGE, wct, /* handler= */ null) + return + } + if (!task.isFreeform) { addMoveToDesktopChanges(wct, task, displayId) } else if (Flags.enableMoveToNextDisplayShortcut()) { @@ -1020,12 +1223,18 @@ class DesktopTasksController( wct.reorder(task.token, /* onTop= */ true, /* includingParents= */ true) } + // TODO: b/394268248 - desk needs to be deactivated when moving the last task and going + // home. if (Flags.enablePerDisplayDesktopWallpaperActivity()) { performDesktopExitCleanupIfNeeded( task.taskId, task.displayId, wct, forceToFullscreen = false, + // TODO: b/371096166 - Temporary turing home relaunch off to prevent home stealing + // display focus. Remove shouldEndUpAtHome = false when home focus handling + // with connected display is implemented in wm core. + shouldEndUpAtHome = false, ) } @@ -1071,9 +1280,8 @@ class DesktopTasksController( } else { // Save current bounds so that task can be restored back to original bounds if necessary // and toggle to the stable bounds. - desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) taskRepository.saveBoundsBeforeMaximize(taskInfo.taskId, currentTaskBounds) - destinationBounds.set(calculateMaximizeBounds(displayLayout, taskInfo)) } @@ -1138,23 +1346,6 @@ class DesktopTasksController( ) } - private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { - if (taskInfo.isResizeable) { - // if resizable then expand to entire stable bounds (full display minus insets) - return Rect(stableBounds) - } else { - // if non-resizable then calculate max bounds according to aspect ratio - val activityAspectRatio = calculateAspectRatio(taskInfo) - val newSize = - maximizeSizeGivenAspectRatio( - taskInfo, - Size(stableBounds.width(), stableBounds.height()), - activityAspectRatio, - ) - return centerInArea(newSize, stableBounds, stableBounds.left, stableBounds.top) - } - } - private fun isMaximizedToStableBoundsEdges( taskInfo: RunningTaskInfo, stableBounds: Rect, @@ -1215,7 +1406,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopWindowDecoration: DesktopModeWindowDecoration, ) { desktopModeEventLogger.logTaskResizingStarted( resizeTrigger, @@ -1237,13 +1427,7 @@ class DesktopTasksController( ) if (DesktopModeFlags.ENABLE_TILE_RESIZING.isTrue()) { - val isTiled = - desktopTilingDecorViewModel.snapToHalfScreen( - taskInfo, - desktopWindowDecoration, - position, - currentDragBounds, - ) + val isTiled = snapEventHandler.snapToHalfScreen(taskInfo, currentDragBounds, position) if (isTiled) { taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate(true) } @@ -1280,7 +1464,6 @@ class DesktopTasksController( position: SnapPosition, resizeTrigger: ResizeTrigger, inputMethod: InputMethod, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (!isSnapResizingAllowed(taskInfo)) { Toast.makeText( @@ -1299,7 +1482,6 @@ class DesktopTasksController( position, resizeTrigger, inputMethod, - desktopModeWindowDecoration, ) } @@ -1311,7 +1493,6 @@ class DesktopTasksController( currentDragBounds: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { releaseVisualIndicator() if (!isSnapResizingAllowed(taskInfo)) { @@ -1359,7 +1540,6 @@ class DesktopTasksController( position, resizeTrigger, DesktopModeEventLogger.getInputMethodFromMotionEvent(motionEvent), - desktopModeWindowDecoration, ) } } @@ -1405,33 +1585,36 @@ class DesktopTasksController( ?: WINDOWING_MODE_UNDEFINED } + private fun prepareForDeskActivation(displayId: Int, wct: WindowContainerTransaction) { + // Move home to front, ensures that we go back home when all desktop windows are closed + val useParamDisplayId = + DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue || + Flags.enablePerDisplayDesktopWallpaperActivity() + moveHomeTask(displayId = if (useParamDisplayId) displayId else context.displayId, wct = wct) + // Currently, we only handle the desktop on the default display really. + if ( + (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && + ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() + ) { + // Add translucent wallpaper activity to show the wallpaper underneath. + addWallpaperActivity(displayId, wct) + } + } + private fun bringDesktopAppsToFrontBeforeShowingNewTask( displayId: Int, wct: WindowContainerTransaction, newTaskIdInFront: Int, ): Int? = bringDesktopAppsToFront(displayId, wct, newTaskIdInFront) + @Deprecated("Use activeDesk() instead.", ReplaceWith("activateDesk()")) private fun bringDesktopAppsToFront( displayId: Int, wct: WindowContainerTransaction, newTaskIdInFront: Int? = null, ): Int? { logV("bringDesktopAppsToFront, newTaskId=%d", newTaskIdInFront) - // Move home to front, ensures that we go back home when all desktop windows are closed - if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - moveHomeTask(wct, toTop = true, displayId) - } else { - moveHomeTask(wct, toTop = true) - } - - // Currently, we only handle the desktop on the default display really. - if ( - (displayId == DEFAULT_DISPLAY || Flags.enablePerDisplayDesktopWallpaperActivity()) && - ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY.isTrue() - ) { - // Add translucent wallpaper activity to show the wallpaper underneath - addWallpaperActivity(displayId, wct) - } + prepareForDeskActivation(displayId, wct) val expandedTasksOrderedFrontToBack = taskRepository.getExpandedTasksOrdered(displayId) // If we're adding a new Task we might need to minimize an old one @@ -1475,15 +1658,11 @@ class DesktopTasksController( return taskIdToMinimize } - private fun moveHomeTask( - wct: WindowContainerTransaction, - toTop: Boolean, - displayId: Int = DEFAULT_DISPLAY, - ) { + private fun moveHomeTask(displayId: Int, wct: WindowContainerTransaction) { shellTaskOrganizer .getRunningTasks(displayId) .firstOrNull { task -> task.activityType == ACTIVITY_TYPE_HOME } - ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ toTop) } + ?.let { homeTask -> wct.reorder(homeTask.getToken(), /* onTop= */ true) } } private fun addLaunchHomePendingIntent(wct: WindowContainerTransaction, displayId: Int) { @@ -1517,11 +1696,16 @@ class DesktopTasksController( private fun addWallpaperActivity(displayId: Int, wct: WindowContainerTransaction) { logV("addWallpaperActivity") if (ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER.isTrue()) { + + // If the wallpaper activity for this display already exists, let's reorder it to top. + val wallpaperActivityToken = desktopWallpaperActivityTokenProvider.getToken(displayId) + if (wallpaperActivityToken != null) { + wct.reorder(wallpaperActivityToken, /* onTop= */ true) + return + } + val intent = Intent(context, DesktopWallpaperActivity::class.java) - if ( - desktopWallpaperActivityTokenProvider.getToken(displayId) == null && - Flags.enablePerDisplayDesktopWallpaperActivity() - ) { + if (Flags.enablePerDisplayDesktopWallpaperActivity()) { intent.addFlags(Intent.FLAG_ACTIVITY_NEW_TASK) intent.addFlags(Intent.FLAG_ACTIVITY_MULTIPLE_TASK) } @@ -1587,33 +1771,59 @@ class DesktopTasksController( } } - /** - * Remove wallpaper activity if task provided is last task and wallpaper activity token is not - * null - */ - private fun performDesktopExitCleanupIfNeeded( - taskId: Int, + private fun willExitDesktop( + triggerTaskId: Int, displayId: Int, - wct: WindowContainerTransaction, forceToFullscreen: Boolean, - shouldEndUpAtHome: Boolean = true, - ) { - taskRepository.setPipShouldKeepDesktopActive(displayId, !forceToFullscreen) + ): Boolean { if (Flags.enablePerDisplayDesktopWallpaperActivity()) { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId, displayId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId, displayId)) { + return false } } else if ( Flags.enableDesktopWindowingPip() && taskRepository.isMinimizedPipPresentInDisplay(displayId) && !forceToFullscreen ) { - return + return false } else { - if (!taskRepository.isOnlyVisibleNonClosingTask(taskId)) { - return + if (!taskRepository.isOnlyVisibleNonClosingTask(triggerTaskId)) { + return false } } + return true + } + + private fun performDesktopExitCleanupIfNeeded( + taskId: Int, + displayId: Int, + wct: WindowContainerTransaction, + forceToFullscreen: Boolean, + shouldEndUpAtHome: Boolean = true, + ): RunOnTransitStart? { + taskRepository.setPipShouldKeepDesktopActive(displayId, keepActive = !forceToFullscreen) + if (!willExitDesktop(taskId, displayId, forceToFullscreen)) { + return null + } + // TODO: b/394268248 - update remaining callers to pass in a |deskId| and apply the + // |RunOnTransitStart| when the transition is started. + return performDesktopExitCleanUp( + wct = wct, + deskId = null, + displayId = displayId, + willExitDesktop = true, + shouldEndUpAtHome = shouldEndUpAtHome, + ) + } + + private fun performDesktopExitCleanUp( + wct: WindowContainerTransaction, + deskId: Int?, + displayId: Int, + willExitDesktop: Boolean, + shouldEndUpAtHome: Boolean = true, + ): RunOnTransitStart? { + if (!willExitDesktop) return null desktopModeEnterExitTransitionListener?.onExitDesktopModeTransitionStarted( FULLSCREEN_ANIMATION_DURATION ) @@ -1623,16 +1833,12 @@ class DesktopTasksController( // intent. addLaunchHomePendingIntent(wct, displayId) } + return prepareDeskDeactivationIfNeeded(wct, deskId) } fun releaseVisualIndicator() { - val t = SurfaceControl.Transaction() - visualIndicator?.releaseVisualIndicator(t) + visualIndicator?.releaseVisualIndicator() visualIndicator = null - syncQueue.runInSync { transaction -> - transaction.merge(t) - t.close() - } } override fun getContext(): Context = context @@ -1735,7 +1941,7 @@ class DesktopTasksController( /** Whether the given [change] in the [transition] is a known desktop change. */ fun isDesktopChange(transition: IBinder, change: TransitionInfo.Change): Boolean { // Only the immersive controller is currently involved in mixed transitions. - return Flags.enableFullyImmersiveInDesktop() && + return DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && desktopImmersiveController.isImmersiveChange(transition, change) } @@ -1746,7 +1952,7 @@ class DesktopTasksController( */ fun shouldPlayDesktopAnimation(info: TransitionRequestInfo): Boolean { // Only immersive mixed transition are currently supported. - if (!Flags.enableFullyImmersiveInDesktop()) return false + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return false val triggerTask = info.triggerTask ?: return false if (!isDesktopModeShowing(triggerTask.displayId)) { return false @@ -1838,8 +2044,10 @@ class DesktopTasksController( unminimizeReason = UnminimizeReason.APP_HANDLE_MENU_BUTTON, ) } else { - moveBackgroundTaskToDesktop( + val deskId = getDefaultDeskId(callingTask.displayId) + moveTaskToDesk( requestedTaskId, + deskId, WindowContainerTransaction(), DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, ) @@ -1852,6 +2060,9 @@ class DesktopTasksController( splitPosition, options.toBundle(), /* hideTaskToken= */ null, + if (enableFlexibleSplit()) + splitScreenController.determineNewInstanceIndex(callingTask) + else SPLIT_INDEX_UNDEFINED, ) } } @@ -1862,9 +2073,10 @@ class DesktopTasksController( // need updates in some cases. val baseActivity = callingTaskInfo.baseActivity ?: return val fillIn: Intent = - userProfileContexts[callingTaskInfo.userId] - ?.packageManager - ?.getLaunchIntentForPackage(baseActivity.packageName) ?: return + userProfileContexts + .getOrCreate(callingTaskInfo.userId) + .packageManager + .getLaunchIntentForPackage(baseActivity.packageName) ?: return fillIn.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK) val launchIntent = PendingIntent.getActivity( @@ -1953,7 +2165,16 @@ class DesktopTasksController( ): WindowContainerTransaction? { logV("DesktopTasksController: handleMidRecentsFreeformTaskLaunch") val wct = WindowContainerTransaction() - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) wct.reorder(task.token, true) return wct } @@ -1977,7 +2198,16 @@ class DesktopTasksController( // launched. We should make this task go to fullscreen instead of freeform. Note // that this means any re-launch of a freeform window outside of desktop will be in // fullscreen as long as default-desktop flag is disabled. - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) return wct } bringDesktopAppsToFrontBeforeShowingNewTask(task.displayId, wct, task.taskId) @@ -2027,7 +2257,7 @@ class DesktopTasksController( return wct } if (!wct.isEmpty) { - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) return wct } return null @@ -2073,7 +2303,16 @@ class DesktopTasksController( // changes we do for similar transitions. The task not having WINDOWING_MODE_UNDEFINED // set when needed can interfere with future split / multi-instance transitions. return WindowContainerTransaction().also { wct -> - addMoveToFullscreenChanges(wct, task) + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) } } return null @@ -2101,10 +2340,25 @@ class DesktopTasksController( } // Already fullscreen, no-op. if (task.isFullscreen) return null - return WindowContainerTransaction().also { wct -> addMoveToFullscreenChanges(wct, task) } + return WindowContainerTransaction().also { wct -> + addMoveToFullscreenChanges( + wct = wct, + taskInfo = task, + willExitDesktop = + willExitDesktop( + triggerTaskId = task.taskId, + displayId = task.displayId, + forceToFullscreen = true, + ), + ) + } } - /** Handle task closing by removing wallpaper activity if it's the last active task */ + /** + * Handle task closing by removing wallpaper activity if it's the last active task. + * + * TODO: b/394268248 - desk needs to be deactivated. + */ private fun handleTaskClosing( task: RunningTaskInfo, transition: IBinder, @@ -2123,7 +2377,7 @@ class DesktopTasksController( if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) { taskRepository.addClosingTask(task.displayId, task.taskId) - desktopTilingDecorViewModel.removeTaskIfTiled(task.displayId, task.taskId) + snapEventHandler.removeTaskIfTiled(task.displayId, task.taskId) } taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( @@ -2138,6 +2392,7 @@ class DesktopTasksController( * different [displayId] if the task should be moved to a different display. */ @VisibleForTesting + @Deprecated("Deprecated with multiple desks", ReplaceWith("prepareMoveTaskToDesk()")) fun addMoveToDesktopChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, @@ -2165,6 +2420,24 @@ class DesktopTasksController( } } + private fun prepareMoveTaskToDesk( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + deskId: Int, + ) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + val displayId = taskRepository.getDisplayForDesk(deskId) + val displayLayout = displayController.getDisplayLayout(displayId) ?: return + val initialBounds = getInitialBounds(displayLayout, taskInfo, displayId) + if (canChangeTaskPosition(taskInfo)) { + wct.setBounds(taskInfo.token, initialBounds) + } + desksOrganizer.moveTaskToDesk(wct, deskId = deskId, task = taskInfo) + if (useDesktopOverrideDensity()) { + wct.setDensityDpi(taskInfo.token, DESKTOP_DENSITY_OVERRIDE) + } + } + /** * Apply changes to move a freeform task from one display to another, which includes handling * density changes between displays. @@ -2216,7 +2489,16 @@ class DesktopTasksController( ): Rect { val bounds = if (ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS.isTrue) { - calculateInitialBounds(displayLayout, taskInfo) + // If caption insets should be excluded from app bounds, ensure caption insets + // are excluded from the ideal initial bounds when scaling non-resizeable apps. + // Caption insets stay fixed and don't scale with bounds. + val captionInsets = + if (desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)) { + getAppHeaderHeight(context) + } else { + 0 + } + calculateInitialBounds(displayLayout, taskInfo, captionInsets = captionInsets) } else { calculateDefaultDesktopTaskBounds(displayLayout) } @@ -2227,10 +2509,15 @@ class DesktopTasksController( return bounds } + /** + * Applies the changes needed to enter fullscreen and returns the id of the desk that needs to + * be deactivated. + */ private fun addMoveToFullscreenChanges( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo, - ) { + willExitDesktop: Boolean, + ): RunOnTransitStart? { val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(taskInfo.displayId)!! val tdaWindowingMode = tdaInfo.configuration.windowConfiguration.windowingMode val targetWindowingMode = @@ -2245,12 +2532,16 @@ class DesktopTasksController( if (useDesktopOverrideDensity()) { wct.setDensityDpi(taskInfo.token, getDefaultDensityDpi()) } - - performDesktopExitCleanupIfNeeded( - taskInfo.taskId, - taskInfo.displayId, - wct, - forceToFullscreen = true, + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + wct.reparent(taskInfo.token, tdaInfo.token, /* onTop= */ true) + } + taskRepository.setPipShouldKeepDesktopActive(taskInfo.displayId, keepActive = false) + val deskId = taskRepository.getDeskIdForTask(taskInfo.taskId) + return performDesktopExitCleanUp( + wct = wct, + deskId = deskId, + displayId = taskInfo.displayId, + willExitDesktop = willExitDesktop, shouldEndUpAtHome = false, ) } @@ -2275,6 +2566,8 @@ class DesktopTasksController( /** * Adds split screen changes to a transaction. Note that bounds are not reset here due to * animation; see {@link onDesktopSplitSelectAnimComplete} + * + * TODO: b/394268248 - desk needs to be deactivated. */ private fun addMoveToSplitChanges(wct: WindowContainerTransaction, taskInfo: RunningTaskInfo) { // This windowing mode is to get the transition animation started; once we complete @@ -2360,20 +2653,129 @@ class DesktopTasksController( ) } - fun removeDesktop(displayId: Int) { - if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return + private fun activateDefaultDeskInDisplay( + displayId: Int, + remoteTransition: RemoteTransition? = null, + ) { + val deskId = getDefaultDeskId(displayId) + activateDesk(deskId, remoteTransition) + } - val tasksToRemove = taskRepository.removeDesk(displayId) + /** Activates the given desk. */ + fun activateDesk(deskId: Int, remoteTransition: RemoteTransition? = null) { + val displayId = taskRepository.getDisplayForDesk(deskId) val wct = WindowContainerTransaction() - tasksToRemove.forEach { - val task = shellTaskOrganizer.getRunningTaskInfo(it) - if (task != null) { - wct.removeTask(task.token) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + prepareForDeskActivation(displayId, wct) + desksOrganizer.activateDesk(wct, deskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + // TODO: 362720497 - do non-running tasks need to be restarted with |wct#startTask|? + } + taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( + doesAnyTaskRequireTaskbarRounding(displayId) + ) + } else { + bringDesktopAppsToFront(displayId, wct) + } + + val transitionType = transitionType(remoteTransition) + val handler = + remoteTransition?.let { + OneShotRemoteHandler(transitions.mainExecutor, remoteTransition) + } + + val transition = transitions.startTransition(transitionType, wct, handler) + handler?.setTransition(transition) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksTransitionObserver.addPendingTransition( + DeskTransition.ActivateDesk( + token = transition, + displayId = displayId, + deskId = deskId, + ) + ) + } + + desktopModeEnterExitTransitionListener?.onEnterDesktopModeTransitionStarted( + FREEFORM_ANIMATION_DURATION + ) + } + + /** + * TODO: b/393978539 - Deactivation should not happen in desktop-first devices when going home. + */ + private fun prepareDeskDeactivationIfNeeded( + wct: WindowContainerTransaction, + deskId: Int?, + ): RunOnTransitStart? { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return null + if (deskId == null) return null + desksOrganizer.deactivateDesk(wct, deskId) + return { transition -> + desksTransitionObserver.addPendingTransition( + DeskTransition.DeactivateDesk(token = transition, deskId = deskId) + ) + } + } + + /** Removes the default desk in the given display. */ + @Deprecated("Deprecated with multi-desks.", ReplaceWith("removeDesk()")) + fun removeDefaultDeskInDisplay(displayId: Int) { + val deskId = getDefaultDeskId(displayId) + removeDesk(displayId = displayId, deskId = deskId) + } + + private fun getDefaultDeskId(displayId: Int) = + checkNotNull(taskRepository.getDefaultDeskId(displayId)) { + "Expected a default desk to exist in display: $displayId" + } + + /** Removes the given desk. */ + fun removeDesk(deskId: Int) { + val displayId = taskRepository.getDisplayForDesk(deskId) + removeDesk(displayId = displayId, deskId = deskId) + } + + private fun removeDesk(displayId: Int, deskId: Int) { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return + logV("removeDesk deskId=%d from displayId=%d", deskId, displayId) + + val tasksToRemove = + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + taskRepository.getActiveTaskIdsInDesk(deskId) } else { - recentTasksController?.removeBackgroundTask(it) + // TODO: 362720497 - make sure minimized windows are also removed in WM + // and the repository. + taskRepository.removeDesk(deskId) } + + val wct = WindowContainerTransaction() + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + tasksToRemove.forEach { + val task = shellTaskOrganizer.getRunningTaskInfo(it) + if (task != null) { + wct.removeTask(task.token) + } else { + recentTasksController?.removeBackgroundTask(it) + } + } + } else { + // TODO: 362720497 - double check background tasks are also removed. + desksOrganizer.removeDesk(wct, deskId) + } + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue && wct.isEmpty) return + val transition = transitions.startTransition(TRANSIT_CLOSE, wct, /* handler= */ null) + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { + desksTransitionObserver.addPendingTransition( + DeskTransition.RemoveDesk( + token = transition, + displayId = displayId, + deskId = deskId, + tasks = tasksToRemove, + onDeskRemovedListener = onDeskRemovedListener, + ) + ) } - if (!wct.isEmpty) transitions.startTransition(TRANSIT_CLOSE, wct, null) } /** Enter split by using the focused desktop task in given `displayId`. */ @@ -2420,15 +2822,22 @@ class DesktopTasksController( } /** Requests a task be transitioned from whatever mode it's in to a bubble. */ - fun requestFloat(taskInfo: RunningTaskInfo) { + @JvmOverloads + fun requestFloat(taskInfo: RunningTaskInfo, left: Boolean? = null) { val isDragging = dragToDesktopTransitionHandler.inProgress val shouldRequestFloat = taskInfo.isFullscreen || taskInfo.isFreeform || isDragging || taskInfo.isMultiWindow if (!shouldRequestFloat) return if (isDragging) { releaseVisualIndicator() + val cancelState = + if (left == true) DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT + else DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT + dragToDesktopTransitionHandler.cancelDragToDesktopTransition(cancelState) } else { - bubbleController.ifPresent { it.expandStackAndSelectBubble(taskInfo) } + bubbleController.ifPresent { + it.expandStackAndSelectBubble(taskInfo, /* dragData= */ null) + } } } @@ -2460,7 +2869,7 @@ class DesktopTasksController( taskBounds: Rect, ) { if (taskInfo.windowingMode != WINDOWING_MODE_FREEFORM) return - desktopTilingDecorViewModel.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) + snapEventHandler.removeTaskIfTiled(taskInfo.displayId, taskInfo.taskId) updateVisualIndicator( taskInfo, taskSurface, @@ -2477,10 +2886,18 @@ class DesktopTasksController( taskTop: Float, dragStartState: DragStartState, ): DesktopModeVisualIndicator.IndicatorType { + // If the visual indicator has the wrong start state, it was never cleared from a previous + // drag event and needs to be cleared + if (visualIndicator != null && visualIndicator?.dragStartState != dragStartState) { + Slog.e(TAG, "Visual indicator from previous motion event was never released") + releaseVisualIndicator() + } // If the visual indicator does not exist, create it. val indicator = visualIndicator ?: DesktopModeVisualIndicator( + desktopExecutor, + mainExecutor, syncQueue, taskInfo, displayController, @@ -2492,6 +2909,7 @@ class DesktopTasksController( taskSurface, rootTaskDisplayAreaOrganizer, dragStartState, + bubbleController.getOrNull()?.bubbleDropTargetBoundsProvider, ) if (visualIndicator == null) visualIndicator = indicator return indicator.updateIndicatorType(PointF(inputX, taskTop)) @@ -2517,7 +2935,6 @@ class DesktopTasksController( validDragArea: Rect, dragStartBounds: Rect, motionEvent: MotionEvent, - desktopModeWindowDecoration: DesktopModeWindowDecoration, ) { if (taskInfo.configuration.windowConfiguration.windowingMode != WINDOWING_MODE_FREEFORM) { return @@ -2556,7 +2973,6 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> { @@ -2571,10 +2987,13 @@ class DesktopTasksController( currentDragBounds, dragStartBounds, motionEvent, - desktopModeWindowDecoration, ) } - IndicatorType.NO_INDICATOR -> { + IndicatorType.NO_INDICATOR, + IndicatorType.TO_BUBBLE_LEFT_INDICATOR, + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { + // TODO(b/391928049): add support fof dragging desktop apps to a bubble + // Create a copy so that we can animate from the current bounds if we end up having // to snap the surface back without a WCT change. val destinationBounds = Rect(currentDragBounds) @@ -2701,6 +3120,12 @@ class DesktopTasksController( ) requestSplit(taskInfo, leftOrTop = false) } + IndicatorType.TO_BUBBLE_LEFT_INDICATOR -> { + requestFloat(taskInfo, left = true) + } + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> { + requestFloat(taskInfo, left = false) + } } return indicatorType } @@ -2812,6 +3237,7 @@ class DesktopTasksController( ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOW_ALWAYS pendingIntentLaunchFlags = Intent.FLAG_ACTIVITY_NEW_TASK or Intent.FLAG_ACTIVITY_MULTIPLE_TASK + splashScreenStyle = SPLASH_SCREEN_STYLE_ICON } if (windowingMode == WINDOWING_MODE_FULLSCREEN) { dragAndDropFullscreenCookie = Binder() @@ -2820,7 +3246,12 @@ class DesktopTasksController( val wct = WindowContainerTransaction() wct.sendPendingIntent(launchIntent, null, opts.toBundle()) if (windowingMode == WINDOWING_MODE_FREEFORM) { - desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) + if (DesktopModeFlags.ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX.isTrue()) { + // TODO b/376389593: Use a custom tab tearing transition/animation + startLaunchTransition(TRANSIT_OPEN, wct, launchingTaskId = null) + } else { + desktopModeDragAndDropTransitionHandler.handleDropEvent(wct) + } } else { transitions.startTransition(TRANSIT_OPEN, wct, null) } @@ -2841,12 +3272,12 @@ class DesktopTasksController( logV("onUserChanged previousUserId=%d, newUserId=%d", userId, newUserId) userId = newUserId taskRepository = userRepositories.getProfile(userId) - desktopTilingDecorViewModel.onUserChange() + snapEventHandler.onUserChange() } /** Called when a task's info changes. */ fun onTaskInfoChanged(taskInfo: RunningTaskInfo) { - if (!Flags.enableFullyImmersiveInDesktop()) return + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue) return val inImmersive = taskRepository.isTaskInFullImmersiveState(taskInfo.taskId) val requestingImmersive = taskInfo.requestingImmersive if ( @@ -3011,11 +3442,15 @@ class DesktopTasksController( } override fun createDesk(displayId: Int) { - // TODO: b/362720497 - Implement this API. + executeRemoteCallWithTaskPermission(controller, "createDesk") { c -> + c.createDesk(displayId) + } } override fun activateDesk(deskId: Int, remoteTransition: RemoteTransition?) { - // TODO: b/362720497 - Implement this API. + executeRemoteCallWithTaskPermission(controller, "activateDesk") { c -> + c.activateDesk(deskId, remoteTransition) + } } override fun showDesktopApps(displayId: Int, remoteTransition: RemoteTransition?) { @@ -3066,7 +3501,7 @@ class DesktopTasksController( callback: IMoveToDesktopCallback?, ) { executeRemoteCallWithTaskPermission(controller, "moveTaskToDesktop") { c -> - c.moveTaskToDesktop( + c.moveTaskToDefaultDeskAndActivate( taskId, transitionSource = transitionSource, remoteTransition = remoteTransition, @@ -3077,7 +3512,7 @@ class DesktopTasksController( override fun removeDesktop(displayId: Int) { executeRemoteCallWithTaskPermission(controller, "removeDesktop") { c -> - c.removeDesktop(displayId) + c.removeDefaultDeskInDisplay(displayId) } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt index b3648699ed0b..3ada988ba2a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksTransitionObserver.kt @@ -37,6 +37,7 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.isExitDesktopModeTransition import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -58,6 +59,7 @@ class DesktopTasksTransitionObserver( private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val backAnimationController: BackAnimationController, private val desktopWallpaperActivityTokenProvider: DesktopWallpaperActivityTokenProvider, + private val desksTransitionObserver: DesksTransitionObserver, shellInit: ShellInit, ) : Transitions.TransitionObserver { @@ -87,6 +89,7 @@ class DesktopTasksTransitionObserver( finishTransaction: SurfaceControl.Transaction, ) { // TODO: b/332682201 Update repository state + desksTransitionObserver.onTransitionReady(transition, info) if ( DesktopModeFlags.INCLUDE_TOP_TRANSPARENT_FULLSCREEN_TASK_IN_DESKTOP_HEURISTIC .isTrue() && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MODALS_POLICY.isTrue() diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt index a5ba6612bb1a..c10752d36bf9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopUserRepositories.kt @@ -90,6 +90,11 @@ class DesktopUserRepositories( return desktopRepoByUserId.getOrCreate(profileId) } + fun getUserIdForProfile(profileId: Int): Int { + if (userIdToProfileIdsMap[userId]?.contains(profileId) == true) return userId + else return profileId + } + /** Dumps [DesktopRepository] for each user. */ fun dump(pw: PrintWriter, prefix: String) { desktopRepoByUserId.forEach { key, value -> diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl new file mode 100644 index 000000000000..59add47fc79d --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DisplayDeskState.aidl @@ -0,0 +1,31 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode; + +/** + * Defines the state of desks on a display whose ID is `displayId`, which is: + * - `canCreateDesks`: whether it's possible to create new desks on this display. + * - `activeDeskId`: the currently active desk Id, or `-1` if none is active. + * - `deskId`: the list of desk Ids of the available desks on this display. + */ +parcelable DisplayDeskState { + int displayId; + boolean canCreateDesk; + int activeDeskId; + int[] deskIds; +} + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt index 91f10dc4faf5..0929ae15e668 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DragToDesktopTransitionHandler.kt @@ -37,6 +37,8 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.internal.protolog.ProtoLog import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.animation.FloatProperties +import com.android.wm.shell.bubbles.BubbleController +import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP @@ -49,10 +51,12 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_UND import com.android.wm.shell.shared.split.SplitScreenConstants.SplitPosition import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE import com.android.wm.shell.transition.Transitions.TransitionHandler import com.android.wm.shell.windowdecor.MoveToDesktopAnimator import com.android.wm.shell.windowdecor.MoveToDesktopAnimator.Companion.DRAG_FREEFORM_SCALE import com.android.wm.shell.windowdecor.OnTaskResizeAnimationListener +import java.util.Optional import java.util.function.Supplier import kotlin.math.max @@ -70,7 +74,9 @@ sealed class DragToDesktopTransitionHandler( private val context: Context, private val transitions: Transitions, private val taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + private val desktopUserRepositories: DesktopUserRepositories, protected val interactionJankMonitor: InteractionJankMonitor, + private val bubbleController: Optional<BubbleController>, protected val transactionSupplier: Supplier<SurfaceControl.Transaction>, ) : TransitionHandler { @@ -127,15 +133,18 @@ sealed class DragToDesktopTransitionHandler( pendingIntentCreatorBackgroundActivityStartMode = ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED } - val taskUser = UserHandle.of(taskInfo.userId) + // If we are launching home for a profile of a user, just use the [userId] of that user + // instead of the [profileId] to create the context. + val userToLaunchWith = + UserHandle.of(desktopUserRepositories.getUserIdForProfile(taskInfo.userId)) val pendingIntent = PendingIntent.getActivityAsUser( - context.createContextAsUser(taskUser, /* flags= */ 0), + context.createContextAsUser(userToLaunchWith, /* flags= */ 0), /* requestCode= */ 0, launchHomeIntent, FLAG_MUTABLE or FLAG_ALLOW_UNSAFE_IMPLICIT_INTENT or FILL_IN_COMPONENT, options.toBundle(), - taskUser, + userToLaunchWith, ) val wct = WindowContainerTransaction() // The app that is being dragged into desktop mode might cause new transitions, make this @@ -237,6 +246,21 @@ sealed class DragToDesktopTransitionHandler( state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) requestSplitFromScaledTask(splitPosition, wct) clearState() + } else if ( + state.draggedTaskChange != null && + (cancelState == CancelState.CANCEL_BUBBLE_LEFT || + cancelState == CancelState.CANCEL_BUBBLE_RIGHT) + ) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble + startCancelAnimation() + } else { + // Animation is handled by BubbleController + val wct = WindowContainerTransaction() + restoreWindowOrder(wct, state) + val onLeft = cancelState == CancelState.CANCEL_BUBBLE_LEFT + requestBubbleFromScaledTask(wct, onLeft) + } } else { // There's no dragged task, this can happen when the "cancel" happened too quickly // before the "start" transition is even ready (like on a fling gesture). The @@ -254,20 +278,25 @@ sealed class DragToDesktopTransitionHandler( ) { val state = requireTransitionState() val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") + val animatedTaskBounds = getAnimatedTaskBounds() + requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) + } + + private fun getAnimatedTaskBounds(): Rect { + val state = requireTransitionState() + val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") val taskBounds = Rect(taskInfo.configuration.windowConfiguration.bounds) val taskScale = state.dragAnimator.scale val scaledWidth = taskBounds.width() * taskScale val scaledHeight = taskBounds.height() * taskScale val dragPosition = PointF(state.dragAnimator.position) state.dragAnimator.cancelAnimator() - val animatedTaskBounds = - Rect( - dragPosition.x.toInt(), - dragPosition.y.toInt(), - (dragPosition.x + scaledWidth).toInt(), - (dragPosition.y + scaledHeight).toInt(), - ) - requestSplitSelect(wct, taskInfo, splitPosition, animatedTaskBounds) + return Rect( + dragPosition.x.toInt(), + dragPosition.y.toInt(), + (dragPosition.x + scaledWidth).toInt(), + (dragPosition.y + scaledHeight).toInt(), + ) } private fun requestSplitSelect( @@ -290,6 +319,29 @@ sealed class DragToDesktopTransitionHandler( splitScreenController.requestEnterSplitSelect(taskInfo, wct, splitPosition, taskBounds) } + private fun requestBubbleFromScaledTask(wct: WindowContainerTransaction, onLeft: Boolean) { + // TODO(b/391928049): update density once we can drag from desktop to bubble + val state = requireTransitionState() + val taskInfo = state.draggedTaskChange?.taskInfo ?: error("Expected non-null taskInfo") + val taskBounds = getAnimatedTaskBounds() + state.dragAnimator.cancelAnimator() + requestBubble(wct, taskInfo, onLeft, taskBounds) + } + + private fun requestBubble( + wct: WindowContainerTransaction, + taskInfo: RunningTaskInfo, + onLeft: Boolean, + taskBounds: Rect = Rect(taskInfo.configuration.windowConfiguration.bounds), + ) { + val controller = + bubbleController.orElseThrow { IllegalStateException("BubbleController not set") } + controller.expandStackAndSelectBubble( + taskInfo, + BubbleTransitions.DragData(taskBounds, wct, onLeft), + ) + } + override fun startAnimation( transition: IBinder, info: TransitionInfo, @@ -442,6 +494,21 @@ sealed class DragToDesktopTransitionHandler( state.startTransitionFinishTransaction?.apply() state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) requestSplitSelect(wct, taskInfo, splitPosition) + } else if ( + state.cancelState == CancelState.CANCEL_BUBBLE_LEFT || + state.cancelState == CancelState.CANCEL_BUBBLE_RIGHT + ) { + if (bubbleController.isEmpty || state !is TransitionState.FromFullscreen) { + // TODO(b/388853233): add support for dragging split task to bubble + startCancelDragToDesktopTransition() + return true + } + val taskInfo = + state.draggedTaskChange?.taskInfo ?: error("Expected non-null task info.") + val wct = WindowContainerTransaction() + restoreWindowOrder(wct) + val onLeft = state.cancelState == CancelState.CANCEL_BUBBLE_LEFT + requestBubble(wct, taskInfo, onLeft) } return true } @@ -458,7 +525,8 @@ sealed class DragToDesktopTransitionHandler( override fun mergeAnimation( transition: IBinder, info: TransitionInfo, - t: SurfaceControl.Transaction, + startT: SurfaceControl.Transaction, + finishT: SurfaceControl.Transaction, mergeTarget: IBinder, finishCallback: Transitions.TransitionFinishCallback, ) { @@ -471,6 +539,13 @@ sealed class DragToDesktopTransitionHandler( clearState() return } + // In case of bubble animation, finish the initial desktop drag animation, but keep the + // current animation running and have bubbles take over + if (info.type == TRANSIT_CONVERT_TO_BUBBLE) { + state.startTransitionFinishCb?.onTransitionFinished(/* wct= */ null) + clearState() + return + } val isCancelTransition = info.type == TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP && transition == state.cancelTransitionToken && @@ -488,18 +563,18 @@ sealed class DragToDesktopTransitionHandler( if (isEndTransition) { setupEndDragToDesktop( info, - startTransaction = t, + startTransaction = startT, finishTransaction = startTransactionFinishT, ) // Call finishCallback to merge animation before startTransitionFinishCb is called finishCallback.onTransitionFinished(/* wct= */ null) - animateEndDragToDesktop(startTransaction = t, startTransitionFinishCb) + animateEndDragToDesktop(startTransaction = startT, startTransitionFinishCb) } else if (isCancelTransition) { info.changes.forEach { change -> - t.show(change.leash) + startT.show(change.leash) startTransactionFinishT.show(change.leash) } - t.apply() + startT.apply() finishCallback.onTransitionFinished(/* wct= */ null) startTransitionFinishCb.onTransitionFinished(/* wct= */ null) clearState() @@ -577,7 +652,7 @@ sealed class DragToDesktopTransitionHandler( startPosition.y.toInt() + unscaledStartHeight, ) - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + dragToDesktopStateListener?.onCommitToDesktopAnimationStart() // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. @@ -708,7 +783,7 @@ sealed class DragToDesktopTransitionHandler( addListener( object : AnimatorListenerAdapter() { override fun onAnimationEnd(animation: Animator) { - dragToDesktopStateListener?.onCancelToDesktopAnimationEnd(tx) + dragToDesktopStateListener?.onCancelToDesktopAnimationEnd() // Start the cancel transition to restore order. startCancelDragToDesktopTransition() } @@ -801,9 +876,9 @@ sealed class DragToDesktopTransitionHandler( ) interface DragToDesktopStateListener { - fun onCommitToDesktopAnimationStart(tx: SurfaceControl.Transaction) + fun onCommitToDesktopAnimationStart() - fun onCancelToDesktopAnimationEnd(tx: SurfaceControl.Transaction) + fun onCancelToDesktopAnimationEnd() } sealed class TransitionState { @@ -864,6 +939,10 @@ sealed class DragToDesktopTransitionHandler( CANCEL_SPLIT_LEFT, /** A cancel event where the task will request to enter split on the right side. */ CANCEL_SPLIT_RIGHT, + /** A cancel event where the task will request to bubble on the left side. */ + CANCEL_BUBBLE_LEFT, + /** A cancel event where the task will request to bubble on the right side. */ + CANCEL_BUBBLE_RIGHT, } companion object { @@ -880,7 +959,9 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, + bubbleController: Optional<BubbleController>, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() }, @@ -889,7 +970,9 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, + bubbleController, transactionSupplier, ) { @@ -916,7 +999,9 @@ constructor( context: Context, transitions: Transitions, taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, + desktopUserRepositories: DesktopUserRepositories, interactionJankMonitor: InteractionJankMonitor, + bubbleController: Optional<BubbleController>, transactionSupplier: Supplier<SurfaceControl.Transaction> = Supplier { SurfaceControl.Transaction() }, @@ -925,7 +1010,9 @@ constructor( context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, interactionJankMonitor, + bubbleController, transactionSupplier, ) { @@ -958,9 +1045,16 @@ constructor( super.setupEndDragToDesktop(info, startTransaction, finishTransaction) val state = requireTransitionState() - val homeLeash = state.homeChange?.leash ?: error("Expects home leash to be non-null") - // Hide home on finish to prevent flickering when wallpaper activity flag is enabled - finishTransaction.hide(homeLeash) + val homeLeash = state.homeChange?.leash + if (homeLeash == null) { + ProtoLog.e( + ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE, + "DragToDesktop: home leash is null", + ) + } else { + // Hide home on finish to prevent flickering when wallpaper activity flag is enabled + finishTransaction.hide(homeLeash) + } // Setup freeform tasks before animation state.freeformTaskChanges.forEach { change -> val startScale = FREEFORM_TASKS_INITIAL_SCALE @@ -997,7 +1091,7 @@ constructor( val startBoundsWithOffset = Rect(startBounds).apply { offset(startPosition.x.toInt(), startPosition.y.toInt()) } - dragToDesktopStateListener?.onCommitToDesktopAnimationStart(startTransaction) + dragToDesktopStateListener?.onCommitToDesktopAnimationStart() // Accept the merge by applying the merging transaction (applied by #showResizeVeil) // and finish callback. Show the veil and position the task at the first frame before // starting the final animation. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java index b902bb4394b5..95cc1e68ac11 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandler.java @@ -32,6 +32,7 @@ import android.graphics.Rect; import android.os.Handler; import android.os.IBinder; import android.util.DisplayMetrics; +import android.view.Choreographer; import android.view.SurfaceControl; import android.view.WindowManager; import android.view.WindowManager.TransitionType; @@ -49,9 +50,11 @@ import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.transition.Transitions; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + import java.util.ArrayList; import java.util.List; -import java.util.function.Consumer; import java.util.function.Supplier; @@ -68,7 +71,7 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH @ShellMainThread private final Handler mHandler; private final List<IBinder> mPendingTransitionTokens = new ArrayList<>(); - private Consumer<SurfaceControl.Transaction> mOnAnimationFinishedCallback; + private Function0<Unit> mOnAnimationFinishedCallback; private final Supplier<SurfaceControl.Transaction> mTransactionSupplier; private Point mPosition; @@ -103,14 +106,15 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH * @param position Position of the task when transition is started * @param onAnimationEndCallback to be called after animation */ - public void startTransition(@NonNull DesktopModeTransitionSource transitionSource, + public IBinder startTransition(@NonNull DesktopModeTransitionSource transitionSource, @NonNull WindowContainerTransaction wct, Point position, - Consumer<SurfaceControl.Transaction> onAnimationEndCallback) { + Function0<Unit> onAnimationEndCallback) { mPosition = position; mOnAnimationFinishedCallback = onAnimationEndCallback; final IBinder token = mTransitions.startTransition(getExitTransitionType(transitionSource), wct, this); mPendingTransitionTokens.add(token); + return token; } @Override @@ -184,13 +188,14 @@ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionH t.setPosition(sc, mPosition.x * (1 - fraction), mPosition.y * (1 - fraction)) .setScale(sc, currentScaleX, currentScaleY) .show(sc) + .setFrameTimeline(Choreographer.getInstance().getVsyncId()) .apply(); }); animator.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { if (mOnAnimationFinishedCallback != null) { - mOnAnimationFinishedCallback.accept(finishT); + mOnAnimationFinishedCallback.invoke(); } mInteractionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_EXIT_MODE); mTransitions.getMainExecutor().execute( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl index 6002a4dfe0d9..7ed1581cdfdb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/IDesktopTaskListener.aidl @@ -16,27 +16,56 @@ package com.android.wm.shell.desktopmode; +import com.android.wm.shell.desktopmode.DisplayDeskState; + /** * Allows external processes to register a listener in WMShell to get updates about desktop task * state. */ -interface IDesktopTaskListener { +oneway interface IDesktopTaskListener { + + /** + * Called once when the listener first gets connected to initialize it with the current state of + * desks in Shell. + */ + void onListenerConnected(in DisplayDeskState[] displayDeskStates); /** Desktop tasks visibility has changed. Visible if at least 1 task is visible. */ - oneway void onTasksVisibilityChanged(int displayId, int visibleTasksCount); + void onTasksVisibilityChanged(int displayId, int visibleTasksCount); /** @deprecated this is no longer supported. */ - oneway void onStashedChanged(int displayId, boolean stashed); + void onStashedChanged(int displayId, boolean stashed); /** * Shows taskbar corner radius when running desktop tasks are updated if * [hasTasksRequiringTaskbarRounding] is true. */ - oneway void onTaskbarCornerRoundingUpdate(boolean hasTasksRequiringTaskbarRounding); + void onTaskbarCornerRoundingUpdate(boolean hasTasksRequiringTaskbarRounding); /** Entering desktop mode transition is started, send the signal with transition duration. */ - oneway void onEnterDesktopModeTransitionStarted(int transitionDuration); + void onEnterDesktopModeTransitionStarted(int transitionDuration); /** Exiting desktop mode transition is started, send the signal with transition duration. */ - oneway void onExitDesktopModeTransitionStarted(int transitionDuration); + void onExitDesktopModeTransitionStarted(int transitionDuration); + + /** + * Called when the conditions that allow the creation of a new desk on the display whose ID is + * `displayId` changes to `canCreateDesks`. It's also called when a new display is added. + */ + void onCanCreateDesksChanged(int displayId, boolean canCreateDesks); + + /** Called when a desk whose ID is `deskId` is added to the display whose ID is `displayId`. */ + void onDeskAdded(int displayId, int deskId); + + /** + * Called when a desk whose ID is `deskId` is removed from the display whose ID is `displayId`. + */ + void onDeskRemoved(int displayId, int deskId); + + /** + * Called when the active desk changes on the display whose ID is `displayId`. + * If `newActiveDesk` is -1, it means a desk is no longer active on the display. + * If `oldActiveDesk` is -1, it means a desk was not active on the display. + */ + void onActiveDeskChanged(int displayId, int newActiveDesk, int oldActiveDesk); }
\ No newline at end of file diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS index afdda8ff865e..47b3ae8fc11b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/OWNERS @@ -3,7 +3,6 @@ atsjenk@google.com jorgegil@google.com madym@google.com pbdr@google.com -tkachenkoi@google.com vaniadesmonda@google.com pragyabajoria@google.com uysalorhan@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt new file mode 100644 index 000000000000..2317274dbbf0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainer.kt @@ -0,0 +1,491 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.RectEvaluator +import android.animation.ValueAnimator +import android.app.ActivityManager +import android.content.Context +import android.graphics.PixelFormat +import android.graphics.Rect +import android.graphics.drawable.LayerDrawable +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import android.view.WindowManager +import android.view.WindowlessWindowManager +import android.view.animation.DecelerateInterpolator +import com.android.internal.annotations.VisibleForTesting +import com.android.window.flags.Flags +import com.android.wm.shell.R +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.common.DisplayLayout +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType +import com.android.wm.shell.shared.annotations.ShellDesktopThread +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory + +/** + * Container for the view / viewhost of the indicator, ensuring it is created / animated off the + * main thread. + */ +@VisibleForTesting +class VisualIndicatorViewContainer +@JvmOverloads +constructor( + @ShellDesktopThread private val desktopExecutor: ShellExecutor, + @ShellMainThread private val mainExecutor: ShellExecutor, + private val indicatorBuilder: SurfaceControl.Builder, + private val syncQueue: SyncTransactionQueue, + private val surfaceControlViewHostFactory: SurfaceControlViewHostFactory = + object : SurfaceControlViewHostFactory {}, + private val bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, +) { + @VisibleForTesting var indicatorView: View? = null + private var indicatorViewHost: SurfaceControlViewHost? = null + // Below variables and the SyncTransactionQueue are the only variables that should + // be accessed from shell main thread. Everything else should be used exclusively + // from the desktop thread. + private var indicatorLeash: SurfaceControl? = null + private var isReleased = false + + /** Create a fullscreen indicator with no animation */ + @ShellMainThread + fun createView( + context: Context, + display: Display, + layout: DisplayLayout, + taskInfo: ActivityManager.RunningTaskInfo, + taskSurface: SurfaceControl, + ) { + if (isReleased) return + desktopExecutor.execute { + val resources = context.resources + val metrics = resources.displayMetrics + val screenWidth: Int + val screenHeight: Int + if (Flags.enableBugFixesForSecondaryDisplay()) { + screenWidth = layout.width() + screenHeight = layout.height() + } else { + screenWidth = metrics.widthPixels + screenHeight = metrics.heightPixels + } + indicatorView = View(context) + val leash = + indicatorBuilder + .setName("Desktop Mode Visual Indicator") + .setContainerLayer() + .setCallsite("DesktopModeVisualIndicator.createView") + .build() + val lp = + WindowManager.LayoutParams( + screenWidth, + screenHeight, + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, + PixelFormat.TRANSPARENT, + ) + lp.title = "Desktop Mode Visual Indicator" + lp.setTrustedOverlay() + lp.inputFeatures = + lp.inputFeatures or WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL + val windowManager = + WindowlessWindowManager( + taskInfo.configuration, + leash, + /* hostInputTransferToken= */ null, + ) + indicatorViewHost = + surfaceControlViewHostFactory.create( + context, + display, + windowManager, + "VisualIndicatorViewContainer", + ) + indicatorView?.let { indicatorViewHost?.setView(it, lp) } + showIndicator(taskSurface, leash) + } + } + + private fun showIndicator(taskSurface: SurfaceControl, leash: SurfaceControl) { + mainExecutor.execute { + indicatorLeash = leash + val t = SurfaceControl.Transaction() + t.show(indicatorLeash) + // We want this indicator to be behind the dragged task, but in front of all others. + t.setRelativeLayer(indicatorLeash, taskSurface, -1) + syncQueue.runInSync { transaction: SurfaceControl.Transaction -> + transaction.merge(t) + t.close() + } + } + } + + @VisibleForTesting + fun getIndicatorBounds(): Rect { + return indicatorView?.background?.getBounds() ?: Rect() + } + + /** + * Takes existing indicator and animates it to bounds reflecting a new indicator type. Should + * only be called from the main thread. + */ + @ShellMainThread + fun transitionIndicator( + taskInfo: ActivityManager.RunningTaskInfo, + displayController: DisplayController, + currentType: IndicatorType, + newType: IndicatorType, + ) { + if (currentType == newType || isReleased) return + desktopExecutor.execute { + val layout = + displayController.getDisplayLayout(taskInfo.displayId) + ?: error("Expected to find DisplayLayout for taskId${taskInfo.taskId}.") + if (currentType == IndicatorType.NO_INDICATOR) { + fadeInIndicator(layout, newType) + } else if (newType == IndicatorType.NO_INDICATOR) { + fadeOutIndicator(layout, currentType, /* finishCallback= */ null) + } else { + val animStartType = IndicatorType.valueOf(currentType.name) + val animator = + indicatorView?.let { + VisualIndicatorAnimator.animateIndicatorType( + it, + layout, + animStartType, + newType, + bubbleBoundsProvider, + ) + } ?: return@execute + animator.start() + } + } + } + + /** + * Fade indicator in as provided type. Animator fades it in while expanding the bounds outwards. + */ + @VisibleForTesting + fun fadeInIndicator(layout: DisplayLayout, type: IndicatorType) { + desktopExecutor.assertCurrentThread() + indicatorView?.let { + it.setBackgroundResource(R.drawable.desktop_windowing_transition_background) + val animator = + VisualIndicatorAnimator.fadeBoundsIn(it, type, layout, bubbleBoundsProvider) + animator.start() + } + } + + /** + * Fade out indicator without fully releasing it. Animator fades it out while shrinking bounds. + * + * @param finishCallback called when animation ends or gets cancelled + */ + fun fadeOutIndicator( + layout: DisplayLayout, + currentType: IndicatorType, + finishCallback: Runnable?, + ) { + if (currentType == IndicatorType.NO_INDICATOR) { + // In rare cases, fade out can be requested before the indicator has determined its + // initial type and started animating in. In this case, no animator is needed. + finishCallback?.run() + return + } + desktopExecutor.execute { + indicatorView?.let { + val animStartType = IndicatorType.valueOf(currentType.name) + val animator = + VisualIndicatorAnimator.fadeBoundsOut( + it, + animStartType, + layout, + bubbleBoundsProvider, + ) + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + if (finishCallback != null) { + mainExecutor.execute(finishCallback) + } + } + } + ) + animator.start() + } + } + } + + /** Release the indicator and its components when it is no longer needed. */ + @ShellMainThread + fun releaseVisualIndicator() { + if (isReleased) return + desktopExecutor.execute { + indicatorViewHost?.release() + indicatorViewHost = null + } + indicatorLeash?.let { + val tx = SurfaceControl.Transaction() + tx.remove(it) + indicatorLeash = null + syncQueue.runInSync { transaction: SurfaceControl.Transaction -> + transaction.merge(tx) + tx.close() + } + } + isReleased = true + } + + /** + * Animator for Desktop Mode transitions which supports bounds and alpha animation. Functions + * should only be called from the desktop executor. + */ + @VisibleForTesting + class VisualIndicatorAnimator(view: View, startBounds: Rect, endBounds: Rect) : + ValueAnimator() { + /** + * Determines how this animator will interact with the view's alpha: Fade in, fade out, or + * no change to alpha + */ + private enum class AlphaAnimType { + ALPHA_FADE_IN_ANIM, + ALPHA_FADE_OUT_ANIM, + ALPHA_NO_CHANGE_ANIM, + } + + private val indicatorView: View = view + @VisibleForTesting val indicatorStartBounds = Rect(startBounds) + @VisibleForTesting val indicatorEndBounds = endBounds + private val mRectEvaluator: RectEvaluator + + init { + setFloatValues(0f, 1f) + mRectEvaluator = RectEvaluator(Rect()) + } + + /** + * Update bounds of view based on current animation fraction. Use of delta is to animate + * bounds independently, in case we need to run multiple animations simultaneously. + * + * @param fraction fraction to use, compared against previous fraction + * @param view the view to update + */ + @ShellDesktopThread + private fun updateBounds(fraction: Float, view: View?) { + if (indicatorStartBounds == indicatorEndBounds) { + return + } + val currentBounds = + mRectEvaluator.evaluate(fraction, indicatorStartBounds, indicatorEndBounds) + view?.background?.bounds = currentBounds + } + + /** + * Fade in the fullscreen indicator + * + * @param fraction current animation fraction + */ + @ShellDesktopThread + private fun updateIndicatorAlpha(fraction: Float, view: View?) { + val drawable = view?.background as LayerDrawable + drawable.findDrawableByLayerId(R.id.indicator_stroke).alpha = + (MAXIMUM_OPACITY * fraction).toInt() + drawable.findDrawableByLayerId(R.id.indicator_solid).alpha = + (MAXIMUM_OPACITY * fraction * INDICATOR_FINAL_OPACITY).toInt() + } + + companion object { + private const val FULLSCREEN_INDICATOR_DURATION = 200 + private const val FULLSCREEN_SCALE_ADJUSTMENT_PERCENT = 0.015f + private const val INDICATOR_FINAL_OPACITY = 0.35f + private const val MAXIMUM_OPACITY = 255 + + @ShellDesktopThread + fun fadeBoundsIn( + view: View, + type: IndicatorType, + displayLayout: DisplayLayout, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val endBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider) + val startBounds = getMinBounds(endBounds) + view.background.bounds = startBounds + + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_IN_ANIM) + return animator + } + + @ShellDesktopThread + fun fadeBoundsOut( + view: View, + type: IndicatorType, + displayLayout: DisplayLayout, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val startBounds = getIndicatorBounds(displayLayout, type, bubbleBoundsProvider) + val endBounds = getMinBounds(startBounds) + view.background.bounds = startBounds + + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_FADE_OUT_ANIM) + return animator + } + + /** + * Create animator for visual indicator changing type (i.e., fullscreen to freeform, + * freeform to split, etc.) + * + * @param view the view for this indicator + * @param displayLayout information about the display the transitioning task is + * currently on + * @param origType the original indicator type + * @param newType the new indicator type + * @param desktopExecutor: the executor for the ShellDesktopThread; should be the only + * thread this function runs on + */ + @ShellDesktopThread + fun animateIndicatorType( + view: View, + displayLayout: DisplayLayout, + origType: IndicatorType, + newType: IndicatorType, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): VisualIndicatorAnimator { + val startBounds = getIndicatorBounds(displayLayout, origType, bubbleBoundsProvider) + val endBounds = getIndicatorBounds(displayLayout, newType, bubbleBoundsProvider) + val animator = VisualIndicatorAnimator(view, startBounds, endBounds) + animator.interpolator = DecelerateInterpolator() + setupIndicatorAnimation(animator, AlphaAnimType.ALPHA_NO_CHANGE_ANIM) + return animator + } + + /** Calculates the bounds the indicator should have when fully faded in. */ + private fun getIndicatorBounds( + layout: DisplayLayout, + type: IndicatorType, + bubbleBoundsProvider: BubbleDropTargetBoundsProvider?, + ): Rect { + val desktopStableBounds = Rect() + layout.getStableBounds(desktopStableBounds) + val padding = desktopStableBounds.top + when (type) { + IndicatorType.TO_FULLSCREEN_INDICATOR -> { + desktopStableBounds.top += padding + desktopStableBounds.bottom -= padding + desktopStableBounds.left += padding + desktopStableBounds.right -= padding + return desktopStableBounds + } + + IndicatorType.TO_DESKTOP_INDICATOR -> { + val adjustmentPercentage = + (1f - DesktopTasksController.DESKTOP_MODE_INITIAL_BOUNDS_SCALE) + return Rect( + (adjustmentPercentage * desktopStableBounds.width() / 2).toInt(), + (adjustmentPercentage * desktopStableBounds.height() / 2).toInt(), + (desktopStableBounds.width() - + (adjustmentPercentage * desktopStableBounds.width() / 2)) + .toInt(), + (desktopStableBounds.height() - + (adjustmentPercentage * desktopStableBounds.height() / 2)) + .toInt(), + ) + } + + IndicatorType.TO_SPLIT_LEFT_INDICATOR -> + return Rect( + padding, + padding, + desktopStableBounds.width() / 2 - padding, + desktopStableBounds.height(), + ) + + IndicatorType.TO_SPLIT_RIGHT_INDICATOR -> + return Rect( + desktopStableBounds.width() / 2 + padding, + padding, + desktopStableBounds.width() - padding, + desktopStableBounds.height(), + ) + IndicatorType.TO_BUBBLE_LEFT_INDICATOR -> + return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ true + ) ?: Rect() + IndicatorType.TO_BUBBLE_RIGHT_INDICATOR -> + return bubbleBoundsProvider?.getBubbleBarExpandedViewDropTargetBounds( + /* onLeft= */ false + ) ?: Rect() + else -> throw IllegalArgumentException("Invalid indicator type provided.") + } + } + + /** Add necessary listener for animation of indicator */ + private fun setupIndicatorAnimation( + animator: VisualIndicatorAnimator, + animType: AlphaAnimType, + ) { + animator.addUpdateListener { a: ValueAnimator -> + animator.updateBounds(a.animatedFraction, animator.indicatorView) + if (animType == AlphaAnimType.ALPHA_FADE_IN_ANIM) { + animator.updateIndicatorAlpha(a.animatedFraction, animator.indicatorView) + } else if (animType == AlphaAnimType.ALPHA_FADE_OUT_ANIM) { + animator.updateIndicatorAlpha( + 1 - a.animatedFraction, + animator.indicatorView, + ) + } + } + animator.addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + animator.indicatorView.background.bounds = animator.indicatorEndBounds + } + } + ) + animator.setDuration(FULLSCREEN_INDICATOR_DURATION.toLong()) + } + + /** + * Return the minimum bounds of a visual indicator, to be used at the end of fading out + * and the start of fading in. + */ + private fun getMinBounds(maxBounds: Rect): Rect { + return Rect( + (maxBounds.left + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())) + .toInt(), + (maxBounds.top + (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())) + .toInt(), + (maxBounds.right - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.width())) + .toInt(), + (maxBounds.bottom - (FULLSCREEN_SCALE_ADJUSTMENT_PERCENT * maxBounds.height())) + .toInt(), + ) + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt index 2bd7a9873a5e..b5490cb4b595 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProvider.kt @@ -20,6 +20,9 @@ import android.util.SparseArray import android.util.SparseBooleanArray import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerToken +import androidx.core.util.keyIterator +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE /** Provides per display window container tokens for [DesktopWallpaperActivity]. */ class DesktopWallpaperActivityTokenProvider { @@ -28,6 +31,7 @@ class DesktopWallpaperActivityTokenProvider { private val wallpaperActivityVisByDisplayId = SparseBooleanArray() fun setToken(token: WindowContainerToken, displayId: Int = DEFAULT_DISPLAY) { + logV("Setting desktop wallpaper activity token for display %s", displayId) wallpaperActivityTokenByDisplayId[displayId] = token } @@ -36,9 +40,21 @@ class DesktopWallpaperActivityTokenProvider { } fun removeToken(displayId: Int = DEFAULT_DISPLAY) { + logV("Remove desktop wallpaper activity token for display %s", displayId) wallpaperActivityTokenByDisplayId.delete(displayId) } + fun removeToken(token: WindowContainerToken) { + val displayId = + wallpaperActivityTokenByDisplayId.keyIterator().asSequence().find { + wallpaperActivityTokenByDisplayId[it] == token + } + if (displayId != null) { + logV("Remove desktop wallpaper activity token for display %s", displayId) + wallpaperActivityTokenByDisplayId.delete(displayId) + } + } + fun setWallpaperActivityIsVisible( isVisible: Boolean = false, displayId: Int = DEFAULT_DISPLAY, @@ -50,4 +66,12 @@ class DesktopWallpaperActivityTokenProvider { return wallpaperActivityTokenByDisplayId[displayId] != null && wallpaperActivityVisByDisplayId.get(displayId, false) } + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + companion object { + private const val TAG = "DesktopWallpaperActivityTokenProvider" + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt index 39dc48d6d206..f66451462e43 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/AppHandleEducationController.kt @@ -26,6 +26,8 @@ import android.view.View.LAYOUT_DIRECTION_RTL import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.desktopmode.CaptionState +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository import com.android.wm.shell.shared.annotations.ShellBackgroundThread @@ -62,6 +64,7 @@ class AppHandleEducationController( private val windowingEducationViewController: DesktopWindowingEducationTooltipController, @ShellMainThread private val applicationCoroutineScope: CoroutineScope, @ShellBackgroundThread private val backgroundDispatcher: MainCoroutineDispatcher, + private val desktopModeUiEventLogger: DesktopModeUiEventLogger, ) { private lateinit var openHandleMenuCallback: (Int) -> Unit private lateinit var toDesktopModeCallback: (Int, DesktopModeTransitionSource) -> Unit @@ -140,6 +143,27 @@ class AppHandleEducationController( windowingEducationViewController.hideEducationTooltip() } } + + // Listens to a [NoCaption] state change to dismiss any tooltip if the app handle or app + // header is gone or de-focused (e.g. when a user swipes up to home, overview, or enters + // split screen) + applicationCoroutineScope.launch { + if ( + isAppHandleHintViewed() && + isEnterDesktopModeHintViewed() && + isExitDesktopModeHintViewed() + ) + return@launch + windowDecorCaptionHandleRepository.captionStateFlow + .filter { captionState -> + captionState is CaptionState.NoCaption && + !isAppHandleHintViewed() && + !isEnterDesktopModeHintViewed() && + !isExitDesktopModeHintViewed() + } + .flowOn(backgroundDispatcher) + .collectLatest { windowingEducationViewController.hideEducationTooltip() } + } } } @@ -150,6 +174,7 @@ class AppHandleEducationController( private fun showEducation(captionState: CaptionState) { val appHandleBounds = (captionState as CaptionState.AppHandle).globalAppHandleBounds + val taskInfo = captionState.runningTaskInfo val tooltipGlobalCoordinates = Point(appHandleBounds.left + appHandleBounds.width() / 2, appHandleBounds.bottom) // Populate information important to inflate app handle education tooltip. @@ -167,22 +192,34 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.UP, onEducationClickAction = { - openHandleMenuCallback(captionState.runningTaskInfo.taskId) + openHandleMenuCallback(taskInfo.taskId) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_CLICKED, + ) }, onDismissAction = { - // TODO: b/341320146 - Log previous tooltip was dismissed + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_DISMISSED, + ) }, ) windowingEducationViewController.showEducationTooltip( tooltipViewConfig = appHandleTooltipConfig, - taskId = captionState.runningTaskInfo.taskId, + taskId = taskInfo.taskId, + ) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_SHOWN, ) } /** Show tooltip that points to windowing image button in app handle menu */ private suspend fun showWindowingImageButtonTooltip(captionState: CaptionState.AppHandle) { val appInfoPillHeight = getSize(R.dimen.desktop_mode_handle_menu_app_info_pill_height) + val taskInfo = captionState.runningTaskInfo val windowingOptionPillHeight = getSize(R.dimen.desktop_mode_handle_menu_windowing_pill_height) val appHandleMenuWidth = @@ -224,24 +261,36 @@ class AppHandleEducationController( DesktopWindowingEducationTooltipController.TooltipArrowDirection.HORIZONTAL, onEducationClickAction = { toDesktopModeCallback( - captionState.runningTaskInfo.taskId, + taskInfo.taskId, DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON, ) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED, + ) }, onDismissAction = { - // TODO: b/341320146 - Log previous tooltip was dismissed + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED, + ) }, ) windowingEducationViewController.showEducationTooltip( - taskId = captionState.runningTaskInfo.taskId, + taskId = taskInfo.taskId, tooltipViewConfig = windowingImageButtonTooltipConfig, ) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN, + ) } /** Show tooltip that points to app chip button and educates user on how to exit desktop mode */ private suspend fun showExitWindowingTooltip(captionState: CaptionState.AppHeader) { val globalAppChipBounds = captionState.globalAppChipBounds + val taskInfo = captionState.runningTaskInfo val tooltipGlobalCoordinates = Point( if (isRtl()) { @@ -266,16 +315,27 @@ class AppHandleEducationController( arrowDirection = DesktopWindowingEducationTooltipController.TooltipArrowDirection.HORIZONTAL, onDismissAction = { - // TODO: b/341320146 - Log previous tooltip was dismissed + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_DISMISSED, + ) }, onEducationClickAction = { - openHandleMenuCallback(captionState.runningTaskInfo.taskId) + openHandleMenuCallback(taskInfo.taskId) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_CLICKED, + ) }, ) windowingEducationViewController.showEducationTooltip( - taskId = captionState.runningTaskInfo.taskId, + taskId = taskInfo.taskId, tooltipViewConfig = exitWindowingTooltipConfig, ) + desktopModeUiEventLogger.log( + taskInfo, + DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN, + ) } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt index d061e03b9be5..3af52b35bed7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt @@ -22,6 +22,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting @@ -48,6 +49,10 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { DataStoreFactory.create( serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { WindowingEducationProto.getDefaultInstance() } + ), ) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt index e5ad901d1435..f16428dfb90b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppToWebEducationDatastoreRepository.kt @@ -22,6 +22,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.internal.annotations.VisibleForTesting @@ -42,6 +43,10 @@ constructor(private val dataStore: DataStore<WindowingEducationProto>) { DataStoreFactory.create( serializer = WindowingEducationProtoSerializer, produceFile = { context.dataStoreFile(APP_TO_WEB_EDUCATION_DATASTORE_FILEPATH) }, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { WindowingEducationProto.getDefaultInstance() } + ), ) ) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt new file mode 100644 index 000000000000..9dec96933ee5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DeskTransition.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.os.IBinder + +/** Represents shell-started transitions involving desks. */ +sealed class DeskTransition { + /** The transition token. */ + abstract val token: IBinder + + /** A transition to remove a desk and its tasks from a display. */ + data class RemoveDesk( + override val token: IBinder, + val displayId: Int, + val deskId: Int, + val tasks: Set<Int>, + val onDeskRemovedListener: OnDeskRemovedListener?, + ) : DeskTransition() + + /** A transition to activate a desk in its display. */ + data class ActivateDesk(override val token: IBinder, val displayId: Int, val deskId: Int) : + DeskTransition() + + /** A transition to activate a desk by moving an outside task to it. */ + data class ActiveDeskWithTask( + override val token: IBinder, + val displayId: Int, + val deskId: Int, + val enterTaskId: Int, + ) : DeskTransition() + + /** A transition to deactivate a desk. */ + data class DeactivateDesk(override val token: IBinder, val deskId: Int) : DeskTransition() +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt index 5cbb59fbf323..0f2f3711a9a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksOrganizer.kt @@ -27,6 +27,9 @@ interface DesksOrganizer { /** Activates the given desk, making it visible in its display. */ fun activateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Deactivates the given desk, removing it as the default launch container for new tasks. */ + fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) + /** Removes the given desk and its desktop windows. */ fun removeDesk(wct: WindowContainerTransaction, deskId: Int) @@ -37,12 +40,18 @@ interface DesksOrganizer { task: ActivityManager.RunningTaskInfo, ) + /** Whether the change is for the given desk id. */ + fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean + /** * Returns the desk id in which the task in the given change is located at the end of a * transition, if any. */ fun getDeskAtEnd(change: TransitionInfo.Change): Int? + /** Whether the desk is activate according to the given change at the end of a transition. */ + fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean + /** A callback that is invoked when the desk container is created. */ fun interface OnCreateCallback { /** Calls back when the [deskId] has been created. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt new file mode 100644 index 000000000000..e57b56378fb3 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserver.kt @@ -0,0 +1,132 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.desktopmode.multidesks + +import android.os.IBinder +import android.view.WindowManager.TRANSIT_CLOSE +import android.window.DesktopExperienceFlags +import android.window.TransitionInfo +import com.android.internal.protolog.ProtoLog +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE + +/** + * Observer of desk-related transitions, such as adding, removing or activating a whole desk. It + * tracks pending transitions and updates repository state once they finish. + */ +class DesksTransitionObserver( + private val desktopUserRepositories: DesktopUserRepositories, + private val desksOrganizer: DesksOrganizer, +) { + private val deskTransitions = mutableMapOf<IBinder, DeskTransition>() + + /** Adds a pending desk transition to be tracked. */ + fun addPendingTransition(transition: DeskTransition) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + deskTransitions[transition.token] = transition + } + + /** + * Called when any transition is ready, which may include transitions not tracked by this + * observer. + */ + fun onTransitionReady(transition: IBinder, info: TransitionInfo) { + if (!DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) return + val deskTransition = deskTransitions.remove(transition) ?: return + logD("Desk transition ready: %s", deskTransition) + val desktopRepository = desktopUserRepositories.current + when (deskTransition) { + is DeskTransition.RemoveDesk -> { + check(info.type == TRANSIT_CLOSE) { "Expected close transition for desk removal" } + // TODO: b/362720497 - consider verifying the desk was actually removed through the + // DesksOrganizer. The transition info won't have changes if the desk was not + // visible, such as when dismissing from Overview. + val deskId = deskTransition.deskId + val displayId = deskTransition.displayId + desktopRepository.removeDesk(deskTransition.deskId) + deskTransition.onDeskRemovedListener?.onDeskRemoved(displayId, deskId) + } + is DeskTransition.ActivateDesk -> { + val activeDeskChange = + info.changes.find { change -> + desksOrganizer.isDeskActiveAtEnd(change, deskTransition.deskId) + } + activeDeskChange?.let { + desktopRepository.setActiveDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + ) + } + } + is DeskTransition.ActiveDeskWithTask -> { + val withTask = + info.changes.find { change -> + change.taskInfo?.taskId == deskTransition.enterTaskId && + change.taskInfo?.isVisibleRequested == true && + desksOrganizer.getDeskAtEnd(change) == deskTransition.deskId + } + withTask?.let { + desktopRepository.setActiveDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + ) + desktopRepository.addTaskToDesk( + displayId = deskTransition.displayId, + deskId = deskTransition.deskId, + taskId = deskTransition.enterTaskId, + isVisible = true, + ) + } + } + is DeskTransition.DeactivateDesk -> { + var visibleDeactivation = false + for (change in info.changes) { + val isDeskChange = desksOrganizer.isDeskChange(change, deskTransition.deskId) + if (isDeskChange) { + visibleDeactivation = true + continue + } + val taskId = change.taskInfo?.taskId ?: continue + val removedFromDesk = + desktopRepository.getDeskIdForTask(taskId) == deskTransition.deskId && + desksOrganizer.getDeskAtEnd(change) == null + if (removedFromDesk) { + desktopRepository.removeTaskFromDesk( + deskId = deskTransition.deskId, + taskId = taskId, + ) + } + } + // Always deactivate even if there's no change that confirms the desk was + // deactivated. Some interactions, such as the desk deactivating because it's + // occluded by a fullscreen task result in a transition change, but others, such + // as transitioning from an empty desk to home may not. + if (!visibleDeactivation) { + logD("Deactivating desk without transition change") + } + desktopRepository.setDeskInactive(deskId = deskTransition.deskId) + } + } + } + + private fun logD(msg: String, vararg arguments: Any?) { + ProtoLog.d(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private companion object { + private const val TAG = "DesksTransitionObserver" + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt index 79c48c5e9594..339932cabd2c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizer.kt @@ -22,12 +22,13 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.util.SparseArray import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.DesktopExperienceFlags import android.window.TransitionInfo import android.window.WindowContainerTransaction import androidx.core.util.forEach import com.android.internal.annotations.VisibleForTesting import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer.OnCreateCallback import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE @@ -46,7 +47,7 @@ class RootTaskDesksOrganizer( @VisibleForTesting val roots = SparseArray<DeskRoot>() init { - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { shellInit.addInitCallback( { shellCommandHandler.addDumpCallback(this::dump, this) }, this, @@ -82,18 +83,37 @@ class RootTaskDesksOrganizer( ) } + override fun deactivateDesk(wct: WindowContainerTransaction, deskId: Int) { + logV("deactivateDesk %d", deskId) + val root = checkNotNull(roots[deskId]) { "Root not found for desk: $deskId" } + wct.setLaunchRoot( + /* container= */ root.taskInfo.token, + /* windowingModes= */ null, + /* activityTypes= */ null, + ) + } + override fun moveTaskToDesk( wct: WindowContainerTransaction, deskId: Int, task: RunningTaskInfo, ) { val root = roots[deskId] ?: error("Root not found for desk: $deskId") + wct.setWindowingMode(task.token, WINDOWING_MODE_UNDEFINED) wct.reparent(task.token, root.taskInfo.token, /* onTop= */ true) } + override fun isDeskChange(change: TransitionInfo.Change, deskId: Int): Boolean = + roots.contains(deskId) && change.taskInfo?.taskId == deskId + override fun getDeskAtEnd(change: TransitionInfo.Change): Int? = change.taskInfo?.parentTaskId?.takeIf { it in roots } + override fun isDeskActiveAtEnd(change: TransitionInfo.Change, deskId: Int): Boolean = + change.taskInfo?.taskId == deskId && + change.taskInfo?.isVisibleRequested == true && + change.mode == TRANSIT_TO_FRONT + override fun onTaskAppeared(taskInfo: RunningTaskInfo, leash: SurfaceControl) { if (taskInfo.parentTaskId in roots) { val deskId = taskInfo.parentTaskId diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt index 9e41270c21f8..1566544f5303 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopPersistentRepository.kt @@ -24,6 +24,7 @@ import androidx.datastore.core.CorruptionException import androidx.datastore.core.DataStore import androidx.datastore.core.DataStoreFactory import androidx.datastore.core.Serializer +import androidx.datastore.core.handlers.ReplaceFileCorruptionHandler import androidx.datastore.dataStoreFile import com.android.framework.protobuf.InvalidProtocolBufferException import com.android.wm.shell.shared.annotations.ShellBackgroundThread @@ -49,6 +50,10 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, scope = bgCoroutineScope, + corruptionHandler = + ReplaceFileCorruptionHandler( + produceNewData = { DesktopPersistentRepositories.getDefaultInstance() } + ), ) ) @@ -127,7 +132,10 @@ class DesktopPersistentRepository(private val dataStore: DataStore<DesktopPersis .toBuilder() .putDesktopRepoByUser( userId, - currentRepository.toBuilder().putDesktop(desktopId, desktop.build()).build(), + currentRepository + .toBuilder() + .putDesktop(desktopId, desktop.build()) + .build(), ) .build() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt index 5a89451ffdbc..0507e59c06e1 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -17,8 +17,8 @@ package com.android.wm.shell.desktopmode.persistence import android.content.Context +import android.window.DesktopExperienceFlags import android.window.DesktopModeFlags -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.DesktopRepository import com.android.wm.shell.desktopmode.DesktopUserRepositories import com.android.wm.shell.shared.annotations.ShellMainThread @@ -58,7 +58,7 @@ class DesktopRepositoryInitializerImpl( repository.addDesk( displayId = persistentDesktop.displayId, deskId = - if (Flags.enableMultipleDesktopsBackend()) { + if (DesktopExperienceFlags.ENABLE_MULTIPLE_DESKTOPS_BACKEND.isTrue) { persistentDesktop.desktopId } else { // When disabled, desk ids are always the display id. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md index faa97ac4512f..f50d253ddf42 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/changes.md @@ -1,4 +1,5 @@ # Making changes in the Shell +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md index 7070dead9957..9b09904527bf 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/dagger.md @@ -1,4 +1,5 @@ # Usage of Dagger in the Shell library +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md index 09e627c0e02c..dd5827af97d9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/debugging.md @@ -1,4 +1,5 @@ # Debugging in the Shell +[Back to home](README.md) --- @@ -50,6 +51,11 @@ adb shell wm shell protolog enable-text TAG adb shell wm shell protolog disable-text TAG ``` +### R8 optimizations & ProtoLog + +If the APK that the Shell library is included into has R8 optimizations enabled, then you may need +to update the proguard flags to keep the generated protolog classes (ie. AOSP SystemUI's [proguard.flags](base/packages/SystemUI/proguard_common.flags)). + ## Winscope Tracing The Winscope tool is extremely useful in determining what is happening on-screen in both @@ -57,25 +63,42 @@ WindowManager and SurfaceFlinger. Follow [go/winscope](http://go/winscope-help) use the tool. This trace will contain all the information about the windows/activities/surfaces on screen. -## WindowManager/SurfaceFlinger hierarchy dump +## WindowManager/SurfaceFlinger/InputDispatcher information A quick way to view the WindowManager hierarchy without a winscope trace is via the wm dumps: ```shell adb shell dumpsys activity containers +# The output lists the containers in the hierarchy from top -> bottom in z-order +``` + +To get more information about windows on the screen: +```shell +# All windows in WM +adb shell dumpsys window -a +# The windows are listed from top -> bottom in z-order + +# Visible windows only +adb shell dumpsys window -a visible ``` Likewise, the SurfaceFlinger hierarchy can be dumped for inspection by running: ```shell adb shell dumpsys SurfaceFlinger -# Search output for "Layer Hierarchy" +# Search output for "Layer Hierarchy", the surfaces in the table are listed bottom -> top in z-order +``` + +And the visible input windows can be dumped via: +```shell +adb shell dumpsys input +# Search output for "Windows:", they are ordered top -> bottom in z-order ``` ## Tracing global SurfaceControl transaction updates While Winscope traces are very useful, it sometimes doesn't give you enough information about which part of the code is initiating the transaction updates. In such cases, it can be helpful to get -stack traces when specific surface transaction calls are made, which is possible by enabling the -following system properties for example: +stack traces when specific surface transaction calls are made (regardless of process), which is +possible by enabling the following system properties for example: ```shell # Enabling adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,setPosition # matches the name of the SurfaceControlTransaction methods @@ -94,9 +117,16 @@ properties. It is not necessary to set both `log_match_call` and `log_match_name`, but note logs can be quite noisy if unfiltered. -It can sometimes be useful to trace specific logs and when they are applied (sometimes we build -transactions that can be applied later). You can do this by adding the "merge" and "apply" calls to -the set of requested calls: +### Tracing transaction merge & apply + +Tracing the method calls on SurfaceControl.Transaction tells you where a change is requested, but +the changes are not actually committed until the transaction itself is applied. And because +transactions can be passed across processes, or prepared in advance for later application (ie. +when restoring state after a Transition), the ordering of the change logs is not always clear +by itself. + +In such cases, you can also enable the "merge" and "apply" calls to get additional information +about how/when transactions are respectively merged/applied: ```shell # Enabling adb shell setprop persist.wm.debug.sc.tx.log_match_call setAlpha,merge,apply # apply will dump logs of each setAlpha or merge call on that tx @@ -104,6 +134,11 @@ adb reboot adb logcat -s "SurfaceControlRegistry" ``` +Using those logs, you can first look at where the desired change is called, note the transaction +id, and then search the logs for where that transaction id is used. If it is merged into another +transaction, you can continue the search using the merged transaction until you find the final +transaction which is applied. + ## Tracing activity starts & finishes in the app process It's sometimes useful to know when to see a stack trace of when an activity starts in the app code diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md index 061ae00e2b25..f7707da33189 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/extending.md @@ -1,4 +1,5 @@ # Extending the Shell for Products/OEMs +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md index b489fe8ea1a9..bed0fba453d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/overview.md @@ -1,4 +1,5 @@ # What is the WindowManager Shell +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md index 5e92010d4b68..47383b0a81a0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/sysui.md @@ -1,4 +1,5 @@ # Shell & SystemUI +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md index 98af930c4486..b4553131284b 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/testing.md @@ -1,4 +1,5 @@ # Testing +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md index 837a6dd32ff2..bde722357308 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/docs/threading.md @@ -1,4 +1,5 @@ # Threading +[Back to home](README.md) --- diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java index e8996bc03eeb..a67557bd7bd0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragAndDropController.java @@ -62,6 +62,7 @@ import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; @@ -80,6 +81,8 @@ import java.util.ArrayList; import java.util.function.Consumer; import java.util.function.Function; +import dagger.Lazy; + /** * Handles the global drag and drop handling for the Shell. */ @@ -101,6 +104,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll private final GlobalDragListener mGlobalDragListener; private final Transitions mTransitions; private SplitScreenController mSplitScreen; + private Lazy<BubbleBarDragListener> mBubbleBarDragController; private ShellExecutor mMainExecutor; private ArrayList<DragAndDropListener> mListeners = new ArrayList<>(); @@ -143,6 +147,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll IconProvider iconProvider, GlobalDragListener globalDragListener, Transitions transitions, + Lazy<BubbleBarDragListener> bubbleBarDragController, ShellExecutor mainExecutor) { mContext = context; mShellController = shellController; @@ -153,6 +158,7 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll mIconProvider = iconProvider; mGlobalDragListener = globalDragListener; mTransitions = transitions; + mBubbleBarDragController = bubbleBarDragController; mMainExecutor = mainExecutor; shellInit.addInitCallback(this::onInit, this); } @@ -246,7 +252,8 @@ public class DragAndDropController implements RemoteCallable<DragAndDropControll R.layout.global_drop_target, null); rootView.setOnDragListener(this); rootView.setVisibility(View.INVISIBLE); - DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, mIconProvider); + DragLayoutProvider dragLayout = new DragLayout(context, mSplitScreen, + mBubbleBarDragController.get(), mIconProvider); dragLayout.addDraggingView(rootView); try { wm.addView(rootView, layoutParams); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java index 5c72cb7f71a6..b3c1a92f5e1d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/draganddrop/DragLayout.java @@ -38,16 +38,15 @@ import android.annotation.SuppressLint; import android.app.ActivityManager; import android.app.StatusBarManager; import android.content.Context; +import android.content.Intent; import android.content.res.Configuration; import android.content.res.Resources; import android.graphics.Color; import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; -import android.graphics.RectF; import android.graphics.Region; import android.graphics.drawable.Drawable; -import android.util.Log; import android.view.DragEvent; import android.view.SurfaceControl; import android.view.View; @@ -66,9 +65,12 @@ import com.android.internal.logging.InstanceId; import com.android.internal.protolog.ProtoLog; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.R; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.split.SplitScreenUtils; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.shared.animation.Interpolators; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; +import com.android.wm.shell.shared.bubbles.BubbleBarLocation; import com.android.wm.shell.splitscreen.SplitScreenController; import java.io.PrintWriter; @@ -106,9 +108,11 @@ public class DragLayout extends LinearLayout private boolean mIsLeftRightSplit; private SplitDragPolicy.Target mCurrentTarget = null; + private final BubbleBarDragListener mBubbleBarDragListener; + private final Map<BubbleBarLocation, Rect> mBubbleBarLocations = new HashMap<>(); + private BubbleBarLocation mCurrentBubbleBarTarget = null; private DropZoneView mDropZoneView1; private DropZoneView mDropZoneView2; - private int mDisplayMargin; private int mDividerSize; private int mLaunchIntentEdgeMargin; @@ -128,11 +132,14 @@ public class DragLayout extends LinearLayout // Used with enableFlexibleSplit() flag @SuppressLint("WrongConstant") - public DragLayout(Context context, SplitScreenController splitScreenController, + public DragLayout(Context context, + SplitScreenController splitScreenController, + BubbleBarDragListener bubbleBarDragListener, IconProvider iconProvider) { super(context); mSplitScreenController = splitScreenController; mIconProvider = iconProvider; + mBubbleBarDragListener = bubbleBarDragListener; mPolicy = new SplitDragPolicy(context, splitScreenController, this); mStatusBarManager = context.getSystemService(StatusBarManager.class); mLastConfiguration.setTo(context.getResources().getConfiguration()); @@ -188,6 +195,12 @@ public class DragLayout extends LinearLayout protected void onLayout(boolean changed, int l, int t, int r, int b) { super.onLayout(changed, l, t, r, b); updateTouchableRegion(); + updateBubbleBarRegions(l, t, r, b); + } + + private void updateBubbleBarRegions(int l, int t, int r, int b) { + mBubbleBarLocations.clear(); + mBubbleBarLocations.putAll(mBubbleBarDragListener.getBubbleBarDropZones(l, t, r, b)); } /** @@ -400,6 +413,8 @@ public class DragLayout extends LinearLayout } private void updateDropZoneSizesForSingleTask() { + resetDropZoneTranslations(); + final LinearLayout.LayoutParams dropZoneView1 = (LayoutParams) mDropZoneView1.getLayoutParams(); final LinearLayout.LayoutParams dropZoneView2 = @@ -414,6 +429,19 @@ public class DragLayout extends LinearLayout mDropZoneView2.setLayoutParams(dropZoneView2); } + /** Zeroes out translationX and translationY on all drop zone views. */ + void resetDropZoneTranslations() { + setDropZoneTranslations(0, 0); + } + + /** Sets translationX and translationY on all drop zone views. */ + void setDropZoneTranslations(int x, int y) { + mDropZoneView1.setTranslationX(x); + mDropZoneView1.setTranslationY(y); + mDropZoneView2.setTranslationX(x); + mDropZoneView2.setTranslationY(y); + } + /** * Sets the size of the two drop zones based on the provided bounds. The divider sits between * the views and its size is included in the calculations. @@ -422,6 +450,15 @@ public class DragLayout extends LinearLayout * @param bounds2 bounds to apply to the second dropzone view, null if split in half. */ private void updateDropZoneSizes(Rect bounds1, Rect bounds2) { + if (bounds1 == null || bounds2 == null) { + // We're entering 50:50 split screen from a single app, no need for any translations. + resetDropZoneTranslations(); + } else { + // We're already in split, so align our drop zones to match the left/top app edge. This + // is necessary because the left/top app can be offscreen. + setDropZoneTranslations(bounds1.left, bounds1.top); + } + final int halfDivider = mDividerSize / 2; final LinearLayout.LayoutParams dropZoneView1 = (LayoutParams) mDropZoneView1.getLayoutParams(); @@ -514,17 +551,18 @@ public class DragLayout extends LinearLayout if (mHasDropped) { return; } + // if event is over the bubble don't let split handle it + if (interceptBubbleBarEvent(x, y)) { + mLastPosition.set(x, y); + return; + } // Find containing region, if the same as mCurrentRegion, then skip, otherwise, animate the // visibility of the current region SplitDragPolicy.Target target = mPolicy.getTargetAtLocation(x, y); if (mCurrentTarget != target) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current target: %s", target); if (target == null) { - // Animating to no target - animateSplitContainers(false, null /* animCompleteCallback */); - if (enableFlexibleSplit()) { - animateHighlight(target); - } + animateToNoTarget(); } else if (mCurrentTarget == null) { if (mPolicy.getNumTargets() == 1) { animateFullscreenContainer(true); @@ -565,6 +603,50 @@ public class DragLayout extends LinearLayout mLastPosition.set(x, y); } + private boolean interceptBubbleBarEvent(int x, int y) { + BubbleBarLocation bubbleBarLocation = getBubbleBarLocation(x, y); + boolean isOverTheBubbleBar = bubbleBarLocation != null; + if (mCurrentBubbleBarTarget != bubbleBarLocation) { + ProtoLog.v(ShellProtoLogGroup.WM_SHELL_DRAG_AND_DROP, "Current bubble bar location: %s", + isOverTheBubbleBar); + mCurrentBubbleBarTarget = bubbleBarLocation; + if (isOverTheBubbleBar) { + mBubbleBarDragListener.onDragItemOverBubbleBarDragZone(bubbleBarLocation); + if (mCurrentTarget != null) { + animateToNoTarget(); + mCurrentTarget = null; + } + } else { + mBubbleBarDragListener.onItemDraggedOutsideBubbleBarDropZone(); + } + //TODO(b/388894910): handle accessibility + } + return isOverTheBubbleBar; + } + + @Nullable + private BubbleBarLocation getBubbleBarLocation(int x, int y) { + Intent appData = mSession.appData; + if (appData == null) { + // there is no app data, so drop event over the bubble bar can not be handled + return null; + } + for (BubbleBarLocation location : mBubbleBarLocations.keySet()) { + if (mBubbleBarLocations.get(location).contains(x, y)) { + return location; + } + } + return null; + } + + private void animateToNoTarget() { + // Animating to no target + animateSplitContainers(false, null /* animCompleteCallback */); + if (enableFlexibleSplit()) { + animateHighlight(null); + } + } + /** * Hides the drag layout and animates out the visible drop targets. */ @@ -596,11 +678,18 @@ public class DragLayout extends LinearLayout */ public boolean drop(DragEvent event, @NonNull SurfaceControl dragSurface, @Nullable WindowContainerToken hideTaskToken, Runnable dropCompleteCallback) { - final boolean handledDrop = mCurrentTarget != null; + final boolean handledDrop = mCurrentTarget != null || mCurrentBubbleBarTarget != null; mHasDropped = true; - - // Process the drop - mPolicy.onDropped(mCurrentTarget, hideTaskToken); + Intent appData = mSession.appData; + + // Process the drop exclusive by DropTarget OR by the BubbleBar + if (mCurrentTarget != null) { + mPolicy.onDropped(mCurrentTarget, hideTaskToken); + } else if (appData != null && mCurrentBubbleBarTarget != null + && BubbleAnythingFlagHelper.enableCreateAnyBubble()) { + mBubbleBarDragListener.onItemDroppedOverBubbleBarDragZone(mCurrentBubbleBarTarget, + appData); + } // Start animating the drop UI out with the drag surface hide(event, dropCompleteCallback); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java index 52b6c62b0721..b60fb5e7bfdd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionHandler.java @@ -93,7 +93,8 @@ public class FreeformTaskTransitionHandler } @Override - public IBinder startMinimizedModeTransition(WindowContainerTransaction wct) { + public IBinder startMinimizedModeTransition( + WindowContainerTransaction wct, int taskId, boolean isLastTask) { final int type = Transitions.TRANSIT_MINIMIZE; final IBinder token = mTransitions.startTransition(type, wct, this); mPendingTransitionTokens.add(token); @@ -175,7 +176,9 @@ public class FreeformTaskTransitionHandler @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ArrayList<Animator> animations = mAnimations.get(mergeTarget); if (animations == null) return; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java index b6d19b657705..8059b94685ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserver.java @@ -21,13 +21,13 @@ import android.content.Context; import android.os.IBinder; import android.view.SurfaceControl; import android.view.WindowManager; +import android.window.DesktopModeFlags; import android.window.TransitionInfo; import android.window.WindowContainerToken; import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; -import com.android.window.flags.Flags; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; @@ -85,7 +85,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT) { - if (Flags.enableFullyImmersiveInDesktop()) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced and the repository // is updated from there **before** the |mWindowDecorViewModel| methods are invoked. // Otherwise window decoration relayout won't run with the immersive state up to date. @@ -191,7 +191,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionStarting(@NonNull IBinder transition) { - if (Flags.enableFullyImmersiveInDesktop()) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced. mDesktopImmersiveController.ifPresent(h -> h.onTransitionStarting(transition)); } @@ -199,7 +199,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionMerged(@NonNull IBinder merged, @NonNull IBinder playing) { - if (Flags.enableFullyImmersiveInDesktop()) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced. mDesktopImmersiveController.ifPresent(h -> h.onTransitionMerged(merged, playing)); } @@ -224,7 +224,7 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs @Override public void onTransitionFinished(@NonNull IBinder transition, boolean aborted) { - if (Flags.enableFullyImmersiveInDesktop()) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { // TODO(b/367268953): Remove when DesktopTaskListener is introduced. mDesktopImmersiveController.ifPresent(h -> h.onTransitionFinished(transition, aborted)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java index a874a5be426d..822934c1e646 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarter.java @@ -38,10 +38,13 @@ public interface FreeformTaskTransitionStarter { * Starts window minimization transition * * @param wct the {@link WindowContainerTransaction} that changes the windowing mode + * @param taskId the task id of the task being minimized + * @param isLastTask true if the task being minimized is the last visible task * * @return the started transition */ - IBinder startMinimizedModeTransition(WindowContainerTransaction wct); + IBinder startMinimizedModeTransition( + WindowContainerTransaction wct, int taskId, boolean isLastTask); /** * Starts close window transition @@ -60,4 +63,4 @@ public interface FreeformTaskTransitionStarter { * @return the started transition */ IBinder startPipTransition(WindowContainerTransaction wct); -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS index 83b5bf658459..44d46eea9c55 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/OWNERS @@ -4,7 +4,6 @@ jorgegil@google.com madym@google.com nmusgrave@google.com pbdr@google.com -tkachenkoi@google.com vaniadesmonda@google.com pragyabajoria@google.com uysalorhan@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java index d2ceb67030fc..ef216b1ae987 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/fullscreen/FullscreenTaskListener.java @@ -30,6 +30,8 @@ import androidx.annotation.NonNull; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; +import com.android.wm.shell.desktopmode.DesktopWallpaperActivity; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.protolog.ShellProtoLogGroup; import com.android.wm.shell.recents.RecentTasksController; import com.android.wm.shell.sysui.ShellInit; @@ -57,23 +59,30 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { private final SyncTransactionQueue mSyncQueue; private final Optional<RecentTasksController> mRecentTasksOptional; private final Optional<WindowDecorViewModel> mWindowDecorViewModelOptional; + private final Optional<DesktopWallpaperActivityTokenProvider> + mDesktopWallpaperActivityTokenProviderOptional; + /** * This constructor is used by downstream products. */ public FullscreenTaskListener(SyncTransactionQueue syncQueue) { this(null /* shellInit */, null /* shellTaskOrganizer */, syncQueue, Optional.empty(), - Optional.empty()); + Optional.empty(), Optional.empty()); } public FullscreenTaskListener(ShellInit shellInit, ShellTaskOrganizer shellTaskOrganizer, SyncTransactionQueue syncQueue, Optional<RecentTasksController> recentTasksOptional, - Optional<WindowDecorViewModel> windowDecorViewModelOptional) { + Optional<WindowDecorViewModel> windowDecorViewModelOptional, + Optional<DesktopWallpaperActivityTokenProvider> + desktopWallpaperActivityTokenProviderOptional) { mShellTaskOrganizer = shellTaskOrganizer; mSyncQueue = syncQueue; mRecentTasksOptional = recentTasksOptional; mWindowDecorViewModelOptional = windowDecorViewModelOptional; + mDesktopWallpaperActivityTokenProviderOptional = + desktopWallpaperActivityTokenProviderOptional; // Note: Some derivative FullscreenTaskListener implementations do not use ShellInit if (shellInit != null) { shellInit.addInitCallback(this::onInit, this); @@ -162,6 +171,12 @@ public class FullscreenTaskListener implements ShellTaskOrganizer.TaskListener { taskInfo.taskId); mTasks.remove(taskInfo.taskId); mWindowDecorViewModelOptional.ifPresent(v -> v.onTaskVanished(taskInfo)); + mDesktopWallpaperActivityTokenProviderOptional.ifPresent( + provider -> { + if (DesktopWallpaperActivity.isWallpaperTask(taskInfo)) { + provider.removeToken(taskInfo.getToken()); + } + }); if (Transitions.ENABLE_SHELL_TRANSITIONS) return; if (mWindowDecorViewModelOptional.isPresent()) { mWindowDecorViewModelOptional.get().destroyWindowDecoration(taskInfo); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java index f8e6285b0493..d666126b91ba 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/keyguard/KeyguardTransitionHandler.java @@ -277,7 +277,8 @@ public class KeyguardTransitionHandler @Override public void mergeAnimation(@NonNull IBinder nextTransition, @NonNull TransitionInfo nextInfo, - @NonNull SurfaceControl.Transaction nextT, @NonNull IBinder currentTransition, + @NonNull SurfaceControl.Transaction nextT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder currentTransition, @NonNull TransitionFinishCallback nextFinishCallback) { final StartedTransition playing = mStartedTransitions.get(currentTransition); if (playing == null) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java index 2f3c15208621..f0e6ae45c389 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransition.java @@ -372,7 +372,9 @@ public class PipTransition extends PipTransitionController { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { end(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java index da3181096d98..cef18f55b86d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTransitionController.java @@ -145,7 +145,7 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Called when the Shell wants to start an exit-via-expand from Pip transition/animation. */ - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { // Default implementation does nothing. } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java index f4c2a33079ba..ac94dac0e6a3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipTouchHandler.java @@ -909,10 +909,6 @@ public class PipTouchHandler { && mMenuState != MENU_STATE_FULL) { // If using pinch to zoom, double-tap functions as resizing between max/min size if (mPipResizeGestureHandler.isUsingPinchToZoom()) { - final boolean toExpand = mPipBoundsState.getBounds().width() - < mPipBoundsState.getMaxSize().x - && mPipBoundsState.getBounds().height() - < mPipBoundsState.getMaxSize().y; if (mMenuController.isMenuVisible()) { mMenuController.hideMenu(ANIM_TYPE_NONE, false /* resize */); } @@ -931,6 +927,7 @@ public class PipTouchHandler { } else { animateToUnexpandedState(getUserResizeBounds()); } + mPipBoundsState.setHasUserResizedPip(true); } else { // Expand to fullscreen if this is a double tap // the PiP should be frozen until the transition ends diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java index e74870d4d139..5894ea8d0b5c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipMenuController.java @@ -32,6 +32,7 @@ import android.view.View; import android.view.ViewRootImpl; import android.view.WindowManager; import android.view.WindowManagerGlobal; +import android.view.accessibility.AccessibilityManager; import android.window.SurfaceSyncGroup; import androidx.annotation.Nullable; @@ -63,6 +64,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis private TvPipMenuView mPipMenuView; private TvPipBackgroundView mPipBackgroundView; + private final AccessibilityManager mA11yManager; + private boolean mIsReloading; private static final int PIP_MENU_FORCE_CLOSE_DELAY_MS = 10_000; private final Runnable mClosePipMenuRunnable = this::closeMenu; @@ -107,6 +110,8 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis mSystemWindows = systemWindows; mMainHandler = mainHandler; + mA11yManager = context.getSystemService(AccessibilityManager.class); + // We need to "close" the menu the platform call for all the system dialogs to close (for // example, on the Home button press). final BroadcastReceiver closeSystemDialogsBroadcastReceiver = new BroadcastReceiver() { @@ -499,7 +504,9 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis switchToMenuMode(menuMode); } else { if (isMenuOpen(menuMode)) { - mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + if (!mA11yManager.isEnabled()) { + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } mMenuModeOnFocus = menuMode; } // Send a request to gain window focus if the menu is open, or lose window focus @@ -594,8 +601,10 @@ public class TvPipMenuController implements PipMenuController, TvPipMenuView.Lis public void onUserInteracting() { ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: onUserInteracting - mCurrentMenuMode=%s", TAG, getMenuModeString()); - mMainHandler.removeCallbacks(mClosePipMenuRunnable); - mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + if (mMainHandler.hasCallbacks(mClosePipMenuRunnable)) { + mMainHandler.removeCallbacks(mClosePipMenuRunnable); + mMainHandler.postDelayed(mClosePipMenuRunnable, PIP_MENU_FORCE_CLOSE_DELAY_MS); + } } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java index d3ae411469cc..0fa6a116350e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/tv/TvPipTransition.java @@ -653,7 +653,9 @@ public class TvPipTransition extends PipTransitionController { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: merge animation", TAG); if (mCurrentAnimator != null && mCurrentAnimator.isRunning()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java index 026482004d51..c4d065f158a4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelper.java @@ -19,7 +19,6 @@ package com.android.wm.shell.pip2; import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; -import android.graphics.RectF; import android.view.Choreographer; import android.view.SurfaceControl; @@ -29,99 +28,19 @@ import com.android.wm.shell.R; * Abstracts the common operations on {@link SurfaceControl.Transaction} for PiP transition. */ public class PipSurfaceTransactionHelper { - /** for {@link #scale(SurfaceControl.Transaction, SurfaceControl, Rect, Rect)} operation */ private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; - private final RectF mTmpSourceRectF = new RectF(); - private final RectF mTmpDestinationRectF = new RectF(); private final Rect mTmpDestinationRect = new Rect(); - private int mCornerRadius; - private int mShadowRadius; + private final int mCornerRadius; + private final int mShadowRadius; public PipSurfaceTransactionHelper(Context context) { - onDensityOrFontScaleChanged(context); - } - - /** - * Called when display size or font size of settings changed - * - * @param context the current context - */ - public void onDensityOrFontScaleChanged(Context context) { mCornerRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_corner_radius); mShadowRadius = context.getResources().getDimensionPixelSize(R.dimen.pip_shadow_radius); } /** - * Operates the alpha on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper alpha(SurfaceControl.Transaction tx, SurfaceControl leash, - float alpha) { - tx.setAlpha(leash, alpha); - return this; - } - - /** - * Operates the crop (and position) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper crop(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect destinationBounds) { - tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) - .setPosition(leash, destinationBounds.left, destinationBounds.top); - return this; - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds) { - mTmpDestinationRectF.set(destinationBounds); - return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */); - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, RectF destinationBounds) { - return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */); - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds, float degrees) { - mTmpDestinationRectF.set(destinationBounds); - return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); - } - - /** - * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, RectF destinationBounds, float degrees) { - mTmpSourceRectF.set(sourceBounds); - // We want the matrix to position the surface relative to the screen coordinates so offset - // the source to 0,0 - mTmpSourceRectF.offsetTo(0, 0); - mTmpDestinationRectF.set(destinationBounds); - mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); - mTmpTransform.postRotate(degrees, - mTmpDestinationRectF.centerX(), mTmpDestinationRectF.centerY()); - tx.setMatrix(leash, mTmpTransform, mTmpFloat9); - return this; - } - - /** * Operates the scale (setMatrix) on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ @@ -205,19 +124,6 @@ public class PipSurfaceTransactionHelper { } /** - * Resets the scale (setMatrix) on a given transaction and leash if there's any - * - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper resetScale(SurfaceControl.Transaction tx, - SurfaceControl leash, - Rect destinationBounds) { - tx.setMatrix(leash, Matrix.IDENTITY_MATRIX, mTmpFloat9) - .setPosition(leash, destinationBounds.left, destinationBounds.top); - return this; - } - - /** * Operates the round corner radius on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ @@ -228,18 +134,6 @@ public class PipSurfaceTransactionHelper { } /** - * Operates the round corner radius on a given transaction and leash, scaled by bounds - * @return same {@link PipSurfaceTransactionHelper} instance for method chaining - */ - public PipSurfaceTransactionHelper round(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect fromBounds, Rect toBounds) { - final float scale = (float) (Math.hypot(fromBounds.width(), fromBounds.height()) - / Math.hypot(toBounds.width(), toBounds.height())); - tx.setCornerRadius(leash, mCornerRadius * scale); - return this; - } - - /** * Operates the shadow radius on a given transaction and leash * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java index b4cf8905d02e..88ac865c24b9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipAppIconOverlay.java @@ -26,6 +26,7 @@ import android.graphics.Color; import android.graphics.Matrix; import android.graphics.Rect; import android.graphics.drawable.Drawable; +import android.hardware.HardwareBuffer; import android.util.TypedValue; import android.view.SurfaceControl; @@ -39,7 +40,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { private final Context mContext; private final int mAppIconSizePx; - private final Rect mAppBounds; private final int mOverlayHalfSize; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; @@ -56,10 +56,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { final int overlaySize = getOverlaySize(appBounds, destinationBounds); mOverlayHalfSize = overlaySize >> 1; - // When the activity is in the secondary split, make sure the scaling center is not - // offset. - mAppBounds = new Rect(0, 0, appBounds.width(), appBounds.height()); - mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); mLeash = new SurfaceControl.Builder() @@ -85,12 +81,17 @@ public final class PipAppIconOverlay extends PipContentOverlay { @Override public void attach(SurfaceControl.Transaction tx, SurfaceControl parentLeash) { + final HardwareBuffer buffer = mBitmap.getHardwareBuffer(); tx.show(mLeash); tx.setLayer(mLeash, Integer.MAX_VALUE); - tx.setBuffer(mLeash, mBitmap.getHardwareBuffer()); + tx.setBuffer(mLeash, buffer); tx.setAlpha(mLeash, 0f); tx.reparent(mLeash, parentLeash); tx.apply(); + // Cleanup the bitmap and buffer after setting up the leash + mBitmap.recycle(); + mBitmap = null; + buffer.close(); } @Override @@ -108,16 +109,6 @@ public final class PipAppIconOverlay extends PipContentOverlay { .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); } - - - @Override - public void detach(SurfaceControl.Transaction tx) { - super.detach(tx); - if (mBitmap != null && !mBitmap.isRecycled()) { - mBitmap.recycle(); - } - } - private void prepareAppIconOverlay(Drawable appIcon) { final Canvas canvas = new Canvas(); canvas.setBitmap(mBitmap); @@ -139,6 +130,8 @@ public final class PipAppIconOverlay extends PipContentOverlay { mOverlayHalfSize + mAppIconSizePx / 2); appIcon.setBounds(appIconBounds); appIcon.draw(canvas); + Bitmap oldBitmap = mBitmap; mBitmap = mBitmap.copy(Bitmap.Config.HARDWARE, false /* mutable */); + oldBitmap.recycle(); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java index 71697596afd3..a837e7d308eb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDismissTargetHandler.java @@ -296,6 +296,7 @@ public class PipDismissTargetHandler implements ViewTreeObserver.OnPreDrawListen return; } + mMagneticTarget.updateLocationOnScreen(); createOrUpdateDismissTarget(); if (mTargetViewContainer.getVisibility() != View.VISIBLE) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDragToResizeHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDragToResizeHandler.java new file mode 100644 index 000000000000..bd0b810b2a44 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipDragToResizeHandler.java @@ -0,0 +1,218 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.pip2.phone; + +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_BOTTOM; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_LEFT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_RIGHT; +import static com.android.internal.policy.TaskResizingAlgorithm.CTRL_TOP; +import static com.android.wm.shell.pip2.phone.PipMenuView.ANIM_TYPE_NONE; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.graphics.Region; +import android.view.MotionEvent; + +import com.android.internal.policy.TaskResizingAlgorithm; +import com.android.wm.shell.R; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; + +import java.util.function.Function; + +/** Helper for handling drag-corner-to-resize gestures. */ +public class PipDragToResizeHandler { + private final Context mContext; + private final PipResizeGestureHandler mPipResizeGestureHandler; + private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipScheduler mPipScheduler; + + private final Region mTmpRegion = new Region(); + private final Rect mDragCornerSize = new Rect(); + private final Rect mTmpTopLeftCorner = new Rect(); + private final Rect mTmpTopRightCorner = new Rect(); + private final Rect mTmpBottomLeftCorner = new Rect(); + private final Rect mTmpBottomRightCorner = new Rect(); + private final Rect mDisplayBounds = new Rect(); + private final Function<Rect, Rect> mMovementBoundsSupplier; + private int mDelta; + + public PipDragToResizeHandler(Context context, PipResizeGestureHandler pipResizeGestureHandler, + PipBoundsState pipBoundsState, + PhonePipMenuController phonePipMenuController, PipBoundsAlgorithm pipBoundsAlgorithm, + PipScheduler pipScheduler, Function<Rect, Rect> movementBoundsSupplier) { + mContext = context; + mPipResizeGestureHandler = pipResizeGestureHandler; + mPipBoundsState = pipBoundsState; + mPhonePipMenuController = phonePipMenuController; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipScheduler = pipScheduler; + mMovementBoundsSupplier = movementBoundsSupplier; + } + + /** Invoked by {@link PipResizeGestureHandler#reloadResources}. */ + void reloadResources() { + final Resources res = mContext.getResources(); + mDelta = res.getDimensionPixelSize(R.dimen.pip_resize_edge_size); + } + + /** Invoked by {@link PipResizeGestureHandler#onInputEvent} if drag-corner-to-resize is + * enabled. */ + void onDragCornerResize(MotionEvent ev, Rect lastResizeBounds, PointF downPoint, + Rect downBounds, Point minSize, Point maxSize, float touchSlop) { + int action = ev.getActionMasked(); + float x = ev.getX(); + float y = ev.getY(); + if (action == MotionEvent.ACTION_DOWN) { + lastResizeBounds.setEmpty(); + final boolean allowGesture = isWithinDragResizeRegion((int) x, (int) y); + mPipResizeGestureHandler.setAllowGesture(allowGesture); + if (allowGesture) { + setCtrlType((int) x, (int) y); + downPoint.set(x, y); + downBounds.set(mPipBoundsState.getBounds()); + } + } else if (mPipResizeGestureHandler.getAllowGesture()) { + switch (action) { + case MotionEvent.ACTION_POINTER_DOWN: + // We do not support multi touch for resizing via drag + mPipResizeGestureHandler.setAllowGesture(false); + break; + case MotionEvent.ACTION_MOVE: + final boolean thresholdCrossed = mPipResizeGestureHandler.getThresholdCrossed(); + // Capture inputs + if (!mPipResizeGestureHandler.getThresholdCrossed() + && Math.hypot(x - downPoint.x, y - downPoint.y) > touchSlop) { + mPipResizeGestureHandler.setThresholdCrossed(true); + // Reset the down to begin resizing from this point + downPoint.set(x, y); + mPipResizeGestureHandler.pilferPointers(); + } + if (mPipResizeGestureHandler.getThresholdCrossed()) { + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(ANIM_TYPE_NONE, + false /* resize */); + } + final Rect currentPipBounds = mPipBoundsState.getBounds(); + lastResizeBounds.set(TaskResizingAlgorithm.resizeDrag(x, y, + downPoint.x, downPoint.y, currentPipBounds, + mPipResizeGestureHandler.getCtrlType(), minSize.x, + minSize.y, maxSize, true, + downBounds.width() > downBounds.height())); + mPipBoundsAlgorithm.transformBoundsToAspectRatio(lastResizeBounds, + mPipBoundsState.getAspectRatio(), false /* useCurrentMinEdgeSize */, + true /* useCurrentSize */); + mPipScheduler.scheduleUserResizePip(lastResizeBounds); + mPipBoundsState.setHasUserResizedPip(true); + } + break; + case MotionEvent.ACTION_UP: + case MotionEvent.ACTION_CANCEL: + mPipResizeGestureHandler.finishResize(); + break; + } + } + } + + /** + * Check whether the current x,y coordinate is within the region in which drag-resize should + * start. + * This consists of 4 small squares on the 4 corners of the PIP window, a quarter of which + * overlaps with the PIP window while the rest goes outside of the PIP window. + * _ _ _ _ + * |_|_|_________|_|_| + * |_|_| |_|_| + * | PIP | + * | WINDOW | + * _|_ _|_ + * |_|_|_________|_|_| + * |_|_| |_|_| + */ + boolean isWithinDragResizeRegion(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.getBounds(); + if (currentPipBounds == null) { + return false; + } + resetDragCorners(); + mTmpTopLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpTopRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.top - mDelta / 2); + mTmpBottomLeftCorner.offset(currentPipBounds.left - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + mTmpBottomRightCorner.offset(currentPipBounds.right - mDelta / 2, + currentPipBounds.bottom - mDelta / 2); + + mTmpRegion.setEmpty(); + mTmpRegion.op(mTmpTopLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpTopRightCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomLeftCorner, Region.Op.UNION); + mTmpRegion.op(mTmpBottomRightCorner, Region.Op.UNION); + + return mTmpRegion.contains(x, y); + } + + private void resetDragCorners() { + mDragCornerSize.set(0, 0, mDelta, mDelta); + mTmpTopLeftCorner.set(mDragCornerSize); + mTmpTopRightCorner.set(mDragCornerSize); + mTmpBottomLeftCorner.set(mDragCornerSize); + mTmpBottomRightCorner.set(mDragCornerSize); + } + + private void setCtrlType(int x, int y) { + final Rect currentPipBounds = mPipBoundsState.getBounds(); + int ctrlType = mPipResizeGestureHandler.getCtrlType(); + + Rect movementBounds = mMovementBoundsSupplier.apply(currentPipBounds); + + mDisplayBounds.set(movementBounds.left, + movementBounds.top, + movementBounds.right + currentPipBounds.width(), + movementBounds.bottom + currentPipBounds.height()); + + if (mTmpTopLeftCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.left != mDisplayBounds.left) { + ctrlType |= CTRL_LEFT; + ctrlType |= CTRL_TOP; + } + if (mTmpTopRightCorner.contains(x, y) && currentPipBounds.top != mDisplayBounds.top + && currentPipBounds.right != mDisplayBounds.right) { + ctrlType |= CTRL_RIGHT; + ctrlType |= CTRL_TOP; + } + if (mTmpBottomRightCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.right != mDisplayBounds.right) { + ctrlType |= CTRL_RIGHT; + ctrlType |= CTRL_BOTTOM; + } + if (mTmpBottomLeftCorner.contains(x, y) + && currentPipBounds.bottom != mDisplayBounds.bottom + && currentPipBounds.left != mDisplayBounds.left) { + ctrlType |= CTRL_LEFT; + ctrlType |= CTRL_BOTTOM; + } + + mPipResizeGestureHandler.setCtrlType(ctrlType); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java new file mode 100644 index 000000000000..1e41af379864 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipPinchToResizeHandler.java @@ -0,0 +1,129 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.pip2.phone; + +import android.graphics.Point; +import android.graphics.PointF; +import android.graphics.Rect; +import android.view.MotionEvent; + +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; + +/** Helper for handling pinch-to-resize gestures. */ +public class PipPinchToResizeHandler { + private final PipResizeGestureHandler mPipResizeGestureHandler; + private final PipBoundsState mPipBoundsState; + private final PhonePipMenuController mPhonePipMenuController; + private final PipScheduler mPipScheduler; + private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; + + private int mFirstIndex = -1; + private int mSecondIndex = -1; + + public PipPinchToResizeHandler(PipResizeGestureHandler pipResizeGestureHandler, + PipBoundsState pipBoundsState, PhonePipMenuController phonePipMenuController, + PipScheduler pipScheduler) { + mPipResizeGestureHandler = pipResizeGestureHandler; + mPipBoundsState = pipBoundsState; + mPhonePipMenuController = phonePipMenuController; + mPipScheduler = pipScheduler; + + mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + } + + /** Invoked by {@link PipResizeGestureHandler#onInputEvent} if pinch-to-resize is enabled. */ + void onPinchResize(MotionEvent ev, PointF downPoint, PointF downSecondPoint, Rect downBounds, + PointF lastPoint, PointF lastSecondPoint, Rect lastResizeBounds, float touchSlop, + Point minSize, Point maxSize) { + int action = ev.getActionMasked(); + + if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { + mFirstIndex = -1; + mSecondIndex = -1; + mPipResizeGestureHandler.setAllowGesture(false); + mPipResizeGestureHandler.finishResize(); + } + + if (ev.getPointerCount() != 2) { + return; + } + + final Rect pipBounds = mPipBoundsState.getBounds(); + if (action == MotionEvent.ACTION_POINTER_DOWN) { + if (mFirstIndex == -1 && mSecondIndex == -1 + && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) + && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { + mPipResizeGestureHandler.setAllowGesture(true); + mFirstIndex = 0; + mSecondIndex = 1; + downPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); + downSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); + downBounds.set(pipBounds); + + lastPoint.set(downPoint); + lastSecondPoint.set(lastSecondPoint); + lastResizeBounds.set(downBounds); + + // start the high perf session as the second pointer gets detected + mPipResizeGestureHandler.startHighPerfSession(); + } + } + + if (action == MotionEvent.ACTION_MOVE) { + if (mFirstIndex == -1 || mSecondIndex == -1) { + return; + } + + float x0 = ev.getRawX(mFirstIndex); + float y0 = ev.getRawY(mFirstIndex); + float x1 = ev.getRawX(mSecondIndex); + float y1 = ev.getRawY(mSecondIndex); + lastPoint.set(x0, y0); + lastSecondPoint.set(x1, y1); + + // Capture inputs + if (!mPipResizeGestureHandler.getThresholdCrossed() + && (distanceBetween(downSecondPoint, lastSecondPoint) > touchSlop + || distanceBetween(downPoint, lastPoint) > touchSlop)) { + mPipResizeGestureHandler.pilferPointers(); + mPipResizeGestureHandler.setThresholdCrossed(true); + // Reset the down to begin resizing from this point + downPoint.set(lastPoint); + downSecondPoint.set(lastSecondPoint); + + if (mPhonePipMenuController.isMenuVisible()) { + mPhonePipMenuController.hideMenu(); + } + } + + if (mPipResizeGestureHandler.getThresholdCrossed()) { + final float angle = mPinchResizingAlgorithm.calculateBoundsAndAngle(downPoint, + downSecondPoint, lastPoint, lastSecondPoint, minSize, maxSize, + downBounds, lastResizeBounds); + + mPipResizeGestureHandler.setAngle(angle); + mPipScheduler.scheduleUserResizePip(lastResizeBounds, angle); + mPipBoundsState.setHasUserResizedPip(true); + } + } + } + + private float distanceBetween(PointF p1, PointF p2) { + return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); + } + +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java index e4be3f60f86e..b869bf153c34 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipResizeGestureHandler.java @@ -44,13 +44,14 @@ import com.android.wm.shell.R; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipPerfHintController; -import com.android.wm.shell.common.pip.PipPinchResizingAlgorithm; import com.android.wm.shell.common.pip.PipUiEventLogger; import com.android.wm.shell.pip2.animation.PipResizeAnimator; import java.io.PrintWriter; +import java.util.function.Function; /** * Helper on top of PipTouchHandler that handles inputs OUTSIDE of the PIP window, which is used to @@ -72,8 +73,8 @@ public class PipResizeGestureHandler implements private final PipTransitionState mPipTransitionState; private final PhonePipMenuController mPhonePipMenuController; private final PipDisplayLayoutState mPipDisplayLayoutState; + private final PipDesktopState mPipDesktopState; private final PipUiEventLogger mPipUiEventLogger; - private final PipPinchResizingAlgorithm mPinchResizingAlgorithm; private final ShellExecutor mMainExecutor; private final PointF mDownPoint = new PointF(); @@ -93,16 +94,18 @@ public class PipResizeGestureHandler implements private boolean mIsAttached; private boolean mIsEnabled; private boolean mEnablePinchResize; + private boolean mEnableDragCornerResize; private boolean mIsSysUiStateValid; private boolean mThresholdCrossed; private boolean mOngoingPinchToResize = false; private boolean mWaitingForBoundsChangeTransition = false; private float mAngle = 0; - int mFirstIndex = -1; - int mSecondIndex = -1; + private InputMonitor mInputMonitor; private InputEventReceiver mInputEventReceiver; + private PipDragToResizeHandler mPipDragToResizeHandler; + private PipPinchToResizeHandler mPipPinchToResizeHandler; @Nullable private final PipPerfHintController mPipPerfHintController; @@ -121,7 +124,9 @@ public class PipResizeGestureHandler implements PipTransitionState pipTransitionState, PipUiEventLogger pipUiEventLogger, PhonePipMenuController menuActivityController, + Function<Rect, Rect> movementBoundsSupplier, PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, ShellExecutor mainExecutor, @Nullable PipPerfHintController pipPerfHintController) { mContext = context; @@ -137,8 +142,13 @@ public class PipResizeGestureHandler implements mPhonePipMenuController = menuActivityController; mPipDisplayLayoutState = pipDisplayLayoutState; + mPipDesktopState = pipDesktopState; mPipUiEventLogger = pipUiEventLogger; - mPinchResizingAlgorithm = new PipPinchResizingAlgorithm(); + + mPipDragToResizeHandler = new PipDragToResizeHandler(context, this, pipBoundsState, + menuActivityController, pipBoundsAlgorithm, pipScheduler, movementBoundsSupplier); + mPipPinchToResizeHandler = new PipPinchToResizeHandler(this, pipBoundsState, + menuActivityController, pipScheduler); } void init() { @@ -163,6 +173,7 @@ public class PipResizeGestureHandler implements } private void reloadResources() { + mPipDragToResizeHandler.reloadResources(); mTouchSlop = ViewConfiguration.get(mContext).getScaledTouchSlop(); } @@ -180,6 +191,8 @@ public class PipResizeGestureHandler implements void onActivityPinned() { mIsAttached = true; updateIsEnabled(); + // Only enable drag-corner-to-resize if PiP was entered when Desktop Mode session is active. + mEnableDragCornerResize = mPipDesktopState.isPipInDesktopMode(); } void onActivityUnpinned() { @@ -211,9 +224,44 @@ public class PipResizeGestureHandler implements } } + boolean getAllowGesture() { + return mAllowGesture; + } + + void setAllowGesture(boolean allowGesture) { + mAllowGesture = allowGesture; + } + + boolean getThresholdCrossed() { + return mThresholdCrossed; + } + + void setThresholdCrossed(boolean thresholdCrossed) { + mThresholdCrossed = thresholdCrossed; + } + + int getCtrlType() { + return mCtrlType; + } + + void setCtrlType(int ctrlType) { + mCtrlType = ctrlType; + } + + void setAngle(float angle) { + mAngle = angle; + } + + void startHighPerfSession() { + if (mPipPerfHintController != null) { + mPipHighPerfSession = mPipPerfHintController.startSession( + this::onHighPerfSessionTimeout, "onPinchResize"); + } + } + @VisibleForTesting void onInputEvent(InputEvent ev) { - if (!mEnablePinchResize) { + if (!mEnableDragCornerResize && !mEnablePinchResize) { // No need to handle anything if resizing isn't enabled. return; } @@ -240,7 +288,12 @@ public class PipResizeGestureHandler implements } if (mOngoingPinchToResize) { - onPinchResize(mv); + mPipPinchToResizeHandler.onPinchResize(mv, mDownPoint, mDownSecondPoint, + mDownBounds, mLastPoint, mLastSecondPoint, mLastResizeBounds, mTouchSlop, + mMinSize, mMaxSize); + } else if (mEnableDragCornerResize) { + mPipDragToResizeHandler.onDragCornerResize(mv, mLastResizeBounds, mDownPoint, + mDownBounds, mMinSize, mMaxSize, mTouchSlop); } } } @@ -261,20 +314,31 @@ public class PipResizeGestureHandler implements } boolean willStartResizeGesture(MotionEvent ev) { - if (ev.getActionMasked() == MotionEvent.ACTION_POINTER_DOWN) { - if (mEnablePinchResize && ev.getPointerCount() == 2) { - onPinchResize(ev); - mOngoingPinchToResize = mAllowGesture; - return mAllowGesture; - } + switch (ev.getActionMasked()) { + case MotionEvent.ACTION_DOWN: + if (mEnableDragCornerResize && mPipDragToResizeHandler.isWithinDragResizeRegion( + (int) ev.getRawX(), + (int) ev.getRawY())) { + return true; + } + break; + + case MotionEvent.ACTION_POINTER_DOWN: + if (mEnablePinchResize && ev.getPointerCount() == 2) { + mPipPinchToResizeHandler.onPinchResize(ev, mDownPoint, mDownSecondPoint, + mDownBounds, mLastPoint, mLastSecondPoint, mLastResizeBounds, + mTouchSlop, mMinSize, mMaxSize); + mOngoingPinchToResize = mAllowGesture; + return mAllowGesture; + } + break; + + default: + break; } return false; } - private boolean isInValidSysUiState() { - return mIsSysUiStateValid; - } - private void onHighPerfSessionTimeout(PipPerfHintController.PipHighPerfSession session) {} private void cleanUpHighPerfSessionMaybe() { @@ -285,83 +349,6 @@ public class PipResizeGestureHandler implements } } - @VisibleForTesting - void onPinchResize(MotionEvent ev) { - int action = ev.getActionMasked(); - - if (action == MotionEvent.ACTION_UP || action == MotionEvent.ACTION_CANCEL) { - mFirstIndex = -1; - mSecondIndex = -1; - mAllowGesture = false; - finishResize(); - } - - if (ev.getPointerCount() != 2) { - return; - } - - final Rect pipBounds = mPipBoundsState.getBounds(); - if (action == MotionEvent.ACTION_POINTER_DOWN) { - if (mFirstIndex == -1 && mSecondIndex == -1 - && pipBounds.contains((int) ev.getRawX(0), (int) ev.getRawY(0)) - && pipBounds.contains((int) ev.getRawX(1), (int) ev.getRawY(1))) { - mAllowGesture = true; - mFirstIndex = 0; - mSecondIndex = 1; - mDownPoint.set(ev.getRawX(mFirstIndex), ev.getRawY(mFirstIndex)); - mDownSecondPoint.set(ev.getRawX(mSecondIndex), ev.getRawY(mSecondIndex)); - mDownBounds.set(pipBounds); - - mLastPoint.set(mDownPoint); - mLastSecondPoint.set(mLastSecondPoint); - mLastResizeBounds.set(mDownBounds); - - // start the high perf session as the second pointer gets detected - if (mPipPerfHintController != null) { - mPipHighPerfSession = mPipPerfHintController.startSession( - this::onHighPerfSessionTimeout, "onPinchResize"); - } - } - } - - if (action == MotionEvent.ACTION_MOVE) { - if (mFirstIndex == -1 || mSecondIndex == -1) { - return; - } - - float x0 = ev.getRawX(mFirstIndex); - float y0 = ev.getRawY(mFirstIndex); - float x1 = ev.getRawX(mSecondIndex); - float y1 = ev.getRawY(mSecondIndex); - mLastPoint.set(x0, y0); - mLastSecondPoint.set(x1, y1); - - // Capture inputs - if (!mThresholdCrossed - && (distanceBetween(mDownSecondPoint, mLastSecondPoint) > mTouchSlop - || distanceBetween(mDownPoint, mLastPoint) > mTouchSlop)) { - pilferPointers(); - mThresholdCrossed = true; - // Reset the down to begin resizing from this point - mDownPoint.set(mLastPoint); - mDownSecondPoint.set(mLastSecondPoint); - - if (mPhonePipMenuController.isMenuVisible()) { - mPhonePipMenuController.hideMenu(); - } - } - - if (mThresholdCrossed) { - mAngle = mPinchResizingAlgorithm.calculateBoundsAndAngle(mDownPoint, - mDownSecondPoint, mLastPoint, mLastSecondPoint, mMinSize, mMaxSize, - mDownBounds, mLastResizeBounds); - - mPipScheduler.scheduleUserResizePip(mLastResizeBounds, mAngle); - mPipBoundsState.setHasUserResizedPip(true); - } - } - } - private void snapToMovementBoundsEdge(Rect bounds, Rect movementBounds) { final int leftEdge = bounds.left; @@ -404,17 +391,21 @@ public class PipResizeGestureHandler implements // mPipTaskOrganizer.scheduleFinishResizePip(finalBounds, mUpdateResizeBoundsCallback); } - private void finishResize() { + /** Handles additional resizing and state changes after gesture resizing is done. */ + void finishResize() { if (mLastResizeBounds.isEmpty()) { resetState(); } - if (!mOngoingPinchToResize) { - return; - } // Cache initial bounds after release for animation before mLastResizeBounds are modified. mStartBoundsAfterRelease.set(mLastResizeBounds); + // Drag-corner-to-resize - we don't need to adjust the bounds at this point + if (!mOngoingPinchToResize) { + scheduleBoundsChange(); + return; + } + // If user resize is pretty close to max size, just auto resize to max. if (mLastResizeBounds.width() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.x || mLastResizeBounds.height() >= PINCH_RESIZE_AUTO_MAX_RATIO * mMaxSize.y) { @@ -438,6 +429,10 @@ public class PipResizeGestureHandler implements mLastResizeBounds, movementBounds); mPipBoundsAlgorithm.applySnapFraction(mLastResizeBounds, snapFraction); + scheduleBoundsChange(); + } + + private void scheduleBoundsChange() { // Update the transition state to schedule a resize transition. Bundle extra = new Bundle(); extra.putBoolean(RESIZE_BOUNDS_CHANGE, true); @@ -489,10 +484,6 @@ public class PipResizeGestureHandler implements mOhmOffset = offset; } - private float distanceBetween(PointF p1, PointF p2) { - return (float) Math.hypot(p2.x - p1.x, p2.y - p1.y); - } - private void resizeRectAboutCenter(Rect rect, int w, int h) { int cx = rect.centerX(); int cy = rect.centerY(); @@ -573,6 +564,7 @@ public class PipResizeGestureHandler implements pw.println(innerPrefix + "mIsAttached=" + mIsAttached); pw.println(innerPrefix + "mIsEnabled=" + mIsEnabled); pw.println(innerPrefix + "mEnablePinchResize=" + mEnablePinchResize); + pw.println(innerPrefix + "mEnableDragCornerResize=" + mEnableDragCornerResize); pw.println(innerPrefix + "mThresholdCrossed=" + mThresholdCrossed); pw.println(innerPrefix + "mOhmOffset=" + mOhmOffset); pw.println(innerPrefix + "mMinSize=" + mMinSize); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java index 21b0820f523a..df7a25af8376 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipScheduler.java @@ -16,14 +16,10 @@ package com.android.wm.shell.pip2.phone; -import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; -import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; - import android.content.Context; import android.graphics.Matrix; import android.graphics.Rect; import android.view.SurfaceControl; -import android.window.DisplayAreaInfo; import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; @@ -32,18 +28,16 @@ import androidx.annotation.Nullable; import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.window.flags.Flags; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.shared.split.SplitScreenConstants; +import com.android.wm.shell.splitscreen.SplitScreenController; -import java.util.Objects; import java.util.Optional; /** @@ -56,10 +50,8 @@ public class PipScheduler { private final PipBoundsState mPipBoundsState; private final ShellExecutor mMainExecutor; private final PipTransitionState mPipTransitionState; - private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; - private final Optional<DesktopWallpaperActivityTokenProvider> - mDesktopWallpaperActivityTokenProviderOptional; - private final RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + private final PipDesktopState mPipDesktopState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; private PipTransitionController mPipTransitionController; private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; @@ -72,18 +64,14 @@ public class PipScheduler { PipBoundsState pipBoundsState, ShellExecutor mainExecutor, PipTransitionState pipTransitionState, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, - Optional<DesktopWallpaperActivityTokenProvider> - desktopWallpaperActivityTokenProviderOptional, - RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer) { + Optional<SplitScreenController> splitScreenControllerOptional, + PipDesktopState pipDesktopState) { mContext = context; mPipBoundsState = pipBoundsState; mMainExecutor = mainExecutor; mPipTransitionState = pipTransitionState; - mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; - mDesktopWallpaperActivityTokenProviderOptional = - desktopWallpaperActivityTokenProviderOptional; - mRootTaskDisplayAreaOrganizer = rootTaskDisplayAreaOrganizer; + mPipDesktopState = pipDesktopState; + mSplitScreenControllerOptional = splitScreenControllerOptional; mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); @@ -105,7 +93,7 @@ public class PipScheduler { wct.setBounds(pipTaskToken, null); // if we are hitting a multi-activity case // windowing mode change will reparent to original host task - wct.setWindowingMode(pipTaskToken, getOutPipWindowingMode()); + wct.setWindowingMode(pipTaskToken, mPipDesktopState.getOutPipWindowingMode()); return wct; } @@ -115,10 +103,23 @@ public class PipScheduler { public void scheduleExitPipViaExpand() { mMainExecutor.execute(() -> { if (!mPipTransitionState.isInPip()) return; - WindowContainerTransaction wct = getExitPipViaExpandTransaction(); - if (wct != null) { - mPipTransitionController.startExpandTransition(wct); - } + + final WindowContainerTransaction expandWct = getExitPipViaExpandTransaction(); + if (expandWct == null) return; + + final WindowContainerTransaction wct = new WindowContainerTransaction(); + mSplitScreenControllerOptional.ifPresent(splitScreenController -> { + int lastParentTaskId = mPipTransitionState.getPipTaskInfo() + .lastParentTaskIdBeforePip; + if (splitScreenController.isTaskInSplitScreen(lastParentTaskId)) { + splitScreenController.prepareEnterSplitScreen(wct, + null /* taskInfo */, SplitScreenConstants.SPLIT_POSITION_UNDEFINED); + } + }); + + boolean toSplit = !wct.isEmpty(); + wct.merge(expandWct, true /* transfer */); + mPipTransitionController.startExpandTransition(wct, toSplit); }); } @@ -235,55 +236,6 @@ public class PipScheduler { maybeUpdateMovementBounds(); } - /** Returns whether the display is in freeform windowing mode. */ - private boolean isDisplayInFreeform() { - final DisplayAreaInfo tdaInfo = mRootTaskDisplayAreaOrganizer.getDisplayAreaInfo( - Objects.requireNonNull(mPipTransitionState.getPipTaskInfo()).displayId); - if (tdaInfo != null) { - return tdaInfo.configuration.windowConfiguration.getWindowingMode() - == WINDOWING_MODE_FREEFORM; - } - return false; - } - - /** Returns whether PiP is exiting while we're in desktop mode. */ - private boolean isPipExitingToDesktopMode() { - // Early return if PiP in Desktop Windowing is not supported. - if (!Flags.enableDesktopWindowingPip() || mDesktopUserRepositoriesOptional.isEmpty() - || mDesktopWallpaperActivityTokenProviderOptional.isEmpty()) { - return false; - } - final int displayId = Objects.requireNonNull( - mPipTransitionState.getPipTaskInfo()).displayId; - return mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount(displayId) - > 0 - || mDesktopWallpaperActivityTokenProviderOptional.get().isWallpaperActivityVisible( - displayId) - || isDisplayInFreeform(); - } - - /** - * The windowing mode to restore to when resizing out of PIP direction. Defaults to undefined - * and can be overridden to restore to an alternate windowing mode. - */ - private int getOutPipWindowingMode() { - // If we are exiting PiP while the device is in Desktop mode (the task should expand to - // freeform windowing mode): - // 1) If the display windowing mode is freeform, set windowing mode to undefined so it will - // resolve the windowing mode to the display's windowing mode. - // 2) If the display windowing mode is not freeform, set windowing mode to freeform. - if (isPipExitingToDesktopMode()) { - if (isDisplayInFreeform()) { - return WINDOWING_MODE_UNDEFINED; - } else { - return WINDOWING_MODE_FREEFORM; - } - } - - // By default, or if the task is going to fullscreen, reset the windowing mode to undefined. - return WINDOWING_MODE_UNDEFINED; - } - @VisibleForTesting void setSurfaceControlTransactionFactory( @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java index 2f9371536a16..dbcbf3663827 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTaskListener.java @@ -132,8 +132,11 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, "onTaskInfoChanged: %s, state=%s oldParams=%s newParams=%s", taskInfo.topActivity, mPipTransitionState, mPictureInPictureParams, params); setPictureInPictureParams(params); + // Note: params is nullable while mPictureInPictureParams is never null float newAspectRatio = mPictureInPictureParams.getAspectRatioFloat(); - if (PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { + if (mPictureInPictureParams.hasSetAspectRatio() + && mPipBoundsAlgorithm.isValidPictureInPictureAspectRatio(newAspectRatio) + && PipUtils.aspectRatioChanged(newAspectRatio, mPipBoundsState.getAspectRatio())) { mPipTransitionState.setOnIdlePipTransitionStateRunnable(() -> { onAspectRatioChanged(newAspectRatio); }); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java index 35cd1a2e681f..72346b335a8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java @@ -59,6 +59,7 @@ import com.android.wm.shell.common.FloatingContentCoordinator; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipDoubleTapHelper; import com.android.wm.shell.common.pip.PipPerfHintController; @@ -187,6 +188,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha @NonNull PipScheduler pipScheduler, @NonNull SizeSpecSource sizeSpecSource, @NonNull PipDisplayLayoutState pipDisplayLayoutState, + PipDesktopState pipDesktopState, DisplayController displayController, PipMotionHelper pipMotionHelper, FloatingContentCoordinator floatingContentCoordinator, @@ -226,7 +228,8 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha mainExecutor); mPipResizeGestureHandler = new PipResizeGestureHandler(context, pipBoundsAlgorithm, pipBoundsState, mTouchState, mPipScheduler, mPipTransitionState, pipUiEventLogger, - menuController, mPipDisplayLayoutState, mainExecutor, mPipPerfHintController); + menuController, this::getMovementBounds, mPipDisplayLayoutState, pipDesktopState, + mainExecutor, mPipPerfHintController); mPipBoundsState.addOnAspectRatioChangedCallback(aspectRatio -> { updateMinMaxSize(aspectRatio); onAspectRatioChanged(); @@ -987,6 +990,7 @@ public class PipTouchHandler implements PipTransitionState.PipTransitionStateCha } else { animateToUnexpandedState(getUserResizeBounds()); } + mPipBoundsState.setHasUserResizedPip(true); } else { // Expand to fullscreen if this is a double tap // the PiP should be frozen until the transition ends diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 03327bf463e3..035c93db7ee4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -16,7 +16,6 @@ package com.android.wm.shell.pip2.phone; -import static android.app.WindowConfiguration.ROTATION_UNDEFINED; import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Surface.ROTATION_0; @@ -29,7 +28,13 @@ import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipChange; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_RESIZE_PIP; import static com.android.wm.shell.transition.Transitions.transitTypeToString; @@ -45,7 +50,6 @@ import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; -import android.view.Surface; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -56,26 +60,23 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; -import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ComponentUtils; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.common.pip.PipDisplayLayoutState; import com.android.wm.shell.common.pip.PipMenuController; import com.android.wm.shell.common.pip.PipUtils; -import com.android.wm.shell.desktopmode.DesktopRepository; -import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; import com.android.wm.shell.pip2.animation.PipEnterAnimator; -import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.transition.PipExpandHandler; import com.android.wm.shell.shared.TransitionUtil; -import com.android.wm.shell.shared.pip.PipContentOverlay; +import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; @@ -117,9 +118,7 @@ public class PipTransition extends PipTransitionController implements private final PipDisplayLayoutState mPipDisplayLayoutState; private final DisplayController mDisplayController; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; - private final Optional<DesktopUserRepositories> mDesktopUserRepositoriesOptional; - private final Optional<DesktopWallpaperActivityTokenProvider> - mDesktopWallpaperActivityTokenProviderOptional; + private final PipDesktopState mPipDesktopState; // // Transition caches @@ -138,6 +137,7 @@ public class PipTransition extends PipTransitionController implements // // Internal state and relevant cached info // + private final PipExpandHandler mExpandHandler; private Transitions.TransitionFinishCallback mFinishCallback; @@ -159,9 +159,8 @@ public class PipTransition extends PipTransitionController implements PipDisplayLayoutState pipDisplayLayoutState, PipUiStateChangeController pipUiStateChangeController, DisplayController displayController, - Optional<DesktopUserRepositories> desktopUserRepositoriesOptional, - Optional<DesktopWallpaperActivityTokenProvider> - desktopWallpaperActivityTokenProviderOptional) { + Optional<SplitScreenController> splitScreenControllerOptional, + PipDesktopState pipDesktopState) { super(shellInit, shellTaskOrganizer, transitions, pipBoundsState, pipMenuController, pipBoundsAlgorithm); @@ -174,9 +173,10 @@ public class PipTransition extends PipTransitionController implements mPipDisplayLayoutState = pipDisplayLayoutState; mDisplayController = displayController; mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mContext); - mDesktopUserRepositoriesOptional = desktopUserRepositoriesOptional; - mDesktopWallpaperActivityTokenProviderOptional = - desktopWallpaperActivityTokenProviderOptional; + mPipDesktopState = pipDesktopState; + + mExpandHandler = new PipExpandHandler(mContext, pipBoundsState, pipBoundsAlgorithm, + pipTransitionState, pipDisplayLayoutState, splitScreenControllerOptional); } @Override @@ -196,10 +196,11 @@ public class PipTransition extends PipTransitionController implements // @Override - public void startExpandTransition(WindowContainerTransaction out) { + public void startExpandTransition(WindowContainerTransaction out, boolean toSplit) { if (out == null) return; mPipTransitionState.setState(PipTransitionState.EXITING_PIP); - mExitViaExpandTransition = mTransitions.startTransition(TRANSIT_EXIT_PIP, out, this); + mExitViaExpandTransition = mTransitions.startTransition(toSplit ? TRANSIT_EXIT_PIP_TO_SPLIT + : TRANSIT_EXIT_PIP, out, this); } @Override @@ -247,12 +248,15 @@ public class PipTransition extends PipTransitionController implements @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { - // Just jump-cut the current animation if any, but do not merge. if (info.getType() == TRANSIT_EXIT_PIP) { end(); } + mExpandHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); } @Override @@ -300,7 +304,8 @@ public class PipTransition extends PipTransitionController implements finishCallback); } else if (transition == mExitViaExpandTransition) { mExitViaExpandTransition = null; - return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); + return mExpandHandler.startAnimation(transition, info, startTransaction, + finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); @@ -310,6 +315,9 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.setState(PipTransitionState.EXITING_PIP); return startRemoveAnimation(info, startTransaction, finishTransaction, finishCallback); } + // For any unhandled transition, make sure the PiP surface is properly updated, + // i.e. corner and shadow radius. + syncPipSurfaceState(info, startTransaction, finishTransaction); return false; } @@ -431,7 +439,7 @@ public class PipTransition extends PipTransitionController implements final Rect destinationBounds = pipChange.getEndAbsBounds(); final SurfaceControl swipePipToHomeOverlay = mPipTransitionState.getSwipePipToHomeOverlay(); if (swipePipToHomeOverlay != null) { - final int overlaySize = PipContentOverlay.PipAppIconOverlay.getOverlaySize( + final int overlaySize = PipAppIconOverlay.getOverlaySize( mPipTransitionState.getSwipePipToHomeAppBounds(), destinationBounds); // It is possible we reparent the PIP activity to a new PIP task (in multi-activity // apps), so we should also reparent the overlay to the final PIP task. @@ -443,7 +451,7 @@ public class PipTransition extends PipTransitionController implements (destinationBounds.height() - overlaySize) / 2f); } - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -503,7 +511,7 @@ public class PipTransition extends PipTransitionController implements final Rect adjustedSourceRectHint = getAdjustedSourceRectHint(info, pipChange, pipActivityChange); - final int delta = getFixedRotationDelta(info, pipChange); + final int delta = getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); if (delta != ROTATION_0) { // Update transition target changes in place to prepare for fixed rotation. handleBoundsEnterFixedRotation(info, pipChange, pipActivityChange); @@ -592,27 +600,6 @@ public class PipTransition extends PipTransitionController implements endBounds.top + activityEndOffset.y); } - private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { - final Rect endBounds = outPipTaskChange.getEndAbsBounds(); - final int width = endBounds.width(); - final int height = endBounds.height(); - final int left = endBounds.left; - final int top = endBounds.top; - int newTop, newLeft; - - if (delta == Surface.ROTATION_90) { - newLeft = top; - newTop = -(left + width); - } else { - newLeft = -(height + top); - newTop = left; - } - // Modify the endBounds, rotating and placing them potentially off-screen, so that - // as we translate and rotate around the origin, we place them right into the target. - endBounds.set(newLeft, newTop, newLeft + height, newTop + width); - } - - private boolean startAlphaTypeEnterAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -640,83 +627,6 @@ public class PipTransition extends PipTransitionController implements return true; } - private boolean startExpandAnimation(@NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction startTransaction, - @NonNull SurfaceControl.Transaction finishTransaction, - @NonNull Transitions.TransitionFinishCallback finishCallback) { - WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); - - TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); - if (pipChange == null) { - // pipChange is null, check to see if we've reparented the PIP activity for - // the multi activity case. If so we should use the activity leash instead - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() == null - && change.getLastParent() != null - && change.getLastParent().equals(pipToken)) { - pipChange = change; - break; - } - } - - // failsafe - if (pipChange == null) { - return false; - } - } - mFinishCallback = finishCallback; - - // The parent change if we were in a multi-activity PiP; null if single activity PiP. - final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null - ? getChangeByToken(info, pipChange.getParent()) : null; - if (parentBeforePip != null) { - // For multi activity, we need to manually set the leash layer - startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); - } - - final Rect startBounds = pipChange.getStartAbsBounds(); - final Rect endBounds = pipChange.getEndAbsBounds(); - final SurfaceControl pipLeash = getLeash(pipChange); - - PictureInPictureParams params = null; - if (pipChange.getTaskInfo() != null) { - // single activity - params = getPipParams(pipChange); - } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { - // multi activity - params = getPipParams(parentBeforePip); - } - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, - startBounds); - - // We define delta = startRotation - endRotation, so we need to flip the sign. - final int delta = -getFixedRotationDelta(info, pipChange); - if (delta != ROTATION_0) { - // Update PiP target change in place to prepare for fixed rotation; - handleExpandFixedRotation(pipChange, delta); - } - - PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, - startTransaction, finishTransaction, endBounds, startBounds, endBounds, - sourceRectHint, delta); - animator.setAnimationEndCallback(() -> { - if (parentBeforePip != null) { - // TODO b/377362511: Animate local leash instead to also handle letterbox case. - // For multi-activity, set the crop to be null - finishTransaction.setCrop(pipLeash, null); - } - finishTransition(); - }); - cacheAndStartTransitionAnimator(animator); - - // Save the PiP bounds in case, we re-enter the PiP with the same component. - float snapFraction = mPipBoundsAlgorithm.getSnapFraction( - mPipBoundsState.getBounds()); - mPipBoundsState.saveReentryState(snapFraction); - - return true; - } - private boolean startRemoveAnimation(@NonNull TransitionInfo info, @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @@ -725,6 +635,12 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.getPipTaskToken()); mFinishCallback = finishCallback; + if (isPipClosing(info)) { + // If PiP is removed via a close (e.g. finishing of the activity), then + // clear out the PiP cache related to that activity component (e.g. reentry state). + mPipBoundsState.setLastPipComponentName(null /* lastPipComponentName */); + } + finishTransaction.setAlpha(pipChange.getLeash(), 0f); if (mPendingRemoveWithFadeout) { PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipChange.getLeash(), @@ -744,29 +660,6 @@ public class PipTransition extends PipTransitionController implements // Various helpers to resolve transition requests and infos // - @Nullable - private TransitionInfo.Change getPipChange(TransitionInfo info) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { - return change; - } - } - return null; - } - - @Nullable - private TransitionInfo.Change getChangeByToken(TransitionInfo info, - WindowContainerToken token) { - for (TransitionInfo.Change change : info.getChanges()) { - if (change.getTaskInfo() != null - && change.getTaskInfo().getToken().equals(token)) { - return change; - } - } - return null; - } - @NonNull private Rect getAdjustedSourceRectHint(@NonNull TransitionInfo info, @NonNull TransitionInfo.Change pipTaskChange, @@ -790,11 +683,11 @@ public class PipTransition extends PipTransitionController implements Rect cutoutInsets = parentBeforePip != null ? parentBeforePip.getTaskInfo().displayCutoutInsets : pipTaskChange.getTaskInfo().displayCutoutInsets; - if (cutoutInsets != null - && getFixedRotationDelta(info, pipTaskChange) == ROTATION_90) { + if (cutoutInsets != null && getFixedRotationDelta(info, pipTaskChange, + mPipDisplayLayoutState) == ROTATION_90) { adjustedSourceRectHint.offset(cutoutInsets.left, cutoutInsets.top); } - if (Flags.enableDesktopWindowingPip()) { + if (mPipDesktopState.isDesktopWindowingPipEnabled()) { adjustedSourceRectHint.offset(-pipActivityChange.getStartAbsBounds().left, -pipActivityChange.getStartAbsBounds().top); } @@ -808,25 +701,6 @@ public class PipTransition extends PipTransitionController implements return adjustedSourceRectHint; } - @Surface.Rotation - private int getFixedRotationDelta(@NonNull TransitionInfo info, - @NonNull TransitionInfo.Change pipChange) { - TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); - int startRotation = pipChange.getStartRotation(); - if (pipChange.getEndRotation() != ROTATION_UNDEFINED - && startRotation != pipChange.getEndRotation()) { - // If PiP change was collected along with the display change and the orientation change - // happened in sync with the PiP change, then do not treat this as fixed-rotation case. - return ROTATION_0; - } - - int endRotation = fixedRotationChange != null - ? fixedRotationChange.getEndFixedRotation() : mPipDisplayLayoutState.getRotation(); - int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 - : startRotation - endRotation; - return delta; - } - private void prepareOtherTargetTransforms(TransitionInfo info, SurfaceControl.Transaction startTransaction, SurfaceControl.Transaction finishTransaction) { @@ -854,7 +728,8 @@ public class PipTransition extends PipTransitionController implements // If PiP is enabled on Connected Displays, update PipDisplayLayoutState to have the correct // display info that PiP is entering in. - if (Flags.enableConnectedDisplaysPip()) { + if (mPipDesktopState.isConnectedDisplaysPipEnabled() + && pipTask.displayId != mPipDisplayLayoutState.getDisplayId()) { final DisplayLayout displayLayout = mDisplayController.getDisplayLayout( pipTask.displayId); if (displayLayout != null) { @@ -902,12 +777,7 @@ public class PipTransition extends PipTransitionController implements // Since opening a new task while in Desktop Mode always first open in Fullscreen // until DesktopMode Shell code resolves it to Freeform, PipTransition will get a // possibility to handle it also. In this case return false to not have it enter PiP. - final boolean isInDesktopSession = !mDesktopUserRepositoriesOptional.isEmpty() - && (mDesktopUserRepositoriesOptional.get().getCurrent().getVisibleTaskCount( - pipTask.displayId) > 0 - || mDesktopUserRepositoriesOptional.get().getCurrent() - .isMinimizedPipPresentInDisplay(pipTask.displayId)); - if (isInDesktopSession) { + if (mPipDesktopState.isPipEnteringInDesktopMode(pipTask)) { return false; } @@ -952,13 +822,29 @@ public class PipTransition extends PipTransitionController implements boolean isPipMovedToBack = info.getType() == TRANSIT_TO_BACK && pipChange.getMode() == TRANSIT_TO_BACK; - boolean isPipClosed = info.getType() == TRANSIT_CLOSE - && pipChange.getMode() == TRANSIT_CLOSE; // If PiP is dismissed by user (i.e. via dismiss button in PiP menu) boolean isPipDismissed = info.getType() == TRANSIT_REMOVE_PIP && pipChange.getMode() == TRANSIT_TO_BACK; // PiP is being removed if the pinned task is either moved to back, closed, or dismissed. - return isPipMovedToBack || isPipClosed || isPipDismissed; + return isPipMovedToBack || isPipClosing(info) || isPipDismissed; + } + + private boolean isPipClosing(@NonNull TransitionInfo info) { + if (mPipTransitionState.getPipTaskToken() == null) { + // PiP removal makes sense if enter-PiP has cached a valid pinned task token. + return false; + } + TransitionInfo.Change pipChange = info.getChange(mPipTransitionState.getPipTaskToken()); + TransitionInfo.Change pipActivityChange = info.getChanges().stream().filter(change -> + change.getTaskInfo() == null && change.getParent() != null + && change.getParent() == mPipTransitionState.getPipTaskToken()) + .findFirst().orElse(null); + + boolean isPipTaskClosed = pipChange != null + && pipChange.getMode() == TRANSIT_CLOSE; + boolean isPipActivityClosed = pipActivityChange != null + && pipActivityChange.getMode() == TRANSIT_CLOSE; + return isPipTaskClosed || isPipActivityClosed; } private void prepareConfigAtEndActivity(@NonNull SurfaceControl.Transaction startTx, @@ -1002,20 +888,6 @@ public class PipTransition extends PipTransitionController implements mTransitionAnimator.start(); } - @NonNull - private static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { - return pipChange.getTaskInfo().pictureInPictureParams != null - ? pipChange.getTaskInfo().pictureInPictureParams - : new PictureInPictureParams.Builder().build(); - } - - @NonNull - private static SurfaceControl getLeash(TransitionInfo.Change change) { - SurfaceControl leash = change.getLeash(); - Preconditions.checkNotNull(leash, "Leash is null for change=" + change); - return leash; - } - // // Miscellaneous callbacks and listeners // @@ -1065,26 +937,13 @@ public class PipTransition extends PipTransitionController implements "Unexpected bundle for " + mPipTransitionState); break; case PipTransitionState.EXITED_PIP: - final TaskInfo pipTask = mPipTransitionState.getPipTaskInfo(); - final boolean desktopPipEnabled = Flags.enableDesktopWindowingPip() - && mDesktopUserRepositoriesOptional.isPresent() - && mDesktopWallpaperActivityTokenProviderOptional.isPresent(); - if (desktopPipEnabled && pipTask != null) { - final DesktopRepository desktopRepository = - mDesktopUserRepositoriesOptional.get().getCurrent(); - final boolean wallpaperIsVisible = - mDesktopWallpaperActivityTokenProviderOptional.get() - .isWallpaperActivityVisible(pipTask.displayId); - if (desktopRepository.getVisibleTaskCount(pipTask.displayId) == 0 - && wallpaperIsVisible) { - mTransitions.startTransition( - TRANSIT_TO_BACK, - new WindowContainerTransaction().reorder( - mDesktopWallpaperActivityTokenProviderOptional.get() - .getToken(pipTask.displayId), /* onTop= */ false), - null - ); - } + if (mPipDesktopState.shouldExitPipExitDesktopMode()) { + mTransitions.startTransition( + TRANSIT_TO_BACK, + mPipDesktopState.getWallpaperActivityTokenWct( + mPipTransitionState.getPipTaskInfo().getDisplayId()), + null /* firstHandler */ + ); } mPipTransitionState.setPinnedTaskLeash(null); mPipTransitionState.setPipTaskInfo(null); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index 8805cbb0dfbd..18c9a705dcf7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -314,7 +314,8 @@ public class PipTransitionState { mSwipePipToHomeAppBounds.setEmpty(); } - @Nullable WindowContainerToken getPipTaskToken() { + @Nullable + public WindowContainerToken getPipTaskToken() { return mPipTaskInfo != null ? mPipTaskInfo.getToken() : null; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java new file mode 100644 index 000000000000..db4942b2fb95 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandler.java @@ -0,0 +1,331 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone.transition; + +import static android.view.Surface.ROTATION_0; + +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getChangeByToken; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getFixedRotationDelta; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getLeash; +import static com.android.wm.shell.pip2.phone.transition.PipTransitionUtils.getPipParams; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import android.animation.ValueAnimator; +import android.annotation.NonNull; +import android.annotation.Nullable; +import android.app.PictureInPictureParams; +import android.content.Context; +import android.graphics.Rect; +import android.os.IBinder; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.protolog.ProtoLog; +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.protolog.ShellProtoLogGroup; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.Transitions; + +import java.util.Optional; + +public class PipExpandHandler implements Transitions.TransitionHandler { + private final Context mContext; + private final PipBoundsState mPipBoundsState; + private final PipBoundsAlgorithm mPipBoundsAlgorithm; + private final PipTransitionState mPipTransitionState; + private final PipDisplayLayoutState mPipDisplayLayoutState; + private final Optional<SplitScreenController> mSplitScreenControllerOptional; + + @Nullable + private Transitions.TransitionFinishCallback mFinishCallback; + @Nullable + private ValueAnimator mTransitionAnimator; + + private PipExpandAnimatorSupplier mPipExpandAnimatorSupplier; + + public PipExpandHandler(Context context, + PipBoundsState pipBoundsState, + PipBoundsAlgorithm pipBoundsAlgorithm, + PipTransitionState pipTransitionState, + PipDisplayLayoutState pipDisplayLayoutState, + Optional<SplitScreenController> splitScreenControllerOptional) { + mContext = context; + mPipBoundsState = pipBoundsState; + mPipBoundsAlgorithm = pipBoundsAlgorithm; + mPipTransitionState = pipTransitionState; + mPipDisplayLayoutState = pipDisplayLayoutState; + mSplitScreenControllerOptional = splitScreenControllerOptional; + + mPipExpandAnimatorSupplier = PipExpandAnimator::new; + } + + @Override + public WindowContainerTransaction handleRequest(@NonNull IBinder transition, + @NonNull TransitionRequestInfo request) { + // All Exit-via-Expand from PiP transitions are Shell initiated. + return null; + } + + @Override + public boolean startAnimation(@NonNull IBinder transition, + @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + switch (info.getType()) { + case TRANSIT_EXIT_PIP: + return startExpandAnimation(info, startTransaction, finishTransaction, + finishCallback); + case TRANSIT_EXIT_PIP_TO_SPLIT: + return startExpandToSplitAnimation(info, startTransaction, finishTransaction, + finishCallback); + } + return false; + } + + @Override + public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + end(); + } + + /** + * Ends the animation if such is running in the context of expanding out of PiP. + */ + public void end() { + if (mTransitionAnimator != null && mTransitionAnimator.isRunning()) { + mTransitionAnimator.end(); + mTransitionAnimator = null; + } + } + + private boolean startExpandAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + TransitionInfo.Change pipChange = getChangeByToken(info, pipToken); + if (pipChange == null) { + // pipChange is null, check to see if we've reparented the PIP activity for + // the multi activity case. If so we should use the activity leash instead + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + + // failsafe + if (pipChange == null) { + return false; + } + } + mFinishCallback = finishCallback; + + // The parent change if we were in a multi-activity PiP; null if single activity PiP. + final TransitionInfo.Change parentBeforePip = pipChange.getTaskInfo() == null + ? getChangeByToken(info, pipChange.getParent()) : null; + if (parentBeforePip != null) { + // For multi activity, we need to manually set the leash layer + startTransaction.setLayer(parentBeforePip.getLeash(), Integer.MAX_VALUE - 1); + } + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + final SurfaceControl pipLeash = getLeash(pipChange); + + PictureInPictureParams params = null; + if (pipChange.getTaskInfo() != null) { + // single activity + params = getPipParams(pipChange); + } else if (parentBeforePip != null && parentBeforePip.getTaskInfo() != null) { + // multi activity + params = getPipParams(parentBeforePip); + } + final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, endBounds, + startBounds); + + // We define delta = startRotation - endRotation, so we need to flip the sign. + final int delta = -getFixedRotationDelta(info, pipChange, mPipDisplayLayoutState); + if (delta != ROTATION_0) { + // Update PiP target change in place to prepare for fixed rotation; + handleExpandFixedRotation(pipChange, delta); + } + + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + sourceRectHint, delta); + animator.setAnimationEndCallback(() -> { + if (parentBeforePip != null) { + // TODO b/377362511: Animate local leash instead to also handle letterbox case. + // For multi-activity, set the crop to be null + finishTransaction.setCrop(pipLeash, null); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private boolean startExpandToSplitAnimation(@NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull Transitions.TransitionFinishCallback finishCallback) { + WindowContainerToken pipToken = mPipTransitionState.getPipTaskToken(); + + // Expanding PiP to Split-screen makes sense only if we are dealing with multi-activity PiP + // and the lastParentBeforePip is still in one of the split-stages. + // + // This means we should be animating the PiP activity leash, since we do the reparenting + // of the PiP activity back to its original task in startWCT. + TransitionInfo.Change pipChange = null; + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() == null + && change.getLastParent() != null + && change.getLastParent().equals(pipToken)) { + pipChange = change; + break; + } + } + // failsafe + if (pipChange == null || pipChange.getLeash() == null) { + return false; + } + mFinishCallback = finishCallback; + + // Get the original parent before PiP. If original task hosting the PiP activity was + // already visible, then it's not participating in this transition; in that case, + // parentBeforePip would be null. + final TransitionInfo.Change parentBeforePip = getChangeByToken(info, pipChange.getParent()); + + final Rect startBounds = pipChange.getStartAbsBounds(); + final Rect endBounds = pipChange.getEndAbsBounds(); + if (parentBeforePip != null) { + // Since we have the parent task amongst the targets, all PiP activity + // leash translations will be relative to the original task, NOT the root leash. + startBounds.offset(-parentBeforePip.getStartAbsBounds().left, + -parentBeforePip.getStartAbsBounds().top); + endBounds.offset(-parentBeforePip.getEndAbsBounds().left, + -parentBeforePip.getEndAbsBounds().top); + } + + final SurfaceControl pipLeash = pipChange.getLeash(); + PipExpandAnimator animator = mPipExpandAnimatorSupplier.get(mContext, pipLeash, + startTransaction, finishTransaction, endBounds, startBounds, endBounds, + null /* srcRectHint */, ROTATION_0 /* delta */); + + + mSplitScreenControllerOptional.ifPresent(splitController -> { + splitController.finishEnterSplitScreen(finishTransaction); + }); + + animator.setAnimationEndCallback(() -> { + if (parentBeforePip == null) { + // After PipExpandAnimator is done modifying finishTransaction, we need to make + // sure PiP activity leash is offset at origin relative to its task as we reparent + // targets back from the transition root leash. + finishTransaction.setPosition(pipLeash, 0, 0); + } + finishTransition(); + }); + cacheAndStartTransitionAnimator(animator); + saveReentryState(); + return true; + } + + private void finishTransition() { + final int currentState = mPipTransitionState.getState(); + if (currentState != PipTransitionState.EXITING_PIP) { + ProtoLog.e(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, + "Unexpected state %s as we are finishing an exit-via-expand transition", + mPipTransitionState); + } + mPipTransitionState.setState(PipTransitionState.EXITED_PIP); + + if (mFinishCallback != null) { + // Need to unset mFinishCallback first because onTransitionFinished can re-enter this + // handler if there is a pending PiP animation. + final Transitions.TransitionFinishCallback finishCallback = mFinishCallback; + mFinishCallback = null; + finishCallback.onTransitionFinished(null /* finishWct */); + } + } + + private void handleExpandFixedRotation(TransitionInfo.Change outPipTaskChange, int delta) { + final Rect endBounds = outPipTaskChange.getEndAbsBounds(); + final int width = endBounds.width(); + final int height = endBounds.height(); + final int left = endBounds.left; + final int top = endBounds.top; + int newTop, newLeft; + + if (delta == Surface.ROTATION_90) { + newLeft = top; + newTop = -(left + width); + } else { + newLeft = -(height + top); + newTop = left; + } + // Modify the endBounds, rotating and placing them potentially off-screen, so that + // as we translate and rotate around the origin, we place them right into the target. + endBounds.set(newLeft, newTop, newLeft + height, newTop + width); + } + + private void saveReentryState() { + float snapFraction = mPipBoundsAlgorithm.getSnapFraction( + mPipBoundsState.getBounds()); + mPipBoundsState.saveReentryState(snapFraction); + } + + private void cacheAndStartTransitionAnimator(@NonNull ValueAnimator animator) { + mTransitionAnimator = animator; + mTransitionAnimator.start(); + } + + @VisibleForTesting + interface PipExpandAnimatorSupplier { + PipExpandAnimator get(Context context, + @NonNull SurfaceControl leash, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + @NonNull Rect baseBounds, + @NonNull Rect startBounds, + @NonNull Rect endBounds, + @Nullable Rect sourceRectHint, + @Surface.Rotation int rotation); + } + + @VisibleForTesting + void setPipExpandAnimatorSupplier(@NonNull PipExpandAnimatorSupplier supplier) { + mPipExpandAnimatorSupplier = supplier; + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java new file mode 100644 index 000000000000..01cda6c91108 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/transition/PipTransitionUtils.java @@ -0,0 +1,133 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone.transition; + +import static android.app.WindowConfiguration.ROTATION_UNDEFINED; +import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED; +import static android.view.Surface.ROTATION_0; + +import android.annotation.NonNull; +import android.app.PictureInPictureParams; +import android.view.Surface; +import android.view.SurfaceControl; +import android.window.TransitionInfo; +import android.window.WindowContainerToken; + +import androidx.annotation.Nullable; + +import com.android.internal.util.Preconditions; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; + +/** + * A set of utility methods to help resolve PiP transitions. + */ +public class PipTransitionUtils { + + /** + * @return change for a pinned mode task; null if no such task is in the list of changes. + */ + @Nullable + public static TransitionInfo.Change getPipChange(TransitionInfo info) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_PINNED) { + return change; + } + } + return null; + } + + /** + * @return change for a task with the provided token; null if no task with such token found. + */ + @Nullable + public static TransitionInfo.Change getChangeByToken(TransitionInfo info, + WindowContainerToken token) { + for (TransitionInfo.Change change : info.getChanges()) { + if (change.getTaskInfo() != null + && change.getTaskInfo().getToken().equals(token)) { + return change; + } + } + return null; + } + + /** + * @return the leash to interact with the container this change represents. + * @throws NullPointerException if the leash is null. + */ + @NonNull + public static SurfaceControl getLeash(TransitionInfo.Change change) { + SurfaceControl leash = change.getLeash(); + Preconditions.checkNotNull(leash, "Leash is null for change=" + change); + return leash; + } + + /** + * Get the rotation delta in a potential fixed rotation transition. + * + * Whenever PiP participates in fixed rotation, its actual orientation isn't updated + * in the initial transition as per the async rotation convention. + * + * @param pipChange PiP change to verify that PiP task's rotation wasn't updated already. + * @param pipDisplayLayoutState display layout state that PiP component keeps track of. + */ + @Surface.Rotation + public static int getFixedRotationDelta(@NonNull TransitionInfo info, + @NonNull TransitionInfo.Change pipChange, + @NonNull PipDisplayLayoutState pipDisplayLayoutState) { + TransitionInfo.Change fixedRotationChange = findFixedRotationChange(info); + int startRotation = pipChange.getStartRotation(); + if (pipChange.getEndRotation() != ROTATION_UNDEFINED + && startRotation != pipChange.getEndRotation()) { + // If PiP change was collected along with the display change and the orientation change + // happened in sync with the PiP change, then do not treat this as fixed-rotation case. + return ROTATION_0; + } + + int endRotation = fixedRotationChange != null + ? fixedRotationChange.getEndFixedRotation() : pipDisplayLayoutState.getRotation(); + int delta = endRotation == ROTATION_UNDEFINED ? ROTATION_0 + : startRotation - endRotation; + return delta; + } + + /** + * Gets a change amongst the transition targets that is in a different final orientation than + * the display, signalling a potential fixed rotation transition. + */ + @Nullable + public static TransitionInfo.Change findFixedRotationChange(@NonNull TransitionInfo info) { + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getEndFixedRotation() != ROTATION_UNDEFINED) { + return change; + } + } + return null; + } + + /** + * @return {@link PictureInPictureParams} provided by the client from the PiP change. + */ + @NonNull + public static PictureInPictureParams getPipParams(@NonNull TransitionInfo.Change pipChange) { + return pipChange.getTaskInfo().pictureInPictureParams != null + ? pipChange.getTaskInfo().pictureInPictureParams + : new PictureInPictureParams.Builder().build(); + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl index 8cdb8c4512a9..f8d84e4f3c21 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentsAnimationRunner.aidl @@ -59,11 +59,12 @@ oneway interface IRecentsAnimationRunner { void onAnimationStart(in IRecentsAnimationController controller, in RemoteAnimationTarget[] apps, in RemoteAnimationTarget[] wallpapers, in Rect homeContentInsets, in Rect minimizedHomeBounds, in Bundle extras, - in TransitionInfo info) = 2; + in @nullable TransitionInfo info) = 2; /** * Called when the task of an activity that has been started while the recents animation * was running becomes ready for control. */ - void onTasksAppeared(in RemoteAnimationTarget[] app) = 3; + void onTasksAppeared(in RemoteAnimationTarget[] app, + in @nullable TransitionInfo transitionInfo) = 3; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java index 4f2e028a1df0..2fa09664b73f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentTasksController.java @@ -381,7 +381,8 @@ public class RecentTasksController implements TaskStackListenerCallback, private void notifyRunningTaskAppeared(RunningTaskInfo taskInfo) { if (mListener == null || !shouldEnableRunningTasksForDesktopMode() - || taskInfo.realActivity == null) { + || taskInfo.realActivity == null + || excludeTaskFromGeneratedList(taskInfo)) { return; } try { @@ -397,7 +398,8 @@ public class RecentTasksController implements TaskStackListenerCallback, private void notifyRunningTaskChanged(RunningTaskInfo taskInfo) { if (mListener == null || !shouldEnableRunningTasksForDesktopMode() - || taskInfo.realActivity == null) { + || taskInfo.realActivity == null + || excludeTaskFromGeneratedList(taskInfo)) { return; } try { @@ -413,7 +415,8 @@ public class RecentTasksController implements TaskStackListenerCallback, private void notifyRunningTaskVanished(RunningTaskInfo taskInfo) { if (mListener == null || !shouldEnableRunningTasksForDesktopMode() - || taskInfo.realActivity == null) { + || taskInfo.realActivity == null + || excludeTaskFromGeneratedList(taskInfo)) { return; } try { @@ -430,7 +433,8 @@ public class RecentTasksController implements TaskStackListenerCallback, if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() || taskInfo.realActivity == null - || enableShellTopTaskTracking()) { + || enableShellTopTaskTracking() + || excludeTaskFromGeneratedList(taskInfo)) { return; } try { @@ -447,7 +451,8 @@ public class RecentTasksController implements TaskStackListenerCallback, if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() || taskInfo.realActivity == null - || enableShellTopTaskTracking()) { + || enableShellTopTaskTracking() + || excludeTaskFromGeneratedList(taskInfo)) { return; } try { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java index 55133780f517..847a0383e7d0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/RecentsTransitionHandler.java @@ -29,6 +29,7 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_FRONT; +import static android.window.DesktopModeFlags.ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static android.window.TransitionInfo.FLAG_MOVED_TO_TOP; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; @@ -46,6 +47,7 @@ import android.app.ActivityManager; import android.app.ActivityTaskManager; import android.app.IApplicationThread; import android.app.PendingIntent; +import android.content.Context; import android.content.Intent; import android.graphics.Color; import android.graphics.Rect; @@ -73,6 +75,7 @@ import com.android.internal.annotations.VisibleForTesting; import com.android.internal.os.IResultReceiver; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.Flags; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipUtils; @@ -307,7 +310,9 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, @Override public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { final RecentsController controller = findController(mergeTarget); if (controller == null) { @@ -315,7 +320,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, "RecentsTransitionHandler.mergeAnimation: no controller found"); return; } - controller.merge(info, t, mergeTarget, finishCallback); + controller.merge(info, startT, finishT, mergeTarget, finishCallback); } @Override @@ -791,7 +796,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " unhandled root taskId=%d", taskInfo.taskId); } - } else if (TransitionUtil.isDividerBar(change)) { + } else if (TransitionUtil.isDividerBar(change) + || TransitionUtil.isDimLayer(change)) { final RemoteAnimationTarget target = TransitionUtil.newTarget(change, belowLayers - i, info, t, mLeashMap); // Add this as a app and we will separate them on launcher side by window type. @@ -910,7 +916,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, * before any unhandled transitions. */ @SuppressLint("NewApi") - void merge(TransitionInfo info, SurfaceControl.Transaction t, IBinder mergeTarget, + void merge(TransitionInfo info, SurfaceControl.Transaction startT, + SurfaceControl.Transaction finishT, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mFinishCB == null) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, @@ -1070,8 +1077,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, Slog.e(TAG, "Returning to recents without closing any opening tasks."); } // Setup may hide it initially since it doesn't know that overview was still active. - t.show(recentsOpening.getLeash()); - t.setAlpha(recentsOpening.getLeash(), 1.f); + startT.show(recentsOpening.getLeash()); + startT.setAlpha(recentsOpening.getLeash(), 1.f); mState = STATE_NORMAL; } boolean didMergeThings = false; @@ -1140,31 +1147,31 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, mOpeningTasks.add(pausingTask); // Setup hides opening tasks initially, so make it visible again (since we // are already showing it). - t.show(change.getLeash()); - t.setAlpha(change.getLeash(), 1.f); + startT.show(change.getLeash()); + startT.setAlpha(change.getLeash(), 1.f); } else if (isLeaf) { // We are receiving new opening leaf tasks, so convert to onTasksAppeared. final RemoteAnimationTarget target = TransitionUtil.newTarget( - change, layer, info, t, mLeashMap); + change, layer, info, startT, mLeashMap); appearedTargets[nextTargetIdx++] = target; // reparent into the original `mInfo` since that's where we are animating. final TransitionInfo.Root root = TransitionUtil.getRootFor(change, mInfo); final boolean wasClosing = closingIdx >= 0; - t.reparent(target.leash, root.getLeash()); - t.setPosition(target.leash, + startT.reparent(target.leash, root.getLeash()); + startT.setPosition(target.leash, change.getStartAbsBounds().left - root.getOffset().x, change.getStartAbsBounds().top - root.getOffset().y); - t.setLayer(target.leash, layer); + startT.setLayer(target.leash, layer); if (wasClosing) { // App was previously visible and is closing - t.show(target.leash); - t.setAlpha(target.leash, 1f); + startT.show(target.leash); + startT.setAlpha(target.leash, 1f); // Also override the task alpha as it was set earlier when dispatching // the transition and setting up the leash to hide the - t.setAlpha(change.getLeash(), 1f); + startT.setAlpha(change.getLeash(), 1f); } else { // Hide the animation leash, let the listener show it - t.hide(target.leash); + startT.hide(target.leash); } ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " opening new leaf taskId=%d wasClosing=%b", @@ -1173,10 +1180,10 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } else { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, " opening new taskId=%d", change.getTaskInfo().taskId); - t.setLayer(change.getLeash(), layer); + startT.setLayer(change.getLeash(), layer); // Setup hides opening tasks initially, so make it visible since recents // is only animating the leafs. - t.show(change.getLeash()); + startT.show(change.getLeash()); mOpeningTasks.add(new TaskState(change, null)); } } @@ -1192,7 +1199,7 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, // Activity only transition, so consume the merge as it doesn't affect the rest of // recents. Slog.d(TAG, "Got an activity only transition during recents, so apply directly"); - mergeActivityOnly(info, t); + mergeActivityOnly(info, startT); } else if (!didMergeThings) { // Didn't recognize anything in incoming transition so don't merge it. Slog.w(TAG, "Don't know how to merge this transition, foundRecentsClosing=" @@ -1204,14 +1211,20 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, return; } // At this point, we are accepting the merge. - t.apply(); - // not using the incoming anim-only surfaces - info.releaseAnimSurfaces(); + startT.apply(); + // Since we're accepting the merge, update the finish transaction so that changes via + // that transaction will be applied on top of those of the merged transitions + mFinishTransaction = finishT; + boolean passTransitionInfo = ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue(); + if (!passTransitionInfo) { + // not using the incoming anim-only surfaces + info.releaseAnimSurfaces(); + } if (appearedTargets != null) { try { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_RECENTS_TRANSITION, "[%d] RecentsController.merge: calling onTasksAppeared", mInstanceId); - mListener.onTasksAppeared(appearedTargets); + mListener.onTasksAppeared(appearedTargets, passTransitionInfo ? info : null); } catch (RemoteException e) { Slog.e(TAG, "Error sending appeared tasks to recents animation", e); } @@ -1347,6 +1360,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.reorder(mPausingTasks.get(i).mToken, true /* onTop */); t.show(mPausingTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mPausingTasks); if (!mKeyguardLocked && mRecentsTask != null) { wct.restoreTransientOrder(mRecentsTask); } @@ -1384,6 +1399,8 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, for (int i = 0; i < mOpeningTasks.size(); ++i) { t.show(mOpeningTasks.get(i).mTaskSurface); } + setCornerRadiusForFreeformTasks( + mRecentTasksController.getContext(), t, mOpeningTasks); for (int i = 0; i < mPausingTasks.size(); ++i) { cleanUpPausingOrClosingTask(mPausingTasks.get(i), wct, t, sendUserLeaveHint); } @@ -1444,6 +1461,11 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, wct.clear(); if (Flags.enableRecentsBookendTransition()) { + // Notify the mixers of the pending finish + for (int i = 0; i < mMixers.size(); ++i) { + mMixers.get(i).handleFinishRecents(returningToApp, wct, t); + } + // In this case, we've already started the PIP transition, so we can // clean up immediately mPendingRunnerFinishCb = runnerFinishCb; @@ -1503,6 +1525,27 @@ public class RecentsTransitionHandler implements Transitions.TransitionHandler, } } + private static void setCornerRadiusForFreeformTasks( + Context context, + SurfaceControl.Transaction t, + ArrayList<TaskState> tasks) { + if (!ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX.isTrue()) { + return; + } + int cornerRadius = getCornerRadius(context); + for (int i = 0; i < tasks.size(); ++i) { + TaskState task = tasks.get(i); + if (task.mTaskInfo != null && task.mTaskInfo.isFreeform()) { + t.setCornerRadius(task.mTaskSurface, cornerRadius); + } + } + } + + private static int getCornerRadius(Context context) { + return context.getResources().getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius); + } + private boolean allAppsAreTranslucent(ArrayList<TaskState> tasks) { if (tasks == null) { return false; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java new file mode 100644 index 000000000000..d2e57e51762b --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitMultiDisplayProvider.java @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.splitscreen; + +import android.window.WindowContainerToken; + +public interface SplitMultiDisplayProvider { + /** + * Returns the WindowContainerToken for the root of the given display ID. + * + * @param displayId The ID of the display. + * @return The {@link WindowContainerToken} associated with the display's root task. + */ + WindowContainerToken getDisplayRootForDisplayId(int displayId); +} 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 ae0159263364..ba30d924e0b1 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 @@ -321,6 +321,10 @@ public class SplitScreenController implements SplitDragPolicy.Starter, return mStageCoordinator; } + public SplitMultiDisplayProvider getMultiDisplayProvider() { + return mStageCoordinator; + } + @Nullable public ActivityManager.RunningTaskInfo getTaskInfo(@SplitPosition int splitPosition) { if (!isSplitScreenVisible() || splitPosition == SPLIT_POSITION_UNDEFINED) { @@ -634,6 +638,14 @@ public class SplitScreenController implements SplitDragPolicy.Starter, } /** + * Starts an existing task via StageCoordinator. + */ + public void startTask(int taskId, @SplitPosition int position, @Nullable Bundle options, + @Nullable WindowContainerToken hideTaskToken, @SplitIndex int index) { + mStageCoordinator.startTask(taskId, position, options, hideTaskToken, index); + } + + /** * See {@link #startShortcut(String, String, int, Bundle, UserHandle)} * @param instanceId to be used by {@link SplitscreenEventLogger} */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java index 3091be574a53..fed336b17f19 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java @@ -461,12 +461,14 @@ class SplitScreenTransitions { return transition; } - void mergeAnimation(IBinder transition, TransitionInfo info, SurfaceControl.Transaction t, + void mergeAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { if (mergeTarget != mAnimatingTransition) return; if (mActiveRemoteHandler != null) { - mActiveRemoteHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mActiveRemoteHandler.mergeAnimation(transition, info, startT, + finishT, mergeTarget, finishCallback); } else { for (int i = mAnimations.size() - 1; i >= 0; --i) { final Animator anim = mAnimations.get(i); 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 722494c05e32..73b42d6f007c 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 @@ -34,15 +34,19 @@ import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; +import static com.android.window.flags.Flags.enableFullScreenWindowOnRemovingSplitScreenStageBugfix; +import static com.android.window.flags.Flags.enableNonDefaultDisplaySplit; import static com.android.wm.shell.Flags.enableFlexibleSplit; import static com.android.wm.shell.Flags.enableFlexibleTwoAppSplit; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_ALIGN_CENTER; import static com.android.wm.shell.common.split.SplitLayout.PARALLAX_FLEX; +import static com.android.wm.shell.common.split.SplitLayout.RESTING_DIM_LAYER; import static com.android.wm.shell.common.split.SplitScreenUtils.reverseSplitPosition; import static com.android.wm.shell.common.split.SplitScreenUtils.splitFailureMessage; import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_SPLIT_SCREEN; import static com.android.wm.shell.shared.TransitionUtil.isClosingType; import static com.android.wm.shell.shared.TransitionUtil.isOpeningType; +import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIM_LAYER; import static com.android.wm.shell.shared.split.SplitScreenConstants.FLAG_IS_DIVIDER_BAR; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; @@ -109,6 +113,7 @@ import android.util.ArraySet; import android.util.IntArray; import android.util.Log; import android.util.Slog; +import android.util.SparseIntArray; import android.view.Choreographer; import android.view.IRemoteAnimationFinishedCallback; import android.view.IRemoteAnimationRunner; @@ -188,7 +193,8 @@ import java.util.function.Predicate; */ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, DisplayController.OnDisplaysChangedListener, Transitions.TransitionHandler, - ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks { + ShellTaskOrganizer.TaskListener, StageTaskListener.StageListenerCallbacks, + SplitMultiDisplayProvider { private static final String TAG = StageCoordinator.class.getSimpleName(); @@ -286,6 +292,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mSplitTransitions.registerSplitAnimListener(listener, executor); } + @Override + public WindowContainerToken getDisplayRootForDisplayId(int displayId) { + if (displayId == DEFAULT_DISPLAY) { + return mRootTaskInfo != null ? mRootTaskInfo.token : null; + } + + // TODO(b/393217881): support different root task on external displays. + return null; // Return null for unknown display IDs + } + class SplitRequest { @SplitPosition int mActivatePosition; @@ -895,6 +911,11 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } options = options != null ? options : new Bundle(); addActivityOptions(options, null); + ActivityManager.RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(taskId); + if (enableFullScreenWindowOnRemovingSplitScreenStageBugfix() && taskInfo != null + && taskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { + prepareTasksForSplitScreen(new int[] {taskId}, wct); + } wct.startTask(taskId, options); mSplitTransitions.startFullscreenTransition(wct, remoteTransition); } @@ -1656,8 +1677,8 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, void prepareExitSplitScreen(@StageType int stageToTop, @NonNull WindowContainerTransaction wct, @ExitReason int exitReason) { if (!isSplitActive()) return; - ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s", - stageTypeToString(stageToTop)); + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "prepareExitSplitScreen: stageToTop=%s reason=%s", + stageTypeToString(stageToTop), exitReasonToString(exitReason)); if (enableFlexibleSplit()) { mStageOrderOperator.getActiveStages().stream() .filter(stage -> stage.getId() != stageToTop) @@ -1685,6 +1706,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, wct.setWindowingMode(taskInfo.token, targetWindowingMode); }); } + // Reparent root task to default display if non default display split is enabled. + if (enableNonDefaultDisplaySplit() && mRootTaskInfo.displayId != DEFAULT_DISPLAY) { + DisplayAreaInfo displayAreaInfo = mRootTDAOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY); + if (displayAreaInfo != null) { + wct.reparent(mRootTaskInfo.token, displayAreaInfo.token, false /* onTop */); + } + } deactivateSplit(wct, stageToTop); mSplitState.exit(); } @@ -1798,6 +1826,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // Ensure divider surface are re-parented back into the hierarchy at the end of the // transition. See Transition#buildFinishTransaction for more detail. finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.show(mRootTaskLeash); @@ -2833,14 +2869,6 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); mSplitTransitions.setDismissTransition(transition, dismissTop, EXIT_REASON_APP_FINISHED); - } else if (isOpening && !mPausingTasks.isEmpty()) { - // One of the splitting task is opening while animating the split pair in - // recents, which means to dismiss the split pair to this task. - int dismissTop = getStageType(stage) == STAGE_TYPE_MAIN - ? STAGE_TYPE_MAIN : STAGE_TYPE_SIDE; - prepareExitSplitScreen(dismissTop, out, EXIT_REASON_APP_FINISHED); - mSplitTransitions.setDismissTransition(transition, dismissTop, - EXIT_REASON_APP_FINISHED); } else if (!isSplitScreenVisible() && isOpening) { // If split is running in the background and the trigger task is appearing into // split, prepare to enter split screen. @@ -2976,10 +3004,13 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, @Override public void mergeAnimation(IBinder transition, TransitionInfo info, - SurfaceControl.Transaction t, IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + IBinder mergeTarget, Transitions.TransitionFinishCallback finishCallback) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "mergeAnimation: transition=%d", info.getDebugId()); - mSplitTransitions.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mSplitTransitions.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); } /** Jump the current transition animation to the end. */ @@ -3012,11 +3043,18 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, final int transitType = info.getType(); TransitionInfo.Change pipChange = null; int closingSplitTaskId = -1; - // This array tracks where we are sending stages (TO_BACK/TO_FRONT) in this transition. - // TODO (b/349828130): Update for n apps (needs to handle different indices than 0/1). - // Also make sure having multiple changes per stage (2+ tasks in one stage) is being - // handled properly. - int[] stageChanges = new int[2]; + // This array tracks if we are sending stages TO_BACK/TO_FRONT in this transition. + // TODO (b/349828130): Also make sure having multiple changes per stage (2+ tasks in + // one stage) is being handled properly. + SparseIntArray stageChanges = new SparseIntArray(); + if (enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages() + .forEach(stage -> stageChanges.put(stage.getId(), -1)); + } else { + stageChanges.put(STAGE_TYPE_MAIN, -1); + stageChanges.put(STAGE_TYPE_SIDE, -1); + } + for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); @@ -3090,14 +3128,12 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // we'll break split closingSplitTaskId = taskId; } - if (transitType == WindowManager.TRANSIT_WAKE) { - // Record which stages are receiving which changes - if ((change.getMode() == TRANSIT_TO_BACK - || change.getMode() == TRANSIT_TO_FRONT) - && (stageOfTaskId == STAGE_TYPE_MAIN - || stageOfTaskId == STAGE_TYPE_SIDE)) { - stageChanges[stageOfTaskId] = change.getMode(); - } + // Record which stages are receiving which changes + if ((change.getMode() == TRANSIT_TO_BACK + || change.getMode() == TRANSIT_TO_FRONT) + && (stageOfTaskId == STAGE_TYPE_MAIN + || stageOfTaskId == STAGE_TYPE_SIDE)) { + stageChanges.put(getStageOfTask(taskId), change.getMode()); } } @@ -3126,8 +3162,16 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, // If keyguard is active, check to see if we have all our stages showing. If one stage // was moved but not the other (which can happen with SHOW_ABOVE_LOCKED apps), we should // break split. - if (mKeyguardActive && stageChanges[STAGE_TYPE_MAIN] != stageChanges[STAGE_TYPE_SIDE]) { - dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + if (mKeyguardActive && stageChanges.size() > 0) { + int firstChangeMode = stageChanges.valueAt(0); + for (int i = 0; i < stageChanges.size(); i++) { + int changeMode = stageChanges.valueAt(i); + // Compare each changeMode to the first one. If any are different, break split. + if (changeMode != firstChangeMode) { + dismissSplitKeepingLastActiveStage(EXIT_REASON_SCREEN_LOCKED_SHOW_ON_TOP); + break; + } + } } final ArraySet<StageTaskListener> dismissStages = record.getShouldDismissedStage(); @@ -3353,12 +3397,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, TransitionInfo.Change sideChild = null; StageTaskListener firstAppStage = null; StageTaskListener secondAppStage = null; + boolean foundPausingTask = false; final WindowContainerTransaction evictWct = new WindowContainerTransaction(); for (int iC = 0; iC < info.getChanges().size(); ++iC) { final TransitionInfo.Change change = info.getChanges().get(iC); final ActivityManager.RunningTaskInfo taskInfo = change.getTaskInfo(); if (taskInfo == null || !taskInfo.hasParentTask()) continue; if (mPausingTasks.contains(taskInfo.taskId)) { + foundPausingTask = true; continue; } StageTaskListener stage = getStageOfTask(taskInfo); @@ -3401,9 +3447,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, prepareExitSplitScreen(dismissTop, cancelWct, EXIT_REASON_UNKNOWN); logExit(EXIT_REASON_UNKNOWN); }); - Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", - "launched 2 tasks in split, but didn't receive " - + "2 tasks in transition. Possibly one of them failed to launch")); + Log.w(TAG, splitFailureMessage("startPendingEnterAnimation", "launched 2 tasks in " + + "split, but didn't receive 2 tasks in transition. Possibly one of them " + + "failed to launch (foundPausingTask=" + foundPausingTask + ")")); if (mRecentTasks.isPresent() && mainChild != null) { mRecentTasks.get().removeSplitPair(mainChild.getTaskInfo().taskId); } @@ -3504,6 +3550,9 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, finishEnterSplitScreen(finishT); addDividerBarToTransition(info, true /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, true /* show */); + } return true; } @@ -3754,10 +3803,14 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } addDividerBarToTransition(info, false /* show */); + if (Flags.enableFlexibleTwoAppSplit()) { + addAllDimLayersToTransition(info, false /* show */); + } } /** Call this when the recents animation canceled during split-screen. */ public void onRecentsInSplitAnimationCanceled() { + ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationCanceled"); mPausingTasks.clear(); setSplitsVisible(false); @@ -3767,31 +3820,10 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, mTaskOrganizer.applyTransaction(wct); } - public void onRecentsInSplitAnimationFinishing(boolean returnToApp, - @NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (!Flags.enableRecentsBookendTransition()) { - // The non-bookend recents transition case will be handled by - // RecentsMixedTransition wrapping the finish callback and calling - // onRecentsInSplitAnimationFinish() - return; - } - - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); - } - - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinish(@NonNull WindowContainerTransaction finishWct, - @NonNull SurfaceControl.Transaction finishT) { - if (Flags.enableRecentsBookendTransition()) { - // The bookend recents transition case will be handled by - // onRecentsInSplitAnimationFinishing above - return; - } - - // Check if the recent transition is finished by returning to the current - // split, so we can restore the divider bar. - boolean returnToApp = false; + /** + * Returns whether the given WCT is reordering any of the split tasks to top. + */ + public boolean wctIsReorderingSplitToTop(@NonNull WindowContainerTransaction finishWct) { for (int i = 0; i < finishWct.getHierarchyOps().size(); ++i) { final WindowContainerTransaction.HierarchyOp op = finishWct.getHierarchyOps().get(i); @@ -3806,20 +3838,33 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, } if (op.getType() == HIERARCHY_OP_TYPE_REORDER && op.getToTop() && anyStageContainsContainer) { - returnToApp = true; + return true; } } - onRecentsInSplitAnimationFinishInner(returnToApp, finishWct, finishT); + return false; } - /** Call this when the recents animation during split-screen finishes. */ - public void onRecentsInSplitAnimationFinishInner(boolean returnToApp, + /** Called when the recents animation during split-screen finishes. */ + public void onRecentsInSplitAnimationFinishing(boolean returnToApp, @NonNull WindowContainerTransaction finishWct, @NonNull SurfaceControl.Transaction finishT) { ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "onRecentsInSplitAnimationFinish: returnToApp=%b", returnToApp); mPausingTasks.clear(); if (returnToApp) { + // Reparent auxiliary surfaces (divider bar and dim layers) back onto their + // original roots. + if (Flags.enableFlexibleSplit()) { + mStageOrderOperator.getActiveStages().forEach(stage -> { + finishT.reparent(stage.mDimLayer, stage.mRootLeash); + finishT.setLayer(stage.mDimLayer, RESTING_DIM_LAYER); + }); + } else if (Flags.enableFlexibleTwoAppSplit()) { + finishT.reparent(mMainStage.mDimLayer, mMainStage.mRootLeash); + finishT.reparent(mSideStage.mDimLayer, mSideStage.mRootLeash); + finishT.setLayer(mMainStage.mDimLayer, RESTING_DIM_LAYER); + finishT.setLayer(mSideStage.mDimLayer, RESTING_DIM_LAYER); + } updateSurfaceBounds(mSplitLayout, finishT, false /* applyResizingOffset */); finishT.reparent(mSplitLayout.getDividerLeash(), mRootTaskLeash); @@ -3886,6 +3931,39 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, info.addChange(barChange); } + /** Add dim layers to the transition, so that they can be hidden/shown when animation starts. */ + private void addAllDimLayersToTransition(@NonNull TransitionInfo info, boolean show) { + if (Flags.enableFlexibleSplit()) { + List<StageTaskListener> stages = mStageOrderOperator.getActiveStages(); + for (int i = 0; i < stages.size(); i++) { + final StageTaskListener stage = stages.get(i); + mSplitState.getCurrentLayout().get(i).roundOut(mTempRect1); + addDimLayerToTransition(info, show, stage, mTempRect1); + } + } else { + addDimLayerToTransition(info, show, mMainStage, getMainStageBounds()); + addDimLayerToTransition(info, show, mSideStage, getSideStageBounds()); + } + } + + /** Adds a single dim layer to the given TransitionInfo. */ + private void addDimLayerToTransition(@NonNull TransitionInfo info, boolean show, + StageTaskListener stage, Rect bounds) { + final SurfaceControl dimLayer = stage.mDimLayer; + if (dimLayer == null || !dimLayer.isValid()) { + Slog.w(TAG, "addDimLayerToTransition but leash was released or not created"); + } else { + final TransitionInfo.Change change = + new TransitionInfo.Change(null /* token */, dimLayer); + change.setParent(mRootTaskInfo.token); + change.setStartAbsBounds(bounds); + change.setEndAbsBounds(bounds); + change.setMode(show ? TRANSIT_TO_FRONT : TRANSIT_TO_BACK); + change.setFlags(FLAG_IS_DIM_LAYER); + info.addChange(change); + } + } + @NeverCompile @Override public void dump(@NonNull PrintWriter pw, String prefix) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java index 1eaae7ec83d9..a6f872634ee9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/taskview/TaskViewTransitions.java @@ -52,6 +52,7 @@ import com.android.wm.shell.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.TransitionUtil; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -571,7 +572,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV @NonNull SurfaceControl.Transaction startTransaction, @NonNull SurfaceControl.Transaction finishTransaction, @NonNull Transitions.TransitionFinishCallback finishCallback) { - PendingTransition pending = findPending(transition); + final PendingTransition pending = findPending(transition); if (pending != null) { mPending.remove(pending); } @@ -586,10 +587,11 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV WindowContainerTransaction wct = null; for (int i = 0; i < info.getChanges().size(); ++i) { final TransitionInfo.Change chg = info.getChanges().get(i); - if (chg.getTaskInfo() == null) continue; + final ActivityManager.RunningTaskInfo taskInfo = chg.getTaskInfo(); + if (taskInfo == null) continue; if (TransitionUtil.isClosingType(chg.getMode())) { final boolean isHide = chg.getMode() == TRANSIT_TO_BACK; - TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); + TaskViewTaskController tv = findTaskView(taskInfo); if (tv == null && !isHide) { // TaskView can be null when closing changesHandled++; @@ -599,7 +601,7 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV if (pending != null) { Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + + taskInfo.taskId); } continue; } @@ -615,46 +617,64 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV } changesHandled++; } else if (TransitionUtil.isOpeningType(chg.getMode())) { - final boolean taskIsNew = chg.getMode() == TRANSIT_OPEN; - final TaskViewTaskController tv; - if (taskIsNew) { - if (pending == null - || !chg.getTaskInfo().containsLaunchCookie(pending.mLaunchCookie)) { + boolean isNewInTaskView = false; + TaskViewTaskController tv; + if (chg.getMode() == TRANSIT_OPEN) { + isNewInTaskView = true; + if (pending == null || !taskInfo.containsLaunchCookie(pending.mLaunchCookie)) { Slog.e(TAG, "Found a launching TaskView in the wrong transition. All " + "TaskView launches should be initiated by shell and in their " - + "own transition: " + chg.getTaskInfo().taskId); + + "own transition: " + taskInfo.taskId); continue; } stillNeedsMatchingLaunch = false; tv = pending.mTaskView; } else { - tv = findTaskView(chg.getTaskInfo()); - if (tv == null) { - if (pending != null) { - Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " - + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + tv = findTaskView(taskInfo); + if (tv == null && pending != null) { + if (BubbleAnythingFlagHelper.enableCreateAnyBubble() + && chg.getMode() == TRANSIT_TO_FRONT + && pending.mTaskView.getPendingInfo() != null + && pending.mTaskView.getPendingInfo().taskId == taskInfo.taskId) { + // In this case an existing task, not currently in TaskView, is + // brought to the front to be moved into TaskView. This is still + // "new" from TaskView's perspective. (e.g. task being moved into a + // bubble) + isNewInTaskView = true; + stillNeedsMatchingLaunch = false; + tv = pending.mTaskView; + } else { + Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. " + + "This shouldn't happen, so there may be a visual " + + "artifact: " + taskInfo.taskId); } - continue; } + if (tv == null) continue; } if (wct == null) wct = new WindowContainerTransaction(); - prepareOpenAnimation(tv, taskIsNew, startTransaction, finishTransaction, - chg.getTaskInfo(), chg.getLeash(), wct); + prepareOpenAnimation(tv, isNewInTaskView, startTransaction, finishTransaction, + taskInfo, chg.getLeash(), wct); changesHandled++; } else if (chg.getMode() == TRANSIT_CHANGE) { - TaskViewTaskController tv = findTaskView(chg.getTaskInfo()); + TaskViewTaskController tv = findTaskView(taskInfo); if (tv == null) { if (pending != null) { Slog.w(TAG, "Found a non-TaskView task in a TaskView Transition. This " + "shouldn't happen, so there may be a visual artifact: " - + chg.getTaskInfo().taskId); + + taskInfo.taskId); } continue; } - startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); - finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) - .setPosition(chg.getLeash(), 0, 0); + final Rect boundsOnScreen = tv.prepareOpen(chg.getTaskInfo(), chg.getLeash()); + if (boundsOnScreen != null) { + if (wct == null) wct = new WindowContainerTransaction(); + updateBounds(tv, boundsOnScreen, startTransaction, finishTransaction, + chg.getTaskInfo(), chg.getLeash(), wct); + } else { + startTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()); + finishTransaction.reparent(chg.getLeash(), tv.getSurfaceControl()) + .setPosition(chg.getLeash(), 0, 0); + } changesHandled++; } } @@ -683,30 +703,8 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV WindowContainerTransaction wct) { final Rect boundsOnScreen = taskView.prepareOpen(taskInfo, leash); if (boundsOnScreen != null) { - final SurfaceControl tvSurface = taskView.getSurfaceControl(); - // Surface is ready, so just reparent the task to this surface control - startTransaction.reparent(leash, tvSurface) - .show(leash); - // Also reparent on finishTransaction since the finishTransaction will reparent back - // to its "original" parent by default. - if (finishTransaction != null) { - finishTransaction.reparent(leash, tvSurface) - .setPosition(leash, 0, 0) - // TODO: maybe once b/280900002 is fixed this will be unnecessary - .setWindowCrop(leash, boundsOnScreen.width(), boundsOnScreen.height()); - } - if (useRepo()) { - final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); - if (state != null) { - state.mBounds.set(boundsOnScreen); - state.mVisible = true; - } - } else { - updateBoundsState(taskView, boundsOnScreen); - updateVisibilityState(taskView, true /* visible */); - } - wct.setBounds(taskInfo.token, boundsOnScreen); - taskView.applyCaptionInsetsIfNeeded(); + updateBounds(taskView, boundsOnScreen, startTransaction, finishTransaction, taskInfo, + leash, wct); } else { // The surface has already been destroyed before the task has appeared, // so go ahead and hide the task entirely @@ -730,6 +728,36 @@ public class TaskViewTransitions implements Transitions.TransitionHandler, TaskV taskView.notifyAppeared(newTask); } + private void updateBounds(TaskViewTaskController taskView, Rect boundsOnScreen, + SurfaceControl.Transaction startTransaction, + SurfaceControl.Transaction finishTransaction, + ActivityManager.RunningTaskInfo taskInfo, SurfaceControl leash, + WindowContainerTransaction wct) { + final SurfaceControl tvSurface = taskView.getSurfaceControl(); + // Surface is ready, so just reparent the task to this surface control + startTransaction.reparent(leash, tvSurface) + .show(leash); + // Also reparent on finishTransaction since the finishTransaction will reparent back + // to its "original" parent by default. + if (finishTransaction != null) { + finishTransaction.reparent(leash, tvSurface) + .setPosition(leash, 0, 0) + .setWindowCrop(leash, boundsOnScreen.width(), boundsOnScreen.height()); + } + if (useRepo()) { + final TaskViewRepository.TaskViewState state = mTaskViewRepo.byTaskView(taskView); + if (state != null) { + state.mBounds.set(boundsOnScreen); + state.mVisible = true; + } + } else { + updateBoundsState(taskView, boundsOnScreen); + updateVisibilityState(taskView, true /* visible */); + } + wct.setBounds(taskInfo.token, boundsOnScreen); + taskView.applyCaptionInsetsIfNeeded(); + } + /** Interface for running an external transition in this object's pending queue. */ public interface ExternalTransition { /** Starts a transition and returns an identifying key for lookup. */ diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java index d8e7c2ccb15f..743bd052995e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedHandler.java @@ -176,7 +176,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, abstract void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback); abstract void onTransitionConsumed( @@ -691,7 +693,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { for (int i = 0; i < mActiveTransitions.size(); ++i) { if (mActiveTransitions.get(i).mTransition != mergeTarget) continue; @@ -701,7 +705,7 @@ public class DefaultMixedHandler implements MixedTransitionHandler, // Already done, so no need to end it. return; } - mixed.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mixed.mergeAnimation(transition, info, startT, finishT, mergeTarget, finishCallback); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java index 29a58d7f75dc..1853ffa96dfc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultMixedTransition.java @@ -384,7 +384,8 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { switch (mType) { case TYPE_DISPLAY_AND_SPLIT_CHANGE: @@ -394,7 +395,7 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { case TYPE_ENTER_PIP_FROM_ACTIVITY_EMBEDDING: mPipHandler.end(); mActivityEmbeddingController.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); return; case TYPE_ENTER_PIP_FROM_SPLIT: if (mAnimType == ANIM_TYPE_GOING_HOME) { @@ -405,26 +406,28 @@ class DefaultMixedTransition extends DefaultMixedHandler.MixedTransition { mPipHandler.end(); if (mLeftoversHandler != null) { mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); } } return; case TYPE_KEYGUARD: - mKeyguardHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mKeyguardHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_OPTIONS_REMOTE_AND_PIP_OR_DESKTOP_CHANGE: mPipHandler.end(); if (mLeftoversHandler != null) { mLeftoversHandler.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); } return; case TYPE_UNFOLD: - mUnfoldHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mUnfoldHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_OPEN_IN_DESKTOP: mDesktopTasksController.mergeAnimation( - transition, info, t, mergeTarget, finishCallback); + transition, info, startT, finishT, mergeTarget, finishCallback); return; default: throw new IllegalStateException("Playing a default mixed transition with unknown or" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java index ac6e4c5cd69e..01428e60582e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/DefaultTransitionHandler.java @@ -39,9 +39,12 @@ import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_ROTATE; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_SEAMLESS; import static android.view.WindowManager.LayoutParams.ROTATION_ANIMATION_UNSPECIFIED; import static android.view.WindowManager.TRANSIT_CHANGE; +import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_UNOCCLUDE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_RELAUNCH; import static android.view.WindowManager.TRANSIT_TO_BACK; +import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_OWNER_THUMBNAIL; import static android.window.TransitionInfo.FLAG_CROSS_PROFILE_WORK_THUMBNAIL; import static android.window.TransitionInfo.FLAG_DISPLAY_HAS_ALERT_WINDOWS; @@ -55,6 +58,7 @@ import static android.window.TransitionInfo.FLAG_SHOW_WALLPAPER; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; import static android.window.TransitionInfo.FLAG_TRANSLUCENT; +import static com.android.internal.jank.Cuj.CUJ_DEFAULT_TASK_TO_TASK_ANIMATION; import static com.android.internal.policy.TransitionAnimation.DEFAULT_APP_TRANSITION_DURATION; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CHANGE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_CLOSE; @@ -101,6 +105,7 @@ import android.window.WindowContainerTransaction; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.policy.ScreenDecorationsUtils; import com.android.internal.policy.TransitionAnimation; import com.android.internal.protolog.ProtoLog; @@ -144,6 +149,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { private Drawable mEnterpriseThumbnailDrawable; + final InteractionJankMonitor mInteractionJankMonitor; + private BroadcastReceiver mEnterpriseResourceUpdatedReceiver = new BroadcastReceiver() { @Override public void onReceive(Context context, Intent intent) { @@ -161,7 +168,8 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @NonNull TransactionPool transactionPool, @NonNull ShellExecutor mainExecutor, @NonNull Handler mainHandler, @NonNull ShellExecutor animExecutor, - @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer) { + @NonNull RootTaskDisplayAreaOrganizer rootTDAOrganizer, + @NonNull InteractionJankMonitor interactionJankMonitor) { mDisplayController = displayController; mTransactionPool = transactionPool; mContext = context; @@ -173,6 +181,7 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { mDevicePolicyManager = mContext.getSystemService(DevicePolicyManager.class); shellInit.addInitCallback(this::onInit, this); mRootTDAOrganizer = rootTDAOrganizer; + mInteractionJankMonitor = interactionJankMonitor; } private void onInit() { @@ -321,8 +330,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final ArrayList<Animator> animations = new ArrayList<>(); mAnimations.put(transition, animations); + final boolean isTaskTransition = isTaskTransition(info); + if (isTaskTransition) { + mInteractionJankMonitor.begin(info.getRoot(0).getLeash(), mContext, + mMainHandler, CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } + final Runnable onAnimFinish = () -> { if (!animations.isEmpty()) return; + if (isTaskTransition) { + mInteractionJankMonitor.end(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } mAnimations.remove(transition); finishCallback.onTransitionFinished(null /* wct */); }; @@ -451,6 +469,17 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final int type = getTransitionTypeFromInfo(info); Animation a = loadAnimation(type, info, change, wallpaperTransit, isDreamTransition); if (a != null) { + final int displayId = isTask ? change.getTaskInfo().displayId + : info.getRoot(TransitionUtil.rootIndexFor(change, info)) + .getDisplayId(); + final Context displayContext = + mDisplayController.getDisplayContext(displayId); + if (displayContext != null + && displayContext.getResources().getConfiguration().isScreenRound()) { + // ensure that any animation on a round display is using rounded corners + a.setHasRoundedCorners(true); + } + if (isTask) { final boolean isTranslucent = (change.getFlags() & FLAG_TRANSLUCENT) != 0; if (!isTranslucent && TransitionUtil.isOpenOrCloseMode(mode) @@ -504,11 +533,6 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final float cornerRadius; if (a.hasRoundedCorners()) { - final int displayId = isTask ? change.getTaskInfo().displayId - : info.getRoot(TransitionUtil.rootIndexFor(change, info)) - .getDisplayId(); - final Context displayContext = - mDisplayController.getDisplayContext(displayId); cornerRadius = displayContext == null ? 0 : ScreenDecorationsUtils.getWindowCornerRadius(displayContext); } else { @@ -672,6 +696,30 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { } /** + * A task transition is defined as a transition where there is exaclty one open/to_front task + * and one close/to_back task. Nothing else is allowed to be included in the transition + */ + public static boolean isTaskTransition(@NonNull TransitionInfo info) { + if (info.getChanges().size() != 2) { + return false; + } + boolean hasOpeningTask = false; + boolean hasClosingTask = false; + + for (int i = info.getChanges().size() - 1; i >= 0; --i) { + final TransitionInfo.Change change = info.getChanges().get(i); + if (change.getTaskInfo() == null) { + // A non-task is in the transition + return false; + } + int mode = change.getMode(); + hasOpeningTask |= mode == TRANSIT_OPEN || mode == TRANSIT_TO_FRONT; + hasClosingTask |= mode == TRANSIT_CLOSE || mode == TRANSIT_TO_BACK; + } + return hasOpeningTask && hasClosingTask; + } + + /** * Does `info` only contain translucent visibility changes (CHANGEs are ignored). We select * different animations and z-orders for these */ @@ -708,7 +756,9 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ArrayList<Animator> anims = mAnimations.get(mergeTarget); if (anims == null) return; @@ -978,4 +1028,10 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { || animType == ANIM_CLIP_REVEAL || animType == ANIM_OPEN_CROSS_PROFILE_APPS || animType == ANIM_FROM_STYLE; } + + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + mInteractionJankMonitor.cancel(CUJ_DEFAULT_TASK_TO_TASK_ANIMATION); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java index 209fc39b096a..ec737389c351 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/OneShotRemoteHandler.java @@ -96,7 +96,9 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "Merging registered One-shot remote" + " transition %s for (#%d).", mRemote, info.getDebugId()); @@ -111,7 +113,7 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { // process won't be cleared if the remote applied it. We don't actually know if the // remote applied the transaction, but applying twice will break surfaceflinger // so just assume the worst-case and clear the local transaction. - t.clear(); + startT.clear(); mMainExecutor.execute(() -> { finishCallback.onTransitionFinished(wct); }); @@ -121,8 +123,8 @@ public class OneShotRemoteHandler implements Transitions.TransitionHandler { // If the remote is actually in the same process, then make a copy of parameters since // remote impls assume that they have to clean-up native references. final SurfaceControl.Transaction remoteT = - RemoteTransitionHandler.copyIfLocal(t, mRemote.getRemoteTransition()); - final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + RemoteTransitionHandler.copyIfLocal(startT, mRemote.getRemoteTransition()); + final TransitionInfo remoteInfo = remoteT == startT ? info : info.localRemoteCopy(); mRemote.getRemoteTransition().mergeAnimation( transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java index 1847af07f275..1e926c57ca61 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RecentsMixedTransition.java @@ -159,9 +159,17 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // If pair-to-pair switching, the post-recents clean-up isn't needed. wct = wct != null ? wct : new WindowContainerTransaction(); if (mAnimType != ANIM_TYPE_PAIR_TO_PAIR) { - // TODO(b/346588978): Only called if !enableRecentsBookendTransition(), can remove - // once that rolls out - mSplitHandler.onRecentsInSplitAnimationFinish(wct, finishTransaction); + // We've dispatched to the mLeftoversHandler to handle the rest of the transition + // and called onRecentsInSplitAnimationStart(), but if the recents handler is not + // actually handling the transition, then onRecentsInSplitAnimationFinishing() + // won't actually get called by the recents handler. In such cases, we still need + // to clean up after the changes from the start call. + boolean splitNotifiedByRecents = mRecentsHandler == mLeftoversHandler; + if (!splitNotifiedByRecents) { + mSplitHandler.onRecentsInSplitAnimationFinishing( + mSplitHandler.wctIsReorderingSplitToTop(wct), + wct, finishTransaction); + } } else { // notify pair-to-pair recents animation finish mSplitHandler.onRecentsPairToPairAnimationFinish(wct); @@ -193,21 +201,24 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { @Override void mergeAnimation( @NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { switch (mType) { case TYPE_RECENTS_DURING_DESKTOP: - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; case TYPE_RECENTS_DURING_KEYGUARD: if ((info.getFlags() & TRANSIT_FLAG_KEYGUARD_UNOCCLUDING) != 0) { - handoverTransitionLeashes(mInfo, info, t, mFinishT); + handoverTransitionLeashes(mInfo, info, startT, finishT); if (animateKeyguard( - this, info, t, mFinishT, mFinishCB, mKeyguardHandler, mPipHandler)) { + this, info, startT, finishT, mFinishCB, mKeyguardHandler, + mPipHandler)) { finishCallback.onTransitionFinished(null); } } - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, finishCallback); return; case TYPE_RECENTS_DURING_SPLIT: @@ -216,7 +227,8 @@ class RecentsMixedTransition extends DefaultMixedHandler.MixedTransition { // another pair. mAnimType = DefaultMixedHandler.MixedTransition.ANIM_TYPE_PAIR_TO_PAIR; } - mLeftoversHandler.mergeAnimation(transition, info, t, mergeTarget, finishCallback); + mLeftoversHandler.mergeAnimation(transition, info, startT, finishT, mergeTarget, + finishCallback); return; default: throw new IllegalStateException("Playing a Recents mixed transition with unknown or" diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java index dec28fefd789..c4a410b0e28a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/RemoteTransitionHandler.java @@ -211,7 +211,9 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { final RemoteTransition remoteTransition = mRequestedRemotes.get(mergeTarget); if (remoteTransition == null) return; @@ -230,7 +232,7 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { // process won't be cleared if the remote applied it. We don't actually know if the // remote applied the transaction, but applying twice will break surfaceflinger // so just assume the worst-case and clear the local transaction. - t.clear(); + startT.clear(); mMainExecutor.execute(() -> { if (!mRequestedRemotes.containsKey(mergeTarget)) { Log.e(TAG, "Merged transition finished after it's mergeTarget (the " @@ -245,8 +247,8 @@ public class RemoteTransitionHandler implements Transitions.TransitionHandler { try { // If the remote is actually in the same process, then make a copy of parameters since // remote impls assume that they have to clean-up native references. - final SurfaceControl.Transaction remoteT = copyIfLocal(t, remote); - final TransitionInfo remoteInfo = remoteT == t ? info : info.localRemoteCopy(); + final SurfaceControl.Transaction remoteT = copyIfLocal(startT, remote); + final TransitionInfo remoteInfo = remoteT == startT ? info : info.localRemoteCopy(); remote.mergeAnimation(transition, remoteInfo, remoteT, mergeTarget, cb); } catch (RemoteException e) { Log.e(Transitions.TAG, "Error attempting to merge remote transition.", e); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java index b83b7e2f07a3..e28a7fa159c5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/Transitions.java @@ -77,6 +77,7 @@ import androidx.annotation.BinderThread; import com.android.internal.R; import com.android.internal.annotations.VisibleForTesting; +import com.android.internal.jank.InteractionJankMonitor; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -193,6 +194,12 @@ public class Transitions implements RemoteCallable<Transitions>, /** Transition to end the recents transition */ public static final int TRANSIT_END_RECENTS_TRANSITION = TRANSIT_FIRST_CUSTOM + 22; + /** Transition type for app compat reachability. */ + public static final int TRANSIT_MOVE_LETTERBOX_REACHABILITY = TRANSIT_FIRST_CUSTOM + 23; + + /** Transition type for converting a task to a bubble. */ + public static final int TRANSIT_CONVERT_TO_BUBBLE = TRANSIT_FIRST_CUSTOM + 24; + /** Transition type for desktop mode transitions. */ public static final int TRANSIT_DESKTOP_MODE_TYPES = WindowManager.TRANSIT_FIRST_CUSTOM + 100; @@ -332,7 +339,8 @@ public class Transitions implements RemoteCallable<Transitions>, mDisplayController = displayController; mPlayerImpl = new TransitionPlayerImpl(); mDefaultTransitionHandler = new DefaultTransitionHandler(context, shellInit, - displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer); + displayController, pool, mainExecutor, mainHandler, animExecutor, rootTDAOrganizer, + InteractionJankMonitor.getInstance()); mRemoteTransitionHandler = new RemoteTransitionHandler(mMainExecutor); mShellCommandHandler = shellCommandHandler; mShellController = shellController; @@ -922,7 +930,7 @@ public class Transitions implements RemoteCallable<Transitions>, + " %s is still animating. Notify the animating transition" + " in case they can be merged", ready, playing); mTransitionTracer.logMergeRequested(ready.mInfo.getDebugId(), playing.mInfo.getDebugId()); - playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, + playing.mHandler.mergeAnimation(ready.mToken, ready.mInfo, ready.mStartT, ready.mFinishT, playing.mToken, (wct) -> onMerged(playingToken, readyToken)); } @@ -1356,7 +1364,7 @@ public class Transitions implements RemoteCallable<Transitions>, // fast-forward. ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, " Attempt to merge sync %s" + " into %s via a SLEEP proxy", nextSync, playing); - playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, + playing.mHandler.mergeAnimation(nextSync.mToken, dummyInfo, dummyT, dummyT, playing.mToken, (wct) -> {}); // it's possible to complete immediately. If that happens, just repeat the signal // loop until we either finish everything or start playing an animation that isn't @@ -1404,7 +1412,9 @@ public class Transitions implements RemoteCallable<Transitions>, * @param finishTransaction the transaction given to the handler to be applied after the * transition animation. Unlike startTransaction, the handler is NOT * expected to apply this transaction. The Transition system will - * apply it when finishCallback is called. + * apply it when finishCallback is called. If additional transitions + * are merged, then the finish transactions for those transitions + * will be applied after this transaction. * @param finishCallback Call this when finished. This MUST be called on main thread. * @return true if transition was handled, false if not (falls-back to default). */ @@ -1414,6 +1424,17 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull TransitionFinishCallback finishCallback); /** + * See {@link #mergeAnimation(IBinder, TransitionInfo, SurfaceControl.Transaction, SurfaceControl.Transaction, IBinder, TransitionFinishCallback)} + * + * This deprecated method header is provided until downstream implementation can migrate to + * the call that takes both start & finish transactions. + */ + @Deprecated + default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { } + + /** * Attempts to merge a different transition's animation into an animation that this handler * is currently playing. If a merge is not possible/supported, this should be a no-op. * @@ -1430,14 +1451,25 @@ public class Transitions implements RemoteCallable<Transitions>, * * @param transition This is the transition that wants to be merged. * @param info Information about what is changing in the transition. - * @param t Contains surface changes that resulted from the transition. + * @param startTransaction The start transaction containing surface changes that resulted + * from the incoming transition. This should be applied by this + * active handler only if it chooses to merge the transition. + * @param finishTransaction The finish transaction for the incoming transition. Unlike + * startTransaction, the handler is NOT expected to apply this + * transaction. If the transition is merged, the Transition system + * will apply after finishCallback is called following the finish + * transaction provided in `#startAnimation()`. * @param mergeTarget This is the transition that we are attempting to merge with (ie. the * one this handler is currently already animating). * @param finishCallback Call this if merged. This MUST be called on main thread. */ default void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, - @NonNull TransitionFinishCallback finishCallback) { } + @NonNull SurfaceControl.Transaction startTransaction, + @NonNull SurfaceControl.Transaction finishTransaction, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { + // Call the legacy implementation by default + mergeAnimation(transition, info, startTransaction, mergeTarget, finishCallback); + } /** * Checks whether this handler is capable of taking over a transition matching `info`. @@ -1840,6 +1872,7 @@ public class Transitions implements RemoteCallable<Transitions>, case TRANSIT_MINIMIZE -> "MINIMIZE"; case TRANSIT_START_RECENTS_TRANSITION -> "START_RECENTS_TRANSITION"; case TRANSIT_END_RECENTS_TRANSITION -> "END_RECENTS_TRANSITION"; + case TRANSIT_CONVERT_TO_BUBBLE -> "CONVERT_TO_BUBBLE"; default -> ""; }; if (typeStr.isEmpty()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java index 3e0e15afc53a..7fd19a7d2a88 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/unfold/UnfoldTransitionHandler.java @@ -225,7 +225,9 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull TransitionFinishCallback finishCallback) { if (info.getType() != TRANSIT_CHANGE) { return; @@ -246,7 +248,7 @@ public class UnfoldTransitionHandler implements TransitionHandler, UnfoldListene } } // Apply changes happening during the unfold animation immediately - t.apply(); + startT.apply(); finishCallback.onTransitionFinished(null); if (getDefaultDisplayChange(info) == DefaultDisplayChange.DEFAULT_DISPLAY_FOLD) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java index 7aa00370ff58..7871179a50de 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecorViewModel.java @@ -339,6 +339,7 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT taskInfo, taskSurface, mMainHandler, + mMainExecutor, mBgExecutor, mMainChoreographer, mSyncQueue, @@ -389,7 +390,9 @@ public class CaptionWindowDecorViewModel implements WindowDecorViewModel, FocusT } else if (id == R.id.back_button) { mTaskOperations.injectBackKey(mDisplayId); } else if (id == R.id.minimize_window) { - mTaskOperations.minimizeTask(mTaskToken); + // This minimize button uses the same effect for any minimization. The last argument + // doesn't matter. + mTaskOperations.minimizeTask(mTaskToken, mTaskId, /* isLastTask= */ false); } else if (id == R.id.maximize_window) { RunningTaskInfo taskInfo = mTaskOrganizer.getRunningTaskInfo(mTaskId); final DisplayAreaInfo rootDisplayAreaInfo = diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java index 23bb2aa616f9..49510c8060fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java @@ -48,6 +48,8 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowManagerGlobal; +import android.window.DesktopModeFlags; import android.window.WindowContainerTransaction; import com.android.internal.annotations.VisibleForTesting; @@ -58,6 +60,7 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; @@ -69,6 +72,7 @@ import com.android.wm.shell.windowdecor.extension.TaskInfoKt; */ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearLayout> { private final Handler mHandler; + private final @ShellMainThread ShellExecutor mMainExecutor; private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final Choreographer mChoreographer; private final SyncTransactionQueue mSyncQueue; @@ -90,6 +94,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL RunningTaskInfo taskInfo, SurfaceControl taskSurface, Handler handler, + @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor, Choreographer choreographer, SyncTransactionQueue syncQueue, @@ -97,6 +102,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, windowDecorViewHostSupplier); mHandler = handler; + mMainExecutor = mainExecutor; mBgExecutor = bgExecutor; mChoreographer = choreographer; mSyncQueue = syncQueue; @@ -287,8 +293,14 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { closeDragResizeListener(); + final ShellExecutor bgExecutor = + DesktopModeFlags.ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD.isTrue() + ? mBgExecutor : mMainExecutor; mDragResizeListener = new DragResizeInputListener( mContext, + WindowManagerGlobal.getWindowSession(), + mMainExecutor, + bgExecutor, mTaskInfo, mHandler, mChoreographer, @@ -299,17 +311,19 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL mSurfaceControlTransactionSupplier, mDisplayController); } - + final DragResizeInputListener newListener = mDragResizeListener; final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - final Resources res = mResult.mRootView.getResources(); - mDragResizeListener.setGeometry(new DragResizeWindowGeometry(0 /* taskCornerRadius */, - new Size(mResult.mWidth, mResult.mHeight), - getResizeEdgeHandleSize(res), - getResizeHandleEdgeInset(res), getFineResizeCornerSize(res), - getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE), - touchSlop); + final DragResizeWindowGeometry newGeometry = new DragResizeWindowGeometry( + 0 /* taskCornerRadius */, + new Size(mResult.mWidth, mResult.mHeight), + getResizeEdgeHandleSize(res), + getResizeHandleEdgeInset(res), getFineResizeCornerSize(res), + getLargeResizeCornerSize(res), DragResizeWindowGeometry.DisabledEdge.NONE); + newListener.addInitializedCallback(() -> { + mDragResizeListener.setGeometry(newGeometry, touchSlop); + }); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt index 575aac381c42..02a5433147ca 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenu.kt @@ -25,11 +25,11 @@ import android.view.SurfaceControlViewHost import android.view.WindowInsets.Type.systemBars import android.view.WindowManager import android.view.WindowlessWindowManager +import android.window.DesktopModeFlags import android.window.TaskConstants import android.window.TaskSnapshot import androidx.compose.ui.graphics.toArgb import com.android.internal.annotations.VisibleForTesting -import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.common.DisplayController import com.android.wm.shell.desktopmode.DesktopUserRepositories @@ -76,7 +76,7 @@ class DesktopHeaderManageWindowsMenu( val flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH val desktopRepository = desktopUserRepositories.getProfile(callerTaskInfo.userId) - menuViewContainer = if (Flags.enableFullyImmersiveInDesktop() + menuViewContainer = if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && desktopRepository.isTaskInFullImmersiveState(callerTaskInfo.taskId)) { // Use system view container so that forcibly shown system bars take effect in // immersive. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java index 195e8195089f..5a6ea214e561 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModel.java @@ -50,7 +50,6 @@ import android.app.ActivityManager.RunningTaskInfo; import android.app.ActivityTaskManager; import android.app.IActivityManager; import android.app.IActivityTaskManager; -import android.content.ComponentName; import android.content.Context; import android.content.Intent; import android.graphics.Point; @@ -132,6 +131,7 @@ import com.android.wm.shell.recents.RecentsTransitionStateListener; import com.android.wm.shell.shared.FocusTransitionListener; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper; import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; @@ -149,6 +149,8 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; +import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel; +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Pair; @@ -173,7 +175,7 @@ import java.util.function.Supplier; */ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, - FocusTransitionListener { + FocusTransitionListener, SnapEventHandler { private static final String TAG = "DesktopModeWindowDecorViewModel"; private final DesktopModeWindowDecoration.Factory mDesktopModeWindowDecorFactory; @@ -255,6 +257,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private final WindowDecorTaskResourceLoader mTaskResourceLoader; private final RecentsTransitionHandler mRecentsTransitionHandler; private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; + private final DesktopTilingDecorViewModel mDesktopTilingDecorViewModel; public DesktopModeWindowDecorViewModel( Context context, @@ -292,7 +295,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy) { + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel) { this( context, shellExecutor, @@ -335,7 +339,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, desktopModeUiEventLogger, taskResourceLoader, recentsTransitionHandler, - desktopModeCompatPolicy); + desktopModeCompatPolicy, + desktopTilingDecorViewModel); } @VisibleForTesting @@ -381,7 +386,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, DesktopModeUiEventLogger desktopModeUiEventLogger, WindowDecorTaskResourceLoader taskResourceLoader, RecentsTransitionHandler recentsTransitionHandler, - DesktopModeCompatPolicy desktopModeCompatPolicy) { + DesktopModeCompatPolicy desktopModeCompatPolicy, + DesktopTilingDecorViewModel desktopTilingDecorViewModel) { mContext = context; mMainExecutor = shellExecutor; mMainHandler = mainHandler; @@ -452,7 +458,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mTaskResourceLoader = taskResourceLoader; mRecentsTransitionHandler = recentsTransitionHandler; mDesktopModeCompatPolicy = desktopModeCompatPolicy; - + mDesktopTilingDecorViewModel = desktopTilingDecorViewModel; + mDesktopTasksController.setSnapEventHandler(this); shellInit.addInitCallback(this::onInit, this); } @@ -723,8 +730,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.mTaskInfo, left ? SnapPosition.LEFT : SnapPosition.RIGHT, left ? ResizeTrigger.SNAP_LEFT_MENU : ResizeTrigger.SNAP_RIGHT_MENU, - inputMethod, - decoration); + inputMethod); decoration.closeHandleMenu(); decoration.closeMaximizeMenu(); @@ -755,7 +761,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // App sometimes draws before the insets from WindowDecoration#relayout have // been added, so they must be added here decoration.addCaptionInset(wct); - mDesktopTasksController.moveTaskToDesktop(taskId, wct, source, + mDesktopTasksController.moveTaskToDefaultDeskAndActivate(taskId, wct, source, /* remoteTransition= */ null, /* moveToDesktopCallback */ null); decoration.closeHandleMenu(); @@ -885,6 +891,33 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return snapshotList; } + @Override + public boolean snapToHalfScreen(@NonNull RunningTaskInfo taskInfo, + @NonNull Rect currentDragBounds, @NonNull SnapPosition position) { + return mDesktopTilingDecorViewModel.snapToHalfScreen(taskInfo, + mWindowDecorByTaskId.get(taskInfo.taskId), position, currentDragBounds); + } + + @Override + public void removeTaskIfTiled(int displayId, int taskId) { + mDesktopTilingDecorViewModel.removeTaskIfTiled(displayId, taskId); + } + + @Override + public void onUserChange() { + mDesktopTilingDecorViewModel.onUserChange(); + } + + @Override + public void onOverviewAnimationStateChange(boolean running) { + mDesktopTilingDecorViewModel.onOverviewAnimationStateChange(running); + } + + @Override + public boolean moveTaskToFrontIfTiled(@NonNull RunningTaskInfo taskInfo) { + return mDesktopTilingDecorViewModel.moveTaskToFrontIfTiled(taskInfo); + } + private class DesktopModeTouchEventListener extends GestureDetector.SimpleOnGestureListener implements View.OnClickListener, View.OnTouchListener, View.OnLongClickListener, View.OnGenericMotionListener, DragDetector.MotionEventHandler { @@ -904,13 +937,14 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, * Whether to pilfer the next motion event to send cancellations to the windows below. * Useful when the caption window is spy and the gesture should be handled by the system * instead of by the app for their custom header content. - * Should not have any effect when {@link Flags#enableAccessibleCustomHeaders()}, because - * a spy window is not used then. + * Should not have any effect when + * {@link DesktopModeFlags#ENABLE_ACCESSIBLE_CUSTOM_HEADERS}, because a spy window is not + * used then. */ private boolean mIsCustomHeaderGesture; private boolean mIsResizeGesture; private boolean mIsDragging; - private boolean mTouchscreenInUse; + private boolean mLongClickDisabled; private int mDragPointerId = -1; private MotionEvent mMotionEvent; @@ -950,7 +984,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDesktopTasksController.onDesktopWindowClose( wct, mDisplayId, decoration.mTaskInfo); final IBinder transition = mTaskOperations.closeTask(mTaskToken, wct); - if (transition != null && runOnTransitionStart != null) { + if (transition != null) { runOnTransitionStart.invoke(transition); } } @@ -974,7 +1008,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // should shared with the maximize menu's maximize/restore actions. final DesktopRepository desktopRepository = mDesktopUserRepositories.getProfile( decoration.mTaskInfo.userId); - if (Flags.enableFullyImmersiveInDesktop() + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && desktopRepository.isTaskInFullImmersiveState( decoration.mTaskInfo.taskId)) { // Task is in immersive and should exit. @@ -996,10 +1030,12 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mMotionEvent = e; final int id = v.getId(); final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); - if ((e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN) { - mTouchscreenInUse = e.getActionMasked() != ACTION_UP - && e.getActionMasked() != ACTION_CANCEL; - } + final boolean touchscreenSource = + (e.getSource() & SOURCE_TOUCHSCREEN) == SOURCE_TOUCHSCREEN; + // Disable long click during events from a non-touchscreen source + mLongClickDisabled = !touchscreenSource && e.getActionMasked() != ACTION_UP + && e.getActionMasked() != ACTION_CANCEL; + if (id != R.id.caption_handle && id != R.id.desktop_mode_caption && id != R.id.open_menu_button && id != R.id.close_window && id != R.id.maximize_window && id != R.id.minimize_window) { @@ -1047,7 +1083,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return false; } if (mInputManager != null - && !Flags.enableAccessibleCustomHeaders()) { + && !DesktopModeFlags.ENABLE_ACCESSIBLE_CUSTOM_HEADERS.isTrue()) { ViewRootImpl viewRootImpl = v.getViewRootImpl(); if (viewRootImpl != null) { // Pilfer so that windows below receive cancellations for this gesture. @@ -1069,7 +1105,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, @Override public boolean onLongClick(View v) { final int id = v.getId(); - if (id == R.id.maximize_window && mTouchscreenInUse) { + if (id == R.id.maximize_window && !mLongClickDisabled) { final DesktopModeWindowDecoration decoration = mWindowDecorByTaskId.get(mTaskId); moveTaskToFront(decoration.mTaskInfo); if (decoration.isMaximizeMenuActive()) { @@ -1235,8 +1271,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, taskInfo, decoration.mTaskSurface, new PointF(e.getRawX(dragPointerIdx), e.getRawY(dragPointerIdx)), newTaskBounds, decoration.calculateValidDragArea(), - new Rect(mOnDragStartInitialBounds), e, - mWindowDecorByTaskId.get(taskInfo.taskId)); + new Rect(mOnDragStartInitialBounds), e); if (touchingButton) { // We need the input event to not be consumed here to end the ripple // effect on the touched button. We will reset drag state in the ensuing @@ -1440,10 +1475,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mDragToDesktopAnimationStartBounds.set( relevantDecor.mTaskInfo.configuration.windowConfiguration.getBounds()); boolean dragFromStatusBarAllowed = false; - if (DesktopModeStatus.canEnterDesktopMode(mContext)) { + final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); + if (DesktopModeStatus.canEnterDesktopMode(mContext) + || BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { // In proto2 any full screen or multi-window task can be dragged to // freeform. - final int windowingMode = relevantDecor.mTaskInfo.getWindowingMode(); dragFromStatusBarAllowed = windowingMode == WINDOWING_MODE_FULLSCREEN || windowingMode == WINDOWING_MODE_MULTI_WINDOW; } @@ -1495,7 +1531,11 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // Do not create an indicator at all if we're not past transition height. DisplayLayout layout = mDisplayController .getDisplayLayout(relevantDecor.mTaskInfo.displayId); - if (ev.getRawY() < 2 * layout.stableInsets().top + // It's possible task is not at the top of the screen (e.g. bottom of vertical + // Splitscreen) + final int taskTop = relevantDecor.mTaskInfo.configuration.windowConfiguration + .getBounds().top; + if (ev.getRawY() < 2 * layout.stableInsets().top + taskTop && mMoveToDesktopAnimator == null) { return; } @@ -1654,11 +1694,9 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (mDesktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing(taskInfo)) { return false; } - if (isPartOfDefaultHomePackage(taskInfo)) { - return false; - } - final boolean isOnLargeScreen = taskInfo.getConfiguration().smallestScreenWidthDp - >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; + final boolean isOnLargeScreen = + mDisplayController.getDisplay(taskInfo.displayId).getMinSizeDimensionDp() + >= WindowManager.LARGE_SCREEN_SMALLEST_SCREEN_WIDTH_DP; if (!DesktopModeStatus.canEnterDesktopMode(mContext) && DesktopModeStatus.overridesShowAppHandle(mContext) && !isOnLargeScreen) { // Devices with multiple screens may enable the app handle but it should not show on @@ -1672,14 +1710,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, && !taskInfo.configuration.windowConfiguration.isAlwaysOnTop(); } - private boolean isPartOfDefaultHomePackage(RunningTaskInfo taskInfo) { - final ComponentName currentDefaultHome = - mContext.getPackageManager().getHomeActivities(new ArrayList<>()); - return currentDefaultHome != null && taskInfo.baseActivity != null - && currentDefaultHome.getPackageName() - .equals(taskInfo.baseActivity.getPackageName()); - } - private void createWindowDecoration( ActivityManager.RunningTaskInfo taskInfo, SurfaceControl taskSurface, @@ -1717,7 +1747,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mWindowDecorViewHostSupplier, mMultiInstanceHelper, mWindowDecorCaptionHandleRepository, - mDesktopModeEventLogger); + mDesktopModeEventLogger, + mDesktopModeCompatPolicy); mWindowDecorByTaskId.put(taskInfo.taskId, windowDecoration); final TaskPositioner taskPositioner = mTaskPositionerFactory.create( @@ -1998,7 +2029,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, continue; } if (decor.mTaskInfo.displayId == displayId - && Flags.enableDesktopWindowingImmersiveHandleHiding()) { + && DesktopModeFlags + .ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING.isTrue()) { decor.onInsetsStateChanged(insetsState); } if (!DesktopModeFlags.ENABLE_HANDLE_INPUT_FIX.isTrue()) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java index 387dbfa807fc..dca376f7df0e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java @@ -27,18 +27,17 @@ import static android.view.MotionEvent.ACTION_UP; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION; import static android.window.DesktopModeFlags.ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS; - import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopMode; import static com.android.wm.shell.shared.desktopmode.DesktopModeStatus.canEnterDesktopModeOrShowAppHandle; import static com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; +import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.DisabledEdge.NONE; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getFineResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getLargeResizeCornerSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeEdgeHandleSize; import static com.android.wm.shell.windowdecor.DragResizeWindowGeometry.getResizeHandleEdgeInset; -import static com.android.wm.shell.windowdecor.DragPositioningCallbackUtility.DragEventListener; import android.annotation.NonNull; import android.annotation.Nullable; @@ -69,6 +68,7 @@ import android.view.View; import android.view.ViewConfiguration; import android.view.WindowInsets; import android.view.WindowManager; +import android.view.WindowManagerGlobal; import android.widget.ImageButton; import android.window.DesktopModeFlags; import android.window.TaskSnapshot; @@ -95,6 +95,7 @@ import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; import com.android.wm.shell.shared.annotations.ShellBackgroundThread; import com.android.wm.shell.shared.annotations.ShellMainThread; +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.multiinstance.ManageWindowsViewContainer; @@ -192,6 +193,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final HandleMenuFactory mHandleMenuFactory; private final AppToWebGenericLinksParser mGenericLinksParser; private final AssistContentRequester mAssistContentRequester; + private final DesktopModeCompatPolicy mDesktopModeCompatPolicy; // Hover state for the maximize menu and button. The menu will remain open as long as either of // these is true. See {@link #onMaximizeHoverStateChanged()}. @@ -233,7 +235,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @NonNull WindowDecorViewHostSupplier<WindowDecorViewHost> windowDecorViewHostSupplier, MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, - DesktopModeEventLogger desktopModeEventLogger) { + DesktopModeEventLogger desktopModeEventLogger, + DesktopModeCompatPolicy desktopModeCompatPolicy) { this (context, userContext, displayController, taskResourceLoader, splitScreenController, desktopUserRepositories, taskOrganizer, taskInfo, taskSurface, handler, mainExecutor, mainDispatcher, bgScope, bgExecutor, choreographer, syncQueue, @@ -246,7 +249,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin windowDecorViewHostSupplier, DefaultMaximizeMenuFactory.INSTANCE, DefaultHandleMenuFactory.INSTANCE, multiInstanceHelper, - windowDecorCaptionHandleRepository, desktopModeEventLogger); + windowDecorCaptionHandleRepository, desktopModeEventLogger, + desktopModeCompatPolicy); } DesktopModeWindowDecoration( @@ -281,7 +285,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin HandleMenuFactory handleMenuFactory, MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, - DesktopModeEventLogger desktopModeEventLogger) { + DesktopModeEventLogger desktopModeEventLogger, + DesktopModeCompatPolicy desktopModeCompatPolicy) { super(context, userContext, displayController, taskOrganizer, taskInfo, taskSurface, surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, windowContainerTransactionSupplier, surfaceControlSupplier, @@ -306,6 +311,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mDesktopUserRepositories = desktopUserRepositories; mTaskResourceLoader = taskResourceLoader; mTaskResourceLoader.onWindowDecorCreated(taskInfo); + mDesktopModeCompatPolicy = desktopModeCompatPolicy; } /** @@ -474,7 +480,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (shouldDelayUpdate) { return; } - updateDragResizeListener(mDecorationContainerSurface, inFullImmersive); + updateDragResizeListenerIfNeeded(mDecorationContainerSurface, inFullImmersive); } @@ -484,7 +490,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin boolean hasGlobalFocus, @NonNull Region displayExclusionRegion) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); - if (Flags.enableDesktopWindowingAppToWeb()) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_APP_TO_WEB.isTrue()) { setCapturedLink(taskInfo.capturedLink, taskInfo.capturedLinkTimestamp); } @@ -508,7 +514,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin applyStartTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus, - displayExclusionRegion, mIsRecentsTransitionRunning); + displayExclusionRegion, mIsRecentsTransitionRunning, + mDesktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(taskInfo)); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -579,8 +586,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeHandleMenu(); closeManageWindowsMenu(); closeMaximizeMenu(); + notifyNoCaptionHandle(); } - updateDragResizeListener(oldDecorationSurface, inFullImmersive); + updateDragResizeListenerIfNeeded(oldDecorationSurface, inFullImmersive); updateMaximizeMenu(startT, inFullImmersive); Trace.endSection(); // DesktopModeWindowDecoration#relayout } @@ -658,22 +666,42 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return mUserContext.getUser(); } - private void updateDragResizeListener(SurfaceControl oldDecorationSurface, + private void updateDragResizeListenerIfNeeded(@Nullable SurfaceControl containerSurface, boolean inFullImmersive) { + final boolean taskPositionChanged = !mTaskInfo.positionInParent.equals(mPositionInParent); if (!isDragResizable(mTaskInfo, inFullImmersive)) { - if (!mTaskInfo.positionInParent.equals(mPositionInParent)) { + if (taskPositionChanged) { // We still want to track caption bar's exclusion region on a non-resizeable task. updateExclusionRegion(inFullImmersive); } closeDragResizeListener(); return; } + updateDragResizeListener(containerSurface, + (geometryChanged) -> { + if (geometryChanged || taskPositionChanged) { + updateExclusionRegion(inFullImmersive); + } + }); + } - if (oldDecorationSurface != mDecorationContainerSurface || mDragResizeListener == null) { + private void updateDragResizeListener(@Nullable SurfaceControl containerSurface, + Consumer<Boolean> onUpdateFinished) { + final boolean containerSurfaceChanged = containerSurface != mDecorationContainerSurface; + final boolean isFirstDragResizeListener = mDragResizeListener == null; + final boolean shouldCreateListener = containerSurfaceChanged || isFirstDragResizeListener; + if (containerSurfaceChanged) { closeDragResizeListener(); - Trace.beginSection("DesktopModeWindowDecoration#relayout-DragResizeInputListener"); + } + if (shouldCreateListener) { + final ShellExecutor bgExecutor = + DesktopModeFlags.ENABLE_DRAG_RESIZE_SET_UP_IN_BG_THREAD.isTrue() + ? mBgExecutor : mMainExecutor; mDragResizeListener = new DragResizeInputListener( mContext, + WindowManagerGlobal.getWindowSession(), + mMainExecutor, + bgExecutor, mTaskInfo, mHandler, mChoreographer, @@ -684,24 +712,20 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mSurfaceControlTransactionSupplier, mDisplayController, mDesktopModeEventLogger); - Trace.endSection(); } - + final DragResizeInputListener newListener = mDragResizeListener; final int touchSlop = ViewConfiguration.get(mResult.mRootView.getContext()) .getScaledTouchSlop(); - - // If either task geometry or position have changed, update this task's - // exclusion region listener final Resources res = mResult.mRootView.getResources(); - if (mDragResizeListener.setGeometry( - new DragResizeWindowGeometry(mRelayoutParams.mCornerRadius, - new Size(mResult.mWidth, mResult.mHeight), - getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res), - getFineResizeCornerSize(res), getLargeResizeCornerSize(res), - mDisabledResizingEdge), touchSlop) - || !mTaskInfo.positionInParent.equals(mPositionInParent)) { - updateExclusionRegion(inFullImmersive); - } + final DragResizeWindowGeometry newGeometry = new DragResizeWindowGeometry( + mRelayoutParams.mCornerRadius, + new Size(mResult.mWidth, mResult.mHeight), + getResizeEdgeHandleSize(res), getResizeHandleEdgeInset(res), + getFineResizeCornerSize(res), getLargeResizeCornerSize(res), + mDisabledResizingEdge); + newListener.addInitializedCallback(() -> { + onUpdateFinished.accept(newListener.setGeometry(newGeometry, touchSlop)); + }); } private static boolean isDragResizable(ActivityManager.RunningTaskInfo taskInfo, @@ -717,7 +741,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private void notifyCaptionStateChanged() { - // TODO: b/366159408 - Ensure bounds sent with notification account for RTL mode. if (!canEnterDesktopMode(mContext) || !isEducationEnabled()) { return; } @@ -732,11 +755,14 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } else { // App header is visible since `mWindowDecorViewHolder` is of type // [AppHeaderViewHolder]. - ((AppHeaderViewHolder) mWindowDecorViewHolder).runOnAppChipGlobalLayout( - () -> { - notifyAppHeaderStateChanged(); - return Unit.INSTANCE; - }); + final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder); + if (appHeader != null) { + appHeader.runOnAppChipGlobalLayout( + () -> { + notifyAppHeaderStateChanged(); + return Unit.INSTANCE; + }); + } } } @@ -767,11 +793,11 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private void notifyAppHeaderStateChanged() { - if (isAppHandle(mWindowDecorViewHolder) || mWindowDecorViewHolder == null) { + final AppHeaderViewHolder appHeader = asAppHeader(mWindowDecorViewHolder); + if (appHeader == null) { return; } - final Rect appChipPositionInWindow = - ((AppHeaderViewHolder) mWindowDecorViewHolder).getAppChipLocationInWindow(); + final Rect appChipPositionInWindow = appHeader.getAppChipLocationInWindow(); final Rect taskBounds = mTaskInfo.configuration.windowConfiguration.getBounds(); final Rect appChipGlobalPosition = new Rect( taskBounds.left + appChipPositionInWindow.left, @@ -794,8 +820,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (!mTaskInfo.isVisible()) { closeMaximizeMenu(); } else { - final int menuWidth = calculateMaximizeMenuWidth(); - mMaximizeMenu.positionMenu(calculateMaximizeMenuPosition(menuWidth), startT); + mMaximizeMenu.positionMenu(startT); } } @@ -847,6 +872,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mOnCaptionButtonClickListener, mOnCaptionLongClickListener, mOnCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, mOnMaximizeHoverListener); } throw new IllegalArgumentException("Unexpected layout resource id"); @@ -886,7 +914,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @NonNull InsetsState displayInsetsState, boolean hasGlobalFocus, @NonNull Region displayExclusionRegion, - boolean shouldIgnoreCornerRadius) { + boolean shouldIgnoreCornerRadius, + boolean shouldExcludeCaptionFromAppBounds) { final int captionLayoutId = getDesktopModeWindowDecorLayoutId(taskInfo.getWindowingMode()); final boolean isAppHeader = captionLayoutId == R.layout.desktop_mode_app_header; @@ -904,7 +933,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin relayoutParams.mAsyncViewHost = isAppHandle; final boolean showCaption; - if (Flags.enableFullyImmersiveInDesktop()) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { if (inFullImmersiveMode) { showCaption = isStatusBarVisible && !isKeyguardVisibleAndOccluded; } else { @@ -933,7 +962,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // The app is requesting to customize the caption bar, which means input on // customizable/exclusion regions must go to the app instead of to the system. // This may be accomplished with spy windows or custom touchable regions: - if (Flags.enableAccessibleCustomHeaders()) { + if (DesktopModeFlags.ENABLE_ACCESSIBLE_CUSTOM_HEADERS.isTrue()) { // Set the touchable region of the caption to only the areas where input should // be handled by the system (i.e. non custom-excluded areas). The region will // be calculated based on occluding caption elements and exclusion areas @@ -946,17 +975,26 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } } else { if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION.isTrue()) { - // Force-consume the caption bar insets when the app tries to hide the caption. - // This improves app compatibility of immersive apps. - relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING; + if (shouldExcludeCaptionFromAppBounds) { + relayoutParams.mShouldSetAppBounds = true; + } else { + // Force-consume the caption bar insets when the app tries to hide the + // caption. This improves app compatibility of immersive apps. + relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING; + } } } if (ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS.isTrue()) { - // Always force-consume the caption bar insets for maximum app compatibility, - // including non-immersive apps that just don't handle caption insets properly. - relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; + if (shouldExcludeCaptionFromAppBounds) { + relayoutParams.mShouldSetAppBounds = true; + } else { + // Always force-consume the caption bar insets for maximum app compatibility, + // including non-immersive apps that just don't handle caption insets properly. + relayoutParams.mInsetSourceFlags |= FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; + } } - if (Flags.enableFullyImmersiveInDesktop() && inFullImmersiveMode) { + if (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() + && inFullImmersiveMode) { final Insets systemBarInsets = displayInsetsState.calculateInsets( taskInfo.getConfiguration().windowConfiguration.getBounds(), WindowInsets.Type.systemBars() & ~WindowInsets.Type.captionBar(), @@ -1047,27 +1085,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin return Resources.ID_NULL; } - private int calculateMaximizeMenuWidth() { - final boolean showImmersive = Flags.enableFullyImmersiveInDesktop() - && TaskInfoKt.getRequestingImmersive(mTaskInfo); - final boolean showMaximize = true; - final boolean showSnaps = mTaskInfo.isResizeable; - int showCount = 0; - if (showImmersive) showCount++; - if (showMaximize) showCount++; - if (showSnaps) showCount++; - return switch (showCount) { - case 1 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_one_options); - case 2 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_two_options); - case 3 -> loadDimensionPixelSize(mContext.getResources(), - R.dimen.desktop_mode_maximize_menu_width_three_options); - default -> throw new IllegalArgumentException(""); - }; - } - - private PointF calculateMaximizeMenuPosition(int menuWidth) { + private PointF calculateMaximizeMenuPosition(int menuWidth, int menuHeight) { final PointF position = new PointF(); final Resources resources = mContext.getResources(); final DisplayLayout displayLayout = @@ -1083,17 +1101,16 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final int[] maximizeButtonLocation = new int[2]; maximizeWindowButton.getLocationInWindow(maximizeButtonLocation); - final int menuHeight = loadDimensionPixelSize( - resources, R.dimen.desktop_mode_maximize_menu_height); - float menuLeft = (mPositionInParent.x + maximizeButtonLocation[0] - ((float) (menuWidth - maximizeWindowButton.getWidth()) / 2)); float menuTop = (mPositionInParent.y + captionHeight); final float menuRight = menuLeft + menuWidth; final float menuBottom = menuTop + menuHeight; - // If the menu is out of screen bounds, shift it up/left as needed - if (menuRight > displayWidth) { + // If the menu is out of screen bounds, shift it as needed + if (menuLeft < 0) { + menuLeft = 0; + } else if (menuRight > displayWidth) { menuLeft = (displayWidth - menuWidth); } if (menuBottom > displayHeight) { @@ -1270,17 +1287,18 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin * Create and display maximize menu window */ void createMaximizeMenu() { - final int menuWidth = calculateMaximizeMenuWidth(); mMaximizeMenu = mMaximizeMenuFactory.create(mSyncQueue, mRootTaskDisplayAreaOrganizer, mDisplayController, mTaskInfo, mContext, - calculateMaximizeMenuPosition(menuWidth), mSurfaceControlTransactionSupplier); + (width, height) -> calculateMaximizeMenuPosition(width, height), + mSurfaceControlTransactionSupplier); mMaximizeMenu.show( - /* isTaskInImmersiveMode= */ Flags.enableFullyImmersiveInDesktop() + /* isTaskInImmersiveMode= */ + DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && mDesktopUserRepositories.getProfile(mTaskInfo.userId) .isTaskInFullImmersiveState(mTaskInfo.taskId), - /* menuWidth= */ menuWidth, - /* showImmersiveOption= */ Flags.enableFullyImmersiveInDesktop() + /* showImmersiveOption= */ + DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue() && TaskInfoKt.getRequestingImmersive(mTaskInfo), /* showSnapOptions= */ mTaskInfo.isResizeable, mOnMaximizeOrRestoreClickListener, @@ -1362,7 +1380,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin updateGenericLink(); final boolean supportsMultiInstance = mMultiInstanceHelper .supportsMultiInstanceSplit(mTaskInfo.baseActivity, mTaskInfo.userId) - && Flags.enableDesktopWindowingMultiInstanceFeatures(); + && DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_MULTI_INSTANCE_FEATURES.isTrue(); final boolean shouldShowManageWindowsButton = supportsMultiInstance && mMinimumInstancesFound; final boolean shouldShowChangeAspectRatioButton = HandleMenu.Companion @@ -1710,7 +1728,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private Region getGlobalExclusionRegion(boolean inFullImmersive) { Region exclusionRegion; - if (mDragResizeListener != null && isDragResizable(mTaskInfo, inFullImmersive)) { + if (mDragResizeListener != null + && isDragResizable(mTaskInfo, inFullImmersive)) { exclusionRegion = mDragResizeListener.getCornersRegion(); } else { exclusionRegion = new Region(); @@ -1737,7 +1756,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private static int getCaptionHeightIdStatic(@WindowingMode int windowingMode) { return windowingMode == WINDOWING_MODE_FULLSCREEN ? com.android.internal.R.dimen.status_bar_height_default - : R.dimen.desktop_mode_freeform_decor_caption_height; + : DesktopModeUtils.getAppHeaderHeightId(); } private int getCaptionHeight(@WindowingMode int windowingMode) { @@ -1788,7 +1807,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } private boolean canOpenMaximizeMenu(boolean animatingTaskResizeOrReposition) { - if (!Flags.enableFullyImmersiveInDesktop()) { + if (!DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue()) { return !animatingTaskResizeOrReposition; } final boolean inImmersiveAndRequesting = @@ -1835,7 +1854,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin windowDecorViewHostSupplier, MultiInstanceHelper multiInstanceHelper, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, - DesktopModeEventLogger desktopModeEventLogger) { + DesktopModeEventLogger desktopModeEventLogger, + DesktopModeCompatPolicy desktopModeCompatPolicy) { return new DesktopModeWindowDecoration( context, userContext, @@ -1860,7 +1880,8 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin windowDecorViewHostSupplier, multiInstanceHelper, windowDecorCaptionHandleRepository, - desktopModeEventLogger); + desktopModeEventLogger, + desktopModeCompatPolicy); } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java index b531079f18c1..7a4a834e9dc2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DragResizeInputListener.java @@ -43,6 +43,7 @@ import android.os.Binder; import android.os.Handler; import android.os.IBinder; import android.os.RemoteException; +import android.os.Trace; import android.util.Size; import android.view.Choreographer; import android.view.IWindowSession; @@ -54,14 +55,19 @@ import android.view.PointerIcon; import android.view.SurfaceControl; import android.view.View; import android.view.ViewConfiguration; -import android.view.WindowManagerGlobal; import android.window.InputTransferToken; +import com.android.internal.annotations.VisibleForTesting; import com.android.internal.protolog.ProtoLog; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.DisplayLayout; +import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.desktopmode.DesktopModeEventLogger; +import com.android.wm.shell.shared.annotations.ShellBackgroundThread; +import com.android.wm.shell.shared.annotations.ShellMainThread; +import java.util.ArrayList; +import java.util.List; import java.util.function.Consumer; import java.util.function.Supplier; @@ -73,28 +79,45 @@ import java.util.function.Supplier; */ class DragResizeInputListener implements AutoCloseable { private static final String TAG = "DragResizeInputListener"; - private final IWindowSession mWindowSession = WindowManagerGlobal.getWindowSession(); + private final IWindowSession mWindowSession; + private final TaskResizeInputEventReceiverFactory mEventReceiverFactory; + private final Supplier<SurfaceControl.Builder> mSurfaceControlBuilderSupplier; private final Supplier<SurfaceControl.Transaction> mSurfaceControlTransactionSupplier; private final int mDisplayId; - private final IBinder mClientToken; + @VisibleForTesting + final IBinder mClientToken; private final SurfaceControl mDecorationSurface; - private final InputChannel mInputChannel; - private final TaskResizeInputEventReceiver mInputEventReceiver; + private InputChannel mInputChannel; + private TaskResizeInputEventReceiver mInputEventReceiver; private final Context mContext; + private final @ShellBackgroundThread ShellExecutor mBgExecutor; private final RunningTaskInfo mTaskInfo; - private final SurfaceControl mInputSinkSurface; - private final IBinder mSinkClientToken; - private final InputChannel mSinkInputChannel; + private final Handler mHandler; + private final Choreographer mChoreographer; + private SurfaceControl mInputSinkSurface; + @VisibleForTesting + final IBinder mSinkClientToken; + private InputChannel mSinkInputChannel; private final DisplayController mDisplayController; + /** TODO: b/396490344 - this desktop-specific class should be abstracted out of here. */ private final DesktopModeEventLogger mDesktopModeEventLogger; + private final DragPositioningCallback mDragPositioningCallback; private final Region mTouchRegion = new Region(); + private final List<Runnable> mOnInitializedCallbacks = new ArrayList<>(); + + private final Runnable mInitInputChannels; + private boolean mClosed = false; DragResizeInputListener( Context context, + IWindowSession windowSession, + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor, + TaskResizeInputEventReceiverFactory eventReceiverFactory, RunningTaskInfo taskInfo, Handler handler, Choreographer choreographer, @@ -106,20 +129,138 @@ class DragResizeInputListener implements AutoCloseable { DisplayController displayController, DesktopModeEventLogger desktopModeEventLogger) { mContext = context; + mWindowSession = windowSession; + mBgExecutor = bgExecutor; + mEventReceiverFactory = eventReceiverFactory; mTaskInfo = taskInfo; - mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; + mHandler = handler; + mChoreographer = choreographer; mDisplayId = displayId; mDecorationSurface = decorationSurface; + mDragPositioningCallback = callback; + mSurfaceControlBuilderSupplier = surfaceControlBuilderSupplier; + mSurfaceControlTransactionSupplier = surfaceControlTransactionSupplier; mDisplayController = displayController; mDesktopModeEventLogger = desktopModeEventLogger; mClientToken = new Binder(); + mSinkClientToken = new Binder(); + + // Setting up input channels for both the resize listener and the input sink requires + // multiple blocking binder calls, so it's moved to a bg thread to keep the shell.main + // thread free. + // The input event receiver must be created back in the shell.main thread though because + // its geometry and util methods are updated/queried from the shell.main thread. + mInitInputChannels = () -> { + final InputSetUpResult result = setUpInputChannels(mDisplayId, mWindowSession, + mDecorationSurface, mClientToken, mSinkClientToken, + mSurfaceControlBuilderSupplier, + mSurfaceControlTransactionSupplier); + mainExecutor.execute(() -> { + if (mClosed) { + return; + } + mInputSinkSurface = result.mInputSinkSurface; + mInputChannel = result.mInputChannel; + mSinkInputChannel = result.mSinkInputChannel; + Trace.beginSection("DragResizeInputListener#ctor-initReceiver"); + mInputEventReceiver = mEventReceiverFactory.create( + mContext, + mTaskInfo, + mInputChannel, + mDragPositioningCallback, + mHandler, + mChoreographer, + () -> { + final DisplayLayout layout = + mDisplayController.getDisplayLayout(mDisplayId); + return new Size(layout.width(), layout.height()); + }, + this::updateSinkInputChannel, + mDesktopModeEventLogger); + mInputEventReceiver.setTouchSlop( + ViewConfiguration.get(mContext).getScaledTouchSlop()); + for (Runnable initCallback : mOnInitializedCallbacks) { + initCallback.run(); + } + mOnInitializedCallbacks.clear(); + Trace.endSection(); + }); + }; + bgExecutor.execute(mInitInputChannels); + } + + DragResizeInputListener( + Context context, + IWindowSession windowSession, + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor, + RunningTaskInfo taskInfo, + Handler handler, + Choreographer choreographer, + int displayId, + SurfaceControl decorationSurface, + DragPositioningCallback callback, + Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + DisplayController displayController, + DesktopModeEventLogger desktopModeEventLogger) { + this(context, windowSession, mainExecutor, bgExecutor, + new DefaultTaskResizeInputEventReceiverFactory(), taskInfo, + handler, choreographer, displayId, decorationSurface, callback, + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, + displayController, desktopModeEventLogger); + } + + DragResizeInputListener( + Context context, + IWindowSession windowSession, + @ShellMainThread ShellExecutor mainExecutor, + @ShellBackgroundThread ShellExecutor bgExecutor, + RunningTaskInfo taskInfo, + Handler handler, + Choreographer choreographer, + int displayId, + SurfaceControl decorationSurface, + DragPositioningCallback callback, + Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, + DisplayController displayController) { + this(context, windowSession, mainExecutor, bgExecutor, taskInfo, + handler, choreographer, displayId, decorationSurface, callback, + surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, + displayController, new DesktopModeEventLogger()); + } + + /** + * Registers a callback to be invoked when the input listener has finished initializing. If + * already finished, the callback will be invoked immediately. + */ + void addInitializedCallback(Runnable onReady) { + if (mInputEventReceiver != null) { + onReady.run(); + return; + } + mOnInitializedCallbacks.add(onReady); + } + + @ShellBackgroundThread + private static InputSetUpResult setUpInputChannels( + int displayId, + @NonNull IWindowSession windowSession, + @NonNull SurfaceControl decorationSurface, + @NonNull IBinder clientToken, + @NonNull IBinder sinkClientToken, + @NonNull Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, + @NonNull Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier) { + Trace.beginSection("DragResizeInputListener#setUpInputChannels"); final InputTransferToken inputTransferToken = new InputTransferToken(); - mInputChannel = new InputChannel(); + final InputChannel inputChannel = new InputChannel(); + final InputChannel sinkInputChannel = new InputChannel(); try { - mWindowSession.grantInputChannel( - mDisplayId, - mDecorationSurface, - mClientToken, + windowSession.grantInputChannel( + displayId, + decorationSurface, + clientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, @@ -127,37 +268,27 @@ class DragResizeInputListener implements AutoCloseable { TYPE_APPLICATION, null /* windowToken */, inputTransferToken, - TAG + " of " + decorationSurface.toString(), - mInputChannel); + TAG + " of " + decorationSurface, + inputChannel); } catch (RemoteException e) { e.rethrowFromSystemServer(); } - mInputEventReceiver = new TaskResizeInputEventReceiver(context, mTaskInfo, mInputChannel, - callback, - handler, choreographer, () -> { - final DisplayLayout layout = mDisplayController.getDisplayLayout(mDisplayId); - return new Size(layout.width(), layout.height()); - }, this::updateSinkInputChannel, mDesktopModeEventLogger); - mInputEventReceiver.setTouchSlop(ViewConfiguration.get(context).getScaledTouchSlop()); - - mInputSinkSurface = surfaceControlBuilderSupplier.get() + final SurfaceControl inputSinkSurface = surfaceControlBuilderSupplier.get() .setName("TaskInputSink of " + decorationSurface) .setContainerLayer() - .setParent(mDecorationSurface) - .setCallsite("DragResizeInputListener.constructor") + .setParent(decorationSurface) + .setCallsite("DragResizeInputListener.setUpInputChannels") .build(); - mSurfaceControlTransactionSupplier.get() - .setLayer(mInputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) - .show(mInputSinkSurface) + surfaceControlTransactionSupplier.get() + .setLayer(inputSinkSurface, WindowDecoration.INPUT_SINK_Z_ORDER) + .show(inputSinkSurface) .apply(); - mSinkClientToken = new Binder(); - mSinkInputChannel = new InputChannel(); try { - mWindowSession.grantInputChannel( - mDisplayId, - mInputSinkSurface, - mSinkClientToken, + windowSession.grantInputChannel( + displayId, + inputSinkSurface, + sinkClientToken, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, @@ -166,26 +297,12 @@ class DragResizeInputListener implements AutoCloseable { null /* windowToken */, inputTransferToken, "TaskInputSink of " + decorationSurface, - mSinkInputChannel); + sinkInputChannel); } catch (RemoteException e) { e.rethrowFromSystemServer(); } - } - - DragResizeInputListener( - Context context, - RunningTaskInfo taskInfo, - Handler handler, - Choreographer choreographer, - int displayId, - SurfaceControl decorationSurface, - DragPositioningCallback callback, - Supplier<SurfaceControl.Builder> surfaceControlBuilderSupplier, - Supplier<SurfaceControl.Transaction> surfaceControlTransactionSupplier, - DisplayController displayController) { - this(context, taskInfo, handler, choreographer, displayId, decorationSurface, callback, - surfaceControlBuilderSupplier, surfaceControlTransactionSupplier, displayController, - new DesktopModeEventLogger()); + Trace.endSection(); + return new InputSetUpResult(inputSinkSurface, inputChannel, sinkInputChannel); } /** @@ -268,35 +385,101 @@ class DragResizeInputListener implements AutoCloseable { } boolean shouldHandleEvent(@NonNull MotionEvent e, @NonNull Point offset) { - return mInputEventReceiver.shouldHandleEvent(e, offset); + return mInputEventReceiver != null && mInputEventReceiver.shouldHandleEvent(e, offset); } boolean isHandlingDragResize() { - return mInputEventReceiver.isHandlingEvents(); + return mInputEventReceiver != null && mInputEventReceiver.isHandlingEvents(); } @Override public void close() { - mInputEventReceiver.dispose(); - mInputChannel.dispose(); - try { - mWindowSession.remove(mClientToken); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + mClosed = true; + if (mInitInputChannels != null) { + mBgExecutor.removeCallbacks(mInitInputChannels); + } + if (mInputEventReceiver != null) { + mInputEventReceiver.dispose(); + } + if (mInputChannel != null) { + mInputChannel.dispose(); + } + if (mSinkInputChannel != null) { + mSinkInputChannel.dispose(); } - mSinkInputChannel.dispose(); - try { - mWindowSession.remove(mSinkClientToken); - } catch (RemoteException e) { - e.rethrowFromSystemServer(); + if (mInputSinkSurface != null) { + mSurfaceControlTransactionSupplier.get() + .remove(mInputSinkSurface) + .apply(); } - mSurfaceControlTransactionSupplier.get() - .remove(mInputSinkSurface) - .apply(); + + mBgExecutor.execute(() -> { + try { + mWindowSession.remove(mClientToken); + mWindowSession.remove(mSinkClientToken); + } catch (RemoteException e) { + e.rethrowFromSystemServer(); + } + }); } - private static class TaskResizeInputEventReceiver extends InputEventReceiver implements + private static class InputSetUpResult { + final @NonNull SurfaceControl mInputSinkSurface; + final @NonNull InputChannel mInputChannel; + final @NonNull InputChannel mSinkInputChannel; + + InputSetUpResult(@NonNull SurfaceControl inputSinkSurface, + @NonNull InputChannel inputChannel, + @NonNull InputChannel sinkInputChannel) { + mInputSinkSurface = inputSinkSurface; + mInputChannel = inputChannel; + mSinkInputChannel = sinkInputChannel; + } + } + + /** A factory that creates {@link TaskResizeInputEventReceiver}s. */ + interface TaskResizeInputEventReceiverFactory { + @NonNull + TaskResizeInputEventReceiver create( + @NonNull Context context, + @NonNull RunningTaskInfo taskInfo, + @NonNull InputChannel inputChannel, + @NonNull DragPositioningCallback callback, + @NonNull Handler handler, + @NonNull Choreographer choreographer, + @NonNull Supplier<Size> displayLayoutSizeSupplier, + @NonNull Consumer<Region> touchRegionConsumer, + @NonNull DesktopModeEventLogger desktopModeEventLogger + ); + } + + /** A default implementation of {@link TaskResizeInputEventReceiverFactory}. */ + static class DefaultTaskResizeInputEventReceiverFactory + implements TaskResizeInputEventReceiverFactory { + @Override + @NonNull + public TaskResizeInputEventReceiver create( + @NonNull Context context, + @NonNull RunningTaskInfo taskInfo, + @NonNull InputChannel inputChannel, + @NonNull DragPositioningCallback callback, + @NonNull Handler handler, + @NonNull Choreographer choreographer, + @NonNull Supplier<Size> displayLayoutSizeSupplier, + @NonNull Consumer<Region> touchRegionConsumer, + @NonNull DesktopModeEventLogger desktopModeEventLogger) { + return new TaskResizeInputEventReceiver(context, taskInfo, inputChannel, callback, + handler, choreographer, displayLayoutSizeSupplier, touchRegionConsumer, + desktopModeEventLogger); + } + } + + /** + * An input event receiver to handle motion events on the task's corners and edges for + * drag-resizing, as well as keeping the input sink updated. + */ + static class TaskResizeInputEventReceiver extends InputEventReceiver implements DragDetector.MotionEventHandler { @NonNull private final Context mContext; @NonNull private final RunningTaskInfo mTaskInfo; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt index ab30d617af54..cfd068860589 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/FixedAspectRatioTaskPositionerDecorator.kt @@ -20,6 +20,7 @@ import android.app.ActivityManager.RunningTaskInfo import android.graphics.PointF import android.graphics.Rect import com.android.internal.annotations.VisibleForTesting +import com.android.wm.shell.desktopmode.calculateAspectRatio import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_BOTTOM import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_LEFT import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_RIGHT @@ -27,8 +28,6 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_TOP import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFINED import com.android.wm.shell.windowdecor.DragPositioningCallback.CtrlType import kotlin.math.abs -import kotlin.math.max -import kotlin.math.min /** * [AbstractTaskPositionerDecorator] implementation for validating the coordinates associated with a @@ -59,8 +58,7 @@ class FixedAspectRatioTaskPositionerDecorator ( lastValidPoint.set(x, y) val startingBoundWidth = lastRepositionedBounds.width() val startingBoundHeight = lastRepositionedBounds.height() - startingAspectRatio = max(startingBoundWidth, startingBoundHeight).toFloat() / - min(startingBoundWidth, startingBoundHeight).toFloat() + startingAspectRatio = calculateAspectRatio(windowDecoration.mTaskInfo) isTaskPortrait = startingBoundWidth <= startingBoundHeight lastRepositionedBounds.set( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt index b21c3f522eab..458815d19658 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleImageButton.kt @@ -17,9 +17,12 @@ package com.android.wm.shell.windowdecor import android.animation.ValueAnimator +import android.annotation.DimenRes +import android.content.res.Resources; import android.content.Context import android.util.AttributeSet import android.widget.ImageButton +import com.android.wm.shell.R /** * [ImageButton] for the handle at the top of fullscreen apps. Has custom hover @@ -30,13 +33,23 @@ class HandleImageButton (context: Context?, attrs: AttributeSet?) : ImageButton(context, attrs) { private val handleAnimator = ValueAnimator() + /** Final horizontal padding for hover enter. **/ + private val HANDLE_HOVER_ENTER_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_hovered) + /** Final horizontal padding for press down. **/ + private val HANDLE_PRESS_DOWN_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_touched) + /** Default horizontal padding. **/ + private val HANDLE_DEFAULT_PADDING = loadDimensionPixelSize( + R.dimen.desktop_mode_fullscreen_decor_caption_horizontal_padding_default) + override fun onHoverChanged(hovered: Boolean) { super.onHoverChanged(hovered) if (hovered) { - animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_SCALE) + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_HOVER_ENTER_PADDING) } else { if (!isPressed) { - animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + animateHandle(HANDLE_HOVER_ANIM_DURATION, HANDLE_DEFAULT_PADDING) } } } @@ -45,35 +58,37 @@ class HandleImageButton (context: Context?, attrs: AttributeSet?) : if (isPressed != pressed) { super.setPressed(pressed) if (pressed) { - animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_SCALE) + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_PRESS_DOWN_PADDING) } else { - animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_SCALE) + animateHandle(HANDLE_PRESS_ANIM_DURATION, HANDLE_DEFAULT_PADDING) } } } - private fun animateHandle(duration: Long, endScale: Float) { + private fun animateHandle(duration: Long, endPadding: Int) { if (handleAnimator.isRunning) { handleAnimator.cancel() } handleAnimator.duration = duration - handleAnimator.setFloatValues(scaleX, endScale) + handleAnimator.setIntValues(paddingLeft, endPadding) handleAnimator.addUpdateListener { animator -> - scaleX = animator.animatedValue as Float + val padding = animator.animatedValue as Int + setPadding(padding, paddingTop, padding, paddingBottom) } handleAnimator.start() } + private fun loadDimensionPixelSize(@DimenRes resourceId: Int): Int { + if (resourceId == Resources.ID_NULL) { + return 0 + } + return context.resources.getDimensionPixelSize(resourceId) + } + companion object { /** The duration of animations related to hover state. **/ private const val HANDLE_HOVER_ANIM_DURATION = 300L /** The duration of animations related to pressed state. **/ private const val HANDLE_PRESS_ANIM_DURATION = 200L - /** Ending scale for hover enter. **/ - private const val HANDLE_HOVER_ENTER_SCALE = 1.2f - /** Ending scale for press down. **/ - private const val HANDLE_PRESS_DOWN_SCALE = 0.85f - /** Default scale for handle. **/ - private const val HANDLE_DEFAULT_SCALE = 1f } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt index 053850480ecc..ff50672953c9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenu.kt @@ -35,15 +35,18 @@ import android.view.SurfaceControl import android.view.View import android.view.WindowInsets.Type.systemBars import android.view.WindowManager -import android.widget.Button import android.widget.ImageButton import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.Space import android.widget.TextView import android.window.DesktopModeFlags import android.window.SurfaceSyncGroup import androidx.annotation.StringRes import androidx.annotation.VisibleForTesting import androidx.compose.ui.graphics.toArgb +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat.ACTION_CLICK import androidx.core.view.isGone import com.android.window.flags.Flags import com.android.wm.shell.R @@ -55,8 +58,8 @@ import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalViewContainer import com.android.wm.shell.windowdecor.common.DecorThemeUtil -import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader +import com.android.wm.shell.windowdecor.common.calculateMenuPosition import com.android.wm.shell.windowdecor.extension.isFullscreen import com.android.wm.shell.windowdecor.extension.isMultiWindow import com.android.wm.shell.windowdecor.extension.isPinned @@ -250,6 +253,7 @@ class HandleMenu( view = handleMenuView.rootView, forciblyShownTypes = if (forceShowSystemBars) { systemBars() } else { 0 }, ignoreCutouts = Flags.showAppHandleLargeScreens() + || BubbleAnythingFlagHelper.enableBubbleToFullscreen() ) } else { parentDecor.addWindow( @@ -470,7 +474,7 @@ class HandleMenu( @VisibleForTesting val appIconView = appInfoPill.requireViewById<ImageView>(R.id.application_icon) @VisibleForTesting - val appNameView = appInfoPill.requireViewById<TextView>(R.id.application_name) + val appNameView = appInfoPill.requireViewById<MarqueedTextView>(R.id.application_name) // Windowing Pill. private val windowingPill = rootView.requireViewById<View>(R.id.windowing_pill) @@ -479,21 +483,28 @@ class HandleMenu( private val splitscreenBtn = windowingPill.requireViewById<ImageButton>( R.id.split_screen_button) private val floatingBtn = windowingPill.requireViewById<ImageButton>(R.id.floating_button) + private val floatingBtnSpace = windowingPill.requireViewById<Space>( + R.id.floating_button_space) + private val desktopBtn = windowingPill.requireViewById<ImageButton>(R.id.desktop_button) + private val desktopBtnSpace = windowingPill.requireViewById<Space>( + R.id.desktop_button_space) // More Actions Pill. private val moreActionsPill = rootView.requireViewById<View>(R.id.more_actions_pill) - private val screenshotBtn = moreActionsPill.requireViewById<Button>(R.id.screenshot_button) - private val newWindowBtn = moreActionsPill.requireViewById<Button>(R.id.new_window_button) + private val screenshotBtn = moreActionsPill.requireViewById<HandleMenuActionButton>( + R.id.screenshot_button) + private val newWindowBtn = moreActionsPill.requireViewById<HandleMenuActionButton>( + R.id.new_window_button) private val manageWindowBtn = moreActionsPill - .requireViewById<Button>(R.id.manage_windows_button) + .requireViewById<HandleMenuActionButton>(R.id.manage_windows_button) private val changeAspectRatioBtn = moreActionsPill - .requireViewById<Button>(R.id.change_aspect_ratio_button) + .requireViewById<HandleMenuActionButton>(R.id.change_aspect_ratio_button) // Open in Browser/App Pill. private val openInAppOrBrowserPill = rootView.requireViewById<View>( R.id.open_in_app_or_browser_pill) - private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<Button>( + private val openInAppOrBrowserBtn = openInAppOrBrowserPill.requireViewById<View>( R.id.open_in_app_or_browser_button) private val openByDefaultBtn = openInAppOrBrowserPill.requireViewById<ImageButton>( R.id.open_by_default_button) @@ -536,6 +547,38 @@ class HandleMenu( } return@setOnTouchListener true } + + with(context) { + // Update a11y announcement out to say "double tap to enter Fullscreen" + ViewCompat.replaceAccessibilityAction( + fullscreenBtn, ACTION_CLICK, + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.fullscreen_text) + ), + null, + ) + + // Update a11y announcement out to say "double tap to enter Desktop View" + ViewCompat.replaceAccessibilityAction( + desktopBtn, ACTION_CLICK, + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.desktop_text) + ), + null, + ) + + // Update a11y announcement to say "double tap to enter Split Screen" + ViewCompat.replaceAccessibilityAction( + splitscreenBtn, ACTION_CLICK, + getString( + R.string.app_handle_menu_accessibility_announce, + getString(R.string.split_screen_text) + ), + null, + ) + } } /** Binds the menu views to the new data. */ @@ -641,6 +684,7 @@ class HandleMenu( this.taskInfo = this@HandleMenuView.taskInfo } appNameView.setTextColor(style.textColor) + appNameView.startMarquee() } private fun bindWindowingPill(style: MenuStyle) { @@ -648,6 +692,7 @@ class HandleMenu( if (!BubbleAnythingFlagHelper.enableBubbleToFullscreen()) { floatingBtn.visibility = View.GONE + floatingBtnSpace.visibility = View.GONE } fullscreenBtn.isSelected = taskInfo.isFullscreen @@ -676,11 +721,17 @@ class HandleMenu( ).forEach { val button = it.first val shouldShow = it.second - button.apply { - isGone = !shouldShow + + val buttonRoot = button.requireViewById<LinearLayout>(R.id.action_button) + val label = buttonRoot.requireViewById<MarqueedTextView>(R.id.label) + val image = buttonRoot.requireViewById<ImageView>(R.id.image) + + button.isGone = !shouldShow + label.apply { setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + startMarquee() } + image.imageTintList = ColorStateList.valueOf(style.textColor) } } @@ -695,12 +746,17 @@ class HandleMenu( } else { getString(R.string.open_in_browser_text) } - openInAppOrBrowserBtn.apply { + + val label = openInAppOrBrowserBtn.requireViewById<MarqueedTextView>(R.id.label) + val image = openInAppOrBrowserBtn.requireViewById<ImageView>(R.id.image) + openInAppOrBrowserBtn.contentDescription = btnText + label.apply { text = btnText - contentDescription = btnText setTextColor(style.textColor) - compoundDrawableTintList = ColorStateList.valueOf(style.textColor) + startMarquee() } + image.imageTintList = ColorStateList.valueOf(style.textColor) + openByDefaultBtn.isGone = isBrowserApp openByDefaultBtn.imageTintList = ColorStateList.valueOf(style.textColor) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt new file mode 100644 index 000000000000..4b2e473d6ec2 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuActionButton.kt @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.annotation.ColorInt +import android.annotation.IdRes +import android.content.Context +import android.content.res.ColorStateList +import android.util.AttributeSet +import android.view.LayoutInflater +import android.widget.ImageView +import android.widget.LinearLayout +import android.widget.TextView +import androidx.core.content.withStyledAttributes +import androidx.core.view.isGone +import com.android.wm.shell.R + +/** + * Button-like component used to display the "Additional options" elements of the Handle menu window + * decoration. + * + * The possible options for which this button is used for are "Screenshot", "New Window", "Manage + * Windows" and "Change Aspect Ratio". + */ +class HandleMenuActionButton @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = 0 +) : LinearLayout(context, attrs, defStyleAttr) { + + private val rootElement: LinearLayout + private val iconView: ImageView + private val textView: MarqueedTextView + + init { + val view = LayoutInflater.from(context).inflate( + R.layout.desktop_mode_window_decor_handle_menu_action_button, this, true) + rootElement = findViewById(R.id.action_button) + iconView = findViewById(R.id.image) + textView = findViewById(R.id.label) + + context.withStyledAttributes(attrs, R.styleable.HandleMenuActionButton) { + textView.text = getString(R.styleable.HandleMenuActionButton_android_text) + textView.setTextColor(getColor(R.styleable.HandleMenuActionButton_android_textColor, 0)) + iconView.setImageResource(getResourceId( + R.styleable.HandleMenuActionButton_android_src, 0)) + iconView.imageTintList = getColorStateList( + R.styleable.HandleMenuActionButton_android_drawableTint) + } + } + + /** + * Sets a listener to be invoked when this view is clicked. + * + * @param l the [OnClickListener] that receives click events. + */ + override fun setOnClickListener(l: OnClickListener?) { + rootElement.setOnClickListener(l) + } + + /** + * Sets the text color for the text inside the button. + * + * @param color the color to set for the text, as a color integer. + */ + fun setTextColor(@ColorInt color: Int) { + textView.setTextColor(color) + } + + /** + * Sets the icon for the button using a resource ID. + * + * @param resourceId the resource ID of the drawable to set as the icon. + */ + fun setIconResource(@IdRes resourceId: Int) { + iconView.setImageResource(resourceId) + } + + /** + * Sets the text to display inside the button. + * + * @param text the text to display. + */ + fun setText(text: CharSequence?) { + textView.text = text + } + + /** + * Sets the tint color for the icon. + * + * @param color the color to use for the tint, as a color integer. + */ + fun setDrawableTint(@ColorInt color: Int) { + iconView.imageTintList = ColorStateList.valueOf(color) + } + + /** + * Gets or sets the tint applied to the icon. + * + * @return The [ColorStateList] representing the tint, or null if no tint is applied. + */ + var compoundDrawableTintList: ColorStateList? + get() = iconView.imageTintList + set(value) { + iconView.imageTintList = value + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt index 470e5a1d88b4..75f90bb9c38e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/HandleMenuAnimator.kt @@ -327,7 +327,7 @@ class HandleMenuAnimator( } // Open in Browser Button Opacity Animation - val button = openInAppOrBrowserPill.requireViewById<Button>(R.id.open_in_app_or_browser_button) + val button = openInAppOrBrowserPill.requireViewById<View>(R.id.open_in_app_or_browser_button) animators += ObjectAnimator.ofFloat(button, ALPHA, 1f).apply { startDelay = BODY_ALPHA_OPEN_DELAY diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt new file mode 100644 index 000000000000..733b6221ac0e --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MarqueedTextView.kt @@ -0,0 +1,48 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.content.Context +import android.util.AttributeSet +import android.widget.TextView + +/** A custom [TextView] that allows better control over marquee animation used to ellipsize text. */ +class MarqueedTextView @JvmOverloads constructor( + context: Context, + attrs: AttributeSet? = null, + defStyleAttr: Int = android.R.attr.textViewStyle +) : TextView(context, attrs, defStyleAttr) { + + /** + * Starts marquee animation if the layout attributes for this object include + * `android:ellipsize=marquee`, `android:singleLine=true`, and + * `android:scrollHorizontally=true`. + */ + override public fun startMarquee() { + super.startMarquee() + } + + /** + * Must always return [true] since [TextView.startMarquee()] requires view to be selected or + * focused in order to start the marquee animation. + * + * We are not using [TextView.setSelected()] as this would dispatch undesired accessibility + * events. + */ + override fun isSelected() : Boolean { + return true + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt index e23ebe6634ff..581d1867ddf0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeButtonView.kt @@ -53,6 +53,10 @@ class MaximizeButtonView( (stubProgressBarContainer.inflate() as FrameLayout) .requireViewById(R.id.progress_bar) } + private val maximizeButtonText = + context.resources.getString(R.string.desktop_mode_maximize_menu_maximize_button_text) + private val restoreButtonText = + context.resources.getString(R.string.desktop_mode_maximize_menu_restore_button_text) init { LayoutInflater.from(context).inflate(R.layout.maximize_menu_button, this, true) @@ -154,6 +158,12 @@ class MaximizeButtonView( /** Set the drawable resource to use for the maximize button. */ fun setIcon(@DrawableRes icon: Int) { maximizeWindow.setImageResource(icon) + when (icon) { + R.drawable.decor_desktop_mode_immersive_or_maximize_exit_button_dark -> + maximizeWindow.contentDescription = restoreButtonText + R.drawable.decor_desktop_mode_maximize_button_dark -> + maximizeWindow.contentDescription = maximizeButtonText + } } companion object { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt index 1ce0366728b9..ad3525af3f94 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MaximizeMenu.kt @@ -34,6 +34,7 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.StateListDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.util.StateSet import android.view.LayoutInflater import android.view.MotionEvent.ACTION_HOVER_ENTER @@ -51,12 +52,16 @@ import android.view.ViewGroup import android.view.WindowManager import android.view.WindowlessWindowManager import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.Button import android.widget.TextView import android.window.TaskConstants import androidx.compose.material3.ColorScheme import androidx.compose.ui.graphics.toArgb import androidx.core.animation.addListener +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import androidx.core.view.isGone import androidx.core.view.isVisible import com.android.wm.shell.R @@ -85,7 +90,7 @@ class MaximizeMenu( private val displayController: DisplayController, private val taskInfo: RunningTaskInfo, private val decorWindowContext: Context, - private val menuPosition: PointF, + private val positionSupplier: (Int, Int) -> PointF, private val transactionSupplier: Supplier<Transaction> = Supplier { Transaction() } ) { private var maximizeMenu: AdditionalViewHostViewContainer? = null @@ -95,19 +100,19 @@ class MaximizeMenu( private val cornerRadius = loadDimensionPixelSize( R.dimen.desktop_mode_maximize_menu_corner_radius ).toFloat() - private val menuHeight = loadDimensionPixelSize(R.dimen.desktop_mode_maximize_menu_height) + private lateinit var menuPosition: PointF private val menuPadding = loadDimensionPixelSize(R.dimen.desktop_mode_menu_padding) /** Position the menu relative to the caption's position. */ - fun positionMenu(position: PointF, t: Transaction) { - menuPosition.set(position) + fun positionMenu(t: Transaction) { + menuPosition = positionSupplier(maximizeMenuView?.measureWidth() ?: 0, + maximizeMenuView?.measureHeight() ?: 0) t.setPosition(leash, menuPosition.x, menuPosition.y) } /** Creates and shows the maximize window. */ fun show( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeOrRestoreClickListener: () -> Unit, @@ -120,7 +125,6 @@ class MaximizeMenu( if (maximizeMenu != null) return createMaximizeMenu( isTaskInImmersiveMode = isTaskInImmersiveMode, - menuWidth = menuWidth, showImmersiveOption = showImmersiveOption, showSnapOptions = showSnapOptions, onMaximizeClickListener = onMaximizeOrRestoreClickListener, @@ -156,7 +160,6 @@ class MaximizeMenu( /** Create a maximize menu that is attached to the display area. */ private fun createMaximizeMenu( isTaskInImmersiveMode: Boolean, - menuWidth: Int, showImmersiveOption: Boolean, showSnapOptions: Boolean, onMaximizeClickListener: () -> Unit, @@ -173,16 +176,6 @@ class MaximizeMenu( .setName("Maximize Menu") .setContainerLayer() .build() - val lp = WindowManager.LayoutParams( - menuWidth, - menuHeight, - WindowManager.LayoutParams.TYPE_APPLICATION, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, - PixelFormat.TRANSPARENT - ) - lp.title = "Maximize Menu for Task=" + taskInfo.taskId - lp.setTrustedOverlay() val windowManager = WindowlessWindowManager( taskInfo.configuration, leash, @@ -202,7 +195,6 @@ class MaximizeMenu( MaximizeMenuView.ImmersiveConfig.Hidden }, showSnapOptions = showSnapOptions, - menuHeight = menuHeight, menuPadding = menuPadding, ).also { menuView -> menuView.bind(taskInfo) @@ -212,6 +204,19 @@ class MaximizeMenu( menuView.onRightSnapClickListener = onRightSnapClickListener menuView.onMenuHoverListener = onHoverListener menuView.onOutsideTouchListener = onOutsideTouchListener + val menuWidth = menuView.measureWidth() + val menuHeight = menuView.measureHeight() + menuPosition = positionSupplier(menuWidth, menuHeight) + val lp = WindowManager.LayoutParams( + menuWidth.toInt(), + menuHeight.toInt(), + WindowManager.LayoutParams.TYPE_APPLICATION, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + or WindowManager.LayoutParams.FLAG_WATCH_OUTSIDE_TOUCH, + PixelFormat.TRANSPARENT + ) + lp.title = "Maximize Menu for Task=" + taskInfo.taskId + lp.setTrustedOverlay() viewHost.setView(menuView.rootView, lp) } @@ -263,7 +268,6 @@ class MaximizeMenu( private val sizeToggleDirection: SizeToggleDirection, immersiveConfig: ImmersiveConfig, showSnapOptions: Boolean, - private val menuHeight: Int, private val menuPadding: Int ) { val rootView = LayoutInflater.from(context) @@ -286,13 +290,24 @@ class MaximizeMenu( requireViewById(R.id.maximize_menu_snap_container) as View private val snapWindowText = requireViewById(R.id.maximize_menu_snap_window_text) as TextView - private val snapRightButton = - requireViewById(R.id.maximize_menu_snap_right_button) as Button - private val snapLeftButton = - requireViewById(R.id.maximize_menu_snap_left_button) as Button private val snapButtonsLayout = requireViewById(R.id.maximize_menu_snap_menu_layout) + // If layout direction is RTL, maximize menu will be mirrored, switching the order of the + // snap right/left buttons. + val isRtl: Boolean = + (context.resources.configuration.layoutDirection == View.LAYOUT_DIRECTION_RTL) + private val snapRightButton = if (isRtl) { + requireViewById(R.id.maximize_menu_snap_left_button) as Button + } else { + requireViewById(R.id.maximize_menu_snap_right_button) as Button + } + private val snapLeftButton = if (isRtl) { + requireViewById(R.id.maximize_menu_snap_right_button) as Button + } else { + requireViewById(R.id.maximize_menu_snap_left_button) as Button + } + private val decorThemeUtil = DecorThemeUtil(context) private val outlineRadius = context.resources @@ -403,6 +418,96 @@ class MaximizeMenu( true } + sizeToggleButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onMaximizeClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapLeftButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onLeftSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + snapRightButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + if (action == AccessibilityAction.ACTION_CLICK.id) { + onRightSnapClickListener?.invoke() + } + return super.performAccessibilityAction(host, action, args) + } + } + + with(context.resources) { + ViewCompat.replaceAccessibilityAction( + snapLeftButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_left_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + snapRightButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_snap_right_text), + null + ) + + ViewCompat.replaceAccessibilityAction( + sizeToggleButton, + AccessibilityActionCompat.ACTION_CLICK, + getString(R.string.maximize_menu_talkback_action_maximize_restore_text), + null + ) + } + // Maximize/restore button. val sizeToggleBtnTextId = if (sizeToggleDirection == SizeToggleDirection.RESTORE) R.string.desktop_mode_maximize_menu_restore_button_text @@ -477,7 +582,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -498,7 +603,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight, 0f).apply { + (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight(), 0f).apply { duration = OPEN_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = EMPHASIZED_DECELERATE }, @@ -561,7 +666,7 @@ class MaximizeMenu( // the menu. val value = animatedValue as Float val topPadding = menuPadding - - ((1 - value) * menuHeight).toInt() + ((1 - value) * measureHeight()).toInt() container.setPadding(menuPadding, topPadding, menuPadding, menuPadding) } @@ -582,7 +687,7 @@ class MaximizeMenu( } }, ObjectAnimator.ofFloat(rootView, TRANSLATION_Y, - 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * menuHeight).apply { + 0f, (STARTING_MENU_HEIGHT_SCALE - 1) * measureHeight()).apply { duration = CLOSE_MENU_HEIGHT_ANIMATION_DURATION_MS interpolator = FAST_OUT_LINEAR_IN }, @@ -686,6 +791,18 @@ class MaximizeMenu( ) } + /** Measure width of the root view of this menu. */ + fun measureWidth() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredWidth() + } + + /** Measure height of the root view of this menu. */ + fun measureHeight() : Int { + rootView.measure(View.MeasureSpec.UNSPECIFIED, View.MeasureSpec.UNSPECIFIED); + return rootView.getMeasuredHeight() + } + private fun deactivateSnapOptions() { // TODO(b/346440693): the background/colorStateList set on these buttons is overridden // to a static resource & color on manually tracked hover events, which defeats the @@ -930,7 +1047,7 @@ interface MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu } @@ -943,7 +1060,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController: DisplayController, taskInfo: RunningTaskInfo, decorWindowContext: Context, - menuPosition: PointF, + positionSupplier: (Int, Int) -> PointF, transactionSupplier: Supplier<Transaction> ): MaximizeMenu { return MaximizeMenu( @@ -952,7 +1069,7 @@ object DefaultMaximizeMenuFactory : MaximizeMenuFactory { displayController, taskInfo, decorWindowContext, - menuPosition, + positionSupplier, transactionSupplier ) } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt index 07496eb0e526..bb20292a51d4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositioner.kt @@ -199,7 +199,7 @@ class MultiDisplayVeiledResizeTaskPositioner( // TODO(b/383069173): Render drag indicator(s) t.setPosition( - desktopWindowDecoration.mTaskSurface, + desktopWindowDecoration.leash, repositionTaskBounds.left.toFloat(), repositionTaskBounds.top.toFloat(), ) @@ -237,8 +237,12 @@ class MultiDisplayVeiledResizeTaskPositioner( val startDisplayLayout = displayController.getDisplayLayout(startDisplayId) val currentDisplayLayout = displayController.getDisplayLayout(displayId) - if (startDisplayLayout == null || currentDisplayLayout == null) { - // Fall back to single-display drag behavior if any display layout is unavailable. + if (startDisplayId == displayId + || startDisplayLayout == null || currentDisplayLayout == null) { + // Fall back to single-display drag behavior if: + // 1. The drag destination display is the same as the start display. This prevents + // unnecessary animations caused by minor width/height changes due to DPI scaling. + // 2. Either the starting or current display layout is unavailable. DragPositioningCallbackUtility.updateTaskBounds( repositionTaskBounds, taskBoundsAtDragStart, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS index 3f828f547920..992402528f4f 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/OWNERS @@ -1,3 +1,2 @@ -jorgegil@google.com mattsziklay@google.com mdehaini@google.com diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java index bc85d2b40748..45ba4413814c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/TaskOperations.java @@ -86,14 +86,18 @@ class TaskOperations { return null; } - IBinder minimizeTask(WindowContainerToken taskToken) { - return minimizeTask(taskToken, new WindowContainerTransaction()); + IBinder minimizeTask(WindowContainerToken taskToken, int taskId, boolean isLastTask) { + return minimizeTask(taskToken, taskId, isLastTask, new WindowContainerTransaction()); } - IBinder minimizeTask(WindowContainerToken taskToken, WindowContainerTransaction wct) { + IBinder minimizeTask( + WindowContainerToken taskToken, + int taskId, + boolean isLastTask, + WindowContainerTransaction wct) { wct.reorder(taskToken, false); if (Transitions.ENABLE_SHELL_TRANSITIONS) { - return mTransitionStarter.startMinimizedModeTransition(wct); + return mTransitionStarter.startMinimizedModeTransition(wct, taskId, isLastTask); } else { mSyncQueue.queue(wct); return null; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java index 3fcb09349033..4002dc572897 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java @@ -302,7 +302,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Trace.endSection(); Trace.beginSection("WindowDecoration#relayout-updateViewHost"); - outResult.mRootView.setPadding(0, params.mCaptionTopPadding, 0, 0); + outResult.mRootView.setPadding( + outResult.mRootView.getPaddingLeft(), + params.mCaptionTopPadding, + outResult.mRootView.getPaddingRight(), + outResult.mRootView.getPaddingBottom()); final Rect localCaptionBounds = new Rect( outResult.mCaptionX, outResult.mCaptionY, @@ -357,6 +361,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } outResult.mRootView = rootView; + final boolean fontScaleChanged = mWindowDecorConfig != null + && mWindowDecorConfig.fontScale != mTaskInfo.configuration.fontScale; final int oldDensityDpi = mWindowDecorConfig != null ? mWindowDecorConfig.densityDpi : DENSITY_DPI_UNDEFINED; final int oldNightMode = mWindowDecorConfig != null @@ -371,7 +377,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> || mDisplay.getDisplayId() != mTaskInfo.displayId || oldLayoutResId != mLayoutResId || oldNightMode != newNightMode - || mDecorWindowContext == null) { + || mDecorWindowContext == null + || fontScaleChanged) { releaseViews(wct); if (!obtainDisplayOrRegisterListener()) { @@ -470,8 +477,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } final WindowDecorationInsets newInsets = new WindowDecorationInsets( - mTaskInfo.token, mOwner, captionInsetsRect, boundingRects, - params.mInsetSourceFlags, params.mIsInsetSource); + mTaskInfo.token, mOwner, captionInsetsRect, taskBounds, boundingRects, + params.mInsetSourceFlags, params.mIsInsetSource, params.mShouldSetAppBounds); if (!newInsets.equals(mWindowDecorationInsets)) { // Add or update this caption as an insets source. mWindowDecorationInsets = newInsets; @@ -793,8 +800,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> final int captionHeight = loadDimensionPixelSize(mContext.getResources(), captionHeightId); final Rect captionInsets = new Rect(0, 0, 0, captionHeight); final WindowDecorationInsets newInsets = new WindowDecorationInsets(mTaskInfo.token, - mOwner, captionInsets, null /* boundingRets */, 0 /* flags */, - true /* shouldAddCaptionInset */); + mOwner, captionInsets, null /* taskFrame */, null /* boundingRects */, + 0 /* flags */, true /* shouldAddCaptionInset */, false /* excludedFromAppBounds */); if (!newInsets.equals(mWindowDecorationInsets)) { mWindowDecorationInsets = newInsets; mWindowDecorationInsets.update(wct); @@ -825,6 +832,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> boolean mApplyStartTransactionOnDraw; boolean mSetTaskVisibilityPositionAndCrop; boolean mHasGlobalFocus; + boolean mShouldSetAppBounds; void reset() { mLayoutResId = Resources.ID_NULL; @@ -848,6 +856,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mWindowDecorConfig = null; mAsyncViewHost = false; mHasGlobalFocus = false; + mShouldSetAppBounds = false; } boolean hasInputFeatureSpy() { @@ -905,7 +914,6 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> } } - @VisibleForTesting public interface SurfaceControlViewHostFactory { default SurfaceControlViewHost create(Context c, Display d, WindowlessWindowManager wmm) { return new SurfaceControlViewHost(c, d, wmm, "WindowDecoration"); @@ -921,19 +929,23 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private final WindowContainerToken mToken; private final Binder mOwner; private final Rect mFrame; + private final Rect mTaskFrame; private final Rect[] mBoundingRects; private final @InsetsSource.Flags int mFlags; private final boolean mShouldAddCaptionInset; + private final boolean mExcludedFromAppBounds; private WindowDecorationInsets(WindowContainerToken token, Binder owner, Rect frame, - Rect[] boundingRects, @InsetsSource.Flags int flags, - boolean shouldAddCaptionInset) { + Rect taskFrame, Rect[] boundingRects, @InsetsSource.Flags int flags, + boolean shouldAddCaptionInset, boolean excludedFromAppBounds) { mToken = token; mOwner = owner; mFrame = frame; + mTaskFrame = taskFrame; mBoundingRects = boundingRects; mFlags = flags; mShouldAddCaptionInset = shouldAddCaptionInset; + mExcludedFromAppBounds = excludedFromAppBounds; } void update(WindowContainerTransaction wct) { @@ -942,12 +954,20 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mFlags); wct.addInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures(), mFrame, mBoundingRects, 0 /* flags */); + if (mExcludedFromAppBounds) { + final Rect appBounds = new Rect(mTaskFrame); + appBounds.top += mFrame.height(); + wct.setAppBounds(mToken, appBounds); + } } } void remove(WindowContainerTransaction wct) { wct.removeInsetsSource(mToken, mOwner, INDEX, captionBar()); wct.removeInsetsSource(mToken, mOwner, INDEX, mandatorySystemGestures()); + if (mExcludedFromAppBounds) { + wct.setAppBounds(mToken, new Rect()); + } } @Override @@ -956,9 +976,11 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (!(o instanceof WindowDecoration.WindowDecorationInsets that)) return false; return Objects.equals(mToken, that.mToken) && Objects.equals(mOwner, that.mOwner) && Objects.equals(mFrame, that.mFrame) + && Objects.equals(mTaskFrame, that.mTaskFrame) && Objects.deepEquals(mBoundingRects, that.mBoundingRects) && mFlags == that.mFlags - && mShouldAddCaptionInset == that.mShouldAddCaptionInset; + && mShouldAddCaptionInset == that.mShouldAddCaptionInset + && mExcludedFromAppBounds == that.mExcludedFromAppBounds; } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt index 1bc48f89ea6d..801048adda4d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoader.kt @@ -153,9 +153,7 @@ class WindowDecorTaskResourceLoader( private fun loadAppResources(taskInfo: RunningTaskInfo): AppResources { Trace.beginSection("$TAG#loadAppResources") try { - val pm = checkNotNull(userProfilesContexts[taskInfo.userId]?.packageManager) { - "Could not get context for user ${taskInfo.userId}" - } + val pm = userProfilesContexts.getOrCreate(taskInfo.userId).packageManager val activityInfo = getActivityInfo(taskInfo, pm) val appName = pm.getApplicationLabel(activityInfo.applicationInfo) val appIconDrawable = iconProvider.getIcon(activityInfo) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt index 4a09614029dc..a5592f81a39e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHost.kt @@ -50,9 +50,8 @@ class ReusableWindowDecorViewHost( @VisibleForTesting val viewHostAdapter: SurfaceControlViewHostAdapter = SurfaceControlViewHostAdapter(context, display), + private val rootView: FrameLayout = FrameLayout(context) ) : WindowDecorViewHost, Warmable { - @VisibleForTesting val rootView = FrameLayout(context) - private var currentUpdateJob: Job? = null override val surfaceControl: SurfaceControl @@ -131,8 +130,10 @@ class ReusableWindowDecorViewHost( Trace.beginSection("ReusableWindowDecorViewHost#updateViewHost") viewHostAdapter.prepareViewHost(configuration, touchableRegion) onDrawTransaction?.let { viewHostAdapter.applyTransactionOnDraw(it) } - rootView.removeAllViews() - rootView.addView(view) + if (view.parent != rootView) { + rootView.removeAllViews() + rootView.addView(view) + } viewHostAdapter.updateView(rootView, attrs) Trace.endSection() } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt new file mode 100644 index 000000000000..52e24d6fe0d0 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/SnapEventHandler.kt @@ -0,0 +1,43 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.windowdecor.tiling + +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import com.android.wm.shell.desktopmode.DesktopTasksController.SnapPosition + +/** Interface for handling snap to half screen events. */ +interface SnapEventHandler { + /** Snaps an app to half the screen for tiling. */ + fun snapToHalfScreen( + taskInfo: RunningTaskInfo, + currentDragBounds: Rect, + position: SnapPosition, + ): Boolean + + /** Removes a task from tiling if it's tiled, for example on task exiting. */ + fun removeTaskIfTiled(displayId: Int, taskId: Int) + + /** Notifies the tiling handler of user switch. */ + fun onUserChange() + + /** Notifies the tiling handler of overview animation state change. */ + fun onOverviewAnimationStateChange(running: Boolean) + + /** If a task is tiled, delegate moving to front to tiling infrastructure. */ + fun moveTaskToFrontIfTiled(taskInfo: RunningTaskInfo): Boolean +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt index 1264c013faf5..2948fdaf16af 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHandleViewHolder.kt @@ -41,6 +41,7 @@ import com.android.internal.policy.SystemBarUtils import com.android.window.flags.Flags import com.android.wm.shell.R import com.android.wm.shell.shared.animation.Interpolators +import com.android.wm.shell.shared.bubbles.BubbleAnythingFlagHelper import com.android.wm.shell.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer @@ -146,6 +147,7 @@ internal class AppHandleViewHolder( taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, ignoreCutouts = Flags.showAppHandleLargeScreens() + || BubbleAnythingFlagHelper.enableBubbleToFullscreen() ) val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View") val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " + diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt index 9f8ca7740182..90c865e502fc 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/viewholder/AppHeaderViewHolder.kt @@ -27,10 +27,13 @@ import android.graphics.drawable.LayerDrawable import android.graphics.drawable.RippleDrawable import android.graphics.drawable.ShapeDrawable import android.graphics.drawable.shapes.RoundRectShape +import android.os.Bundle import android.view.View import android.view.View.OnLongClickListener import android.view.ViewTreeObserver.OnGlobalLayoutListener import android.view.accessibility.AccessibilityEvent +import android.view.accessibility.AccessibilityNodeInfo +import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import android.widget.ImageView import android.widget.TextView @@ -46,9 +49,10 @@ import com.android.internal.R.color.materialColorSecondaryContainer import com.android.internal.R.color.materialColorSurfaceContainerHigh import com.android.internal.R.color.materialColorSurfaceContainerLow import com.android.internal.R.color.materialColorSurfaceDim -import com.android.window.flags.Flags import com.android.wm.shell.R import android.window.DesktopModeFlags +import androidx.core.view.ViewCompat +import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat import com.android.wm.shell.windowdecor.MaximizeButtonView import com.android.wm.shell.windowdecor.common.DecorThemeUtil import com.android.wm.shell.windowdecor.common.OPACITY_100 @@ -71,7 +75,10 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, private val onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, - onMaximizeHoverAnimationFinishedListener: () -> Unit + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, + onMaximizeHoverAnimationFinishedListener: () -> Unit, ) : WindowDecorationViewHolder<AppHeaderViewHolder.HeaderData>(rootView) { data class HeaderData( @@ -153,6 +160,97 @@ class AppHeaderViewHolder( minimizeWindowButton.setOnTouchListener(onCaptionTouchListener) maximizeButtonView.onHoverAnimationFinishedListener = onMaximizeHoverAnimationFinishedListener + + val a11yActionSnapLeft = AccessibilityAction( + R.id.action_snap_left, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_left) + ) + val a11yActionSnapRight = AccessibilityAction( + R.id.action_snap_right, + context.resources.getString(R.string.desktop_mode_a11y_action_snap_right) + ) + val a11yActionMaximizeRestore = AccessibilityAction( + R.id.action_maximize_restore, + context.resources.getString(R.string.desktop_mode_a11y_action_maximize_restore) + ) + + captionHandle.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + maximizeWindowButton.accessibilityDelegate = object : View.AccessibilityDelegate() { + override fun onInitializeAccessibilityNodeInfo( + host: View, + info: AccessibilityNodeInfo + ) { + super.onInitializeAccessibilityNodeInfo(host, info) + info.addAction(AccessibilityAction.ACTION_CLICK) + info.addAction(a11yActionSnapLeft) + info.addAction(a11yActionSnapRight) + info.addAction(a11yActionMaximizeRestore) + host.isClickable = true + } + + override fun performAccessibilityAction( + host: View, + action: Int, + args: Bundle? + ): Boolean { + when (action) { + AccessibilityAction.ACTION_CLICK.id -> host.performClick() + R.id.action_snap_left -> mOnLeftSnapClickListener.invoke() + R.id.action_snap_right -> mOnRightSnapClickListener.invoke() + R.id.action_maximize_restore -> mOnMaximizeOrRestoreClickListener.invoke() + } + + return super.performAccessibilityAction(host, action, args) + } + } + + // Update a11y announcement to say "double tap to open menu" + ViewCompat.replaceAccessibilityAction( + openMenuButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.app_handle_chip_accessibility_announce), + null + ) + + // Update a11y announcement to say "double tap to maximize or restore window size" + ViewCompat.replaceAccessibilityAction( + maximizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.maximize_button_talkback_action_maximize_restore_text), + null + ) + + // Update a11y announcement out to say "double tap to minimize app window" + ViewCompat.replaceAccessibilityAction( + minimizeWindowButton, + AccessibilityActionCompat.ACTION_CLICK, + context.getString(R.string.minimize_button_talkback_action_maximize_restore_text), + null + ) } override fun bindData(data: HeaderData) { @@ -168,6 +266,8 @@ class AppHeaderViewHolder( /** Sets the app's name in the header. */ fun setAppName(name: CharSequence) { appNameTextView.text = name + openMenuButton.contentDescription = + context.getString(R.string.desktop_mode_app_header_chip_text, name) } /** Sets the app's icon in the header. */ @@ -370,7 +470,8 @@ class AppHeaderViewHolder( private fun shouldShowExitFullImmersiveOrMaximizeIcon( isTaskMaximized: Boolean, inFullImmersiveState: Boolean - ): Boolean = (Flags.enableFullyImmersiveInDesktop() && inFullImmersiveState) || isTaskMaximized + ): Boolean = (DesktopModeFlags.ENABLE_FULLY_IMMERSIVE_IN_DESKTOP.isTrue && inFullImmersiveState) + || isTaskMaximized private fun getHeaderStyle(header: Header): HeaderStyle { return HeaderStyle( @@ -628,6 +729,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener: View.OnClickListener, onLongClickListener: OnLongClickListener, onCaptionGenericMotionListener: View.OnGenericMotionListener, + mOnLeftSnapClickListener: () -> Unit, + mOnRightSnapClickListener: () -> Unit, + mOnMaximizeOrRestoreClickListener: () -> Unit, onMaximizeHoverAnimationFinishedListener: () -> Unit, ): AppHeaderViewHolder = AppHeaderViewHolder( rootView, @@ -635,6 +739,9 @@ class AppHeaderViewHolder( onCaptionButtonClickListener, onLongClickListener, onCaptionGenericMotionListener, + mOnLeftSnapClickListener, + mOnRightSnapClickListener, + mOnMaximizeOrRestoreClickListener, onMaximizeHoverAnimationFinishedListener, ) } diff --git a/libs/WindowManager/Shell/tests/OWNERS b/libs/WindowManager/Shell/tests/OWNERS index 19829e7e5677..bac8e5062128 100644 --- a/libs/WindowManager/Shell/tests/OWNERS +++ b/libs/WindowManager/Shell/tests/OWNERS @@ -12,7 +12,6 @@ atsjenk@google.com jorgegil@google.com vaniadesmonda@google.com pbdr@google.com -tkachenkoi@google.com mpodolian@google.com jeremysim@google.com peanutbutter@google.com diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp index 50581f7e01f3..7585c977809e 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/Android.bp @@ -33,6 +33,7 @@ android_test { "WMShellFlickerTestsBase", "WMShellScenariosDesktopMode", "WMShellTestUtils", + "ui-trace-collector", ], data: ["trace_config/*"], } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml index 1bbbefadaa03..8fc974d4381e 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/AndroidManifest.xml @@ -47,6 +47,8 @@ <uses-permission android:name="android.permission.STATUS_BAR_SERVICE" /> <!-- Allow the test to connect to perfetto trace processor --> <uses-permission android:name="android.permission.INTERNET"/> + <!-- Use trusted virtual displays to emulate an external display --> + <uses-permission android:name="android.permission.ADD_TRUSTED_DISPLAY"/> <!-- Allow the test to write directly to /sdcard/ and connect to trace processor --> <application android:requestLegacyExternalStorage="true" diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt index ab1ac1a0efa3..4c443d7501f7 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/DesktopModeFlickerScenarios.kt @@ -19,7 +19,7 @@ package com.android.wm.shell.flicker import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_HEIGHT import android.tools.PlatformConsts.DESKTOP_MODE_MINIMUM_WINDOW_WIDTH import android.tools.flicker.AssertionInvocationGroup -import android.tools.flicker.assertors.assertions.AppLayerIncreasesInSize +import android.tools.flicker.assertors.assertions.ResizeVeilKeepsIncreasingInSize import android.tools.flicker.assertors.assertions.AppLayerIsInvisibleAtEnd import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAlways import android.tools.flicker.assertors.assertions.AppLayerIsVisibleAtStart @@ -168,9 +168,11 @@ class DesktopModeFlickerScenarios { TaggedCujTransitionMatcher(associatedTransitionRequired = false) ) .build(), - // TODO(373638597) Add AppLayerIncreasesInSize assertion - assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS - ) + assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + + listOf( + ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) + ) val EDGE_RESIZE = FlickerConfigEntry( @@ -184,7 +186,7 @@ class DesktopModeFlickerScenarios { .build(), assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( - AppLayerIncreasesInSize(DESKTOP_MODE_APP), + ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP), ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) @@ -223,9 +225,9 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( - // TODO(373638597) Add AppLayerIncreasesInSize assertion AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), - AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) + AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP), + ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP), ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), ) @@ -368,7 +370,7 @@ class DesktopModeFlickerScenarios { ), assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( - AppLayerIncreasesInSize(DESKTOP_MODE_APP), + ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP), AppWindowHasMaxDisplayHeight(DESKTOP_MODE_APP), AppWindowHasMaxDisplayWidth(DESKTOP_MODE_APP) ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), @@ -393,7 +395,7 @@ class DesktopModeFlickerScenarios { assertions = AssertionTemplates.DESKTOP_MODE_APP_VISIBILITY_ASSERTIONS + listOf( - AppLayerIncreasesInSize(DESKTOP_MODE_APP), + ResizeVeilKeepsIncreasingInSize(DESKTOP_MODE_APP), AppWindowMaintainsAspectRatioAlways(DESKTOP_MODE_APP), AppWindowHasMaxBoundsInOnlyOneDimension(DESKTOP_MODE_APP) ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), @@ -541,5 +543,29 @@ class DesktopModeFlickerScenarios { AppWindowBecomesPinned(DESKTOP_MODE_APP), ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }) ) + + val OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED = + FlickerConfigEntry( + scenarioId = ScenarioId("OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"), + extractor = + ShellTransitionScenarioExtractor( + transitionMatcher = + object : ITransitionMatcher { + override fun findAll( + transitions: Collection<Transition> + ): Collection<Transition> { + return listOf(transitions + .filter { it.type == TransitionType.OPEN } + .maxByOrNull { it.id }!!) + } + } + ), + assertions = + listOf( + AppWindowBecomesVisible(DESKTOP_MODE_APP), + AppWindowOnTopAtEnd(DESKTOP_MODE_APP), + AppWindowBecomesVisible(DESKTOP_WALLPAPER), + ).associateBy({ it }, { AssertionInvocationGroup.BLOCKING }), + ) } } diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt new file mode 100644 index 000000000000..66d2ea95c67f --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/flicker-service/src/com/android/wm/shell/flicker/OpenAppWithExternalDisplayConnected.kt @@ -0,0 +1,49 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.flicker + +import android.tools.flicker.FlickerConfig +import android.tools.flicker.annotation.ExpectedScenarios +import android.tools.flicker.annotation.FlickerConfigProvider +import android.tools.flicker.config.FlickerConfig +import android.tools.flicker.config.FlickerServiceConfig +import android.tools.flicker.junit.FlickerServiceJUnit4ClassRunner +import com.android.wm.shell.flicker.DesktopModeFlickerScenarios.Companion.OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED +import com.android.wm.shell.scenarios.OpenAppWithExternalDisplayConnected +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Open an app on the default display when an external display is connected. + * + * Assert that the app launches in desktop mode. + */ +@RunWith(FlickerServiceJUnit4ClassRunner::class) +class OpenAppWithExternalDisplayConnected : OpenAppWithExternalDisplayConnected() { + @ExpectedScenarios(["OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED"]) + @Test + override fun openAppWithExternalDisplayConnected() = super.openAppWithExternalDisplayConnected() + + companion object { + @JvmStatic + @FlickerConfigProvider + fun flickerConfigProvider(): FlickerConfig = + FlickerConfig() + .use(FlickerServiceConfig.DEFAULT) + .use(OPEN_APP_WHEN_EXTERNAL_DISPLAY_CONNECTED) + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt new file mode 100644 index 000000000000..cc9a799fb50c --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/OpenAppWithExternalDisplayConnectedTest.kt @@ -0,0 +1,27 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.functional + +import android.platform.test.annotations.Postsubmit +import com.android.wm.shell.scenarios.OpenAppWithExternalDisplayConnected +import org.junit.runner.RunWith +import org.junit.runners.BlockJUnit4ClassRunner + +/* Functional test for [OpenAppWithExternalDisplayConnected]. */ +@RunWith(BlockJUnit4ClassRunner::class) +@Postsubmit +class OpenAppWithExternalDisplayConnectedTest : OpenAppWithExternalDisplayConnected() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt index 8d04749d76a5..2115f70faad0 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDrag.kt @@ -50,7 +50,7 @@ constructor( @Test open fun enterDesktopWithDrag() { // By default this method uses drag to desktop - testApp.enterDesktopMode(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt index 814478af67c1..9a1919304675 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopWithDragExistingWindows.kt @@ -62,7 +62,7 @@ constructor( @Test open fun reenterDesktopWithDrag() { // By default this method uses drag to desktop - testApp.enterDesktopMode(wmHelper, device) + testApp.enterDesktopMode(wmHelper, device, shouldUseDragToDesktop = true) } @After diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt index d6c3266e915c..b5d4dbaa6f37 100644 --- a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/MinimizeAutoPipAppWindow.kt @@ -54,6 +54,7 @@ abstract class MinimizeAutoPipAppWindow { fun setup() { Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) Assume.assumeTrue(Flags.enableMinimizeButton()) + Assume.assumeTrue(com.android.wm.shell.Flags.enablePip2()) testApp.enterDesktopMode(wmHelper, device) pipApp.launchViaIntent(wmHelper) pipApp.enableAutoEnterForPipActivity() diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt new file mode 100644 index 000000000000..81c46f13b384 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/OpenAppWithExternalDisplayConnected.kt @@ -0,0 +1,103 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.scenarios + +import android.app.Instrumentation +import android.content.Context +import android.hardware.display.DisplayManager +import android.hardware.display.VirtualDisplay +import android.tools.NavBar +import android.tools.Rotation +import android.tools.flicker.rules.ChangeDisplayOrientationRule +import android.tools.traces.parsers.WindowManagerStateHelper +import android.util.DisplayMetrics +import androidx.test.platform.app.InstrumentationRegistry +import androidx.test.uiautomator.UiDevice +import com.android.launcher3.tapl.LauncherInstrumentation +import com.android.server.wm.flicker.helpers.DesktopModeAppHelper +import com.android.server.wm.flicker.helpers.SimpleAppHelper +import com.android.window.flags.Flags +import com.android.wm.shell.ExtendedDisplaySettingsSession +import com.android.wm.shell.Utils +import org.junit.After +import org.junit.Assume +import org.junit.Before +import org.junit.Ignore +import org.junit.Rule +import org.junit.Test + +/** + * Base scenario test for launching an app in desktop mode by default when an external display is + * connected. + */ +@Ignore("Test Base Class") +abstract class OpenAppWithExternalDisplayConnected +constructor(private val rotation: Rotation = Rotation.ROTATION_0) { + private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation() + private val tapl = LauncherInstrumentation() + private val wmHelper = WindowManagerStateHelper(instrumentation) + private val device = UiDevice.getInstance(instrumentation) + private val testApp = DesktopModeAppHelper(SimpleAppHelper(instrumentation)) + private val displayManager = + instrumentation.getContext().getSystemService(Context.DISPLAY_SERVICE) as DisplayManager + private var virtualDisplay: VirtualDisplay? = null + + private val extendedDisplaySettingsSession = + ExtendedDisplaySettingsSession(instrumentation.context.contentResolver) + + @Rule @JvmField val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, rotation) + + @Before + fun setup() { + Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet) + Assume.assumeTrue(Flags.enableDisplayWindowingModeSwitching()) + tapl.setEnableRotation(true) + tapl.setExpectedRotation(rotation.value) + ChangeDisplayOrientationRule.setRotation(rotation) + extendedDisplaySettingsSession.open() + virtualDisplay = displayManager.createVirtualDisplay( + /* displayName= */ DISPLAY_NAME, + /* width= */ DISPLAY_WIDTH, + /* height= */ DISPLAY_HEIGHT, + /* densityDpi= */ DisplayMetrics.DENSITY_DEFAULT, + /* surface= */ null, + /* flags= */ DisplayManager.VIRTUAL_DISPLAY_FLAG_PUBLIC or + DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY or + DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED + ) + } + + @Test + open fun openAppWithExternalDisplayConnected() { + testApp.open() + } + + @After + fun teardown() { + testApp.exit(wmHelper) + virtualDisplay?.let { + it.release() + } + extendedDisplaySettingsSession.close() + } + + companion object { + const val DISPLAY_NAME = "testVirtualDisplay" + const val DISPLAY_HEIGHT = 600 + const val DISPLAY_WIDTH = 800 + } +} diff --git a/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt new file mode 100644 index 000000000000..0b2aacd00aa6 --- /dev/null +++ b/libs/WindowManager/Shell/tests/e2e/utils/src/com/android/wm/shell/ExtendedDisplaySettingsSession.kt @@ -0,0 +1,34 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell + +import android.content.ContentResolver +import android.provider.Settings +import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS + +class ExtendedDisplaySettingsSession(private val contentResolver: ContentResolver) { + private val settingName = DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS + private val initialValue = Settings.Global.getInt(contentResolver, settingName, 0) + + fun open() { + Settings.Global.putInt(contentResolver, settingName, 1) + } + + fun close() { + Settings.Global.putInt(contentResolver, settingName, initialValue) + } +} diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml index 02b2cec8dbdb..ae73dae99d6f 100644 --- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml +++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml @@ -53,10 +53,12 @@ <option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/> <option name="run-command" value="settings put system show_touches 1"/> <option name="run-command" value="settings put system pointer_location 1"/> + <option name="run-command" value="settings put secure glanceable_hub_enabled 0"/> <option name="teardown-command" value="settings delete secure show_ime_with_hard_keyboard"/> <option name="teardown-command" value="settings delete system show_touches"/> <option name="teardown-command" value="settings delete system pointer_location"/> + <option name="teardown-command" value="settings delete secure glanceable_hub_enabled"/> <option name="teardown-command" value="cmd overlay enable com.android.internal.systemui.navbar.gestural"/> </target_preparer> diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt index b5b7847e205d..80e4c47a5f68 100644 --- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt +++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/ExitPipToAppViaExpandButtonTest.kt @@ -16,6 +16,7 @@ package com.android.wm.shell.flicker.pip +import android.platform.test.annotations.FlakyTest import android.platform.test.annotations.RequiresFlagsDisabled import android.tools.flicker.junit.FlickerParametersRunnerFactory import android.tools.flicker.legacy.FlickerBuilder @@ -51,6 +52,7 @@ import org.junit.runners.Parameterized * apps are running before setup * ``` */ +@FlakyTest(bugId = 391734110) @RunWith(Parameterized::class) @Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class) @FixMethodOrder(MethodSorters.NAME_ASCENDING) diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp index 91be5f58b1f7..bff12d026b93 100644 --- a/libs/WindowManager/Shell/tests/unittest/Android.bp +++ b/libs/WindowManager/Shell/tests/unittest/Android.bp @@ -45,6 +45,7 @@ android_test { "androidx.test.rules", "androidx.test.ext.junit", "androidx.datastore_datastore", + "androidx.core_core-animation-testing", "kotlinx_coroutines_test", "androidx.dynamicanimation_dynamicanimation", "dagger2", @@ -64,6 +65,7 @@ android_test { "platform-test-annotations", "flag-junit", "platform-parametric-runner-lib", + "platform-compat-test-rules", ], libs: [ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java index 40b685c243b4..4972fa907ce7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/ShellTestCase.java @@ -23,6 +23,9 @@ import static org.junit.Assume.assumeTrue; import android.content.Context; import android.content.pm.PackageManager; import android.hardware.display.DisplayManager; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableContext; import androidx.test.platform.app.InstrumentationRegistry; @@ -31,6 +34,8 @@ import com.android.internal.protolog.ProtoLog; import org.junit.After; import org.junit.Before; +import org.junit.ClassRule; +import org.junit.Rule; import org.mockito.MockitoAnnotations; /** @@ -38,6 +43,16 @@ import org.mockito.MockitoAnnotations; */ public abstract class ShellTestCase { + @ClassRule + public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); + + @Rule + public final CheckFlagsRule mCheckFlagsRule = + DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Rule + public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); + protected TestableContext mContext; private PackageManager mPm; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java index bba9418db66a..94dc774a6737 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingAnimationRunnerTests.java @@ -41,7 +41,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.animation.AlphaAnimation; import android.view.animation.Animation; import android.window.TransitionInfo; @@ -55,7 +54,6 @@ import com.google.testing.junit.testparameterinjector.TestParameter; import com.google.testing.junit.testparameterinjector.TestParameterInjector; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -73,9 +71,6 @@ import java.util.Arrays; @RunWith(TestParameterInjector.class) public class ActivityEmbeddingAnimationRunnerTests extends ActivityEmbeddingAnimationTestBase { - @Rule - public SetFlagsRule mRule = new SetFlagsRule(); - @Before public void setup() { super.setUp(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java index 39d55079ca3a..9f29ef71930a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/activityembedding/ActivityEmbeddingControllerTests.java @@ -34,7 +34,6 @@ import android.animation.ValueAnimator; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -46,7 +45,6 @@ import com.android.window.flags.Flags; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -64,9 +62,6 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation private static final Rect EMBEDDED_LEFT_BOUNDS = new Rect(0, 0, 500, 500); private static final Rect EMBEDDED_RIGHT_BOUNDS = new Rect(500, 0, 1000, 500); - @Rule - public SetFlagsRule mRule = new SetFlagsRule(); - @Before public void setup() { super.setUp(); @@ -276,7 +271,9 @@ public class ActivityEmbeddingControllerTests extends ActivityEmbeddingAnimation mController.startAnimation(mTransition, info, mStartTransaction, mFinishTransaction, mFinishCallback); verify(mFinishCallback, never()).onTransitionFinished(any()); - mController.mergeAnimation(mTransition, info, new SurfaceControl.Transaction(), + mController.mergeAnimation(mTransition, info, + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), mTransition, (wct) -> {}); verify(mFinishCallback).onTransitionFinished(any()); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java index bbdb90f0a37c..05750a54f566 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java @@ -60,7 +60,6 @@ import android.os.IBinder; import android.os.RemoteCallback; import android.os.RemoteException; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.view.IRemoteAnimationRunner; @@ -91,7 +90,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.Transitions; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -152,9 +150,6 @@ public class BackAnimationControllerTest extends ShellTestCase { private BackAnimationController.BackTransitionHandler mBackTransitionHandler; - @Rule - public SetFlagsRule mSetflagsRule = new SetFlagsRule(); - @Before public void setUp() throws Exception { MockitoAnnotations.initMocks(this); @@ -671,7 +666,7 @@ public class BackAnimationControllerTest extends ShellTestCase { Transitions.TransitionFinishCallback mergeCallback = mock(Transitions.TransitionFinishCallback.class); mBackTransitionHandler.mergeAnimation( - mock(IBinder.class), tInfo2, st, mock(IBinder.class), mergeCallback); + mock(IBinder.class), tInfo2, st, ft, mock(IBinder.class), mergeCallback); mBackTransitionHandler.onAnimationFinished(); verify(callback).onTransitionFinished(any()); verify(mergeCallback).onTransitionFinished(any()); @@ -706,7 +701,7 @@ public class BackAnimationControllerTest extends ShellTestCase { mBackTransitionHandler.mClosePrepareTransition = mock(IBinder.class); mergeCallback = mock(Transitions.TransitionFinishCallback.class); mBackTransitionHandler.mergeAnimation(mBackTransitionHandler.mClosePrepareTransition, - tInfo2, st, mock(IBinder.class), mergeCallback); + tInfo2, st, ft, mock(IBinder.class), mergeCallback); assertTrue("Change should be consumed", tInfo2.getChanges().isEmpty()); verify(callback).onTransitionFinished(any()); } @@ -752,7 +747,7 @@ public class BackAnimationControllerTest extends ShellTestCase { final TransitionInfo closeInfo = createTransitionInfo(TRANSIT_CLOSE, close); Transitions.TransitionFinishCallback mergeCallback = mock(Transitions.TransitionFinishCallback.class); - mBackTransitionHandler.mergeAnimation(mock(IBinder.class), closeInfo, ft, + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), closeInfo, st, ft, mock(IBinder.class), mergeCallback); verify(callback2).onTransitionFinished(any()); verify(mergeCallback, never()).onTransitionFinished(any()); @@ -771,7 +766,7 @@ public class BackAnimationControllerTest extends ShellTestCase { openTaskId2, TRANSIT_OPEN, FLAG_MOVED_TO_TOP); final TransitionInfo openInfo = createTransitionInfo(TRANSIT_OPEN, open2, close); mergeCallback = mock(Transitions.TransitionFinishCallback.class); - mBackTransitionHandler.mergeAnimation(mock(IBinder.class), openInfo, ft, + mBackTransitionHandler.mergeAnimation(mock(IBinder.class), openInfo, st, ft, mock(IBinder.class), mergeCallback); verify(callback3).onTransitionFinished(any()); verify(mergeCallback, never()).onTransitionFinished(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java index 6d7a18d7fca4..2ef6c558b0b5 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackProgressAnimatorTest.java @@ -32,6 +32,8 @@ import android.window.BackProgressAnimator; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.wm.shell.ShellTestCase; + import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; @@ -42,7 +44,7 @@ import java.util.concurrent.TimeUnit; @SmallTest @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner.class) -public class BackProgressAnimatorTest { +public class BackProgressAnimatorTest extends ShellTestCase { private BackProgressAnimator mProgressAnimator; private BackEvent mReceivedBackEvent; private float mTargetProgress = 0.5f; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java index ffcc3446d436..7a7d88b80ce3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleDataTest.java @@ -572,6 +572,22 @@ public class BubbleDataTest extends ShellTestCase { assertThat(update.shouldShowEducation).isTrue(); } + /** Verifies that the update should contain the bubble bar location. */ + @Test + public void test_shouldUpdateBubbleBarLocation() { + // Setup + mBubbleData.setListener(mListener); + + // Test + mBubbleData.notificationEntryUpdated(mBubbleA1, /* suppressFlyout */ true, /* showInShade */ + true, BubbleBarLocation.LEFT); + + // Verify + verifyUpdateReceived(); + BubbleData.Update update = mUpdateCaptor.getValue(); + assertThat(update.mBubbleBarLocation).isEqualTo(BubbleBarLocation.LEFT); + } + /** * Verifies that the update shouldn't show the user education, if the education is required but * the bubble should auto-expand @@ -1367,6 +1383,20 @@ public class BubbleDataTest extends ShellTestCase { } @Test + public void setSelectedBubbleAndExpandStackWithLocation() { + sendUpdatedEntryAtTime(mEntryA1, 1000); + sendUpdatedEntryAtTime(mEntryA2, 2000); + mBubbleData.setListener(mListener); + + mBubbleData.setSelectedBubbleAndExpandStack(mBubbleA1, BubbleBarLocation.LEFT); + + verifyUpdateReceived(); + assertSelectionChangedTo(mBubbleA1); + assertExpandedChangedTo(true); + assertLocationChangedTo(BubbleBarLocation.LEFT); + } + + @Test public void testShowOverflowChanged_hasOverflowBubbles() { assertThat(mBubbleData.getOverflowBubbles()).isEmpty(); sendUpdatedEntryAtTime(mEntryA1, 1000); @@ -1450,6 +1480,12 @@ public class BubbleDataTest extends ShellTestCase { assertWithMessage("selectedBubble").that(update.selectedBubble).isEqualTo(bubble); } + private void assertLocationChangedTo(BubbleBarLocation location) { + BubbleData.Update update = mUpdateCaptor.getValue(); + assertWithMessage("locationChanged").that(update.mBubbleBarLocation) + .isEqualTo(location); + } + private void assertExpandedChangedTo(boolean expected) { BubbleData.Update update = mUpdateCaptor.getValue(); assertWithMessage("expandedChanged").that(update.expandedChanged).isTrue(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java index a1d4a1a301bd..094af9652ea3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleOverflowTest.java @@ -26,7 +26,6 @@ import android.view.WindowManager; import androidx.test.filters.SmallTest; -import com.android.internal.logging.testing.UiEventLoggerFake; import com.android.wm.shell.ShellTestCase; import org.junit.Before; @@ -46,7 +45,6 @@ public class BubbleOverflowTest extends ShellTestCase { private TestableBubblePositioner mPositioner; private BubbleOverflow mOverflow; private BubbleExpandedViewManager mExpandedViewManager; - private BubbleLogger mBubbleLogger; @Mock private BubbleController mBubbleController; @@ -60,7 +58,6 @@ public class BubbleOverflowTest extends ShellTestCase { mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(mBubbleController); mPositioner = new TestableBubblePositioner(mContext, mContext.getSystemService(WindowManager.class)); - mBubbleLogger = new BubbleLogger(new UiEventLoggerFake()); when(mBubbleController.getPositioner()).thenReturn(mPositioner); when(mBubbleController.getStackView()).thenReturn(mBubbleStackView); @@ -80,7 +77,7 @@ public class BubbleOverflowTest extends ShellTestCase { @Test public void test_initialize_forBubbleBar() { - mOverflow.initializeForBubbleBar(mExpandedViewManager, mPositioner, mBubbleLogger); + mOverflow.initializeForBubbleBar(mExpandedViewManager, mPositioner); assertThat(mOverflow.getBubbleBarExpandedView()).isNotNull(); assertThat(mOverflow.getExpandedView()).isNull(); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java index dca5fc4c2fe0..2c0ced4fd8de 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTest.java @@ -22,18 +22,22 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.doReturn; +import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.Notification; import android.app.PendingIntent; +import android.app.TaskInfo; +import android.content.ComponentName; import android.content.Intent; import android.content.pm.ShortcutInfo; import android.content.res.Resources; import android.graphics.drawable.Icon; import android.os.Bundle; import android.os.UserHandle; +import android.service.notification.NotificationListenerService; import android.service.notification.StatusBarNotification; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -60,13 +64,17 @@ public class BubbleTest extends ShellTestCase { @Mock private StatusBarNotification mSbn; @Mock + private NotificationListenerService.Ranking mRanking; + @Mock private ShellExecutor mMainExecutor; @Mock private ShellExecutor mBgExecutor; - private BubbleEntry mBubbleEntry; private Bundle mExtras; - private Bubble mBubble; + + // This entry / bubble are set up with PendingIntent / Icon API for chat + private BubbleEntry mBubbleEntry; + private Bubble mChatBubble; @Mock private Bubbles.BubbleMetadataFlagListener mBubbleMetadataFlagListener; @@ -83,11 +91,16 @@ public class BubbleTest extends ShellTestCase { PendingIntent.getActivity(mContext, 0, target, PendingIntent.FLAG_MUTABLE), Icon.createWithResource(mContext, R.drawable.bubble_ic_create_bubble)) .build(); + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("shortcutId") + .build(); when(mSbn.getNotification()).thenReturn(mNotif); when(mNotif.getBubbleMetadata()).thenReturn(metadata); when(mSbn.getKey()).thenReturn("mock"); - mBubbleEntry = new BubbleEntry(mSbn, null, true, false, false, false); - mBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, + when(mRanking.getConversationShortcutInfo()).thenReturn(shortcutInfo); + + mBubbleEntry = new BubbleEntry(mSbn, mRanking, true, false, false, false); + mChatBubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, mBgExecutor); } @@ -152,42 +165,113 @@ public class BubbleTest extends ShellTestCase { @Test public void testBubbleMetadataFlagListener_change_notified() { - assertThat(mBubble.showInShade()).isTrue(); + assertThat(mChatBubble.showInShade()).isTrue(); - mBubble.setSuppressNotification(true); + mChatBubble.setSuppressNotification(true); - assertThat(mBubble.showInShade()).isFalse(); + assertThat(mChatBubble.showInShade()).isFalse(); - verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mBubble); + verify(mBubbleMetadataFlagListener).onBubbleMetadataFlagChanged(mChatBubble); } @Test public void testBubbleMetadataFlagListener_noChange_doesntNotify() { - assertThat(mBubble.showInShade()).isTrue(); + assertThat(mChatBubble.showInShade()).isTrue(); - mBubble.setSuppressNotification(false); + mChatBubble.setSuppressNotification(false); verify(mBubbleMetadataFlagListener, never()).onBubbleMetadataFlagChanged(any()); } @Test - public void testBubbleIsConversation_hasConversationShortcut() { - Bubble bubble = createBubbleWithShortcut(); - assertThat(bubble.getShortcutInfo()).isNotNull(); - assertThat(bubble.isConversation()).isTrue(); + public void testBubbleType_conversationShortcut() { + Bubble bubble = createChatBubble(true /* useShortcut */); + assertThat(bubble.isChat()).isTrue(); } @Test - public void testBubbleIsConversation_hasNoShortcut() { - Bubble bubble = new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, - mBgExecutor); - assertThat(bubble.getShortcutInfo()).isNull(); - assertThat(bubble.isConversation()).isFalse(); + public void testBubbleType_conversationPendingIntent() { + Bubble bubble = createChatBubble(false /* useShortcut */); + assertThat(bubble.isChat()).isTrue(); + } + + @Test + public void testBubbleType_note() { + Bubble bubble = Bubble.createNotesBubble(createIntent(), UserHandle.of(0), + mock(Icon.class), + mMainExecutor, mBgExecutor); + assertThat(bubble.isNote()).isTrue(); + } + + @Test + public void testBubbleType_shortcut() { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("mockShortcutId") + .build(); + Bubble bubble = Bubble.createShortcutBubble(shortcutInfo, mMainExecutor, mBgExecutor); + assertThat(bubble.isShortcut()).isTrue(); + } + + @Test + public void testBubbleType_intent() { + Bubble bubble = Bubble.createAppBubble(createIntent(), UserHandle.of(0), + mock(Icon.class), + mMainExecutor, mBgExecutor); + assertThat(bubble.isApp()).isTrue(); + } + + @Test + public void testBubbleType_taskId() { + TaskInfo info = mock(TaskInfo.class); + ComponentName componentName = mock(ComponentName.class); + when(componentName.getPackageName()).thenReturn(mContext.getPackageName()); + info.taskId = 1; + info.baseActivity = componentName; + Bubble bubble = Bubble.createTaskBubble(info, UserHandle.of(0), + mock(Icon.class), + mMainExecutor, mBgExecutor); + assertThat(bubble.isApp()).isTrue(); + } + + @Test + public void testShowAppBadge_chat() { + Bubble bubble = createChatBubble(true /* useShortcut */); + assertThat(bubble.isChat()).isTrue(); + assertThat(bubble.showAppBadge()).isTrue(); + } + + @Test + public void testShowAppBadge_note() { + Bubble bubble = Bubble.createNotesBubble(createIntent(), UserHandle.of(0), + mock(Icon.class), + mMainExecutor, mBgExecutor); + assertThat(bubble.isNote()).isTrue(); + assertThat(bubble.showAppBadge()).isTrue(); + } + + @Test + public void testShowAppBadge_app() { + Bubble bubble = Bubble.createAppBubble(createIntent(), UserHandle.of(0), + mock(Icon.class), + mMainExecutor, mBgExecutor); + assertThat(bubble.isApp()).isTrue(); + assertThat(bubble.showAppBadge()).isFalse(); + } + + @Test + public void testShowAppBadge_shortcut() { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("mockShortcutId") + .build(); + Bubble bubble = Bubble.createShortcutBubble(shortcutInfo, + mMainExecutor, mBgExecutor); + assertThat(bubble.isShortcut()).isTrue(); + assertThat(bubble.showAppBadge()).isTrue(); } @Test public void testBubbleAsBubbleBarBubble_withShortcut() { - Bubble bubble = createBubbleWithShortcut(); + Bubble bubble = createChatBubble(true /* useShortcut */); BubbleInfo bubbleInfo = bubble.asBubbleBarBubble(); assertThat(bubble.getShortcutInfo()).isNotNull(); @@ -199,7 +283,7 @@ public class BubbleTest extends ShellTestCase { } @Test - public void testBubbleAsBubbleBarBubble_withoutShortcut() { + public void testBubbleAsBubbleBarBubble_withIntent() { Intent intent = new Intent(mContext, BubblesTestActivity.class); intent.setPackage(mContext.getPackageName()); Bubble bubble = Bubble.createAppBubble(intent, new UserHandle(1 /* userId */), @@ -213,12 +297,23 @@ public class BubbleTest extends ShellTestCase { assertThat(bubbleInfo.getPackageName()).isEqualTo(bubble.getPackageName()); } - private Bubble createBubbleWithShortcut() { - ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) - .setId("mockShortcutId") - .build(); - return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL, - "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, - mMainExecutor, mBgExecutor, mBubbleMetadataFlagListener); + private Intent createIntent() { + Intent intent = new Intent(mContext, BubblesTestActivity.class); + intent.setPackage(mContext.getPackageName()); + return intent; + } + + private Bubble createChatBubble(boolean useShortcut) { + if (useShortcut) { + ShortcutInfo shortcutInfo = new ShortcutInfo.Builder(mContext) + .setId("mockShortcutId") + .build(); + return new Bubble("mockKey", shortcutInfo, 10, Resources.ID_NULL, + "mockTitle", 0 /* taskId */, "mockLocus", true /* isDismissible */, + mMainExecutor, mBgExecutor, mBubbleMetadataFlagListener); + } else { + return new Bubble(mBubbleEntry, mBubbleMetadataFlagListener, null, mMainExecutor, + mBgExecutor); + } } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java index 9d0ddbc6de12..42310caba1c6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleTransitionsTest.java @@ -17,6 +17,10 @@ package com.android.wm.shell.bubbles; import static android.view.WindowManager.TRANSIT_CHANGE; +import static com.android.wm.shell.transition.Transitions.TRANSIT_CONVERT_TO_BUBBLE; + +import static com.google.common.truth.Truth.assertThat; + import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; @@ -31,12 +35,14 @@ import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; import android.app.ActivityManager; +import android.graphics.Rect; import android.os.IBinder; import android.view.SurfaceControl; import android.view.ViewRootImpl; import android.window.IWindowContainerToken; import android.window.TransitionInfo; import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; @@ -65,6 +71,10 @@ import org.mockito.MockitoAnnotations; */ @SmallTest public class BubbleTransitionsTest extends ShellTestCase { + + private static final int FULLSCREEN_TASK_WIDTH = 200; + private static final int FULLSCREEN_TASK_HEIGHT = 100; + @Mock private BubbleData mBubbleData; @Mock @@ -78,8 +88,6 @@ public class BubbleTransitionsTest extends ShellTestCase { @Mock private BubblePositioner mBubblePositioner; @Mock - private BubbleLogger mBubbleLogger; - @Mock private BubbleStackView mStackView; @Mock private BubbleBarLayerView mLayerView; @@ -119,10 +127,7 @@ public class BubbleTransitionsTest extends ShellTestCase { private ActivityManager.RunningTaskInfo setupBubble() { ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); - final IWindowContainerToken itoken = mock(IWindowContainerToken.class); - final IBinder asBinder = mock(IBinder.class); - when(itoken.asBinder()).thenReturn(asBinder); - WindowContainerToken token = new WindowContainerToken(itoken); + WindowContainerToken token = createMockToken(); taskInfo.token = token; final TaskView tv = mock(TaskView.class); final TaskViewTaskController tvtc = mock(TaskViewTaskController.class); @@ -133,13 +138,32 @@ public class BubbleTransitionsTest extends ShellTestCase { return taskInfo; } + private TransitionInfo setupFullscreenTaskTransition(ActivityManager.RunningTaskInfo taskInfo) { + final TransitionInfo info = new TransitionInfo(TRANSIT_CONVERT_TO_BUBBLE, 0); + final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, + mock(SurfaceControl.class)); + chg.setTaskInfo(taskInfo); + chg.setMode(TRANSIT_CHANGE); + chg.setStartAbsBounds(new Rect(0, 0, FULLSCREEN_TASK_WIDTH, FULLSCREEN_TASK_HEIGHT)); + info.addChange(chg); + info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); + return info; + } + + private WindowContainerToken createMockToken() { + final IWindowContainerToken itoken = mock(IWindowContainerToken.class); + final IBinder asBinder = mock(IBinder.class); + when(itoken.asBinder()).thenReturn(asBinder); + return new WindowContainerToken(itoken); + } + @Test public void testConvertToBubble() { // Basic walk-through of convert-to-bubble transition stages ActivityManager.RunningTaskInfo taskInfo = setupBubble(); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, - mBubbleLogger, mStackView, mLayerView, mIconFactory, false); + mStackView, mLayerView, mIconFactory, null, false); final BubbleTransitions.ConvertToBubble ctb = (BubbleTransitions.ConvertToBubble) bt; ctb.onInflated(mBubble); when(mLayerView.canExpandView(any())).thenReturn(true); @@ -148,13 +172,7 @@ public class BubbleTransitionsTest extends ShellTestCase { // Ensure we are communicating with the taskviewtransitions queue assertTrue(mTaskViewTransitions.hasPending()); - final TransitionInfo info = new TransitionInfo(TRANSIT_CHANGE, 0); - final TransitionInfo.Change chg = new TransitionInfo.Change(taskInfo.token, - mock(SurfaceControl.class)); - chg.setTaskInfo(taskInfo); - chg.setMode(TRANSIT_CHANGE); - info.addChange(chg); - info.addRoot(new TransitionInfo.Root(0, mock(SurfaceControl.class), 0, 0)); + final TransitionInfo info = setupFullscreenTaskTransition(taskInfo); SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); final boolean[] finishCalled = new boolean[]{false}; @@ -165,22 +183,69 @@ public class BubbleTransitionsTest extends ShellTestCase { ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb); assertFalse(mTaskViewTransitions.hasPending()); + verify(startT).setPosition(any(), eq(0f), eq(0f)); + verify(mBubbleData).notificationEntryUpdated(eq(mBubble), anyBoolean(), anyBoolean()); - ctb.continueExpand(); clearInvocations(mBubble); verify(mBubble, never()).setPreparingTransition(any()); ctb.surfaceCreated(); - verify(mBubble).setPreparingTransition(isNull()); + // Check that preparing transition is not reset before continueExpand is called + verify(mBubble, never()).setPreparingTransition(any()); ArgumentCaptor<Runnable> animCb = ArgumentCaptor.forClass(Runnable.class); verify(mLayerView).animateConvert(any(), any(), any(), any(), animCb.capture()); + + // continueExpand is now called, check that preparing transition is cleared + ctb.continueExpand(); + verify(mBubble).setPreparingTransition(isNull()); + assertFalse(finishCalled[0]); animCb.getValue().run(); assertTrue(finishCalled[0]); } @Test + public void testConvertToBubble_drag() { + ActivityManager.RunningTaskInfo taskInfo = setupBubble(); + + Rect draggedTaskBounds = new Rect(10, 20, 30, 40); + WindowContainerTransaction pendingWct = new WindowContainerTransaction(); + WindowContainerToken pendingDragOpToken = createMockToken(); + pendingWct.reorder(pendingDragOpToken, /* onTop= */ false); + + BubbleTransitions.DragData dragData = new BubbleTransitions.DragData( + draggedTaskBounds, pendingWct, /* releasedOnLeft= */ false + ); + + final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertToBubble( + mBubble, taskInfo, mExpandedViewManager, mTaskViewFactory, mBubblePositioner, + mStackView, mLayerView, mIconFactory, dragData, false); + final BubbleTransitions.ConvertToBubble ctb = (BubbleTransitions.ConvertToBubble) bt; + + ArgumentCaptor<WindowContainerTransaction> wctCaptor = ArgumentCaptor.forClass( + WindowContainerTransaction.class); + ctb.onInflated(mBubble); + verify(mTransitions).startTransition(anyInt(), wctCaptor.capture(), eq(ctb)); + + // Verify that the WCT has the pending operation from drag data + WindowContainerTransaction transitionWct = wctCaptor.getValue(); + assertThat(transitionWct.getHierarchyOps().stream().anyMatch(op -> op.getType() + == WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER + && op.getContainer() == pendingDragOpToken.asBinder())).isTrue(); + + final TransitionInfo info = setupFullscreenTaskTransition(taskInfo); + SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + Transitions.TransitionFinishCallback finishCb = wct -> {}; + ctb.startAnimation(ctb.mTransition, info, startT, finishT, finishCb); + + // Verify that dragged task bounds are used for the position + verify(startT).setPosition(any(), eq((float) draggedTaskBounds.left), + eq((float) draggedTaskBounds.top)); + } + + @Test public void testConvertFromBubble() { ActivityManager.RunningTaskInfo taskInfo = setupBubble(); final BubbleTransitions.BubbleTransition bt = mBubbleTransitions.startConvertFromBubble( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt index 417b43a9c6c0..22cc65d8ffaf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt @@ -34,7 +34,6 @@ import com.android.wm.shell.ShellTaskOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.bubbles.bar.BubbleBarLayerView -import com.android.wm.shell.bubbles.properties.BubbleProperties import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.DisplayImeController import com.android.wm.shell.common.DisplayInsetsController @@ -143,7 +142,7 @@ class BubbleViewInfoTest : ShellTestCase() { mock<Transitions>(), mock<SyncTransactionQueue>(), mock<IWindowManager>(), - mock<BubbleProperties>() + BubbleResizabilityChecker() ) val bubbleStackViewManager = BubbleStackViewManager.fromBubbleController(bubbleController) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java index f8eb50b978a5..622e4cbf5ece 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubblesTransitionObserverTest.java @@ -38,6 +38,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.transition.TransitionInfoBuilder; import org.junit.Before; @@ -49,7 +50,7 @@ import org.mockito.MockitoAnnotations; * Tests of {@link BubblesTransitionObserver}. */ @SmallTest -public class BubblesTransitionObserverTest { +public class BubblesTransitionObserverTest extends ShellTestCase { @Mock private BubbleController mBubbleController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java index f8ee300e411c..3323740697f3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DevicePostureControllerTest.java @@ -29,6 +29,7 @@ import android.content.Context; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; @@ -41,7 +42,7 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) -public class DevicePostureControllerTest { +public class DevicePostureControllerTest extends ShellTestCase { @Mock private Context mContext; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java index d3de0f7c09b4..3d5e9495e29d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayControllerTests.java @@ -16,26 +16,52 @@ package com.android.wm.shell.common; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; -import android.content.Context; +import android.content.res.Configuration; +import android.graphics.RectF; import android.hardware.display.DisplayManager; +import android.hardware.display.DisplayTopology; +import android.os.RemoteException; +import android.platform.test.annotations.EnableFlags; +import android.testing.TestableContext; +import android.util.SparseArray; +import android.view.Display; +import android.view.DisplayAdjustments; +import android.view.IDisplayWindowListener; import android.view.IWindowManager; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestSyncExecutor; +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.sysui.ShellInit; +import org.junit.After; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.MockitoAnnotations; +import org.mockito.quality.Strictness; + +import java.util.function.Consumer; /** * Tests for the display controller. @@ -46,23 +72,163 @@ import org.mockito.MockitoAnnotations; @SmallTest @RunWith(AndroidJUnit4.class) public class DisplayControllerTests extends ShellTestCase { - - private @Mock Context mContext; - private @Mock IWindowManager mWM; - private @Mock ShellInit mShellInit; - private @Mock ShellExecutor mMainExecutor; - private @Mock DisplayManager mDisplayManager; + @Mock private IWindowManager mWM; + @Mock private ShellInit mShellInit; + @Mock private DisplayManager mDisplayManager; + @Mock private DisplayTopology mMockTopology; + @Mock private DisplayController.OnDisplaysChangedListener mListener; + private StaticMockitoSession mMockitoSession; + private TestSyncExecutor mMainExecutor; + private IDisplayWindowListener mDisplayContainerListener; + private Consumer<DisplayTopology> mCapturedTopologyListener; + private Display mMockDisplay; private DisplayController mController; + private static final int DISPLAY_ID_0 = 0; + private static final int DISPLAY_ID_1 = 1; + private static final RectF DISPLAY_ABS_BOUNDS_0 = new RectF(10, 10, 20, 20); + private static final RectF DISPLAY_ABS_BOUNDS_1 = new RectF(11, 11, 22, 22); @Before - public void setUp() { - MockitoAnnotations.initMocks(this); + public void setUp() throws RemoteException { + mMockitoSession = + ExtendedMockito.mockitoSession() + .initMocks(this) + .mockStatic(DesktopModeStatus.class) + .strictness(Strictness.LENIENT) + .startMocking(); + + mContext = spy(new TestableContext( + androidx.test.platform.app.InstrumentationRegistry.getInstrumentation() + .getContext(), null)); + + mMainExecutor = new TestSyncExecutor(); mController = new DisplayController( mContext, mWM, mShellInit, mMainExecutor, mDisplayManager); + + mMockDisplay = mock(Display.class); + when(mMockDisplay.getDisplayAdjustments()).thenReturn( + new DisplayAdjustments(new Configuration())); + when(mDisplayManager.getDisplay(anyInt())).thenReturn(mMockDisplay); + when(mDisplayManager.getDisplayTopology()).thenReturn(mMockTopology); + doAnswer(invocation -> { + mDisplayContainerListener = invocation.getArgument(0); + return new int[]{DISPLAY_ID_0}; + }).when(mWM).registerDisplayWindowListener(any()); + doAnswer(invocation -> { + mCapturedTopologyListener = invocation.getArgument(1); + return null; + }).when(mDisplayManager).registerTopologyListener(any(), any()); + SparseArray<RectF> absoluteBounds = new SparseArray<>(); + absoluteBounds.put(DISPLAY_ID_0, DISPLAY_ABS_BOUNDS_0); + absoluteBounds.put(DISPLAY_ID_1, DISPLAY_ABS_BOUNDS_1); + when(mMockTopology.getAbsoluteBounds()).thenReturn(absoluteBounds); + } + + @After + public void tearDown() { + if (mMockitoSession != null) { + mMockitoSession.finishMocking(); + } } @Test public void instantiateController_addInitCallback() { verify(mShellInit, times(1)).addInitCallback(any(), eq(mController)); } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canEnterDesktopMode_registerListeners() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager).registerTopologyListener(eq(mMainExecutor), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onInit_canNotEnterDesktopMode_onlyRegisterDisplayWindowListener() + throws RemoteException { + ExtendedMockito.doReturn(false) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + + assertNotNull(mController.getDisplayContext(DISPLAY_ID_0)); + verify(mWM).registerDisplayWindowListener(any()); + verify(mDisplayManager, never()).registerTopologyListener(any(), any()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void addDisplayWindowListener_notifiesExistingDisplaysAndTopology() { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onTopologyChanged(eq(mMockTopology)); + } + + @Test + public void onDisplayAddedAndRemoved_updatesDisplayContexts() throws RemoteException { + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_0)); + verify(mListener).onDisplayAdded(eq(DISPLAY_ID_1)); + assertNotNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mContext).createDisplayContext(eq(mMockDisplay)); + + mDisplayContainerListener.onDisplayRemoved(DISPLAY_ID_1); + + assertNull(mController.getDisplayContext(DISPLAY_ID_1)); + verify(mListener).onDisplayRemoved(eq(DISPLAY_ID_1)); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_updateDisplayLayout() throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + mCapturedTopologyListener.accept(mMockTopology); + + assertEquals(DISPLAY_ABS_BOUNDS_0, mController.getDisplayLayout(DISPLAY_ID_0) + .globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, mController.getDisplayLayout(DISPLAY_ID_1) + .globalBoundsDp()); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_WINDOW_DRAG) + public void onDisplayTopologyChanged_topologyBeforeDisplayAdded_appliesBoundsOnAdd() + throws RemoteException { + ExtendedMockito.doReturn(true) + .when(() -> DesktopModeStatus.canEnterDesktopMode(any())); + mController.onInit(); + mController.addDisplayWindowListener(mListener); + + mCapturedTopologyListener.accept(mMockTopology); + + assertNull(mController.getDisplayLayout(DISPLAY_ID_1)); + + mDisplayContainerListener.onDisplayAdded(DISPLAY_ID_1); + + assertEquals(DISPLAY_ABS_BOUNDS_0, + mController.getDisplayLayout(DISPLAY_ID_0).globalBoundsDp()); + assertEquals(DISPLAY_ABS_BOUNDS_1, + mController.getDisplayLayout(DISPLAY_ID_1).globalBoundsDp()); + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java index 6f3a3ec4fd20..ee9d17706372 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/DisplayImeControllerTest.java @@ -39,8 +39,6 @@ import android.graphics.Point; import android.os.Looper; import android.platform.test.annotations.RequiresFlagsDisabled; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.IWindowManager; import android.view.InsetsSource; import android.view.InsetsSourceControl; @@ -55,7 +53,6 @@ import com.android.wm.shell.shared.TransactionPool; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -70,9 +67,6 @@ import java.util.concurrent.Executor; */ @SmallTest public class DisplayImeControllerTest extends ShellTestCase { - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); - @Mock private SurfaceControl.Transaction mT; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt new file mode 100644 index 000000000000..09c2faaa2670 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/InputChannelSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.view.InputChannel +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [InputChannelSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:InputChannelSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class InputChannelSupplierTest { + + @Test + fun `InputChannelSupplier supplies an InputChannel`() { + val supplier = InputChannelSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is InputChannel + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt new file mode 100644 index 000000000000..8468c636542e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/SuppliersUtilsTest.kt @@ -0,0 +1,34 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import java.util.function.Supplier + +/** + * Utility class we can use to test a []Supplier<T>] of any parameters type [T]. + */ +class SuppliersUtilsTest { + + companion object { + /** + * Allows to check that the object supplied is asserts what in [assertion]. + */ + fun <T> assertSupplierProvidesValue(supplier: Supplier<T>, assertion: (Any?) -> Boolean) { + assert(assertion(supplier.get())) { "Supplier didn't provided what is expected" } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt index ef0b8ab14c81..56d401779654 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/UserProfileContextsTest.kt @@ -69,6 +69,7 @@ class UserProfileContextsTest : ShellTestCase() { } .whenever(baseContext) .createContextAsUser(any<UserHandle>(), anyInt()) + doReturn(DEFAULT_USER).whenever(baseContext).userId // Define users and profiles val currentUser = ActivityManager.getCurrentUser() whenever(userManager.getProfiles(eq(currentUser))) @@ -136,6 +137,25 @@ class UserProfileContextsTest : ShellTestCase() { assertThat(userProfilesContexts[SECOND_PROFILE]?.userId).isEqualTo(SECOND_PROFILE) } + @Test + fun onUserProfilesChanged_keepDefaultUser() { + val userChangeListener = retrieveUserChangeListener() + val newUserContext = createContextForUser(SECOND_USER) + + userChangeListener.onUserChanged(SECOND_USER, newUserContext) + userChangeListener.onUserProfilesChanged(SECOND_PROFILES) + + assertThat(userProfilesContexts[DEFAULT_USER]).isEqualTo(baseContext) + } + + @Test + fun getOrCreate_newUser_shouldCreateTheUser() { + val newContext = userProfilesContexts.getOrCreate(SECOND_USER) + + assertThat(newContext).isNotNull() + assertThat(userProfilesContexts[SECOND_USER]).isEqualTo(newContext) + } + private fun retrieveUserChangeListener(): UserChangeListener { val captor = argumentCaptor<UserChangeListener>() @@ -155,6 +175,7 @@ class UserProfileContextsTest : ShellTestCase() { const val MAIN_PROFILE = 11 const val SECOND_PROFILE = 15 const val SECOND_PROFILE_2 = 17 + const val DEFAULT_USER = 25 val SECOND_PROFILES = listOf( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt new file mode 100644 index 000000000000..c91ef5e6b868 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowContainerTransactionSupplierTest.kt @@ -0,0 +1,42 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowContainerTransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowContainerTransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowContainerTransactionSupplierTest { + + @Test + fun `WindowContainerTransactionSupplier supplies a WindowContainerTransaction`() { + val supplier = WindowContainerTransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is WindowContainerTransaction + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt new file mode 100644 index 000000000000..7b7d96c4294c --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/WindowSessionSupplierTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common + +import android.testing.AndroidTestingRunner +import android.view.IWindowSession +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [WindowSessionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:WindowSessionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class WindowSessionSupplierTest : ShellTestCase() { + + @Test + fun `WindowSessionSupplierTest supplies an IWindowSession`() { + val supplier = WindowSessionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is IWindowSession + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java index e3798e92c092..a6c35f1bd93c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhonePipKeepClearAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhonePipKeepClearAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; @@ -29,9 +29,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhonePipKeepClearAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java index 85f1da5322ea..737735c9efcd 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PhoneSizeSpecSourceTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PhoneSizeSpecSourceTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static org.mockito.Mockito.when; @@ -27,9 +27,6 @@ import android.view.DisplayInfo; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java index 080b0ae006ea..6bda2259b44c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; @@ -32,13 +32,6 @@ import androidx.test.filters.SmallTest; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.common.DisplayLayout; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsAlgorithm; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.PipKeepClearAlgorithmInterface; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java index 304da75f870c..ad664acfdc37 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipBoundsStateTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipBoundsStateTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNotNull; @@ -36,10 +36,6 @@ import androidx.test.filters.SmallTest; import com.android.internal.util.function.TriConsumer; import com.android.wm.shell.R; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PhoneSizeSpecSource; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDisplayLayoutState; -import com.android.wm.shell.common.pip.SizeSpecSource; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java new file mode 100644 index 000000000000..75ad621e1cad --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDesktopStateTest.java @@ -0,0 +1,204 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.pip; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; + +import static com.android.window.flags.Flags.FLAG_ENABLE_CONNECTED_DISPLAYS_PIP; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP; + +import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertTrue; + +import static org.mockito.Mockito.when; + +import android.app.ActivityManager; +import android.platform.test.annotations.EnableFlags; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.window.DisplayAreaInfo; +import android.window.WindowContainerToken; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.RootTaskDisplayAreaOrganizer; +import com.android.wm.shell.desktopmode.DesktopRepository; +import com.android.wm.shell.desktopmode.DesktopUserRepositories; +import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Unit test against {@link PipDesktopState}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) +public class PipDesktopStateTest { + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; + @Mock private Optional<DesktopUserRepositories> mMockDesktopUserRepositoriesOptional; + @Mock private Optional<DesktopWallpaperActivityTokenProvider> + mMockDesktopWallpaperActivityTokenProviderOptional; + @Mock private DesktopUserRepositories mMockDesktopUserRepositories; + @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider; + @Mock private DesktopRepository mMockDesktopRepository; + @Mock private RootTaskDisplayAreaOrganizer mMockRootTaskDisplayAreaOrganizer; + @Mock private ActivityManager.RunningTaskInfo mMockTaskInfo; + + private static final int DISPLAY_ID = 1; + private DisplayAreaInfo mDefaultTda; + private PipDesktopState mPipDesktopState; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockDesktopUserRepositoriesOptional.get()).thenReturn(mMockDesktopUserRepositories); + when(mMockDesktopWallpaperActivityTokenProviderOptional.get()).thenReturn( + mMockDesktopWallpaperActivityTokenProvider); + when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mMockDesktopRepository); + when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(true); + when(mMockDesktopWallpaperActivityTokenProviderOptional.isPresent()).thenReturn(true); + + when(mMockTaskInfo.getDisplayId()).thenReturn(DISPLAY_ID); + when(mMockPipDisplayLayoutState.getDisplayId()).thenReturn(DISPLAY_ID); + + mDefaultTda = new DisplayAreaInfo(Mockito.mock(WindowContainerToken.class), DISPLAY_ID, 0); + when(mMockRootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DISPLAY_ID)).thenReturn( + mDefaultTda); + + mPipDesktopState = new PipDesktopState(mMockPipDisplayLayoutState, + mMockDesktopUserRepositoriesOptional, + mMockDesktopWallpaperActivityTokenProviderOptional, + mMockRootTaskDisplayAreaOrganizer); + } + + @Test + public void isDesktopWindowingPipEnabled_returnsTrue() { + assertTrue(mPipDesktopState.isDesktopWindowingPipEnabled()); + } + + @Test + public void isDesktopWindowingPipEnabled_desktopRepositoryEmpty_returnsFalse() { + when(mMockDesktopUserRepositoriesOptional.isPresent()).thenReturn(false); + + assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled()); + } + + @Test + public void isDesktopWindowingPipEnabled_desktopWallpaperEmpty_returnsFalse() { + when(mMockDesktopWallpaperActivityTokenProviderOptional.isPresent()).thenReturn(false); + + assertFalse(mPipDesktopState.isDesktopWindowingPipEnabled()); + } + + @Test + @EnableFlags(FLAG_ENABLE_CONNECTED_DISPLAYS_PIP) + public void isConnectedDisplaysPipEnabled_returnsTrue() { + assertTrue(mPipDesktopState.isConnectedDisplaysPipEnabled()); + } + + @Test + public void isPipEnteringInDesktopMode_visibleCountZero_minimizedPipPresent_returnsTrue() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); + when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(true); + + assertTrue(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo)); + } + + @Test + public void isPipEnteringInDesktopMode_visibleCountNonzero_minimizedPipAbsent_returnsTrue() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1); + when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(false); + + assertTrue(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo)); + } + + @Test + public void isPipEnteringInDesktopMode_visibleCountZero_minimizedPipAbsent_returnsFalse() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); + when(mMockDesktopRepository.isMinimizedPipPresentInDisplay(DISPLAY_ID)).thenReturn(false); + + assertFalse(mPipDesktopState.isPipEnteringInDesktopMode(mMockTaskInfo)); + } + + @Test + public void shouldExitPipExitDesktopMode_visibleCountZero_wallpaperInvisible_returnsFalse() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); + when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible( + DISPLAY_ID)).thenReturn(false); + + assertFalse(mPipDesktopState.shouldExitPipExitDesktopMode()); + } + + @Test + public void shouldExitPipExitDesktopMode_visibleCountNonzero_wallpaperVisible_returnsFalse() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1); + when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible( + DISPLAY_ID)).thenReturn(true); + + assertFalse(mPipDesktopState.shouldExitPipExitDesktopMode()); + } + + @Test + public void shouldExitPipExitDesktopMode_visibleCountZero_wallpaperVisible_returnsTrue() { + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(0); + when(mMockDesktopWallpaperActivityTokenProvider.isWallpaperActivityVisible( + DISPLAY_ID)).thenReturn(true); + + assertTrue(mPipDesktopState.shouldExitPipExitDesktopMode()); + } + + @Test + public void getOutPipWindowingMode_exitToDesktop_displayFreeform_returnsUndefined() { + // Set visible task count to 1 so isPipExitingToDesktopMode returns true + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1); + setDisplayWindowingMode(WINDOWING_MODE_FREEFORM); + + assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode()); + } + + @Test + public void getOutPipWindowingMode_exitToDesktop_displayFullscreen_returnsFreeform() { + // Set visible task count to 1 so isPipExitingToDesktopMode returns true + when(mMockDesktopRepository.getVisibleTaskCount(DISPLAY_ID)).thenReturn(1); + setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals(WINDOWING_MODE_FREEFORM, mPipDesktopState.getOutPipWindowingMode()); + } + + @Test + public void getOutPipWindowingMode_exitToFullscreen_displayFullscreen_returnsUndefined() { + setDisplayWindowingMode(WINDOWING_MODE_FULLSCREEN); + + assertEquals(WINDOWING_MODE_UNDEFINED, mPipDesktopState.getOutPipWindowingMode()); + } + + private void setDisplayWindowingMode(int windowingMode) { + mDefaultTda.configuration.windowConfiguration.setWindowingMode(windowingMode); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java index b583acda1c9a..1756aad8fc9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipDoubleTapHelperTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipDoubleTapHelperTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2022 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip.phone; +package com.android.wm.shell.common.pip; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_CUSTOM; import static com.android.wm.shell.common.pip.PipDoubleTapHelper.SIZE_SPEC_DEFAULT; @@ -29,8 +29,6 @@ import android.graphics.Rect; import android.testing.AndroidTestingRunner; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipDoubleTapHelper; import org.junit.Assert; import org.junit.Before; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java index ac13d7ffcd61..3e71ab3e1ad4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipSnapAlgorithmTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/pip/PipSnapAlgorithmTest.java @@ -1,5 +1,5 @@ /* - * Copyright (C) 2020 The Android Open Source Project + * Copyright (C) 2025 The Android Open Source Project * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.wm.shell.pip; +package com.android.wm.shell.common.pip; import static org.junit.Assert.assertEquals; @@ -25,8 +25,6 @@ import android.testing.TestableLooper; import androidx.test.filters.SmallTest; import com.android.wm.shell.ShellTestCase; -import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.common.pip.PipSnapAlgorithm; import org.junit.Before; import org.junit.Test; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java index fd3d3b5b6e2f..8c34c1946702 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/DividerViewTest.java @@ -36,6 +36,7 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; 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.common.DisplayController; @@ -66,9 +67,9 @@ public class DividerViewTest extends ShellTestCase { public void setup() { MockitoAnnotations.initMocks(this); Configuration configuration = getConfiguration(); - mSplitLayout = new SplitLayout("TestSplitLayout", mContext, configuration, + mSplitLayout = spy(new SplitLayout("TestSplitLayout", mContext, configuration, mSplitLayoutHandler, mCallbacks, mDisplayController, mDisplayImeController, - mTaskOrganizer, SplitLayout.PARALLAX_NONE, mSplitState, mHandler); + mTaskOrganizer, SplitLayout.PARALLAX_NONE, mSplitState, mHandler)); SplitWindowManager splitWindowManager = new SplitWindowManager("TestSplitWindowManager", mContext, configuration, mCallbacks); @@ -98,6 +99,14 @@ public class DividerViewTest extends ShellTestCase { "false", false); } + @Test + public void swapDividerActionForA11y() { + mDividerView.setAccessibilityDelegate(mDividerView.mHandleDelegate); + mDividerView.getAccessibilityDelegate().performAccessibilityAction(mDividerView, + R.id.action_swap_apps, null); + verify(mSplitLayout, times(1)).onDoubleTappedDivider(); + } + private static MotionEvent getMotionEvent(long eventTime, int action, float x, float y) { MotionEvent.PointerProperties properties = new MotionEvent.PointerProperties(); properties.id = 0; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java new file mode 100644 index 000000000000..22a85fc49a4b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/FlexParallaxSpecTests.java @@ -0,0 +1,401 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.split; + +import static android.view.WindowManager.DOCKED_INVALID; +import static android.view.WindowManager.DOCKED_LEFT; +import static android.view.WindowManager.DOCKED_RIGHT; + +import static com.android.wm.shell.common.split.ResizingEffectPolicy.DEFAULT_OFFSCREEN_DIM; + +import static com.google.common.truth.Truth.assertThat; + +import static org.mockito.Mockito.when; + +import android.graphics.Point; +import android.graphics.Rect; + +import androidx.test.ext.junit.runners.AndroidJUnit4; + +import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +@RunWith(AndroidJUnit4.class) +public class FlexParallaxSpecTests { + ParallaxSpec mFlexSpec = new FlexParallaxSpec(); + + Rect mDisplayBounds = new Rect(0, 0, 1000, 1000); + Rect mRetreatingSurface = new Rect(0, 0, 1000, 1000); + Rect mRetreatingContent = new Rect(0, 0, 1000, 1000); + Rect mAdvancingSurface = new Rect(0, 0, 1000, 1000); + Rect mAdvancingContent = new Rect(0, 0, 1000, 1000); + boolean mIsLeftRightSplit; + boolean mTopLeftShrink; + + int mDimmingSide; + float mDimValue; + Point mRetreatingParallax = new Point(0, 0); + Point mAdvancingParallax = new Point(0, 0); + + @Mock DividerSnapAlgorithm mockSnapAlgorithm; + @Mock SnapTarget mockStartEdge; + @Mock SnapTarget mockFirstTarget; + @Mock SnapTarget mockMiddleTarget; + @Mock SnapTarget mockLastTarget; + @Mock SnapTarget mockEndEdge; + + @Before + public void setup() { + MockitoAnnotations.initMocks(this); + when(mockSnapAlgorithm.getDismissStartTarget()).thenReturn(mockStartEdge); + when(mockSnapAlgorithm.getFirstSplitTarget()).thenReturn(mockFirstTarget); + when(mockSnapAlgorithm.getMiddleTarget()).thenReturn(mockMiddleTarget); + when(mockSnapAlgorithm.getLastSplitTarget()).thenReturn(mockLastTarget); + when(mockSnapAlgorithm.getDismissEndTarget()).thenReturn(mockEndEdge); + + when(mockStartEdge.getPosition()).thenReturn(0); + when(mockFirstTarget.getPosition()).thenReturn(250); + when(mockMiddleTarget.getPosition()).thenReturn(500); + when(mockLastTarget.getPosition()).thenReturn(750); + when(mockEndEdge.getPosition()).thenReturn(1000); + } + + @Test + public void testHorizontalDragFromCenter() { + mIsLeftRightSplit = true; + simulateDragFromCenterToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromCenterToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromLeft() { + mIsLeftRightSplit = true; + simulateDragFromLeftToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromLeftToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isGreaterThan(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + @Test + public void testHorizontalDragFromRight() { + mIsLeftRightSplit = true; + + simulateDragFromRightToLeft(125); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(250); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToLeft(375); + assertThat(mDimmingSide).isEqualTo(DOCKED_LEFT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isGreaterThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(500); + assertThat(mDimmingSide).isEqualTo(DOCKED_INVALID); + assertThat(mDimValue).isEqualTo(0f); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToCenter(625); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(0f); + assertThat(mDimValue).isLessThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isLessThan(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(750); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isEqualTo(DEFAULT_OFFSCREEN_DIM); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + + simulateDragFromRightToRight(875); + assertThat(mDimmingSide).isEqualTo(DOCKED_RIGHT); + assertThat(mDimValue).isGreaterThan(DEFAULT_OFFSCREEN_DIM); + assertThat(mDimValue).isLessThan(1f); + assertThat(mRetreatingParallax.x).isEqualTo(0); + assertThat(mRetreatingParallax.y).isEqualTo(0); + assertThat(mAdvancingParallax.x).isEqualTo(0); + assertThat(mAdvancingParallax.y).isEqualTo(0); + } + + private void simulateDragFromCenterToLeft(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromCenterToRight(int to) { + int from = 500; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToLeft(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = fullOffscreenAppLeft(from); + mAdvancingSurface = onscreenAppRight(to); + mAdvancingContent = onscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToCenter(int to) { + int from = 250; + + mRetreatingSurface = onscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromLeftToRight(int to) { + int from = 250; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = onscreenAppRight(from); + mAdvancingSurface = fullOffscreenAppLeft(to); + mAdvancingContent = fullOffscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToLeft(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToCenter(int to) { + int from = 750; + + mRetreatingSurface = onscreenAppLeft(to); + mRetreatingContent = onscreenAppLeft(from); + mAdvancingSurface = fullOffscreenAppRight(to); + mAdvancingContent = fullOffscreenAppRight(from); + + calculateDimAndParallax(from, to); + } + + private void simulateDragFromRightToRight(int to) { + int from = 750; + + mRetreatingSurface = flexOffscreenAppRight(to); + mRetreatingContent = fullOffscreenAppRight(from); + mAdvancingSurface = onscreenAppLeft(to); + mAdvancingContent = onscreenAppLeft(from); + + calculateDimAndParallax(from, to); + } + + private Rect flexOffscreenAppLeft(int pos) { + return new Rect(pos - (1000 - pos), 0, pos, 1000); + } + + private Rect onscreenAppLeft(int pos) { + return new Rect(0, 0, pos, 1000); + } + + private Rect fullOffscreenAppLeft(int pos) { + return new Rect(Math.min(0, pos - 750), 0, pos, 1000); + } + + private Rect flexOffscreenAppRight(int pos) { + return new Rect(pos, 0, pos * 2, 1000); + } + + private Rect onscreenAppRight(int pos) { + return new Rect(pos, 0, 1000, 1000); + } + + private Rect fullOffscreenAppRight(int pos) { + return new Rect(pos, 0, Math.max(pos + 750, 1000), 1000); + } + + private void calculateDimAndParallax(int from, int to) { + resetParallax(); + mTopLeftShrink = to < from; + mDimmingSide = mFlexSpec.getDimmingSide(to, mockSnapAlgorithm, mIsLeftRightSplit); + mDimValue = mFlexSpec.getDimValue(to, mockSnapAlgorithm); + mFlexSpec.getParallax(mRetreatingParallax, mAdvancingParallax, to, mockSnapAlgorithm, + mIsLeftRightSplit, mDisplayBounds, mRetreatingSurface, mRetreatingContent, + mAdvancingSurface, mAdvancingContent, mDimmingSide, mTopLeftShrink); + } + + private void resetParallax() { + mRetreatingParallax.set(0, 0); + mAdvancingParallax.set(0, 0); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt new file mode 100644 index 000000000000..f88f72356759 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/SurfaceBuilderSupplierTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.SuppliersUtilsTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [SurfaceBuilderSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:SurfaceBuilderSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class SurfaceBuilderSupplierTest { + + @Test + fun `SurfaceBuilderSupplier supplies an SurfaceControl Builder`() { + val supplier = SurfaceBuilderSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is SurfaceControl.Builder + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt new file mode 100644 index 000000000000..12b4d8b5f96b --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransactionSupplierTest.kt @@ -0,0 +1,43 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.common.transition + +import android.testing.AndroidTestingRunner +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.common.SuppliersUtilsTest +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Tests for [TransactionSupplier]. + * + * Build/Install/Run: + * atest WMShellUnitTests:TransactionSupplierTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class TransactionSupplierTest { + + @Test + fun `SurfaceBuilderSupplier supplies a Transaction`() { + val supplier = TransactionSupplier() + SuppliersUtilsTest.assertSupplierProvidesValue(supplier) { + it is SurfaceControl.Transaction + } + } +} 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 b5c9fa151dac..2264adec9a19 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 @@ -49,6 +49,7 @@ 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; @@ -85,7 +86,7 @@ import org.mockito.MockitoAnnotations; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIControllerTest extends CompatUIShellTestCase { +public class CompatUIControllerTest extends ShellTestCase { private static final int DISPLAY_ID = 0; private static final int TASK_ID = 12; 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 2117b062bf57..c567b5fbbb70 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 @@ -38,6 +38,7 @@ 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; @@ -62,7 +63,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUILayoutTest extends CompatUIShellTestCase { +public class CompatUILayoutTest extends ShellTestCase { private static final int TASK_ID = 1; 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 deleted file mode 100644 index 5a49d01f2991..000000000000 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/CompatUIShellTestCase.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * 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 0b37648faeec..8fd7c0ec3099 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,6 +27,8 @@ 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; @@ -42,7 +44,7 @@ import java.util.function.IntSupplier; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIStatusManagerTest extends CompatUIShellTestCase { +public class CompatUIStatusManagerTest extends ShellTestCase { 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 61b6d803c8be..0562bb835671 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 @@ -38,6 +38,7 @@ import android.app.ActivityManager; import android.app.TaskInfo; import android.content.res.Configuration; import android.graphics.Rect; +import android.platform.test.annotations.EnableFlags; import android.platform.test.annotations.RequiresFlagsDisabled; import android.testing.AndroidTestingRunner; import android.util.Pair; @@ -52,6 +53,7 @@ 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; @@ -76,7 +78,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class CompatUIWindowManagerTest extends CompatUIShellTestCase { +public class CompatUIWindowManagerTest extends ShellTestCase { private static final int TASK_ID = 1; private static final int TASK_WIDTH = 2000; @@ -394,8 +396,8 @@ public class CompatUIWindowManagerTest extends CompatUIShellTestCase { @Test @RequiresFlagsDisabled(FLAG_APP_COMPAT_UI_FRAMEWORK) + @EnableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON) public void testShouldShowSizeCompatRestartButton() { - mSetFlagsRule.enableFlags(Flags.FLAG_ALLOW_HIDE_SCM_BUTTON); doReturn(85).when(mCompatUIConfiguration).getHideSizeCompatRestartButtonTolerance(); mWindowManager = new CompatUIWindowManager(mContext, mTaskInfo, mSyncTransactionQueue, mCallback, mTaskListener, mDisplayLayout, new CompatUIHintsState(), 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 e786fef1855c..c6884ea17302 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 @@ -32,6 +32,7 @@ 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.Test; @@ -47,7 +48,7 @@ import org.mockito.MockitoAnnotations; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduDialogLayoutTest extends CompatUIShellTestCase { +public class LetterboxEduDialogLayoutTest extends ShellTestCase { @Mock private Runnable mDismissCallback; 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 09fc082a63e3..cbf5d1bb65dd 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 @@ -62,6 +62,7 @@ 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; @@ -90,7 +91,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class LetterboxEduWindowManagerTest extends CompatUIShellTestCase { +public class LetterboxEduWindowManagerTest extends ShellTestCase { private static final int USER_ID_1 = 1; private static final int USER_ID_2 = 2; 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 02c099b3cfb2..31ea8f76359f 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 @@ -34,6 +34,7 @@ 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.Test; @@ -50,7 +51,7 @@ import org.mockito.MockitoAnnotations; @RunWith(AndroidTestingRunner.class) @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) -public class ReachabilityEduLayoutTest extends CompatUIShellTestCase { +public class ReachabilityEduLayoutTest extends ShellTestCase { private ReachabilityEduLayout mLayout; private View mMoveUpButton; 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 fa04e070250e..1b2c0944777e 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 @@ -30,6 +30,7 @@ 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; @@ -52,7 +53,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class ReachabilityEduWindowManagerTest extends CompatUIShellTestCase { +public class ReachabilityEduWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; @Mock 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 2cded9d9776c..5075453d8c73 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 @@ -34,6 +34,7 @@ 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.Test; @@ -51,7 +52,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogLayoutTest extends CompatUIShellTestCase { +public class RestartDialogLayoutTest extends ShellTestCase { @Mock private Runnable mDismissCallback; @Mock private Consumer<Boolean> mRestartCallback; 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 ebd0f412a0a1..779a5ca10648 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 @@ -28,6 +28,7 @@ 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; @@ -50,7 +51,7 @@ import java.util.function.Consumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class RestartDialogWindowManagerTest extends CompatUIShellTestCase { +public class RestartDialogWindowManagerTest extends ShellTestCase { @Mock private SyncTransactionQueue mSyncTransactionQueue; 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 c6532e13f3cc..2b4d5f125783 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 @@ -38,6 +38,7 @@ 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; @@ -62,7 +63,7 @@ import java.util.function.BiConsumer; */ @RunWith(AndroidTestingRunner.class) @SmallTest -public class UserAspectRatioSettingsLayoutTest extends CompatUIShellTestCase { +public class UserAspectRatioSettingsLayoutTest extends ShellTestCase { private static final int TASK_ID = 1; 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 096e900199ba..af7c1f5d7692 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 @@ -54,6 +54,7 @@ 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; @@ -83,7 +84,7 @@ import java.util.function.Supplier; @RunWith(AndroidTestingRunner.class) @RunWithLooper @SmallTest -public class UserAspectRatioSettingsWindowManagerTest extends CompatUIShellTestCase { +public class UserAspectRatioSettingsWindowManagerTest extends ShellTestCase { private static final int TASK_ID = 1; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt index 319122d1e051..d3a2c9a411ef 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/impl/DefaultCompatUIRepositoryTest.kt @@ -18,7 +18,6 @@ package com.android.wm.shell.compatui.impl import android.graphics.Point -import android.platform.test.flag.junit.DeviceFlagsValueProvider import android.testing.AndroidTestingRunner import android.view.View import androidx.test.filters.SmallTest @@ -29,7 +28,6 @@ import com.android.wm.shell.compatui.api.CompatUISpec import org.junit.Assert.assertEquals import org.junit.Assert.assertNull import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -45,9 +43,6 @@ class DefaultCompatUIRepositoryTest { lateinit var repository: CompatUIRepository - @get:Rule - val mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - @Before fun setUp() { repository = DefaultCompatUIRepository() diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt index 88cc981dd30c..e34884b103f6 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxControllerRobotTest.kt @@ -33,10 +33,10 @@ abstract class LetterboxControllerRobotTest { companion object { @JvmStatic - private val DISPLAY_ID = 1 + val DISPLAY_ID = 1 @JvmStatic - private val TASK_ID = 20 + val TASK_ID = 20 } lateinit var letterboxController: LetterboxController diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt new file mode 100644 index 000000000000..bc3416a88918 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxGestureDelegateTest.kt @@ -0,0 +1,75 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.testing.AndroidTestingRunner +import androidx.test.filters.SmallTest +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.verify + +/** + * Tests for [LetterboxGestureDelegate]. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxGestureDelegateTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LetterboxGestureDelegateTest { + + class DelegateTest : LetterboxGestureListener by LetterboxGestureDelegate + + val delegate = DelegateTest() + + @Before + fun setUp() { + spyOn(LetterboxGestureDelegate) + } + + @Test + fun `When delegating all methods are invoked`() { + val event = motionEventAt(0f, 0f) + with(delegate) { + onDown(event) + onShowPress(event) + onSingleTapUp(event) + onScroll(event, event, 0f, 0f) + onFling(event, event, 0f, 0f) + onLongPress(event) + onSingleTapConfirmed(event) + onDoubleTap(event) + onDoubleTapEvent(event) + onContextClick(event) + } + with(LetterboxGestureDelegate) { + verify(this).onDown(event) + verify(this).onShowPress(event) + verify(this).onSingleTapUp(event) + verify(this).onScroll(event, event, 0f, 0f) + verify(this).onFling(event, event, 0f, 0f) + verify(this).onLongPress(event) + verify(this).onSingleTapConfirmed(event) + verify(this).onDoubleTap(event) + verify(this).onDoubleTapEvent(event) + verify(this).onContextClick(event) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt new file mode 100644 index 000000000000..fa95faee4b6e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxInputControllerTest.kt @@ -0,0 +1,203 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox + +import android.content.Context +import android.graphics.Rect +import android.graphics.Region +import android.os.Handler +import android.os.Looper +import android.testing.AndroidTestingRunner +import android.view.IWindowSession +import android.view.InputChannel +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.InputChannelSupplier +import com.android.wm.shell.common.WindowSessionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxMatchers.asAnyMode +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.TAG +import java.util.function.Consumer +import java.util.function.Supplier +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.times +import org.mockito.kotlin.verify + +/** + * Tests for [LetterboxInputController]. + * + * Build/Install/Run: + * atest WMShellUnitTests:LetterboxInputControllerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class LetterboxInputControllerTest : ShellTestCase() { + + @Test + fun `When creation is requested the surface is created if not present`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + + r.checkInputSurfaceBuilderInvoked() + } + } + + @Test + fun `When creation is requested multiple times the input surface is created once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + + r.checkInputSurfaceBuilderInvoked(times = 1) + } + } + + @Test + fun `A different input surface is created for every key`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest() + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + r.sendCreateSurfaceRequest(displayId = 2) + r.sendCreateSurfaceRequest(displayId = 2, taskId = 2) + + r.checkInputSurfaceBuilderInvoked(times = 3) + } + } + + @Test + fun `Created spy surface is removed once`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.checkInputSurfaceBuilderInvoked() + + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + r.sendDestroySurfaceRequest() + + r.checkTransactionRemovedInvoked() + } + } + @Test + fun `Only existing surfaces receive visibility update`() { + runTestScenario { r -> + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceVisibilityRequest(visible = true) + r.sendUpdateSurfaceVisibilityRequest(visible = true, displayId = 20) + + r.checkVisibilityUpdated(expectedVisibility = true) + } + } + + @Test + fun `Only existing surfaces receive taskBounds update`() { + runTestScenario { r -> + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + + r.checkUpdateSessionRegion(times = 0, region = Region(0, 0, 2000, 1000)) + r.checkSurfaceSizeUpdated(times = 0, expectedWidth = 2000, expectedHeight = 1000) + + r.resetTransitionTest() + + r.sendCreateSurfaceRequest() + r.sendUpdateSurfaceBoundsRequest( + taskBounds = Rect(0, 0, 2000, 1000), + activityBounds = Rect(500, 0, 1500, 1000) + ) + r.checkUpdateSessionRegion(region = Region(0, 0, 2000, 1000)) + r.checkSurfaceSizeUpdated(expectedWidth = 2000, expectedHeight = 1000) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<InputLetterboxControllerRobotTest>) { + consumer.accept(InputLetterboxControllerRobotTest(mContext).apply { initController() }) + } + + class InputLetterboxControllerRobotTest(private val context: Context) : + LetterboxControllerRobotTest() { + + private val inputSurfaceBuilder: LetterboxInputSurfaceBuilder + private val handler = Handler(Looper.getMainLooper()) + private val listener: LetterboxGestureListener + private val listenerSupplier: Supplier<LetterboxGestureListener> + private val windowSessionSupplier: WindowSessionSupplier + private val windowSession: IWindowSession + private val inputChannelSupplier: InputChannelSupplier + + init { + inputSurfaceBuilder = getLetterboxInputSurfaceBuilderMock() + listener = mock<LetterboxGestureListener>() + listenerSupplier = mock<Supplier<LetterboxGestureListener>>() + doReturn(LetterboxGestureDelegate).`when`(listenerSupplier).get() + windowSessionSupplier = mock<WindowSessionSupplier>() + windowSession = mock<IWindowSession>() + doReturn(windowSession).`when`(windowSessionSupplier).get() + inputChannelSupplier = mock<InputChannelSupplier>() + val inputChannels = InputChannel.openInputChannelPair(TAG) + inputChannels.first().dispose() + doReturn(inputChannels[1]).`when`(inputChannelSupplier).get() + } + + override fun buildController(): LetterboxController = + LetterboxInputController( + context, + handler, + inputSurfaceBuilder, + listenerSupplier, + windowSessionSupplier, + inputChannelSupplier + ) + + fun checkInputSurfaceBuilderInvoked( + times: Int = 1, + name: String = "", + callSite: String = "" + ) { + verify(inputSurfaceBuilder, times(times)).createInputSurface( + eq(transaction), + eq(parentLeash), + name.asAnyMode(), + callSite.asAnyMode() + ) + } + + fun checkUpdateSessionRegion(times: Int = 1, displayId: Int = DISPLAY_ID, region: Region) { + verify(windowSession, times(times)).updateInputChannel( + any(), + eq(displayId), + any(), + any(), + any(), + any(), + eq(region) + ) + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt index 2c06dfda7917..3ce1fec32a16 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTestUtils.kt @@ -16,6 +16,8 @@ package com.android.wm.shell.compatui.letterbox +import android.view.MotionEvent.ACTION_DOWN +import android.view.MotionEvent.obtain import android.view.SurfaceControl import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull @@ -37,6 +39,18 @@ fun getTransactionMock(): SurfaceControl.Transaction = mock<SurfaceControl.Trans doReturn(this).`when`(this).setWindowCrop(anyOrNull(), any(), any()) } +/** + * @return A [LetterboxInputSurfaceBuilder] mock to use in tests. + */ +fun getLetterboxInputSurfaceBuilderMock() = mock<LetterboxInputSurfaceBuilder>().apply { + doReturn(SurfaceControl()).`when`(this).createInputSurface( + any(), + any(), + any(), + any() + ) +} + // Utility to make verification mode depending on a [Boolean]. fun Boolean.asMode(): VerificationMode = if (this) times(1) else never() @@ -47,5 +61,10 @@ object LetterboxMatchers { fun String.asAnyMode() = asAnyMode { this.isEmpty() } } +object LetterboxEvents { + fun motionEventAt(x: Float, y: Float) = + obtain(0, 10, ACTION_DOWN, x, y, 0) +} + private inline fun <reified T : Any> T.asAnyMode(condition: () -> Boolean) = (if (condition()) any() else eq(this)) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt index 78bb721d1028..008c499cb88e 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt @@ -20,7 +20,6 @@ import android.graphics.Point import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CLOSE @@ -36,14 +35,12 @@ import com.android.wm.shell.transition.Transitions import com.android.wm.shell.util.TransitionObserverInputBuilder import com.android.wm.shell.util.executeTransitionObserverTest import java.util.function.Consumer -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.doReturn import org.mockito.kotlin.eq import org.mockito.kotlin.mock -import org.mockito.kotlin.times import org.mockito.kotlin.verify /** @@ -56,9 +53,6 @@ import org.mockito.kotlin.verify @SmallTest class LetterboxTransitionObserverTest : ShellTestCase() { - @get:Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - @Test @DisableFlags(Flags.FLAG_APP_COMPAT_REFACTORING) fun `when initialized and flag disabled the observer is not registered`() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt new file mode 100644 index 000000000000..a5f6ced20dc0 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerFactoryTest.kt @@ -0,0 +1,131 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListenerFactory]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerFactoryTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerFactoryTest : ShellTestCase() { + + @Test + fun `When invoked a ReachabilityGestureListenerFactory is created`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkReachabilityGestureListenerCreated() + } + } + + @Test + fun `Right parameters are used for creation`() { + runTestScenario { r -> + r.invokeCreate() + + r.checkRightParamsAreUsed() + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerFactoryRobotTest>) { + val robot = ReachabilityGestureListenerFactoryRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerFactoryRobotTest { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val factory: ReachabilityGestureListenerFactory + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private lateinit var obtainedResult: Any + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + factory = ReachabilityGestureListenerFactory(transitions, animationHandler, wctSupplier) + } + + fun invokeCreate(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + obtainedResult = factory.createReachabilityGestureListener(taskId, token) + } + + fun checkReachabilityGestureListenerCreated(expected: Boolean = true) { + assertEquals(expected, obtainedResult is ReachabilityGestureListener) + } + + fun checkRightParamsAreUsed(taskId: Int = TASK_ID, token: WindowContainerToken? = TOKEN) { + with(obtainedResult as ReachabilityGestureListener) { + // Click outside the bounds + updateActivityBounds(Rect(0, 0, 10, 20)) + onDoubleTap(motionEventAt(50f, 100f)) + // WindowContainerTransactionSupplier is invoked to create a + // WindowContainerTransaction + verify(wctSupplier).get() + // Verify the right params are passed to startAppCompatReachability() + verify(wct).setReachabilityOffset( + token!!, + taskId, + 50, + 100 + ) + // startTransition() is invoked on Transitions with the right parameters + verify(transitions).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt new file mode 100644 index 000000000000..bc10ea578ffb --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/events/ReachabilityGestureListenerTest.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.compatui.letterbox.events + +import android.graphics.Rect +import android.testing.AndroidTestingRunner +import android.window.WindowContainerToken +import android.window.WindowContainerTransaction +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.WindowContainerTransactionSupplier +import com.android.wm.shell.compatui.letterbox.LetterboxEvents.motionEventAt +import com.android.wm.shell.compatui.letterbox.asMode +import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TRANSIT_MOVE_LETTERBOX_REACHABILITY +import java.util.function.Consumer +import kotlin.test.assertEquals +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify + +/** + * Tests for [ReachabilityGestureListener]. + * + * Build/Install/Run: + * atest WMShellUnitTests:ReachabilityGestureListenerTest + */ +@RunWith(AndroidTestingRunner::class) +@SmallTest +class ReachabilityGestureListenerTest : ShellTestCase() { + + @Test + fun `Only events outside the bounds are handled`() { + runTestScenario { r -> + r.updateActivityBounds(Rect(0, 0, 100, 200)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = false, 50, 100) + r.verifyReachabilityTransitionStarted(expected = false) + r.verifyEventIsHandled(expected = false) + + r.updateActivityBounds(Rect(0, 0, 10, 50)) + r.sendMotionEvent(50, 100) + + r.verifyReachabilityTransitionCreated(expected = true, 50, 100) + r.verifyReachabilityTransitionStarted(expected = true) + r.verifyEventIsHandled(expected = true) + } + } + + /** + * Runs a test scenario providing a Robot. + */ + fun runTestScenario(consumer: Consumer<ReachabilityGestureListenerRobotTest>) { + val robot = ReachabilityGestureListenerRobotTest() + consumer.accept(robot) + } + + class ReachabilityGestureListenerRobotTest( + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + + companion object { + @JvmStatic + private val TASK_ID = 1 + + @JvmStatic + private val TOKEN = mock<WindowContainerToken>() + } + + private val reachabilityListener: ReachabilityGestureListener + private val transitions: Transitions + private val animationHandler: Transitions.TransitionHandler + private val wctSupplier: WindowContainerTransactionSupplier + private val wct: WindowContainerTransaction + private var eventHandled = false + + init { + transitions = mock<Transitions>() + animationHandler = mock<Transitions.TransitionHandler>() + wctSupplier = mock<WindowContainerTransactionSupplier>() + wct = mock<WindowContainerTransaction>() + doReturn(wct).`when`(wctSupplier).get() + reachabilityListener = + ReachabilityGestureListener( + taskId, + token, + transitions, + animationHandler, + wctSupplier + ) + } + + fun updateActivityBounds(activityBounds: Rect) { + reachabilityListener.updateActivityBounds(activityBounds) + } + + fun sendMotionEvent(x: Int, y: Int) { + eventHandled = reachabilityListener.onDoubleTap(motionEventAt(x.toFloat(), y.toFloat())) + } + + fun verifyReachabilityTransitionCreated( + expected: Boolean, + x: Int, + y: Int, + taskId: Int = TASK_ID, + token: WindowContainerToken? = TOKEN + ) { + verify(wct, expected.asMode()).setReachabilityOffset( + token!!, + taskId, + x, + y + ) + } + + fun verifyReachabilityTransitionStarted(expected: Boolean = true) { + verify(transitions, expected.asMode()).startTransition( + TRANSIT_MOVE_LETTERBOX_REACHABILITY, + wct, + animationHandler + ) + } + + fun verifyEventIsHandled(expected: Boolean) { + assertEquals(expected, eventHandled) + } + } +} 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 957fdf995776..70a30a3ca7a9 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 @@ -25,7 +25,6 @@ import android.graphics.Rect import android.os.Binder import android.os.UserManager import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.window.WindowContainerTransaction @@ -62,7 +61,6 @@ 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.ArgumentCaptor @@ -89,8 +87,6 @@ import org.mockito.quality.Strictness @ExperimentalCoroutinesApi @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE, FLAG_RESPECT_ORIENTATION_CHANGE_FOR_UNRESIZEABLE) class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @@ -117,7 +113,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { .strictness(Strictness.LENIENT) .spyStatic(DesktopModeStatus::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) 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 fae7363e0676..0d5741fccbcc 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 @@ -23,7 +23,6 @@ import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ContentResolver import android.os.Binder import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.provider.Settings import android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS import android.testing.AndroidTestingRunner @@ -51,7 +50,6 @@ import com.android.wm.shell.transition.Transitions import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.isNull @@ -74,9 +72,6 @@ import org.mockito.quality.Strictness @SmallTest @RunWith(AndroidTestingRunner::class) class DesktopDisplayEventHandlerTest : ShellTestCase() { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var transitions: Transitions @Mock lateinit var displayController: DisplayController 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 47d133b974e6..006c3cae121c 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 @@ -23,7 +23,6 @@ import android.os.Binder import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display.DEFAULT_DISPLAY @@ -73,7 +72,6 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopImmersiveControllerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) @Mock private lateinit var mockTransitions: Transitions 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/DesktopMinimizationTransitionHandlerTest.kt index c705f5a5ac87..4c3325d4d1de 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/DesktopMinimizationTransitionHandlerTest.kt @@ -45,18 +45,18 @@ import org.mockito.kotlin.whenever @SmallTest @RunWithLooper @RunWith(AndroidTestingRunner::class) -class DesktopBackNavigationTransitionHandlerTest : ShellTestCase() { +class DesktopMinimizationTransitionHandlerTest : ShellTestCase() { private val testExecutor = mock<ShellExecutor>() private val closingTaskLeash = mock<SurfaceControl>() private val displayController = mock<DisplayController>() - private lateinit var handler: DesktopBackNavigationTransitionHandler + private lateinit var handler: DesktopMinimizationTransitionHandler @Before fun setUp() { handler = - DesktopBackNavigationTransitionHandler(testExecutor, testExecutor, displayController) + DesktopMinimizationTransitionHandler(testExecutor, testExecutor, displayController) whenever(displayController.getDisplayContext(any())).thenReturn(mContext) } 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 38cd1b4d5ea8..e9f92cfd7c56 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 @@ -27,7 +27,6 @@ import android.os.Handler import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl @@ -42,7 +41,6 @@ import android.window.TransitionInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction import androidx.test.filters.SmallTest -import com.android.internal.jank.Cuj.CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE import com.android.internal.jank.InteractionJankMonitor import com.android.window.flags.Flags import com.android.wm.shell.RootTaskDisplayAreaOrganizer @@ -58,9 +56,9 @@ import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyBoolean import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock import org.mockito.Mockito @@ -83,14 +81,11 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) 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 desktopBackNavigationTransitionHandler: DesktopBackNavigationTransitionHandler + @Mock lateinit var desktopMinimizationTransitionHandler: DesktopMinimizationTransitionHandler @Mock lateinit var desktopImmersiveController: DesktopImmersiveController @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var mockHandler: Handler @@ -113,7 +108,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, desktopImmersiveController, - desktopBackNavigationTransitionHandler, + desktopMinimizationTransitionHandler, interactionJankMonitor, mockHandler, shellInit, @@ -132,17 +127,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test - fun startMinimizedModeTransition_callsFreeformTaskTransitionHandler() { - val wct = WindowContainerTransaction() - whenever(freeformTaskTransitionHandler.startMinimizedModeTransition(any())) - .thenReturn(mock()) - - mixedHandler.startMinimizedModeTransition(wct) - - verify(freeformTaskTransitionHandler).startMinimizedModeTransition(wct) - } - - @Test @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS_BUGFIX) fun startRemoveTransition_callsFreeformTaskTransitionHandler() { val wct = WindowContainerTransaction() @@ -254,13 +238,6 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { any(), eq(mixedHandler), ) - verify(interactionJankMonitor) - .begin( - closingTaskLeash, - context, - mockHandler, - CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, - ) } @Test @@ -544,6 +521,131 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsDisabled_doesNotUseMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever( + freeformTaskTransitionHandler.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(mock()) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = task.taskId, + isLastTask = true, + ) + + verify(freeformTaskTransitionHandler) + .startMinimizedModeTransition(eq(wct), eq(task.taskId), eq(true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_notLastTask_callsMinimizationHandler() { + val wct = WindowContainerTransaction() + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTaskChange = createChange(minimizingTask) + val transition = Binder() + whenever( + transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull()) + ) + .thenReturn(transition) + whenever( + desktopMinimizationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(true) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = minimizingTask.taskId, + isLastTask = false, + ) + val started = + mixedHandler.startAnimation( + transition = transition, + info = + createCloseTransitionInfo( + Transitions.TRANSIT_MINIMIZE, + listOf(minimizingTaskChange), + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) + + assertTrue("Should delegate animation to minimization transition handler", started) + verify(desktopMinimizationTransitionHandler) + .startAnimation( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_BY_MINIMIZE_TRANSITION_BUGFIX) + fun startMinimizedModeTransition_exitByMinimizeTransitionFlagsEnabled_withMinimizingLastTask_dispatchesTransition() { + val wct = WindowContainerTransaction() + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTaskChange = createChange(minimizingTask) + val transition = Binder() + whenever( + transitions.startTransition(eq(Transitions.TRANSIT_MINIMIZE), eq(wct), anyOrNull()) + ) + .thenReturn(transition) + whenever( + desktopMinimizationTransitionHandler.startAnimation( + any(), + any(), + any(), + any(), + any(), + ) + ) + .thenReturn(true) + + mixedHandler.startMinimizedModeTransition( + wct = wct, + taskId = minimizingTask.taskId, + isLastTask = true, + ) + mixedHandler.startAnimation( + transition = transition, + info = + createCloseTransitionInfo( + Transitions.TRANSIT_MINIMIZE, + listOf(minimizingTaskChange), + ), + startTransaction = mock(), + finishTransaction = mock(), + finishCallback = {}, + ) + + verify(transitions) + .dispatchTransition( + eq(transition), + argThat { info -> info.changes.contains(minimizingTaskChange) }, + any(), + any(), + any(), + eq(mixedHandler), + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS_BUGFIX) fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { val wct = WindowContainerTransaction() @@ -654,12 +756,12 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun startAnimation_withMinimizingDesktopTask_callsBackNavigationHandler() { + fun startAnimation_withMinimizingDesktopTask_callsMinimizationHandler() { val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) val transition = Binder() whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) whenever( - desktopBackNavigationTransitionHandler.startAnimation( + desktopMinimizationTransitionHandler.startAnimation( any(), any(), any(), @@ -687,7 +789,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) assertTrue("Should delegate animation to back navigation transition handler", started) - verify(desktopBackNavigationTransitionHandler) + verify(desktopMinimizationTransitionHandler) .startAnimation( eq(transition), argThat { info -> info.changes.contains(minimizingTaskChange) }, @@ -704,7 +806,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val transition = Binder() whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) whenever( - desktopBackNavigationTransitionHandler.startAnimation( + desktopMinimizationTransitionHandler.startAnimation( any(), any(), any(), 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 bddc06204a52..8a5acfa70f50 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 @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo import android.graphics.Rect import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import com.android.dx.mockito.inline.extended.ExtendedMockito.clearInvocations import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn import com.android.dx.mockito.inline.extended.ExtendedMockito.staticMockMarker @@ -65,15 +64,13 @@ class DesktopModeEventLoggerTest : ShellTestCase() { val displayLayout = mock<DisplayLayout>() @JvmField - @Rule(order = 0) + @Rule() val extendedMockitoRule = ExtendedMockitoRule.Builder(this) .mockStatic(FrameworkStatsLog::class.java) .mockStatic(EventLogTags::class.java) .build()!! - @JvmField @Rule(order = 1) val setFlagsRule = SetFlagsRule() - @Before fun setUp() { doReturn(displayLayout).whenever(displayController).getDisplayLayout(anyInt()) 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 016e04039b12..d510570e8839 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 @@ -24,14 +24,12 @@ import android.hardware.input.InputManager import android.hardware.input.InputManager.KeyGestureEventHandler import android.hardware.input.KeyGestureEvent import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner 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 @@ -49,7 +47,6 @@ import com.android.wm.shell.common.DisplayLayout import com.android.wm.shell.desktopmode.DesktopModeEventLogger.Companion.MinimizeReason 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 @@ -64,7 +61,6 @@ 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 @@ -87,8 +83,6 @@ import org.mockito.quality.Strictness @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopModeKeyGestureHandlerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private val rootTaskDisplayAreaOrganizer = mock<RootTaskDisplayAreaOrganizer>() private val shellTaskOrganizer = mock<ShellTaskOrganizer>() private val focusTransitionObserver = mock<FocusTransitionObserver>() @@ -111,12 +105,7 @@ class DesktopModeKeyGestureHandlerTest : ShellTestCase() { @Before fun setUp() { Dispatchers.setMain(StandardTestDispatcher()) - mockitoSession = - mockitoSession() - .strictness(Strictness.LENIENT) - .spyStatic(DesktopModeStatus::class.java) - .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + mockitoSession = mockitoSession().strictness(Strictness.LENIENT).startMocking() testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) 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 e46d2c7147ed..20d50aa32f7f 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 @@ -16,25 +16,34 @@ package com.android.wm.shell.desktopmode +import android.animation.AnimatorTestRule import android.app.ActivityManager.RunningTaskInfo import android.graphics.PointF import android.graphics.Rect +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display import android.view.SurfaceControl import androidx.test.filters.SmallTest import com.android.internal.policy.SystemBarUtils +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE import com.android.wm.shell.R import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase +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.common.SyncTransactionQueue +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider import com.google.common.truth.Truth.assertThat import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mock +import org.mockito.kotlin.any import org.mockito.kotlin.whenever /** @@ -43,23 +52,37 @@ import org.mockito.kotlin.whenever * Usage: atest WMShellUnitTests:DesktopModeVisualIndicatorTest */ @SmallTest +@RunWithLooper @RunWith(AndroidTestingRunner::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopModeVisualIndicatorTest : ShellTestCase() { - @Mock private lateinit var taskInfo: RunningTaskInfo + + @JvmField @Rule val animatorTestRule = AnimatorTestRule(this) + + private lateinit var taskInfo: RunningTaskInfo @Mock private lateinit var syncQueue: SyncTransactionQueue @Mock private lateinit var displayController: DisplayController + @Mock private lateinit var display: Display @Mock private lateinit var taskSurface: SurfaceControl @Mock private lateinit var taskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @Mock private lateinit var displayLayout: DisplayLayout + @Mock private lateinit var bubbleBoundsProvider: BubbleDropTargetBoundsProvider private lateinit var visualIndicator: DesktopModeVisualIndicator + private val desktopExecutor = TestShellExecutor() + private val mainExecutor = TestShellExecutor() @Before fun setUp() { whenever(displayLayout.width()).thenReturn(DISPLAY_BOUNDS.width()) whenever(displayLayout.height()).thenReturn(DISPLAY_BOUNDS.height()) whenever(displayLayout.stableInsets()).thenReturn(STABLE_INSETS) + whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayController.getDisplay(anyInt())).thenReturn(mContext.display) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(any())) + .thenReturn(Rect()) + taskInfo = DesktopTestHelpers.createFullscreenTask() } @Test @@ -173,6 +196,40 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { } @Test + fun testBubbleLeftRegionCalculation() { + val bubbleRegionWidth = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width) + val bubbleRegionHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height) + val expectedRect = Rect(0, 1600 - bubbleRegionHeight, bubbleRegionWidth, 1600) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateBubbleLeftRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + } + + @Test + fun testBubbleRightRegionCalculation() { + val bubbleRegionWidth = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_width) + val bubbleRegionHeight = + context.resources.getDimensionPixelSize(R.dimen.bubble_transform_area_height) + val expectedRect = Rect(2400 - bubbleRegionWidth, 1600 - bubbleRegionHeight, 2400, 1600) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + testRegion = visualIndicator.calculateBubbleRightRegion(displayLayout) + assertThat(testRegion.bounds).isEqualTo(expectedRect) + } + + @Test fun testDefaultIndicators() { createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) var result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) @@ -190,9 +247,107 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) } + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testDefaultIndicatorWithNoDesktop() { + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_isDesktopModeSupported, + false, + ) + mContext.orCreateTestableResources.addOverride( + com.android.internal.R.bool.config_isDesktopModeDevOptionSupported, + false, + ) + + // Fullscreen to center, no desktop indicator + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + var result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + // Fullscreen to split + result = visualIndicator.updateIndicatorType(PointF(10000f, 500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(-10000f, 500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR) + // Fullscreen to bubble + result = visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + // Split to center, no desktop indicator + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_SPLIT) + result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + // Split to fullscreen + result = visualIndicator.updateIndicatorType(PointF(500f, 0f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + // Split to bubble + result = visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_LEFT_INDICATOR) + result = visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + assertThat(result) + .isEqualTo(DesktopModeVisualIndicator.IndicatorType.TO_BUBBLE_RIGHT_INDICATOR) + // Drag app to center, no desktop indicator + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT) + result = visualIndicator.updateIndicatorType(PointF(500f, 500f)) + assertThat(result).isEqualTo(DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testBubbleLeftVisualIndicatorSize() { + val dropTargetBounds = Rect(100, 100, 500, 1500) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ true)) + .thenReturn(dropTargetBounds) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + desktopExecutor.flushAll() + mainExecutor.flushAll() + visualIndicator.updateIndicatorType(PointF(100f, 1500f)) + desktopExecutor.flushAll() + mainExecutor.flushAll() + + animatorTestRule.advanceTimeBy(200) + + assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds) + } + + @Test + @EnableFlags( + com.android.wm.shell.Flags.FLAG_ENABLE_BUBBLE_TO_FULLSCREEN, + com.android.wm.shell.Flags.FLAG_ENABLE_CREATE_ANY_BUBBLE, + ) + fun testBubbleRightVisualIndicatorSize() { + val dropTargetBounds = Rect(1900, 100, 2300, 1500) + whenever(bubbleBoundsProvider.getBubbleBarExpandedViewDropTargetBounds(/* onLeft= */ false)) + .thenReturn(dropTargetBounds) + createVisualIndicator(DesktopModeVisualIndicator.DragStartState.FROM_FULLSCREEN) + desktopExecutor.flushAll() + mainExecutor.flushAll() + visualIndicator.updateIndicatorType(PointF(2300f, 1500f)) + desktopExecutor.flushAll() + mainExecutor.flushAll() + + animatorTestRule.advanceTimeBy(200) + + assertThat(visualIndicator.indicatorBounds).isEqualTo(dropTargetBounds) + } + private fun createVisualIndicator(dragStartState: DesktopModeVisualIndicator.DragStartState) { visualIndicator = DesktopModeVisualIndicator( + desktopExecutor, + mainExecutor, syncQueue, taskInfo, displayController, @@ -200,6 +355,7 @@ class DesktopModeVisualIndicatorTest : ShellTestCase() { taskSurface, taskDisplayAreaOrganizer, dragStartState, + bubbleBoundsProvider, ) } 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 f5c93ee8ffe4..ed9b97d264f7 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 @@ -20,7 +20,6 @@ import android.graphics.Rect import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY import android.view.Display.INVALID_DISPLAY @@ -28,6 +27,7 @@ import androidx.test.filters.SmallTest import com.android.window.flags.Flags import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PIP +import com.android.window.flags.Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor @@ -36,6 +36,7 @@ import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail +import kotlin.test.assertEquals import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -46,14 +47,16 @@ import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.runTest import kotlinx.coroutines.test.setMain import org.junit.After +import org.junit.Assert.assertThrows import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.kotlin.any +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.eq import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -71,8 +74,6 @@ import platform.test.runner.parameterized.Parameters @ExperimentalCoroutinesApi class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) - private lateinit var repo: DesktopRepository private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope @@ -165,6 +166,69 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTask_deskDoesNotExist_throws() { + repo.removeDesk(deskId = 0) + + assertThrows(Exception::class.java) { + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 5, isVisible = true) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_deskDoesNotExist_throws() { + repo.removeDesk(deskId = 2) + + assertThrows(Exception::class.java) { + repo.addTaskToDesk( + displayId = DEFAULT_DISPLAY, + deskId = 2, + taskId = 4, + isVisible = true, + ) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_addsToZOrderList() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(DEFAULT_DISPLAY, deskId = 3) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 6, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 8, isVisible = true) + + val orderedTasks = repo.getFreeformTasksIdsInDeskInZOrder(deskId = 2) + assertThat(orderedTasks[0]).isEqualTo(7) + assertThat(orderedTasks[1]).isEqualTo(6) + assertThat(orderedTasks[2]).isEqualTo(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_visible_addsToVisible() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 5, isVisible = true) + + assertThat(repo.isVisibleTask(5)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun addTaskToDesk_removesFromAllOtherDesks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(DEFAULT_DISPLAY, deskId = 3) + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 2, taskId = 7, isVisible = true) + + repo.addTaskToDesk(displayId = DEFAULT_DISPLAY, deskId = 3, taskId = 7, isVisible = true) + + assertThat(repo.getActiveTaskIdsInDesk(2)).doesNotContain(7) + } + + @Test fun removeActiveTask_notifiesActiveTaskListener() { val listener = TestListener() repo.addActiveTaskListener(listener) @@ -469,8 +533,8 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) - repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) @@ -832,12 +896,12 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) assertThat(repo.isActiveTask(taskId)).isFalse() - assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) + assertThat(listener.activeChangesOnThirdDisplay).isEqualTo(2) } @Test @@ -855,7 +919,7 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { fun removeTask_updatesTaskVisibility() { repo.addDesk(displayId = THIRD_DISPLAY, deskId = THIRD_DISPLAY) val taskId = 1 - repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.removeTask(THIRD_DISPLAY, taskId) @@ -1044,6 +1108,30 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setTaskInFullImmersiveState_inDesk_savedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + assertThat(repo.isTaskInFullImmersiveState(6)).isFalse() + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskInFullImmersiveState_inDesk_removedAsInImmersiveState() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = false) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test fun removeTaskInFullImmersiveState_otherWasImmersive_otherRemainsImmersive() { repo.setTaskInFullImmersiveState(DEFAULT_DISPLAY, taskId = 1, immersive = true) @@ -1080,13 +1168,37 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) - val tasksBeforeRemoval = repo.removeDesk(displayId = DEFAULT_DISPLAY) + val tasksBeforeRemoval = repo.removeDesk(deskId = DEFAULT_DISPLAY) assertThat(tasksBeforeRemoval).containsExactly(1, 2, 3).inOrder() assertThat(repo.getActiveTasks(displayId = DEFAULT_DISPLAY)).isEmpty() } @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_active_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 3) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(3) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleDesks_inactive_removes() { + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 2) + repo.addDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + repo.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = 3) + + repo.removeDesk(deskId = 2) + + assertThat(repo.getDeskIds(displayId = DEFAULT_DISPLAY)).doesNotContain(2) + } + + @Test fun getTaskInFullImmersiveState_byDisplay() { repo.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) repo.setActiveDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) @@ -1168,14 +1280,166 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { assertThat(repo.getActiveTaskIdsInDesk(999)).contains(6) } + @Test + @DisableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk() { + repo.addDesk(SECOND_DISPLAY, SECOND_DISPLAY) + + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = SECOND_DISPLAY)) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDisplayForDesk_multipleDesks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addDesk(DEFAULT_DISPLAY, deskId = 7) + repo.addDesk(SECOND_DISPLAY, deskId = 8) + repo.addDesk(SECOND_DISPLAY, deskId = 9) + + assertEquals(DEFAULT_DISPLAY, repo.getDisplayForDesk(deskId = 7)) + assertEquals(SECOND_DISPLAY, repo.getDisplayForDesk(deskId = 8)) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskActive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun setDeskInactive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.setActiveDesk(DEFAULT_DISPLAY, deskId = 6) + + repo.setDeskInactive(deskId = 6) + + assertThat(repo.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun getDeskIdForTask() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + assertThat(repo.getDeskIdForTask(10)).isEqualTo(6) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeMaximize() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeMaximize(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeMaximize(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_clearsBoundsBeforeImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.saveBoundsBeforeFullImmersive(taskId = 10, bounds = Rect(10, 10, 100, 100)) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.removeBoundsBeforeFullImmersive(taskId = 10)).isNull() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromZOrderList() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getFreeformTasksIdsInDeskInZOrder(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromMinimized() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.minimizeTaskInDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.getMinimizedTaskIdsInDesk(deskId = 6)).doesNotContain(10) + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromImmersive() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + repo.setTaskInFullImmersiveStateInDesk(deskId = 6, taskId = 10, immersive = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isTaskInFullImmersiveState(taskId = 10)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromActiveTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isActiveTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeTaskFromDesk_removesFromVisibleTasks() { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + assertThat(repo.isVisibleTaskInDesk(taskId = 10, deskId = 6)).isFalse() + } + + @Test + @EnableFlags(FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun removeTaskFromDesk_updatesPersistence() = runTest { + repo.addDesk(DEFAULT_DISPLAY, deskId = 6) + repo.addTaskToDesk(DEFAULT_DISPLAY, deskId = 6, taskId = 10, isVisible = true) + clearInvocations(persistentRepository) + + repo.removeTaskFromDesk(deskId = 6, taskId = 10) + + verify(persistentRepository) + .addOrUpdateDesktop( + userId = eq(DEFAULT_USER_ID), + desktopId = eq(6), + visibleTasks = any(), + minimizedTasks = any(), + freeformTasksInZOrder = any(), + ) + } + class TestListener : DesktopRepository.ActiveTasksListener { var activeChangesOnDefaultDisplay = 0 var activeChangesOnSecondaryDisplay = 0 + var activeChangesOnThirdDisplay = 0 override fun onActiveTasksChanged(displayId: Int) { when (displayId) { DEFAULT_DISPLAY -> activeChangesOnDefaultDisplay++ SECOND_DISPLAY -> activeChangesOnSecondaryDisplay++ + THIRD_DISPLAY -> activeChangesOnThirdDisplay++ else -> fail("Active task listener received unexpected display id: $displayId") } } @@ -1184,9 +1448,11 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { class TestVisibilityListener : DesktopRepository.VisibleTasksListener { var visibleTasksCountOnDefaultDisplay = 0 var visibleTasksCountOnSecondaryDisplay = 0 + var visibleTasksCountOnThirdDisplay = 0 var visibleChangesOnDefaultDisplay = 0 var visibleChangesOnSecondaryDisplay = 0 + var visibleChangesOnThirdDisplay = 0 override fun onTasksVisibilityChanged(displayId: Int, visibleTasksCount: Int) { when (displayId) { @@ -1198,6 +1464,10 @@ class DesktopRepositoryTest(flags: FlagsParameterization) : ShellTestCase() { visibleTasksCountOnSecondaryDisplay = visibleTasksCount visibleChangesOnSecondaryDisplay++ } + THIRD_DISPLAY -> { + visibleTasksCountOnThirdDisplay = visibleTasksCount + visibleChangesOnThirdDisplay++ + } else -> fail("Visible task listener received unexpected display id: $displayId") } } 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 12c7ff61399f..50590f021a2a 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 @@ -18,7 +18,6 @@ package com.android.wm.shell.desktopmode import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -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 @@ -26,7 +25,6 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFullscreenTask import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -44,8 +42,6 @@ import org.mockito.kotlin.whenever @RunWith(AndroidTestingRunner::class) class DesktopTaskChangeListenerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener private val desktopUserRepositories = mock<DesktopUserRepositories>() 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 a55cdb34c2fe..fcd92ac2678a 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 @@ -29,12 +29,15 @@ import android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.content.ComponentName +import android.content.Context import android.content.Intent import android.content.pm.ActivityInfo import android.content.pm.ActivityInfo.CONFIG_DENSITY import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_LANDSCAPE import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_PORTRAIT import android.content.pm.ActivityInfo.SCREEN_ORIENTATION_UNSPECIFIED +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager import android.content.res.Configuration.ORIENTATION_LANDSCAPE import android.content.res.Configuration.ORIENTATION_PORTRAIT import android.content.res.Resources @@ -49,7 +52,8 @@ import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.FlagsParameterization -import android.platform.test.flag.junit.SetFlagsRule +import android.testing.TestableContext +import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.view.DragEvent import android.view.Gravity @@ -66,6 +70,7 @@ import android.widget.Toast import android.window.DisplayAreaInfo import android.window.IWindowContainerToken import android.window.RemoteTransition +import android.window.TransitionInfo import android.window.TransitionRequestInfo import android.window.WindowContainerToken import android.window.WindowContainerTransaction @@ -118,7 +123,9 @@ import com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler.FULLSCR import com.android.wm.shell.desktopmode.common.ToggleTaskSizeInteraction import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler +import com.android.wm.shell.desktopmode.multidesks.DeskTransition import com.android.wm.shell.desktopmode.multidesks.DesksOrganizer +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer @@ -143,8 +150,8 @@ import com.android.wm.shell.transition.TestRemoteTransition import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.ENABLE_SHELL_TRANSITIONS import com.android.wm.shell.transition.Transitions.TransitionHandler -import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration -import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME +import com.android.wm.shell.windowdecor.tiling.SnapEventHandler import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import java.util.Optional @@ -165,14 +172,13 @@ import kotlinx.coroutines.test.setMain import org.junit.After import org.junit.Assume.assumeTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith -import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.isA import org.mockito.ArgumentMatchers.isNull import org.mockito.Mock import org.mockito.Mockito +import org.mockito.Mockito.anyBoolean import org.mockito.Mockito.anyInt import org.mockito.Mockito.clearInvocations import org.mockito.Mockito.mock @@ -181,9 +187,9 @@ import org.mockito.Mockito.times import org.mockito.Mockito.verify import org.mockito.kotlin.any import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.argThat import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.atLeastOnce -import org.mockito.kotlin.capture import org.mockito.kotlin.eq import org.mockito.kotlin.whenever import org.mockito.quality.Strictness @@ -201,13 +207,12 @@ import platform.test.runner.parameterized.Parameters @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule(flags) - @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 display: Display @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var syncQueue: SyncTransactionQueue @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer @@ -228,6 +233,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Mock lateinit var multiInstanceHelper: MultiInstanceHelper @Mock lateinit var desktopModeVisualIndicator: DesktopModeVisualIndicator @Mock lateinit var recentTasksController: RecentTasksController + @Mock lateinit var snapEventHandler: SnapEventHandler @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var mockSurface: SurfaceControl @Mock private lateinit var taskbarDesktopTaskListener: TaskbarDesktopTaskListener @@ -240,9 +246,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @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 bubbleController: BubbleController - @Mock private lateinit var desktopWindowDecoration: DesktopModeWindowDecoration @Mock private lateinit var resources: Resources @Mock lateinit var desktopModeEnterExitTransitionListener: DesktopModeEntryExitTransitionListener @@ -254,6 +258,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var overviewToDesktopTransitionObserver: OverviewToDesktopTransitionObserver @Mock private lateinit var desksOrganizer: DesksOrganizer @Mock private lateinit var userProfileContexts: UserProfileContexts + @Mock private lateinit var desksTransitionsObserver: DesksTransitionObserver + @Mock private lateinit var packageManager: PackageManager + @Mock private lateinit var mockDisplayContext: Context private lateinit var controller: DesktopTasksController private lateinit var shellInit: ShellInit @@ -263,8 +270,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() private lateinit var recentsTransitionStateListener: RecentsTransitionStateListener private lateinit var testScope: CoroutineScope private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy + private lateinit var spyContext: TestableContext private val shellExecutor = TestShellExecutor() + private val bgExecutor = TestShellExecutor() // Mock running tasks are registered here so we can get the list from mock shell task organizer private val runningTasks = mutableListOf<RunningTaskInfo>() @@ -276,9 +285,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() 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_LANDSCAPE_BOUNDS = Rect(25, 448, 1575, 1611) private val UNRESIZABLE_PORTRAIT_BOUNDS = Rect(830, 75, 1730, 1275) private val wallpaperToken = MockToken().token() + private val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") @Before fun setUp() { @@ -289,9 +299,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .spyStatic(DesktopModeStatus::class.java) .spyStatic(Toast::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + spyContext = spy(mContext) shellInit = spy(ShellInit(testExecutor)) userRepositories = DesktopUserRepositories( @@ -313,12 +324,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mContext, mockHandler, ) - desktopModeCompatPolicy = DesktopModeCompatPolicy(context) + desktopModeCompatPolicy = spy(DesktopModeCompatPolicy(spyContext)) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } - whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } + whenever(transitions.startTransition(anyInt(), any(), anyOrNull())).thenAnswer { Binder() } whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenAnswer { Binder() } + whenever(exitDesktopTransitionHandler.startTransition(any(), any(), any(), any())) + .thenReturn(Binder()) whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayController.getDisplayContext(anyInt())).thenReturn(mockDisplayContext) + whenever(displayController.getDisplay(anyInt())).thenReturn(display) whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } @@ -351,6 +366,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.NoExit) whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(wallpaperToken) whenever(userProfileContexts[anyInt()]).thenReturn(context) + whenever(userProfileContexts.getOrCreate(anyInt())).thenReturn(context) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -359,17 +375,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shellInit.init() - val captor = ArgumentCaptor.forClass(RecentsTransitionStateListener::class.java) + val captor = argumentCaptor<RecentsTransitionStateListener>() verify(recentsTransitionHandler).addTransitionStateListener(captor.capture()) - recentsTransitionStateListener = captor.value + recentsTransitionStateListener = captor.firstValue controller.taskbarDesktopTaskListener = taskbarDesktopTaskListener + controller.setSnapEventHandler(snapEventHandler) assumeTrue(ENABLE_SHELL_TRANSITIONS) taskRepository = userRepositories.current taskRepository.addDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) taskRepository.setActiveDesk(displayId = DEFAULT_DISPLAY, deskId = DEFAULT_DISPLAY) + + spyContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(homeComponentName) } private fun createController() = @@ -397,17 +417,18 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() recentsTransitionHandler, multiInstanceHelper, shellExecutor, + bgExecutor, Optional.of(desktopTasksLimiter), recentTasksController, mockInteractionJankMonitor, mockHandler, desktopModeEventLogger, desktopModeUiEventLogger, - desktopTilingDecorViewModel, desktopWallpaperActivityTokenProvider, Optional.of(bubbleController), overviewToDesktopTransitionObserver, desksOrganizer, + desksTransitionsObserver, userProfileContexts, desktopModeCompatPolicy, ) @@ -436,7 +457,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun doesAnyTaskRequireTaskbarRounding_toggleResizeOfFreeFormTask_returnTrue() { val task1 = setUpFreeformTask() - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -456,7 +477,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() STABLE_BOUNDS.height(), displayController, ) - assertThat(argumentCaptor.value).isTrue() + assertThat(argumentCaptor.firstValue).isTrue() } @Test @@ -471,7 +492,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val stableBounds = Rect().apply { displayLayout.getStableBounds(this) } val task1 = setUpFreeformTask(bounds = stableBounds, active = true) - val argumentCaptor = ArgumentCaptor.forClass(Boolean::class.java) + val argumentCaptor = argumentCaptor<Boolean>() controller.toggleDesktopTaskSize( task1, ToggleTaskSizeInteraction( @@ -492,7 +513,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(displayController), anyOrNull(), ) - assertThat(argumentCaptor.value).isFalse() + assertThat(argumentCaptor.firstValue).isFalse() } @Test @@ -516,7 +537,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -537,7 +561,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_allAppsInvisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -555,6 +581,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun showDesktopApps_deskInactive_bringsToFront_multipleDesksEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + val deskId = 0 + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + // Wallpaper is moved to front. + wct.assertReorderAt(index = 0, wallpaperToken) + // Desk is activated. + verify(desksOrganizer).activateDesk(wct, deskId) + } + + @Test fun isDesktopModeShowing_noTasks_returnsFalse() { assertThat(controller.isDesktopModeShowing(displayId = 0)).isFalse() } @@ -628,58 +677,83 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, ) - @DisableFlags( - /** TODO: b/362720497 - re-enable when activation is implemented. */ - Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND - ) - fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_bringsTasksToFront() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) - val homeTask = setUpHomeTask(SECOND_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) val task1 = setUpFreeformTask(SECOND_DISPLAY) val task2 = setUpFreeformTask(SECOND_DISPLAY) markTaskHidden(task1) markTaskHidden(task2) + assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task1.taskId) + assertThat(taskRepository.getExpandedTasksOrdered(SECOND_DISPLAY)).contains(task2.taskId) controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) - assertThat(wct.hierarchyOps).hasSize(4) - // Expect order to be from bottom: home, wallpaperIntent, task1, task2 - wct.assertReorderAt(index = 0, homeTask) - wct.assertPendingIntentAt(index = 1, desktopWallpaperIntent) - wct.assertReorderAt(index = 2, task1) - wct.assertReorderAt(index = 3, task2) + wct.assertReorder(task1) + wct.assertReorder(task2) } @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - @DisableFlags( + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, - /** TODO: b/362720497 - re-enable when activation is implemented. */ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_multipleDesksEnabled_bringsDeskToFront() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = 2) + setUpHomeTask(SECOND_DISPLAY) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + verify(desksOrganizer).activateDesk(wct, deskId = 2) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY, + ) + fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_perDisplayWallpaperEnabled_shouldShowWallpaper() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + setUpHomeTask(SECOND_DISPLAY) + + controller.showDesktopApps(SECOND_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertPendingIntent(desktopWallpaperIntent) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_PER_DISPLAY_DESKTOP_WALLPAPER_ACTIVITY) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperEnabled_shouldNotShowWallpaper() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) 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) + wct.assertWithoutPendingIntent(desktopWallpaperIntent) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -701,7 +775,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @DisableFlags( Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, - /** TODO: b/362720497 - re-enable when activation is implemented. */ Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, ) fun showDesktopApps_onSecondaryDisplay_desktopWallpaperDisabled_shouldNotMoveLauncher() { @@ -725,7 +798,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_appsAlreadyVisible_bringsToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskVisible(task1) @@ -743,7 +818,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() @@ -764,7 +842,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_someAppsInvisible_reordersAll_desktopWallpaperEnabled() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersOnlyFreeformTasks() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() markTaskHidden(task1) @@ -781,8 +861,29 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() wct.assertReorderAt(index = 2, task2) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_someAppsInvisible_desktopWallpaperEnabled_reordersAll() { + 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.assertReorderAt(index = 0, wallpaperToken) + wct.assertReorderAt(index = 1, task1) + wct.assertReorderAt(index = 2, task2) + } + @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_noActiveTasks_reorderHomeToTop_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() @@ -796,7 +897,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) - fun showDesktopApps_noActiveTasks_addDesktopWallpaper_desktopWallpaperEnabled() { + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_addsDesktopWallpaper() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) val wct = @@ -805,7 +908,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + fun showDesktopApps_noActiveTasks_desktopWallpaperEnabled_reordersDesktopWallpaper() { + controller.showDesktopApps(DEFAULT_DISPLAY, RemoteTransition(TestRemoteTransition())) + + val wct = + getLatestWct(type = TRANSIT_TO_FRONT, handlerClass = OneShotRemoteHandler::class.java) + wct.assertReorderAt(index = 0, wallpaperToken) + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperDisabled() { taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) @@ -828,6 +945,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplay_desktopWallpaperEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) val homeTaskDefaultDisplay = setUpHomeTask(DEFAULT_DISPLAY) val taskDefaultDisplay = setUpFreeformTask(DEFAULT_DISPLAY) @@ -840,17 +960,63 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() 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) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksDisabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + 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) // Move freeform task to front wct.assertReorderAt(index = 2, taskDefaultDisplay) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun showDesktopApps_twoDisplays_bringsToFrontOnlyOneDisplayTasks_desktopWallpaperEnabled_multiDesksEnabled() { + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(Binder()) + taskRepository.addDesk(displayId = SECOND_DISPLAY, deskId = SECOND_DISPLAY) + 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) + // Move desktop tasks to front + verify(desksOrganizer).activateDesk(wct, deskId = DEFAULT_DISPLAY) + } + + @Test + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun showDesktopApps_desktopWallpaperDisabled_dontReorderMinimizedTask() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() @@ -871,7 +1037,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + /** TODO: b/362720497 - add multi-desk version when minimization is implemented. */ + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun showDesktopApps_desktopWallpaperEnabled_dontReorderMinimizedTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val minimizedTask = setUpFreeformTask() @@ -1185,6 +1354,50 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + fun addMoveToDesktopChanges_excludeCaptionFromAppBounds_nonResizableLandscape() { + setUpLandscapeDisplay() + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_LANDSCAPE, + ) + whenever(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(task)).thenReturn(true) + val initialAspectRatio = calculateAspectRatio(task) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + val captionInsets = getAppHeaderHeight(context) + finalBounds!!.top += captionInsets + val finalAspectRatio = + maxOf(finalBounds.height(), finalBounds.width()) / + minOf(finalBounds.height(), finalBounds.width()).toFloat() + assertThat(finalAspectRatio).isWithin(FLOAT_TOLERANCE).of(initialAspectRatio) + } + + @Test + fun addMoveToDesktopChanges_excludeCaptionFromAppBounds_nonResizablePortrait() { + setUpLandscapeDisplay() + val task = + setUpFullscreenTask( + isResizable = false, + screenOrientation = SCREEN_ORIENTATION_PORTRAIT, + ) + whenever(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds(task)).thenReturn(true) + val initialAspectRatio = calculateAspectRatio(task) + val wct = WindowContainerTransaction() + controller.addMoveToDesktopChanges(wct, task) + + val finalBounds = findBoundsChange(wct, task) + val captionInsets = getAppHeaderHeight(context) + finalBounds!!.top += captionInsets + val finalAspectRatio = + maxOf(finalBounds.height(), finalBounds.width()) / + minOf(finalBounds.height(), finalBounds.width()).toFloat() + assertThat(finalAspectRatio).isWithin(FLOAT_TOLERANCE).of(initialAspectRatio) + } + + @Test fun launchIntent_taskInDesktopMode_transitionStarted() { setUpLandscapeDisplay() val freeformTask = setUpFreeformTask() @@ -1287,11 +1500,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) 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) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1300,11 +1514,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) 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) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) @@ -1313,11 +1528,78 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveTaskToDesktop_nonExistentTask_doesNothing() { - controller.moveTaskToDesktop(999, transitionSource = UNKNOWN) - verifyEnterDesktopWCTNotExecuted() - verify(desktopModeEnterExitTransitionListener, times(0)) - .onEnterDesktopModeTransitionStarted(anyInt()) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_movesTaskToDefaultDesk() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_activatesDesk() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).activateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_triggersEnterDesktopListener() { + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_movesTaskToDesk() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 3, task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_activatesDesk() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).activateDesk(wct, deskId = 3) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveTaskToDesk_nonDefaultDesk_triggersEnterDesktopListener() { + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) } @Test @@ -1327,7 +1609,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) @@ -1337,11 +1619,12 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveTaskToDesktop_desktopWallpaperEnabled_nonRunningTask_launchesInFreeform() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createTaskInfo(1) whenever(shellTaskOrganizer.getRunningTaskInfo(anyInt())).thenReturn(null) whenever(recentTasksController.findTaskInBackground(anyInt())).thenReturn(task) - controller.moveTaskToDesktop(task.taskId, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) with(getLatestEnterDesktopWct()) { // Add desktop wallpaper activity @@ -1353,7 +1636,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksDisabled() { val task = setUpFullscreenTask().apply { isActivityStackTransparent = true @@ -1361,7 +1645,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() numActivities = 1 } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1370,6 +1654,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveRunningTaskToDesktop_topActivityTranslucentWithoutDisplay_taskIsMovedToDesktop_multiDesksEnabled() { + val task = + setUpFullscreenTask().apply { + isActivityStackTransparent = true + isTopActivityNoDisplay = true + numActivities = 1 + } + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) fun moveRunningTaskToDesktop_topActivityTranslucentWithDisplay_doesNothing() { val task = @@ -1379,7 +1683,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() numActivities = 1 } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() verify(desktopModeEnterExitTransitionListener, times(0)) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) @@ -1398,13 +1702,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() isTopActivityNoDisplay = false } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) verifyEnterDesktopWCTNotExecuted() } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) - fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksDisabled() { // Set task as systemUI package val systemUIPackageName = context.resources.getString(com.android.internal.R.string.config_systemUi) @@ -1415,7 +1720,42 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() isTopActivityNoDisplay = true } - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_defaultHomePackageWithDisplay_doesNothing() { + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + verifyEnterDesktopWCTNotExecuted() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun moveRunningTaskToDesktop_defaultHomePackageWithoutDisplay_doesNothing() { + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) @@ -1423,16 +1763,38 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveRunningTaskToDesktop_systemUIActivityWithoutDisplay_doesNothing_multiDesksEnabled() { + // Set task as systemUI package + val systemUIPackageName = + context.resources.getString(com.android.internal.R.string.config_systemUi) + val baseComponent = ComponentName(systemUIPackageName, /* cls= */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = baseComponent + isTopActivityNoDisplay = true + } + + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task = task) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun moveBackgroundTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() 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( + controller.moveTaskToDefaultDeskAndActivate( taskId = task.taskId, transitionSource = UNKNOWN, remoteTransition = RemoteTransition(spy(TestRemoteTransition())), @@ -1440,35 +1802,41 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveRunningTaskToDesktop_remoteTransition_usesOneShotHandler() { - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) - controller.moveRunningTaskToDesktop( - task = setUpFullscreenTask(), + controller.moveTaskToDefaultDeskAndActivate( + taskId = setUpFullscreenTask().taskId, transitionSource = UNKNOWN, remoteTransition = RemoteTransition(spy(TestRemoteTransition())), ) verify(desktopModeEnterExitTransitionListener) .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperDisabled() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Operations should include home task, freeform task @@ -1483,12 +1851,17 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_otherFreeformTasksBroughtToFront_desktopWallpaperEnabled() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask = setUpFreeformTask() val fullscreenTask = setUpFullscreenTask() markTaskHidden(freeformTask) - controller.moveRunningTaskToDesktop(fullscreenTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Operations should include wallpaper intent, freeform task, fullscreen task @@ -1504,6 +1877,44 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) + fun moveRunningTaskToDesktop_desktopWallpaperEnabled_multiDesksEnabled() { + val freeformTask = setUpFreeformTask() + val fullscreenTask = setUpFullscreenTask() + markTaskHidden(freeformTask) + + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) + + val wct = getLatestEnterDesktopWct() + wct.assertReorderAt(index = 0, wallpaperToken) + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, fullscreenTask) + verify(desksOrganizer).activateDesk(wct, deskId = 0) + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_activatesDesk_desktopWallpaperEnabled_multiDesksDisabled() { + val fullscreenTask = setUpFullscreenTask() + + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTask.taskId, + transitionSource = UNKNOWN, + ) + + assertThat(taskRepository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(DEFAULT_DISPLAY) + } + + @Test fun moveRunningTaskToDesktop_onlyFreeformTasksFromCurrentDisplayBroughtToFront() { setUpHomeTask(displayId = DEFAULT_DISPLAY) val freeformTaskDefault = setUpFreeformTask(displayId = DEFAULT_DISPLAY) @@ -1515,7 +1926,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val freeformTaskSecond = setUpFreeformTask(displayId = SECOND_DISPLAY) markTaskHidden(freeformTaskSecond) - controller.moveRunningTaskToDesktop(fullscreenTaskDefault, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + fullscreenTaskDefault.taskId, + transitionSource = UNKNOWN, + ) with(getLatestEnterDesktopWct()) { // Check that hierarchy operations do not include tasks from second display @@ -1529,9 +1943,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveRunningTaskToDesktop_splitTaskExitsSplit() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksDisabled() { val task = setUpSplitScreenTask() - controller.moveRunningTaskToDesktop(task, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() assertThat(wct.changes[task.token.asBinder()]?.windowingMode) .isEqualTo(WINDOWING_MODE_FREEFORM) @@ -1546,12 +1961,27 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveRunningTaskToDesktop_splitTaskExitsSplit_multiDesksEnabled() { + val task = setUpSplitScreenTask() + controller.moveTaskToDefaultDeskAndActivate(task.taskId, transitionSource = UNKNOWN) + val wct = getLatestEnterDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task) + 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) + controller.moveTaskToDefaultDeskAndActivate(task.taskId, 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()) @@ -1563,13 +1993,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) fun moveRunningTaskToDesktop_desktopWallpaperDisabled_bringsTasksOver_dontShowBackTask() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() verify(desktopModeEnterExitTransitionListener) @@ -1585,12 +2018,14 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveRunningTaskToDesktop_desktopWallpaperEnabled_bringsTasksOverLimit_dontShowBackTask() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val newTask = setUpFullscreenTask() val homeTask = setUpHomeTask() - controller.moveRunningTaskToDesktop(newTask, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate(newTask.taskId, transitionSource = UNKNOWN) val wct = getLatestEnterDesktopWct() verify(desktopModeEnterExitTransitionListener) @@ -1609,6 +2044,30 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_reparentsToTaskDisplayArea() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + wct.assertHop(ReparentPredicate(token = task.token, parentToken = tda.token, toTop = true)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_fromDesk_deactivatesDesk() { + val task = setUpFreeformTask() + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test fun moveToFullscreen_tdaFullscreen_windowingModeSetToUndefined() { val task = setUpFreeformTask() val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! @@ -1623,7 +2082,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() val task = setUpFreeformTask() assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) .configuration @@ -1637,9 +2099,87 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_UNDEFINED) - assertThat(wct.hierarchyOps).hasSize(1) + assertThat(wct.hierarchyOps).hasSize(3) // Removes wallpaper activity when leaving desktop wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 1, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 2, task.getToken(), toTop = true) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_removesWallpaperActivity_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + setUpHomeTask() + val task = setUpFreeformTask() + 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.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFullscreen_windowingModeUndefined_homeBehindFullscreen_multiDesksEnabled() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + 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) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + fun moveToFullscreen_tdaFreeform_enforcedDesktop_doesNotReorderHome() { + whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) + .configuration + .windowConfiguration + .windowingMode = WINDOWING_MODE_FREEFORM + + controller.moveToFullscreen(task.taskId, transitionSource = UNKNOWN) + + val wct = getLatestExitDesktopWct() + verify(desktopModeEnterExitTransitionListener) + .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) + // Removes wallpaper activity when leaving desktop but doesn't reorder home or the task + wct.assertReorder(wallpaperToken, toTop = false) + wct.assertWithoutHop(ReorderPredicate(homeTask.token, toTop = null)) } @Test @@ -1657,7 +2197,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER) + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity() { + val homeTask = setUpHomeTask() val task = setUpFreeformTask() assertNotNull(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)) @@ -1672,13 +2214,72 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() assertThat(taskChange.windowingMode).isEqualTo(WINDOWING_MODE_FULLSCREEN) verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) - assertThat(wct.hierarchyOps).hasSize(1) + assertThat(wct.hierarchyOps).hasSize(3) // Removes wallpaper activity when leaving desktop wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 1, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 2, task.getToken(), toTop = true) } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_removesWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + 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.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveToFullscreen_tdaFreeform_windowingModeFullscreen_homeBehindFullscreen_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task = setUpFreeformTask() + + 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) + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity() { + val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() // Setup task2 setUpFreeformTask() @@ -1696,7 +2297,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() verify(desktopModeEnterExitTransitionListener) .onExitDesktopModeTransitionStarted(FULLSCREEN_ANIMATION_DURATION) // Does not remove wallpaper activity, as desktop still has a visible desktop task - assertThat(wct.hierarchyOps).isEmpty() + assertThat(wct.hierarchyOps).hasSize(2) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 0, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 1, task1.getToken(), toTop = true) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveToFullscreen_multipleVisibleNonMinimizedTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + val task1 = setUpFreeformTask() + // Setup task2 + setUpFreeformTask() + + val tdaInfo = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY) + assertNotNull(tdaInfo).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 + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = false)) } @Test @@ -1809,26 +2436,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun moveTaskToFront_remoteTransition_usesOneshotHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.value) + assertIs<OneShotRemoteHandler>(transitionHandlerArgCaptor.firstValue) } @Test fun moveTaskToFront_bringsTasksOverLimit_remoteTransition_usesWindowLimitHandler() { setUpHomeTask() val freeformTasks = List(MAX_TASK_LIMIT + 1) { setUpFreeformTask() } - val transitionHandlerArgCaptor = ArgumentCaptor.forClass(TransitionHandler::class.java) + val transitionHandlerArgCaptor = argumentCaptor<TransitionHandler>() whenever(transitions.startTransition(anyInt(), any(), transitionHandlerArgCaptor.capture())) .thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], RemoteTransition(TestRemoteTransition())) - assertThat(transitionHandlerArgCaptor.value) + assertThat(transitionHandlerArgCaptor.firstValue) .isInstanceOf(DesktopWindowLimitRemoteHandler::class.java) } @@ -2273,7 +2900,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + @EnableFlags( + FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) fun onDesktopWindowClose_minimizedPipNotPresent_exitDesktop() { val freeformTask = setUpFreeformTask() val pipTask = setUpPipTask(autoEnterEnabled = true) @@ -2288,29 +2918,102 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val wct = WindowContainerTransaction() controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, freeformTask) - // Remove wallpaper operation - wct.hierarchyOps.any { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() - } + // Moves wallpaper activity to back when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + + verify(desksOrganizer).deactivateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowClose_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val wct = WindowContainerTransaction() + + val transition = Binder() + val runOnTransitStart = + controller.onDesktopWindowClose(wct, displayId = DEFAULT_DISPLAY, task) + runOnTransitStart(transition) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(transition, deskId = 0)) } @Test fun onDesktopWindowMinimize_noActiveTask_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = false) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false)) + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_deactivatesDesk() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) + verify(desksOrganizer).deactivateDesk(captor.firstValue, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onDesktopWindowMinimize_lastWindow_addsPendingDeactivateTransition() { + val task = setUpFreeformTask() + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + verify(desksTransitionsObserver) + .addPendingTransition(DeskTransition.DeactivateDesk(token = transition, deskId = 0)) + } + + @Test fun onPipTaskMinimize_autoEnterEnabled_startPipTransition() { val task = setUpPipTask(autoEnterEnabled = true) val handler = mock(TransitionHandler::class.java) @@ -2320,18 +3023,26 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) verify(freeformTaskTransitionStarter).startPipTransition(any()) - verify(freeformTaskTransitionStarter, never()).startMinimizedModeTransition(any()) + verify(freeformTaskTransitionStarter, never()) + .startMinimizedModeTransition(any(), anyInt(), anyBoolean()) } @Test fun onPipTaskMinimize_autoEnterDisabled_startMinimizeTransition() { val task = setUpPipTask(autoEnterEnabled = false) - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(Binder()) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(any()) + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(any(), eq(task.taskId), anyBoolean()) verify(freeformTaskTransitionStarter, never()).startPipTransition(any()) } @@ -2344,9 +3055,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val captor = argumentCaptor<WindowContainerTransaction>() verify(freeformTaskTransitionStarter).startPipTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2355,14 +3066,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onDesktopWindowMinimize_singleActiveTask_noWallpaperActivityToken_doesntRemoveWallpaper() { val task = setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK } } @Test @@ -2370,32 +3088,46 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) // The only active task is being minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(true)) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test fun onDesktopWindowMinimize_singleActiveTask_alreadyMinimized_doesntRemoveWallpaper() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) taskRepository.minimizeTask(DEFAULT_DISPLAY, task.taskId) // The only active task is already minimized. controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task.taskId), eq(false)) + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2405,14 +3137,21 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(active = true) setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) - captor.value.hierarchyOps.none { hop -> + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(false)) + captor.firstValue.hierarchyOps.none { hop -> hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() } } @@ -2423,24 +3162,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task1 = setUpFreeformTask(active = true) val task2 = setUpFreeformTask(active = true) val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) // task1 is the only visible task as task2 is minimized. controller.minimizeTask(task1, MinimizeReason.MINIMIZE_BUTTON) // Adds remove wallpaper operation - val captor = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(freeformTaskTransitionStarter).startMinimizedModeTransition(captor.capture()) + val captor = argumentCaptor<WindowContainerTransaction>() + verify(freeformTaskTransitionStarter) + .startMinimizedModeTransition(captor.capture(), eq(task1.taskId), eq(true)) // Adds remove wallpaper operation - captor.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + captor.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test fun onDesktopWindowMinimize_triesToExitImmersive() { val task = setUpFreeformTask() val transition = Binder() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) @@ -2453,7 +3205,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val task = setUpFreeformTask() val transition = Binder() val runOnTransit = RunOnStartTransitionCallback() - whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) .thenReturn(transition) whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task), any())) .thenReturn( @@ -2467,6 +3225,24 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + fun onDesktopWindowMinimize_triesToStopTiling() { + val task = setUpFreeformTask(displayId = DEFAULT_DISPLAY) + val transition = Binder() + whenever( + freeformTaskTransitionStarter.startMinimizedModeTransition( + any(), + anyInt(), + anyBoolean(), + ) + ) + .thenReturn(transition) + + controller.minimizeTask(task, MinimizeReason.MINIMIZE_BUTTON) + + verify(snapEventHandler).removeTaskIfTiled(eq(DEFAULT_DISPLAY), eq(task.taskId)) + } + + @Test fun handleRequest_fullscreenTask_freeformVisible_returnSwitchToFreeformWCT() { val homeTask = setUpHomeTask() val freeformTask = setUpFreeformTask() @@ -2572,6 +3348,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_fullscreenTask_noTasks_enforceDesktop_freeformDisplay_returnFreeformWCT() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FREEFORM @@ -2703,6 +3480,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_freeformNotVisible_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val freeformTask1 = setUpFreeformTask() val freeformTask2 = createFreeformTask() @@ -2737,7 +3515,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_desktopWallpaperEnabled_noOtherTasks_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val task = createFreeformTask() + val result = controller.handleRequest(Binder(), createTransition(task)) assertNotNull(result, "Should handle request") @@ -2765,6 +3545,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY) fun handleRequest_freeformTask_dskWallpaperEnabled_freeformOnOtherDisplayOnly_reorderedToTop() { + whenever(desktopWallpaperActivityTokenProvider.getToken()).thenReturn(null) val taskDefaultDisplay = createFreeformTask(displayId = DEFAULT_DISPLAY) // Second display task createFreeformTask(displayId = SECOND_DISPLAY) @@ -3018,6 +3799,46 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .isEqualTo(WINDOWING_MODE_FREEFORM) } + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_defaultHomePackageWithDisplay_returnSwitchToFullscreenWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun handleRequest_defaultHomePackageWithoutDisplay_returnSwitchToFreeformWCT() { + val freeformTask = setUpFreeformTask() + markTaskVisible(freeformTask) + + val packageManager: PackageManager = org.mockito.kotlin.mock() + val homeActivities = ComponentName("defaultHomePackage", /* class */ "") + val task = + setUpFullscreenTask().apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + } + mContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + + val result = controller.handleRequest(Binder(), createTransition(task)) + assertThat(result?.changes?.get(task.token.asBinder())?.windowingMode) + .isEqualTo(WINDOWING_MODE_FREEFORM) + } + @Test fun handleRequest_systemUIActivityWithDisplay_returnSwitchToFullscreenWCT_enforcedDesktop() { whenever(DesktopModeStatus.enterDesktopByDefaultOnFreeformDisplay(context)).thenReturn(true) @@ -3337,7 +4158,8 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksDisabled() { val task1 = setUpFullscreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -3354,7 +4176,25 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop() { + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_fullscreenTaskIsMovedToDesktop_multiDesksEnabled() { + 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() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task1) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksDisabled() { val task1 = setUpSplitScreenTask() val task2 = setUpFullscreenTask() val task3 = setUpFullscreenTask() @@ -3381,6 +4221,33 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToDesktop_splitScreenTaskIsMovedToDesktop_multiDesksEnabled() { + 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() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task4) + verify(splitScreenController) + .prepareExitSplitScreen( + any(), + anyInt(), + eq(SplitScreenController.EXIT_REASON_DESKTOP_MODE), + ) + } + + @Test fun moveFocusedTaskToFullscreen() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -3416,11 +4283,13 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - wct.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wct.assertReorder(wallpaperToken, toTop = false) } @Test + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity() { + val homeTask = setUpHomeTask() val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() @@ -3435,11 +4304,63 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() 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() + assertThat(wct.hierarchyOps).hasSize(2) + // Moves home task behind the fullscreen task + wct.assertReorderAt(index = 0, homeTask.getToken(), toTop = true) + wct.assertReorderAt(index = 1, task2.getToken(), toTop = true) } @Test - @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PIP) + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_doesNotRemoveWallpaperActivity_multiDesksEnabled() { + val homeTask = setUpHomeTask() + 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() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Does not remove wallpaper activity + wct.assertWithoutHop(ReorderPredicate(wallpaperToken, toTop = null)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun moveFocusedTaskToFullscreen_multipleVisibleTasks_fullscreenOverHome_multiDesksEnabled() { + val homeTask = setUpHomeTask() + 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() + val taskChange = assertNotNull(wct.changes[task2.token.asBinder()]) + assertThat(taskChange.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN + // Moves home task behind the fullscreen task + val homeReorderIndex = wct.indexOfReorder(homeTask, toTop = true) + val fullscreenReorderIndex = wct.indexOfReorder(task2, toTop = true) + assertThat(homeReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isNotEqualTo(-1) + assertThat(fullscreenReorderIndex).isGreaterThan(homeReorderIndex) + } + + @Test + @EnableFlags( + FLAG_ENABLE_DESKTOP_WINDOWING_PIP, + Flags.FLAG_ENABLE_DESKTOP_WALLPAPER_ACTIVITY_FOR_SYSTEM_USER, + ) fun moveFocusedTaskToFullscreen_minimizedPipPresent_removeWallpaperActivity() { val freeformTask = setUpFreeformTask() val pipTask = setUpPipTask(autoEnterEnabled = true) @@ -3457,21 +4378,20 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val taskChange = assertNotNull(wct.changes[freeformTask.token.asBinder()]) assertThat(taskChange.windowingMode) .isEqualTo(WINDOWING_MODE_UNDEFINED) // inherited FULLSCREEN - // Remove wallpaper operation - wct.hierarchyOps.any { hop -> - hop.type == HIERARCHY_OP_TYPE_REMOVE_TASK && hop.container == wallpaperToken.asBinder() - } + // Moves wallpaper activity to back when leaving desktop + wct.assertReorder(wallpaperToken, toTop = false) } @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasks_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_multipleTasks_removesAll() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() val task3 = setUpFreeformTask() taskRepository.minimizeTask(DEFAULT_DISPLAY, task2.taskId) - controller.removeDesktop(displayId = DEFAULT_DISPLAY) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(3) @@ -3482,14 +4402,15 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - fun removeDesktop_multipleTasksWithBackgroundTask_removesAll() { + @DisableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun removeDesk_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) + controller.removeDefaultDeskInDisplay(displayId = DEFAULT_DISPLAY) val wct = getLatestWct(TRANSIT_CLOSE) assertThat(wct.hierarchyOps).hasSize(2) @@ -3499,6 +4420,83 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun removeDesk_multipleDesks_addsPendingTransition() { + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_CLOSE), any(), anyOrNull())) + .thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 2) + + controller.removeDesk(deskId = 2) + + verify(desksOrganizer).removeDesk(any(), eq(2)) + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.RemoveDesk && + this.token == transition && + this.deskId == 2 + } + ) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun activateDesk_multipleDesks_addsPendingTransition() { + val deskId = 0 + val transition = Binder() + val deskChange = mock(TransitionInfo.Change::class.java) + whenever(transitions.startTransition(eq(TRANSIT_TO_FRONT), any(), anyOrNull())) + .thenReturn(transition) + whenever(desksOrganizer.isDeskActiveAtEnd(deskChange, deskId)).thenReturn(true) + // Make desk inactive by activating another desk. + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 1) + taskRepository.setActiveDesk(DEFAULT_DISPLAY, deskId = 1) + + controller.activateDesk(deskId, RemoteTransition(TestRemoteTransition())) + + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.ActivateDesk && + this.token == transition && + this.deskId == 0 + } + ) + } + + @Test + @EnableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION, + Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND, + ) + fun moveTaskToDesk_multipleDesks_addsPendingTransition() { + val transition = Binder() + whenever(enterDesktopTransitionHandler.moveToDesktop(any(), any())).thenReturn(transition) + taskRepository.addDesk(DEFAULT_DISPLAY, deskId = 3) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + task.isVisible = true + + controller.moveTaskToDesk(taskId = task.taskId, deskId = 3, transitionSource = UNKNOWN) + + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.ActiveDeskWithTask && + this.token == transition && + this.deskId == 3 && + this.enterTaskId == task.taskId + } + ) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_WINDOWING_DYNAMIC_INITIAL_BOUNDS) fun dragToDesktop_landscapeDevice_resizable_undefinedOrientation_defaultLandscapeBounds() { val spyController = spy(controller) @@ -3585,7 +4583,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() shouldLetterbox = true, ) setUpLandscapeDisplay() - spyController.onDragPositioningEndThroughStatusBar(PointF(800f, 1280f), task, mockSurface) val wct = getLatestDragToDesktopWct() assertThat(findBoundsChange(wct, task)).isEqualTo(UNRESIZABLE_PORTRAIT_BOUNDS) @@ -3692,6 +4689,76 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun dragToDesktop_movesTaskToDesk() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) + + val wct = getLatestDragToDesktopWct() + verify(desksOrganizer).moveTaskToDesk(wct, deskId = 0, task) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun dragToDesktop_activatesDesk() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) + + val wct = getLatestDragToDesktopWct() + verify(desksOrganizer).activateDesk(wct, deskId = 0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun dragToDesktop_triggersEnterDesktopListener() { + val spyController = spy(controller) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) + + verify(desktopModeEnterExitTransitionListener) + .onEnterDesktopModeTransitionStarted(FREEFORM_ANIMATION_DURATION) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun dragToDesktop_multipleDesks_addsPendingTransition() { + val transition = Binder() + val spyController = spy(controller) + whenever(dragToDesktopTransitionHandler.finishDragToDesktopTransition(any())) + .thenReturn(transition) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR) + val task = setUpFullscreenTask(displayId = DEFAULT_DISPLAY) + + spyController.onDragPositioningEndThroughStatusBar(PointF(200f, 200f), task, mockSurface) + + verify(desksTransitionsObserver) + .addPendingTransition( + argThat { + this is DeskTransition.ActiveDeskWithTask && + this.token == transition && + this.deskId == 0 && + this.enterTaskId == task.taskId + } + ) + } + + @Test fun onDesktopDragMove_endsOutsideValidDragArea_snapsToValidBounds() { val task = setUpFreeformTask() val spyController = spy(controller) @@ -3712,7 +4779,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) val rectAfterEnd = Rect(100, 50, 500, 1150) verify(transitions) @@ -3750,7 +4816,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -3790,7 +4855,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) verify(transitions) @@ -3831,7 +4895,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert the task exits desktop mode @@ -3869,7 +4932,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert bounds set to stable bounds @@ -3925,7 +4987,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() validDragArea = Rect(0, 50, 2000, 2000), dragStartBounds = Rect(), motionEvent, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -3982,7 +5043,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -3990,9 +5051,9 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(task2.configuration.windowConfiguration.bounds), ) - assertThat(wctArgument.value.hierarchyOps).hasSize(1) + assertThat(wctArgument.firstValue.hierarchyOps).hasSize(1) // Removes wallpaper activity when leaving desktop - wctArgument.value.assertReorderAt(index = 0, wallpaperToken, toTop = false) + wctArgument.firstValue.assertReorderAt(index = 0, wallpaperToken, toTop = false) } @Test @@ -4007,7 +5068,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) - val wctArgument = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val wctArgument = argumentCaptor<WindowContainerTransaction>() verify(splitScreenController) .requestEnterSplitSelect( eq(task2), @@ -4016,7 +5077,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(task2.configuration.windowConfiguration.bounds), ) // Does not remove wallpaper activity, as desktop still has visible desktop tasks - assertThat(wctArgument.value.hierarchyOps).isEmpty() + assertThat(wctArgument.firstValue.hierarchyOps).isEmpty() } @Test @@ -4024,7 +5085,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromFullscreenOpensInSplit() { setUpLandscapeDisplay() val task = setUpFullscreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4037,7 +5098,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4046,7 +5107,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() fun newWindow_fromSplitOpensInSplit() { setUpLandscapeDisplay() val task = setUpSplitScreenTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenNewWindow(task) verify(splitScreenController) .startIntent( @@ -4059,7 +5120,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() eq(true), eq(SPLIT_INDEX_UNDEFINED), ) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4154,11 +5215,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpFullscreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull(), any()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4168,11 +5229,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() setUpLandscapeDisplay() val task = setUpSplitScreenTask() val taskToRequest = setUpFreeformTask() - val optionsCaptor = ArgumentCaptor.forClass(Bundle::class.java) + val optionsCaptor = argumentCaptor<Bundle>() runOpenInstance(task, taskToRequest.taskId) verify(splitScreenController) - .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull()) - assertThat(ActivityOptions.fromBundle(optionsCaptor.value).launchWindowingMode) + .startTask(anyInt(), anyInt(), optionsCaptor.capture(), anyOrNull(), any()) + assertThat(ActivityOptions.fromBundle(optionsCaptor.firstValue).launchWindowingMode) .isEqualTo(WINDOWING_MODE_MULTI_WINDOW) } @@ -4345,7 +5406,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, - desktopWindowDecoration, ) // Assert bounds set to stable bounds val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) @@ -4391,7 +5451,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.TOUCH, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) @@ -4435,7 +5494,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) val wct = getLatestToggleResizeDesktopTaskWct(currentDragBounds) assertThat(findBoundsChange(wct, task)).isEqualTo(expectedBounds) @@ -4465,7 +5523,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() currentDragBounds, preDragBounds, motionEvent, - desktopWindowDecoration, ) verify(mReturnToDragStartAnimator) .start( @@ -4490,7 +5547,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert that task is NOT updated via WCT @@ -4517,7 +5573,6 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() SnapPosition.LEFT, ResizeTrigger.SNAP_LEFT_MENU, InputMethod.MOUSE, - desktopWindowDecoration, ) // Assert bounds set to half of the stable bounds @@ -4549,11 +5604,10 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() 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 - } + topActivityInfo.apply { + this?.screenOrientation = SCREEN_ORIENTATION_LANDSCAPE + configuration.windowConfiguration.appBounds = bounds + } appCompatTaskInfo.topActivityAppBounds.set(0, 0, bounds.width(), bounds.height()) isResizeable = false } @@ -4775,38 +5829,90 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() } @Test - fun onUnhandledDrag_newFreeformIntent() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagEnabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, PointF(1200f, 700f), Rect(240, 700, 2160, 1900), + tabTearingAnimationFlagEnabled = true, ) } @Test - fun onUnhandledDrag_newFreeformIntentSplitLeft() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntent_tabTearingAnimationBugfixFlagDisabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + PointF(1200f, 700f), + Rect(240, 700, 2160, 1900), + tabTearingAnimationFlagEnabled = false, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagEnabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, PointF(50f, 700f), Rect(0, 0, 500, 1000), + tabTearingAnimationFlagEnabled = true, ) } @Test - fun onUnhandledDrag_newFreeformIntentSplitRight() { + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitLeft_tabTearingAnimationBugfixFlagDisabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + PointF(50f, 700f), + Rect(0, 0, 500, 1000), + tabTearingAnimationFlagEnabled = false, + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagEnabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + PointF(2500f, 700f), + Rect(500, 0, 1000, 1000), + tabTearingAnimationFlagEnabled = true, + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFreeformIntentSplitRight_tabTearingAnimationBugfixFlagDisabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, PointF(2500f, 700f), Rect(500, 0, 1000, 1000), + tabTearingAnimationFlagEnabled = false, ) } @Test - fun onUnhandledDrag_newFullscreenIntent() { + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagEnabled() { testOnUnhandledDrag( DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, PointF(1200f, 50f), Rect(), + tabTearingAnimationFlagEnabled = true, + ) + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_TAB_TEARING_MINIMIZE_ANIMATION_BUGFIX) + fun onUnhandledDrag_newFullscreenIntent_tabTearingAnimationBugfixFlagDisabled() { + testOnUnhandledDrag( + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + PointF(1200f, 50f), + Rect(), + tabTearingAnimationFlagEnabled = false, ) } @@ -4869,7 +5975,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + taskId = task.taskId, + wct = wct, + transitionSource = UNKNOWN, + ) verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) @@ -4893,7 +6003,11 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() .thenReturn(ExitResult.Exit(exitingTask = 5, runOnTransitionStart = runOnStartTransit)) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) - controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) + controller.moveTaskToDefaultDeskAndActivate( + taskId = task.taskId, + wct = wct, + transitionSource = UNKNOWN, + ) verify(mMockDesktopImmersiveController) .exitImmersiveIfApplicable(eq(wct), eq(task.displayId), eq(task.taskId), any()) @@ -5152,6 +6266,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() indicatorType: DesktopModeVisualIndicator.IndicatorType, inputCoordinate: PointF, expectedBounds: Rect, + tabTearingAnimationFlagEnabled: Boolean, ) { setUpLandscapeDisplay() val task = setUpFreeformTask() @@ -5182,6 +6297,16 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() anyOrNull(), eq(DesktopModeVisualIndicator.DragStartState.DRAGGED_INTENT), ) + whenever( + desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_OPEN), + any(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + ) + .thenReturn(Binder()) spyController.onUnhandledDrag( mockPendingIntent, @@ -5189,24 +6314,37 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() mockDragEvent, mockCallback as Consumer<Boolean>, ) - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() 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()) + verify(transitions).startTransition(any(), arg.capture(), anyOrNull()) } else { expectedWindowingMode = WINDOWING_MODE_FREEFORM - // All other launches use a special handler. - verify(dragAndDropTransitionHandler).handleDropEvent(capture(arg)) + if (tabTearingAnimationFlagEnabled) { + verify(desktopMixedTransitionHandler) + .startLaunchTransition( + eq(TRANSIT_OPEN), + arg.capture(), + anyOrNull(), + anyOrNull(), + anyOrNull(), + ) + } else { + // All other launches use a special handler. + verify(dragAndDropTransitionHandler).handleDropEvent(arg.capture()) + } } assertThat( - ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions) + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) .launchWindowingMode ) .isEqualTo(expectedWindowingMode) - assertThat(ActivityOptions.fromBundle(arg.value.hierarchyOps[0].launchOptions).launchBounds) + assertThat( + ActivityOptions.fromBundle(arg.firstValue.hierarchyOps[0].launchOptions) + .launchBounds + ) .isEqualTo(expectedBounds) } @@ -5243,6 +6381,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() ): RunningTaskInfo { val task = createFreeformTask(displayId, bounds) val activityInfo = ActivityInfo() + activityInfo.applicationInfo = ApplicationInfo() task.topActivityInfo = activityInfo if (background) { whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) @@ -5288,6 +6427,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val activityInfo = ActivityInfo() activityInfo.screenOrientation = screenOrientation activityInfo.windowLayout = ActivityInfo.WindowLayout(0, 0F, 0, 0F, gravity, 0, 0) + activityInfo.applicationInfo = ApplicationInfo() with(task) { topActivityInfo = activityInfo isResizeable = isResizable @@ -5386,52 +6526,49 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() @WindowManager.TransitionType type: Int = TRANSIT_OPEN, handlerClass: Class<out TransitionHandler>? = null, ): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { verify(transitions).startTransition(eq(type), arg.capture(), isA(handlerClass)) } - return arg.value + return arg.lastValue } private fun getLatestToggleResizeDesktopTaskWct( currentBounds: Rect? = null ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(toggleResizeDesktopTaskTransitionHandler, atLeastOnce()) - .startTransition(capture(arg), eq(currentBounds)) - return arg.value + .startTransition(arg.capture(), eq(currentBounds)) + return arg.lastValue } private fun getLatestDesktopMixedTaskWct( @WindowManager.TransitionType type: Int = TRANSIT_OPEN ): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(desktopMixedTransitionHandler) - .startLaunchTransition(eq(type), capture(arg), anyOrNull(), anyOrNull(), anyOrNull()) - return arg.value + .startLaunchTransition(eq(type), arg.capture(), anyOrNull(), anyOrNull(), anyOrNull()) + return arg.lastValue } private fun getLatestEnterDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) - return arg.value + return arg.lastValue } private fun getLatestDragToDesktopWct(): WindowContainerTransaction { - val arg: ArgumentCaptor<WindowContainerTransaction> = - ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(capture(arg)) - return arg.value + val arg = argumentCaptor<WindowContainerTransaction>() + verify(dragToDesktopTransitionHandler).finishDragToDesktopTransition(arg.capture()) + return arg.lastValue } private fun getLatestExitDesktopWct(): WindowContainerTransaction { - val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + val arg = argumentCaptor<WindowContainerTransaction>() verify(exitDesktopTransitionHandler).startTransition(any(), arg.capture(), any(), any()) - return arg.value + return arg.lastValue } private fun findBoundsChange(wct: WindowContainerTransaction, task: RunningTaskInfo): Rect? = @@ -5461,6 +6598,7 @@ class DesktopTasksControllerTest(flags: FlagsParameterization) : ShellTestCase() val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) const val MAX_TASK_LIMIT = 6 private const val TASKBAR_FRAME_HEIGHT = 200 + private const val FLOAT_TOLERANCE = 0.005f @JvmStatic @Parameters(name = "{0}") @@ -5475,6 +6613,60 @@ private fun WindowContainerTransaction.assertIndexInBounds(index: Int) { .isGreaterThan(index) } +private fun WindowContainerTransaction.assertHop( + predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean +) { + assertThat(hierarchyOps.any(predicate)).isTrue() +} + +private fun WindowContainerTransaction.assertWithoutHop( + predicate: (WindowContainerTransaction.HierarchyOp) -> Boolean +) { + assertThat(hierarchyOps.none(predicate)).isTrue() +} + +private fun WindowContainerTransaction.indexOfReorder( + task: RunningTaskInfo, + toTop: Boolean? = null, +): Int { + val hop = hierarchyOps.singleOrNull(ReorderPredicate(task.token, toTop)) ?: return -1 + return hierarchyOps.indexOf(hop) +} + +private class ReorderPredicate(val token: WindowContainerToken, val toTop: Boolean? = null) : + ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = + hop.type == HIERARCHY_OP_TYPE_REORDER && + (toTop == null || hop.toTop == toTop) && + hop.container == token.asBinder() +} + +private class ReparentPredicate( + val token: WindowContainerToken, + val parentToken: WindowContainerToken, + val toTop: Boolean? = null, +) : ((WindowContainerTransaction.HierarchyOp) -> Boolean) { + override fun invoke(hop: WindowContainerTransaction.HierarchyOp): Boolean = + hop.isReparent && + (toTop == null || hop.toTop == toTop) && + hop.container == token.asBinder() && + hop.newParent == parentToken.asBinder() +} + +private fun WindowContainerTransaction.assertReorder( + task: RunningTaskInfo, + toTop: Boolean? = null, +) { + assertReorder(task.token, toTop) +} + +private fun WindowContainerTransaction.assertReorder( + token: WindowContainerToken, + toTop: Boolean? = null, +) { + assertHop(ReorderPredicate(token, toTop)) +} + private fun WindowContainerTransaction.assertReorderAt( index: Int, task: RunningTaskInfo, @@ -5536,6 +6728,20 @@ private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowCont assertThat(op.container).isEqualTo(token.asBinder()) } +private fun WindowContainerTransaction.assertPendingIntent(intent: Intent) { + assertHop { hop -> + hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT && + hop.pendingIntent?.intent?.component == intent.component + } +} + +private fun WindowContainerTransaction.assertWithoutPendingIntent(intent: Intent) { + assertWithoutHop { hop -> + hop.type == HIERARCHY_OP_TYPE_PENDING_INTENT && + hop.pendingIntent?.intent?.component == intent.component + } +} + private fun WindowContainerTransaction.assertPendingIntentAt(index: Int, intent: Intent) { assertIndexInBounds(index) val op = hierarchyOps[index] 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 554b09f130bd..d33209dbc30e 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 @@ -24,7 +24,6 @@ import android.os.IBinder import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl @@ -66,7 +65,6 @@ 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.Mock @@ -88,8 +86,6 @@ import org.mockito.quality.Strictness @ExperimentalCoroutinesApi class DesktopTasksLimiterTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @Mock lateinit var shellTaskOrganizer: ShellTaskOrganizer @Mock lateinit var transitions: Transitions @Mock lateinit var interactionJankMonitor: InteractionJankMonitor 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 ca1e3edb3fd3..c29edece5537 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 @@ -25,7 +25,6 @@ import android.content.Intent import android.os.Binder import android.os.IBinder import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.view.Display.DEFAULT_DISPLAY import android.view.WindowManager import android.view.WindowManager.TRANSIT_CLOSE @@ -48,11 +47,13 @@ import com.android.wm.shell.back.BackAnimationController import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_TASK_DRAG import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider +import com.android.wm.shell.desktopmode.multidesks.DesksTransitionObserver import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions import com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP import com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP +import com.android.wm.shell.util.StubTransaction import com.google.common.truth.Truth.assertThat import com.google.common.truth.Truth.assertWithMessage import org.junit.Before @@ -77,9 +78,6 @@ import org.mockito.kotlin.whenever * Build/Install/Run: atest WMShellUnitTests:DesktopTasksTransitionObserverTest */ class DesktopTasksTransitionObserverTest { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() - @JvmField @Rule val extendedMockitoRule = @@ -96,6 +94,7 @@ class DesktopTasksTransitionObserverTest { private val backAnimationController = mock<BackAnimationController>() private val desktopWallpaperActivityTokenProvider = mock<DesktopWallpaperActivityTokenProvider>() + private val desksTransitionObserver = mock<DesksTransitionObserver>() private val wallpaperToken = MockToken().token() private lateinit var transitionObserver: DesktopTasksTransitionObserver @@ -119,6 +118,7 @@ class DesktopTasksTransitionObserverTest { mixedHandler, backAnimationController, desktopWallpaperActivityTokenProvider, + desksTransitionObserver, shellInit, ) } @@ -415,6 +415,21 @@ class DesktopTasksTransitionObserverTest { verify(taskRepository).setTaskInPip(task.displayId, task.taskId, enterPip = false) } + @Test + fun onTransitionReady_forwardsToDesksTransitionObserver() { + val transition = Binder() + val info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0) + + transitionObserver.onTransitionReady( + transition = transition, + info = info, + StubTransaction(), + StubTransaction(), + ) + + verify(desksTransitionObserver).onTransitionReady(transition, info) + } + private fun createBackNavigationTransition( task: RunningTaskInfo?, type: Int = TRANSIT_TO_BACK, 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 b9e307fa5973..030bb1ace49d 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 @@ -21,7 +21,6 @@ import android.content.pm.UserInfo import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn @@ -44,7 +43,6 @@ 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.spy @@ -56,8 +54,6 @@ import org.mockito.quality.Strictness @RunWith(AndroidTestingRunner::class) @ExperimentalCoroutinesApi class DesktopUserRepositoriesTest : ShellTestCase() { - @get:Rule val setFlagsRule = SetFlagsRule() - private lateinit var userRepositories: DesktopUserRepositories private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope @@ -127,8 +123,26 @@ class DesktopUserRepositoriesTest : ShellTestCase() { assertThat(desktopRepository.userId).isEqualTo(PROFILE_ID_2) } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_HSUM) + fun getUserForProfile_flagEnabled_returnsUserIdForProfile() { + userRepositories.onUserChanged(USER_ID_2, mock()) + val profiles: MutableList<UserInfo> = + mutableListOf( + UserInfo(USER_ID_2, "User profile", 0), + UserInfo(PROFILE_ID_1, "Work profile", 0), + ) + userRepositories.onUserProfilesChanged(profiles) + + val userIdForProfile = userRepositories.getUserIdForProfile(PROFILE_ID_1) + + assertThat(userIdForProfile).isEqualTo(USER_ID_2) + } + private companion object { const val USER_ID_1 = 7 + const val USER_ID_2 = 8 + const val PROFILE_ID_1 = 4 const val PROFILE_ID_2 = 5 } } 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 bf9cf00050dc..85f6cd36992d 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 @@ -25,6 +25,8 @@ import com.android.internal.jank.InteractionJankMonitor import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.bubbles.BubbleController +import com.android.wm.shell.bubbles.BubbleTransitions import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP import com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP @@ -34,6 +36,7 @@ import com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP import com.android.wm.shell.splitscreen.SplitScreenController import com.android.wm.shell.transition.Transitions import com.android.wm.shell.windowdecor.MoveToDesktopAnimator +import java.util.Optional import java.util.function.Supplier import junit.framework.Assert.assertEquals import junit.framework.Assert.assertFalse @@ -48,6 +51,7 @@ import org.mockito.ArgumentMatchers.anyInt import org.mockito.ArgumentMatchers.eq import org.mockito.Mock import org.mockito.MockitoSession +import org.mockito.kotlin.argThat import org.mockito.kotlin.mock import org.mockito.kotlin.never import org.mockito.kotlin.times @@ -70,6 +74,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor @Mock private lateinit var draggedTaskLeash: SurfaceControl @Mock private lateinit var homeTaskLeash: SurfaceControl + @Mock private lateinit var desktopUserRepositories: DesktopUserRepositories + @Mock private lateinit var bubbleController: BubbleController private val transactionSupplier = Supplier { mock<SurfaceControl.Transaction>() } @@ -84,7 +90,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, + Optional.of(bubbleController), transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } @@ -93,7 +101,9 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { context, transitions, taskDisplayAreaOrganizer, + desktopUserRepositories, mockInteractionJankMonitor, + Optional.of(bubbleController), transactionSupplier, ) .apply { setSplitScreenController(splitScreenController) } @@ -166,6 +176,32 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun startDragToDesktop_cancelledBeforeReady_verifyBubbleLeftCancel() { + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT, + ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { isReleasedOnLeft }, + ) + } + + @Test + fun startDragToDesktop_cancelledBeforeReady_verifyBubbleRightCancel() { + performEarlyCancel( + defaultHandler, + DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT, + ) + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { !isReleasedOnLeft }, + ) + } + + @Test fun startDragToDesktop_aborted_finishDropped() { val task = createTask() // Simulate transition is started. @@ -284,6 +320,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { cancelToken, TransitionInfo(TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP, 0), mock<SurfaceControl.Transaction>(), + mock<SurfaceControl.Transaction>(), startToken, mock<Transitions.TransitionFinishCallback>(), ) @@ -339,6 +376,40 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun cancelDragToDesktop_bubbleLeftCancelType_bubbleRequested() { + startDrag(defaultHandler) + + // Then user cancelled it, requesting bubble. + defaultHandler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_LEFT + ) + + // Verify the request went through bubble controller. + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { isReleasedOnLeft }, + ) + } + + @Test + fun cancelDragToDesktop_bubbleRightCancelType_bubbleRequested() { + startDrag(defaultHandler) + + // Then user cancelled it, requesting bubble. + defaultHandler.cancelDragToDesktopTransition( + DragToDesktopTransitionHandler.CancelState.CANCEL_BUBBLE_RIGHT + ) + + // Verify the request went through bubble controller. + verify(bubbleController) + .expandStackAndSelectBubble( + any<RunningTaskInfo>(), + argThat<BubbleTransitions.DragData> { !isReleasedOnLeft }, + ) + } + + @Test fun cancelDragToDesktop_startWasNotReady_animateCancel() { val task = createTask() // Simulate transition is started and is ready to animate. @@ -385,21 +456,23 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { @Test fun mergeAnimation_otherTransition_doesNotMerge() { - val transaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() startDrag(defaultHandler, task) defaultHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo(type = TRANSIT_OPEN, draggedTask = task), - t = transaction, - mergeTarget = mock(), + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, + mergeTarget = mock<IBinder>(), finishCallback = finishCallback, ) // Should NOT have any transaction changes - verifyZeroInteractions(transaction) + verifyZeroInteractions(mergedStartTransaction) // Should NOT merge animation verify(finishCallback, never()).onTransitionFinished(any()) } @@ -408,6 +481,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { fun mergeAnimation_endTransition_mergesAnimation() { val playingFinishTransaction = mock<SurfaceControl.Transaction>() val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = @@ -415,13 +489,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { defaultHandler.onTaskResizeAnimationListener = mock() defaultHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mergedStartTransaction, + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, mergeTarget = startTransition, finishCallback = finishCallback, ) @@ -440,6 +515,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) val playingFinishTransaction = mock<SurfaceControl.Transaction>() val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() val finishCallback = mock<Transitions.TransitionFinishCallback>() val task = createTask() val startTransition = @@ -447,13 +523,14 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { springHandler.onTaskResizeAnimationListener = mock() springHandler.mergeAnimation( - transition = mock(), + transition = mock<IBinder>(), info = createTransitionInfo( type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mergedStartTransaction, + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, mergeTarget = startTransition, finishCallback = finishCallback, ) @@ -470,6 +547,45 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } @Test + fun mergeAnimation_endTransition_springHandler_noStartHomeChange_doesntCrash() { + whenever(dragAnimator.computeCurrentVelocity()).thenReturn(PointF()) + val playingFinishTransaction = mock<SurfaceControl.Transaction>() + val mergedStartTransaction = mock<SurfaceControl.Transaction>() + val mergedFinishTransaction = mock<SurfaceControl.Transaction>() + val finishCallback = mock<Transitions.TransitionFinishCallback>() + val task = createTask() + val startTransition = + startDrag( + springHandler, + task, + finishTransaction = playingFinishTransaction, + homeChange = null, + ) + springHandler.onTaskResizeAnimationListener = mock() + + springHandler.mergeAnimation( + transition = mock<IBinder>(), + info = + createTransitionInfo( + type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, + draggedTask = task, + ), + startT = mergedStartTransaction, + finishT = mergedFinishTransaction, + mergeTarget = startTransition, + finishCallback = finishCallback, + ) + + // Should show dragged task layer in start and finish transaction + verify(mergedStartTransaction).show(draggedTaskLeash) + verify(playingFinishTransaction).show(draggedTaskLeash) + // Should update the dragged task layer + verify(mergedStartTransaction).setLayer(eq(draggedTaskLeash), anyInt()) + // Should merge animation + verify(finishCallback).onTransitionFinished(null) + } + + @Test fun propertyValue_returnsSystemPropertyValue() { val name = "property_name" val value = 10f @@ -564,7 +680,8 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { type = TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP, draggedTask = task, ), - t = mock<SurfaceControl.Transaction>(), + startT = mock<SurfaceControl.Transaction>(), + finishT = mock<SurfaceControl.Transaction>(), mergeTarget = startTransition, finishCallback = mock<Transitions.TransitionFinishCallback>(), ) @@ -581,6 +698,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { handler: DragToDesktopTransitionHandler, task: RunningTaskInfo = createTask(), finishTransaction: SurfaceControl.Transaction = mock(), + homeChange: TransitionInfo.Change? = createHomeChange(), ): IBinder { whenever(dragAnimator.position).thenReturn(PointF()) // Simulate transition is started and is ready to animate. @@ -591,6 +709,7 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { createTransitionInfo( type = TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP, draggedTask = task, + homeChange = homeChange, ), startTransaction = mock(), finishTransaction = finishTransaction, @@ -676,16 +795,13 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { } } - private fun createTransitionInfo(type: Int, draggedTask: RunningTaskInfo) = + private fun createTransitionInfo( + type: Int, + draggedTask: RunningTaskInfo, + homeChange: TransitionInfo.Change? = createHomeChange(), + ) = TransitionInfo(type, /* flags= */ 0).apply { - addChange( // Home. - TransitionInfo.Change(mock(), homeTaskLeash).apply { - parent = null - taskInfo = - TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() - flags = flags or FLAG_IS_WALLPAPER - } - ) + homeChange?.let { addChange(it) } addChange( // Dragged Task. TransitionInfo.Change(mock(), draggedTaskLeash).apply { parent = null @@ -701,6 +817,13 @@ class DragToDesktopTransitionHandlerTest : ShellTestCase() { ) } + private fun createHomeChange() = + TransitionInfo.Change(mock(), homeTaskLeash).apply { + parent = null + taskInfo = TestRunningTaskInfoBuilder().setActivityType(ACTIVITY_TYPE_HOME).build() + flags = flags or FLAG_IS_WALLPAPER + } + private fun systemPropertiesKey(name: String) = "${SpringDragToDesktopTransitionHandler.SYSTEM_PROPERTIES_GROUP}.$name" } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt new file mode 100644 index 000000000000..79b0f1c7eadd --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/VisualIndicatorViewContainerTest.kt @@ -0,0 +1,250 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode + +import android.app.ActivityManager +import android.app.ActivityManager.RunningTaskInfo +import android.graphics.Rect +import android.graphics.drawable.Drawable +import android.graphics.drawable.LayerDrawable +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display +import android.view.SurfaceControl +import android.view.SurfaceControlViewHost +import android.view.View +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestRunningTaskInfoBuilder +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.common.SyncTransactionQueue +import com.android.wm.shell.shared.bubbles.BubbleDropTargetBoundsProvider +import com.android.wm.shell.windowdecor.WindowDecoration.SurfaceControlViewHostFactory +import com.google.common.truth.Truth.assertThat +import kotlin.test.Test +import org.junit.Before +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.Mock +import org.mockito.Mockito.mock +import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.eq +import org.mockito.kotlin.spy +import org.mockito.kotlin.verify +import org.mockito.kotlin.verifyZeroInteractions +import org.mockito.kotlin.whenever + +/** + * Test class for [VisualIndicatorViewContainer] and [VisualIndicatorAnimator] + * + * Usage: atest WMShellUnitTests:VisualIndicatorViewContainerTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +@EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_MODE) +class VisualIndicatorViewContainerTest : ShellTestCase() { + @Mock private lateinit var view: View + @Mock private lateinit var displayLayout: DisplayLayout + @Mock private lateinit var displayController: DisplayController + @Mock private lateinit var taskSurface: SurfaceControl + @Mock private lateinit var syncQueue: SyncTransactionQueue + @Mock private lateinit var mockSurfaceControlViewHostFactory: SurfaceControlViewHostFactory + @Mock private lateinit var mockBackground: LayerDrawable + @Mock private lateinit var bubbleDropTargetBoundsProvider: BubbleDropTargetBoundsProvider + private val taskInfo: RunningTaskInfo = createTaskInfo() + private val mainExecutor = TestShellExecutor() + private val desktopExecutor = TestShellExecutor() + + @Before + fun setUp() { + whenever(displayController.getDisplayLayout(anyInt())).thenReturn(displayLayout) + whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(DISPLAY_BOUNDS) + } + whenever(mockSurfaceControlViewHostFactory.create(any(), any(), any())) + .thenReturn(mock(SurfaceControlViewHost::class.java)) + } + + @Test + fun testTransitionIndicator_sameTypeReturnsEarly() { + val spyViewContainer = setupSpyViewContainer() + // Test early return on startType == endType. + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .transitionIndicator( + eq(taskInfo), + eq(displayController), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + ) + // Assert fadeIn, fadeOut, and animateIndicatorType were not called. + verifyZeroInteractions(spyViewContainer) + } + + @Test + fun testTransitionIndicator_firstTypeNoIndicator_callsFadeIn() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer).fadeInIndicator(any(), any()) + } + + @Test + fun testTransitionIndicator_secondTypeNoIndicator_callsFadeOut() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.NO_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .fadeOutIndicator( + any(), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + anyOrNull(), + ) + } + + @Test + fun testTransitionIndicator_differentTypes_callsTransitionIndicator() { + val spyViewContainer = setupSpyViewContainer() + spyViewContainer.transitionIndicator( + taskInfo, + displayController, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + ) + desktopExecutor.flushAll() + verify(spyViewContainer) + .transitionIndicator( + any(), + any(), + eq(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR), + eq(DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR), + ) + } + + @Test + fun testFadeInBoundsCalculation() { + val spyIndicator = setupSpyViewContainer() + val animator = + spyIndicator.indicatorView?.let { + VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsIn( + it, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + displayLayout, + bubbleDropTargetBoundsProvider, + ) + } + assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(15, 15, 985, 985)) + assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(0, 0, 1000, 1000)) + } + + @Test + fun testFadeOutBoundsCalculation() { + val spyIndicator = setupSpyViewContainer() + val animator = + spyIndicator.indicatorView?.let { + VisualIndicatorViewContainer.VisualIndicatorAnimator.fadeBoundsOut( + it, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + displayLayout, + bubbleDropTargetBoundsProvider, + ) + } + assertThat(animator?.indicatorStartBounds).isEqualTo(Rect(0, 0, 1000, 1000)) + assertThat(animator?.indicatorEndBounds).isEqualTo(Rect(15, 15, 985, 985)) + } + + @Test + fun testChangeIndicatorTypeBoundsCalculation() { + // Test fullscreen to split-left bounds. + var animator = + VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( + view, + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_LEFT_INDICATOR, + bubbleDropTargetBoundsProvider, + ) + // Test desktop to split-right bounds. + animator = + VisualIndicatorViewContainer.VisualIndicatorAnimator.animateIndicatorType( + view, + displayLayout, + DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, + DesktopModeVisualIndicator.IndicatorType.TO_SPLIT_RIGHT_INDICATOR, + bubbleDropTargetBoundsProvider, + ) + } + + private fun setupSpyViewContainer(): VisualIndicatorViewContainer { + val viewContainer = + VisualIndicatorViewContainer( + desktopExecutor, + mainExecutor, + SurfaceControl.Builder(), + syncQueue, + mockSurfaceControlViewHostFactory, + bubbleDropTargetBoundsProvider, + ) + viewContainer.createView( + context, + mock(Display::class.java), + displayLayout, + taskInfo, + taskSurface, + ) + desktopExecutor.flushAll() + viewContainer.indicatorView?.background = mockBackground + whenever(mockBackground.findDrawableByLayerId(anyInt())) + .thenReturn(mock(Drawable::class.java)) + return spy(viewContainer) + } + + private fun createTaskInfo(): RunningTaskInfo { + val taskDescriptionBuilder = ActivityManager.TaskDescription.Builder() + return TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setTaskDescriptionBuilder(taskDescriptionBuilder) + .setVisible(true) + .build() + } + + companion object { + private val DISPLAY_BOUNDS = Rect(0, 0, 1000, 1000) + } +} 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 dfb1b0c8c642..9cb2a055e45d 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 @@ -16,9 +16,12 @@ package com.android.wm.shell.desktopmode.compatui +import android.content.ComponentName import android.content.Intent +import android.content.pm.PackageManager import android.os.Binder import android.testing.AndroidTestingRunner +import android.testing.TestableContext import android.view.SurfaceControl import android.view.WindowManager.TRANSIT_CHANGE import android.view.WindowManager.TRANSIT_CLOSE @@ -37,6 +40,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME import com.google.common.truth.Truth.assertThat import org.junit.Before import org.junit.Test @@ -44,6 +48,7 @@ import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.spy import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -62,16 +67,23 @@ class SystemModalsTransitionHandlerTest : ShellTestCase() { private val desktopRepository = mock<DesktopRepository>() private val startT = mock<SurfaceControl.Transaction>() private val finishT = mock<SurfaceControl.Transaction>() + private val packageManager = mock<PackageManager>() + private val componentName = mock<ComponentName>() + private lateinit var spyContext: TestableContext private lateinit var transitionHandler: SystemModalsTransitionHandler private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy @Before fun setUp() { + spyContext = spy(mContext) // Simulate having one Desktop task so that we see Desktop Mode as active whenever(desktopUserRepositories.current).thenReturn(desktopRepository) whenever(desktopRepository.getVisibleTaskCount(anyInt())).thenReturn(1) - desktopModeCompatPolicy = DesktopModeCompatPolicy(context) + whenever(spyContext.packageManager).thenReturn(packageManager) + whenever(componentName.packageName).thenReturn(HOME_LAUNCHER_PACKAGE_NAME) + whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(componentName) + desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext) transitionHandler = createTransitionHandler() } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt new file mode 100644 index 000000000000..aa4e9aaf248e --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/desktopwallpaperactivity/DesktopWallpaperActivityTokenProviderTest.kt @@ -0,0 +1,128 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.desktopmode.desktopwallpaperactivity + +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper.RunWithLooper +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.wm.shell.MockToken +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +/** + * Test class for [DesktopWallpaperActivityTokenProvider] + * + * Usage: atest WMShellUnitTests:DesktopWallpaperActivityTokenProviderTest + */ +@SmallTest +@RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DesktopWallpaperActivityTokenProviderTest : ShellTestCase() { + + private lateinit var provider: DesktopWallpaperActivityTokenProvider + private val DEFAULT_DISPLAY = 0 + private val SECONDARY_DISPLAY = 1 + + @Before + fun setUp() { + provider = DesktopWallpaperActivityTokenProvider() + } + + @Test + fun setToken_setsTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token) + } + + @Test + fun setToken_overwritesExistingTokenForDisplay() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token2) + } + + @Test + fun getToken_returnsNullForNonExistentDisplay() { + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_withToken_removesTokenForDisplay() { + val token = MockToken().token() + + provider.setToken(token, DEFAULT_DISPLAY) + provider.removeToken(token) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + } + + @Test + fun removeToken_doesNothingForNonExistentDisplay() { + provider.removeToken(SECONDARY_DISPLAY) + + assertThat(provider.getToken(SECONDARY_DISPLAY)).isNull() + } + + @Test + fun removeToken_withNonExistentToken_doesNothing() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.removeToken(token2) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + } + + @Test + fun multipleDisplays_tokensAreIndependent() { + val token1 = MockToken().token() + val token2 = MockToken().token() + + provider.setToken(token1, DEFAULT_DISPLAY) + provider.setToken(token2, SECONDARY_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isEqualTo(token1) + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + + provider.removeToken(DEFAULT_DISPLAY) + + assertThat(provider.getToken(DEFAULT_DISPLAY)).isNull() + assertThat(provider.getToken(SECONDARY_DISPLAY)).isEqualTo(token2) + } +} 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 7d2a8082c43e..08b9e0413e9d 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 @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode.education import android.os.SystemProperties import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableContext import androidx.test.filters.SmallTest @@ -27,6 +26,8 @@ import com.android.modules.utils.testing.ExtendedMockitoRule import com.android.window.flags.Flags import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.CaptionState +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger +import com.android.wm.shell.desktopmode.DesktopModeUiEventLogger.DesktopUiEventEnum import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.APP_HANDLE_EDUCATION_DELAY_MILLIS import com.android.wm.shell.desktopmode.education.AppHandleEducationController.Companion.TOOLTIP_VISIBLE_DURATION_MILLIS @@ -75,7 +76,6 @@ class AppHandleEducationControllerTest : ShellTestCase() { .mockStatic(DesktopModeStatus::class.java) .mockStatic(SystemProperties::class.java) .build()!! - @JvmField @Rule val setFlagsRule = SetFlagsRule() private lateinit var educationController: AppHandleEducationController private lateinit var testableContext: TestableContext @@ -88,6 +88,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { @Mock private lateinit var mockDataStoreRepository: AppHandleEducationDatastoreRepository @Mock private lateinit var mockCaptionHandleRepository: WindowDecorCaptionHandleRepository @Mock private lateinit var mockTooltipController: DesktopWindowingEducationTooltipController + @Mock private lateinit var mockDesktopModeUiEventLogger: DesktopModeUiEventLogger @Before fun setUp() { @@ -107,6 +108,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { mockTooltipController, testScope.backgroundScope, Dispatchers.Main, + mockDesktopModeUiEventLogger, ) } @@ -125,6 +127,8 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) verify(mockDataStoreRepository, times(1)) .updateAppHandleHintViewedTimestampMillis(eq(true)) + verify(mockDesktopModeUiEventLogger, times(1)) + .log(any(), eq(DesktopUiEventEnum.APP_HANDLE_EDUCATION_TOOLTIP_SHOWN)) } @Test @@ -157,6 +161,8 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) verify(mockDataStoreRepository, times(1)) .updateEnterDesktopModeHintViewedTimestampMillis(eq(true)) + verify(mockDesktopModeUiEventLogger, times(1)) + .log(any(), eq(DesktopUiEventEnum.ENTER_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN)) } @Test @@ -172,6 +178,66 @@ class AppHandleEducationControllerTest : ShellTestCase() { verify(mockTooltipController, times(1)).showEducationTooltip(any(), any()) verify(mockDataStoreRepository, times(1)) .updateExitDesktopModeHintViewedTimestampMillis(eq(true)) + verify(mockDesktopModeUiEventLogger, times(1)) + .log(any(), eq(DesktopUiEventEnum.EXIT_DESKTOP_MODE_EDUCATION_TOOLTIP_SHOWN)) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_noCaptionStateNotified_shouldHideAllTooltips() = + testScope.runTest { + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, times(1)).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_appHandleHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_enterDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(enterDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_APP_HANDLE_EDUCATION) + fun init_exitDesktopModeHintViewed_shouldNotListenToNoCaptionNotification() = + testScope.runTest { + testDataStoreFlow.value = + createWindowingEducationProto(exitDesktopModeHintViewedTimestampMillis = 123L) + setShouldShowDesktopModeEducation(true) + + // Simulate no caption state notification + testCaptionStateFlow.value = CaptionState.NoCaption + waitForBufferDelay() + + verify(mockTooltipController, never()).hideEducationTooltip() } @Test @@ -289,8 +355,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { // Mark app handle hint viewed. testDataStoreFlow.value = createWindowingEducationProto(appHandleHintViewedTimestampMillis = 123L) - val systemPropertiesKey = "persist.windowing_force_show_desktop_mode_education" - whenever(SystemProperties.getBoolean(eq(systemPropertiesKey), anyBoolean())) + whenever(SystemProperties.getBoolean(eq(FORCE_SHOW_EDUCATION_SYSPROP), anyBoolean())) .thenReturn(true) setShouldShowDesktopModeEducation(true) @@ -396,5 +461,7 @@ class AppHandleEducationControllerTest : ShellTestCase() { private companion object { val APP_HANDLE_EDUCATION_DELAY_BUFFER_MILLIS: Long = APP_HANDLE_EDUCATION_DELAY_MILLIS + 1000L + + val FORCE_SHOW_EDUCATION_SYSPROP = "persist.windowing_force_show_desktop_mode_education" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt new file mode 100644 index 000000000000..4dcf669f4d25 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/DesksTransitionObserverTest.kt @@ -0,0 +1,247 @@ +/* + * 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.desktopmode.multidesks + +import android.os.Binder +import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_CLOSE +import android.view.WindowManager.TRANSIT_TO_FRONT +import android.window.TransitionInfo +import android.window.TransitionInfo.Change +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopUserRepositories +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Tests for [DesksTransitionObserver]. + * + * Build/Install/Run: atest WMShellUnitTests:DesksTransitionObserverTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesksTransitionObserverTest : ShellTestCase() { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private val mockDesksOrganizer = mock<DesksOrganizer>() + val testScope = TestScope() + + private lateinit var desktopUserRepositories: DesktopUserRepositories + private lateinit var observer: DesksTransitionObserver + + private val repository: DesktopRepository + get() = desktopUserRepositories.current + + @Before + fun setUp() { + desktopUserRepositories = + DesktopUserRepositories( + context, + ShellInit(TestShellExecutor()), + /* shellController= */ mock(), + /* persistentRepository= */ mock(), + /* repositoryInitializer= */ mock(), + testScope, + /* userManager= */ mock(), + ) + observer = DesksTransitionObserver(desktopUserRepositories, mockDesksOrganizer) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_removesFromRepository() { + val transition = Binder() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = null, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(repository.getDeskIds(DEFAULT_DISPLAY)).doesNotContain(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_removeDesk_invokesOnRemoveListener() { + class FakeOnDeskRemovedListener : OnDeskRemovedListener { + var lastDeskRemoved: Int? = null + + override fun onDeskRemoved(lastDisplayId: Int, deskId: Int) { + lastDeskRemoved = deskId + } + } + val transition = Binder() + val removeListener = FakeOnDeskRemovedListener() + val removeTransition = + DeskTransition.RemoveDesk( + transition, + displayId = DEFAULT_DISPLAY, + deskId = 5, + tasks = setOf(10, 11), + onDeskRemovedListener = removeListener, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(removeTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CLOSE, /* flags= */ 0), + ) + + assertThat(removeListener.lastDeskRemoved).isEqualTo(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_activateDesk_updatesRepository() { + val transition = Binder() + val change = Change(mock(), mock()) + whenever(mockDesksOrganizer.isDeskActiveAtEnd(change, deskId = 5)).thenReturn(true) + val activateTransition = + DeskTransition.ActivateDesk(transition, displayId = DEFAULT_DISPLAY, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(activateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(5) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_activateDeskWithTask_updatesRepository() = + testScope.runTest { + val deskId = 5 + val task = createFreeformTask(DEFAULT_DISPLAY).apply { isVisibleRequested = true } + val transition = Binder() + val change = Change(mock(), mock()).apply { taskInfo = task } + whenever(mockDesksOrganizer.getDeskAtEnd(change)).thenReturn(deskId) + val activateTransition = + DeskTransition.ActiveDeskWithTask( + transition, + displayId = DEFAULT_DISPLAY, + deskId = deskId, + enterTaskId = task.taskId, + ) + repository.addDesk(DEFAULT_DISPLAY, deskId = deskId) + + observer.addPendingTransition(activateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_TO_FRONT, /* flags= */ 0).apply { addChange(change) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isEqualTo(deskId) + assertThat(repository.getActiveTaskIdsInDesk(deskId)).contains(task.taskId) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDesk_updatesRepository() { + val transition = Binder() + val deskChange = Change(mock(), mock()) + whenever(mockDesksOrganizer.isDeskChange(deskChange, deskId = 5)).thenReturn(true) + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { addChange(deskChange) }, + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithExitingTask_updatesRepository() { + val transition = Binder() + val exitingTask = createFreeformTask(DEFAULT_DISPLAY) + val exitingTaskChange = Change(mock(), mock()).apply { taskInfo = exitingTask } + whenever(mockDesksOrganizer.getDeskAtEnd(exitingTaskChange)).thenReturn(null) + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + repository.addTaskToDesk( + displayId = DEFAULT_DISPLAY, + deskId = 5, + taskId = exitingTask.taskId, + isVisible = true, + ) + assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isTrue() + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = + TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0).apply { + addChange(exitingTaskChange) + }, + ) + + assertThat(repository.isActiveTaskInDesk(deskId = 5, taskId = exitingTask.taskId)).isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_MULTIPLE_DESKTOPS_BACKEND) + fun onTransitionReady_deactivateDeskWithoutVisibleChange_updatesRepository() { + val transition = Binder() + val deactivateTransition = DeskTransition.DeactivateDesk(transition, deskId = 5) + repository.addDesk(DEFAULT_DISPLAY, deskId = 5) + repository.setActiveDesk(DEFAULT_DISPLAY, deskId = 5) + + observer.addPendingTransition(deactivateTransition) + observer.onTransitionReady( + transition = transition, + info = TransitionInfo(TRANSIT_CHANGE, /* flags= */ 0), + ) + + assertThat(repository.getActiveDeskId(DEFAULT_DISPLAY)).isNull() + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt index a07203d86b75..8b10ca1a2a70 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/multidesks/RootTaskDesksOrganizerTest.kt @@ -15,17 +15,21 @@ */ package com.android.wm.shell.desktopmode.multidesks +import android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED import android.testing.AndroidTestingRunner import android.view.Display import android.view.SurfaceControl +import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo import android.window.WindowContainerTransaction import android.window.WindowContainerTransaction.HierarchyOp +import android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT 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.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.desktopmode.multidesks.RootTaskDesksOrganizer.DeskRoot import com.android.wm.shell.sysui.ShellCommandHandler import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat @@ -102,54 +106,45 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testOnTaskVanished_removesRoot() { - val callback = FakeOnCreateCallback() - organizer.createDesk(Display.DEFAULT_DISPLAY, callback) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - organizer.onTaskVanished(freeformRoot) + organizer.onTaskVanished(desk.taskInfo) - assertThat(organizer.roots.contains(freeformRoot.taskId)).isFalse() + assertThat(organizer.roots.contains(desk.deskId)).isFalse() } @Test fun testDesktopWindowAppearsInDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) - assertThat(organizer.roots[freeformRoot.taskId].children).contains(child.taskId) + assertThat(desk.children).contains(child.taskId) } @Test fun testDesktopWindowDisappearsFromDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) - val child = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val desk = createDesk() + val child = createFreeformTask().apply { parentTaskId = desk.deskId } organizer.onTaskAppeared(child, SurfaceControl()) organizer.onTaskVanished(child) - assertThat(organizer.roots[freeformRoot.taskId].children).doesNotContain(child.taskId) + assertThat(desk.children).doesNotContain(child.taskId) } @Test fun testRemoveDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.removeDesk(wct, freeformRoot.taskId) + organizer.removeDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_ROOT_TASK && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -165,25 +160,23 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testActivateDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val wct = WindowContainerTransaction() - organizer.activateDesk(wct, freeformRoot.taskId) + organizer.activateDesk(wct, desk.deskId) assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_REORDER && hop.toTop && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() assertThat( wct.hierarchyOps.any { hop -> hop.type == HierarchyOp.HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && - hop.container == freeformRoot.token.asBinder() + hop.container == desk.taskInfo.token.asBinder() } ) .isTrue() @@ -199,20 +192,25 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testMoveTaskToDesk() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() val desktopTask = createFreeformTask().apply { parentTaskId = -1 } val wct = WindowContainerTransaction() - organizer.moveTaskToDesk(wct, freeformRoot.taskId, desktopTask) + organizer.moveTaskToDesk(wct, desk.deskId, desktopTask) assertThat( wct.hierarchyOps.any { hop -> hop.isReparent && hop.toTop && hop.container == desktopTask.token.asBinder() && - hop.newParent == freeformRoot.token.asBinder() + hop.newParent == desk.taskInfo.token.asBinder() + } + ) + .isTrue() + assertThat( + wct.changes.any { change -> + change.key == desktopTask.token.asBinder() && + change.value.windowingMode == WINDOWING_MODE_UNDEFINED } ) .isTrue() @@ -231,17 +229,76 @@ class RootTaskDesksOrganizerTest : ShellTestCase() { @Test fun testGetDeskAtEnd() { - organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) - val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } - organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + val desk = createDesk() - val task = createFreeformTask().apply { parentTaskId = freeformRoot.taskId } + val task = createFreeformTask().apply { parentTaskId = desk.deskId } val endDesk = organizer.getDeskAtEnd( TransitionInfo.Change(task.token, SurfaceControl()).apply { taskInfo = task } ) - assertThat(endDesk).isEqualTo(freeformRoot.taskId) + assertThat(endDesk).isEqualTo(desk.deskId) + } + + @Test + fun testIsDeskActiveAtEnd() { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + freeformRoot.isVisibleRequested = true + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + + val isActive = + organizer.isDeskActiveAtEnd( + change = + TransitionInfo.Change(freeformRoot.token, SurfaceControl()).apply { + taskInfo = freeformRoot + mode = TRANSIT_TO_FRONT + }, + deskId = freeformRoot.taskId, + ) + + assertThat(isActive).isTrue() + } + + @Test + fun deactivateDesk_clearsLaunchRoot() { + val wct = WindowContainerTransaction() + val desk = createDesk() + organizer.activateDesk(wct, desk.deskId) + + organizer.deactivateDesk(wct, desk.deskId) + + assertThat( + wct.hierarchyOps.any { hop -> + hop.type == HIERARCHY_OP_TYPE_SET_LAUNCH_ROOT && + hop.container == desk.taskInfo.token.asBinder() && + hop.windowingModes == null && + hop.activityTypes == null + } + ) + .isTrue() + } + + @Test + fun isDeskChange() { + val desk = createDesk() + + assertThat( + organizer.isDeskChange( + TransitionInfo.Change(desk.taskInfo.token, desk.leash).apply { + taskInfo = desk.taskInfo + }, + desk.deskId, + ) + ) + .isTrue() + } + + private fun createDesk(): DeskRoot { + organizer.createDesk(Display.DEFAULT_DISPLAY, FakeOnCreateCallback()) + val freeformRoot = createFreeformTask().apply { parentTaskId = -1 } + organizer.onTaskAppeared(freeformRoot, SurfaceControl()) + return organizer.roots[freeformRoot.taskId] } private class FakeOnCreateCallback : DesksOrganizer.OnCreateCallback { 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 9a8f264e98a4..dd9e6ca0deae 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 @@ -19,7 +19,6 @@ package com.android.wm.shell.desktopmode.persistence import android.os.UserManager import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display.DEFAULT_DISPLAY import androidx.test.filters.SmallTest @@ -42,7 +41,6 @@ import kotlinx.coroutines.test.runTest 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.spy @@ -54,8 +52,6 @@ import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi class DesktopRepositoryInitializerTest : ShellTestCase() { - @JvmField @Rule val setFlagsRule = SetFlagsRule() - private lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java index 1b1a5a909220..06dcd8812350 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/draganddrop/DragAndDropControllerTest.java @@ -47,6 +47,7 @@ import com.android.internal.logging.UiEventLogger; import com.android.launcher3.icons.IconProvider; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.bubbles.bar.BubbleBarDragListener; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.sysui.ShellCommandHandler; @@ -60,6 +61,8 @@ import org.junit.runner.RunWith; import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import dagger.Lazy; + /** * Tests for the drag and drop controller. */ @@ -91,6 +94,8 @@ public class DragAndDropControllerTest extends ShellTestCase { private Transitions mTransitions; @Mock private GlobalDragListener mGlobalDragListener; + @Mock + private Lazy<BubbleBarDragListener> mBubbleBarDragControllerLazy; private DragAndDropController mController; @@ -99,7 +104,8 @@ public class DragAndDropControllerTest extends ShellTestCase { MockitoAnnotations.initMocks(this); mController = new DragAndDropController(mContext, mShellInit, mShellController, mShellCommandHandler, mShellTaskOrganizer, mDisplayController, mUiEventLogger, - mIconProvider, mGlobalDragListener, mTransitions, mMainExecutor); + mIconProvider, mGlobalDragListener, mTransitions, mBubbleBarDragControllerLazy, + mMainExecutor); mController.onInit(); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java index 4174bbd89b76..9509aaf20c9b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskListenerTests.java @@ -35,7 +35,6 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -56,7 +55,6 @@ import com.android.wm.shell.windowdecor.WindowDecorViewModel; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -72,9 +70,6 @@ import java.util.Optional; @RunWith(AndroidJUnit4.class) public final class FreeformTaskListenerTests extends ShellTestCase { - @Rule - public final SetFlagsRule setFlagsRule = new SetFlagsRule(); - @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java index 5aed4611cdc8..bc918450a3cf 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/freeform/FreeformTaskTransitionObserverTest.java @@ -34,7 +34,6 @@ import android.content.Context; import android.content.pm.PackageManager; import android.os.IBinder; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.IWindowContainerToken; import android.window.TransitionInfo; @@ -43,6 +42,7 @@ import android.window.WindowContainerToken; import androidx.test.filters.SmallTest; import com.android.window.flags.Flags; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.desktopmode.DesktopImmersiveController; import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; @@ -60,9 +60,8 @@ import java.util.Optional; /** Tests for {@link FreeformTaskTransitionObserver}. */ @SmallTest -public class FreeformTaskTransitionObserverTest { +public class FreeformTaskTransitionObserverTest extends ShellTestCase { - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); @Mock private ShellInit mShellInit; @Mock private Transitions mTransitions; @Mock private DesktopImmersiveController mDesktopImmersiveController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java index 836f4c24a979..26688236d5be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/PipTaskOrganizerTest.java @@ -37,7 +37,6 @@ import android.content.pm.ActivityInfo; import android.graphics.Rect; import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Rational; @@ -70,8 +69,6 @@ import com.android.wm.shell.pip.phone.PhonePipMenuController; import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentMatchers; @@ -88,11 +85,6 @@ import java.util.Optional; @TestableLooper.RunWithLooper @DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipTaskOrganizerTest extends ShellTestCase { - @ClassRule - public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); - @Rule - public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); - private PipTaskOrganizer mPipTaskOrganizer; @Mock private DisplayController mMockDisplayController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java index 5ef934ce8394..13fce2a27524 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipControllerTest.java @@ -43,7 +43,6 @@ import android.os.Bundle; import android.os.Handler; import android.os.RemoteException; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; @@ -77,8 +76,6 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.ClassRule; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -95,9 +92,6 @@ import java.util.Set; @TestableLooper.RunWithLooper @DisableFlags(Flags.FLAG_ENABLE_PIP2) public class PipControllerTest extends ShellTestCase { - @ClassRule public static final SetFlagsRule.ClassRule mClassRule = new SetFlagsRule.ClassRule(); - @Rule public final SetFlagsRule mSetFlagsRule = mClassRule.createSetFlagsRule(); - private PipController mPipController; private ShellInit mShellInit; private ShellController mShellController; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java index b11715b669f4..273cb2727508 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip/phone/PipTouchHandlerTest.java @@ -25,7 +25,6 @@ import static org.mockito.Mockito.verify; import android.graphics.Rect; import android.platform.test.annotations.DisableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; import android.util.Size; @@ -49,7 +48,6 @@ import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -68,9 +66,6 @@ import java.util.Optional; @SmallTest @TestableLooper.RunWithLooper(setAsMainLooper = true) public class PipTouchHandlerTest extends ShellTestCase { - @Rule - public SetFlagsRule setFlagsRule = new SetFlagsRule(); - private static final int INSET = 10; private static final int PIP_LENGTH = 100; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelperTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelperTest.java new file mode 100644 index 000000000000..a2253cfc95d9 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/PipSurfaceTransactionHelperTest.java @@ -0,0 +1,107 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.R; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipSurfaceTransactionHelper}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipSurfaceTransactionHelperTest { + + private static final int CORNER_RADIUS = 10; + private static final int SHADOW_RADIUS = 20; + + @Mock private Context mMockContext; + @Mock private Resources mMockResources; + @Mock private SurfaceControl.Transaction mMockTransaction; + + private PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; + private SurfaceControl mTestLeash; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getDimensionPixelSize(eq(R.dimen.pip_corner_radius))) + .thenReturn(CORNER_RADIUS); + when(mMockResources.getDimensionPixelSize(eq(R.dimen.pip_shadow_radius))) + .thenReturn(SHADOW_RADIUS); + when(mMockTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + + mPipSurfaceTransactionHelper = new PipSurfaceTransactionHelper(mMockContext); + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipSurfaceTransactionHelperTest") + .setCallsite("PipSurfaceTransactionHelperTest") + .build(); + } + + @Test + public void round_doNotApply_setZeroCornerRadius() { + mPipSurfaceTransactionHelper.round(mMockTransaction, mTestLeash, false /* apply */); + + verify(mMockTransaction).setCornerRadius(eq(mTestLeash), eq(0f)); + } + + @Test + public void round_doApply_setExactCornerRadius() { + mPipSurfaceTransactionHelper.round(mMockTransaction, mTestLeash, true /* apply */); + + verify(mMockTransaction).setCornerRadius(eq(mTestLeash), eq((float) CORNER_RADIUS)); + } + + @Test + public void shadow_doNotApply_setZeroShadowRadius() { + mPipSurfaceTransactionHelper.shadow(mMockTransaction, mTestLeash, false /* apply */); + + verify(mMockTransaction).setShadowRadius(eq(mTestLeash), eq(0f)); + } + + @Test + public void shadow_doApply_setExactShadowRadius() { + mPipSurfaceTransactionHelper.shadow(mMockTransaction, mTestLeash, true /* apply */); + + verify(mMockTransaction).setShadowRadius(eq(mTestLeash), eq((float) SHADOW_RADIUS)); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java index bd857c7dcd45..0c1952910d1a 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipSchedulerTest.java @@ -19,6 +19,7 @@ package com.android.wm.shell.pip2.phone; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.never; import static org.mockito.Mockito.when; @@ -26,7 +27,7 @@ import static org.mockito.kotlin.MatchersKt.eq; import static org.mockito.kotlin.VerificationKt.times; import static org.mockito.kotlin.VerificationKt.verify; -import android.app.TaskInfo; +import android.app.ActivityManager; import android.content.Context; import android.content.res.Resources; import android.graphics.Matrix; @@ -39,23 +40,21 @@ import android.window.WindowContainerTransaction; import androidx.test.filters.SmallTest; -import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.common.pip.PipBoundsState; -import com.android.wm.shell.desktopmode.DesktopRepository; -import com.android.wm.shell.desktopmode.DesktopUserRepositories; -import com.android.wm.shell.desktopmode.desktopwallpaperactivity.DesktopWallpaperActivityTokenProvider; +import com.android.wm.shell.common.pip.PipDesktopState; import com.android.wm.shell.pip.PipTransitionController; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.pip2.animation.PipAlphaAnimator; +import com.android.wm.shell.splitscreen.SplitScreenController; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; +import org.mockito.ArgumentMatchers; import org.mockito.Captor; import org.mockito.Mock; -import org.mockito.Mockito; import org.mockito.MockitoAnnotations; import java.util.Optional; @@ -77,15 +76,14 @@ public class PipSchedulerTest { @Mock private PipBoundsState mMockPipBoundsState; @Mock private ShellExecutor mMockMainExecutor; @Mock private PipTransitionState mMockPipTransitionState; + @Mock private PipDesktopState mMockPipDesktopState; @Mock private PipTransitionController mMockPipTransitionController; @Mock private Runnable mMockUpdateMovementBoundsRunnable; @Mock private WindowContainerToken mMockPipTaskToken; @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; @Mock private SurfaceControl.Transaction mMockTransaction; @Mock private PipAlphaAnimator mMockAlphaAnimator; - @Mock private DesktopUserRepositories mMockDesktopUserRepositories; - @Mock private DesktopWallpaperActivityTokenProvider mMockDesktopWallpaperActivityTokenProvider; - @Mock private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer; + @Mock private SplitScreenController mMockSplitScreenController; @Captor private ArgumentCaptor<Runnable> mRunnableArgumentCaptor; @Captor private ArgumentCaptor<WindowContainerTransaction> mWctArgumentCaptor; @@ -101,14 +99,10 @@ public class PipSchedulerTest { when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) .thenReturn(mMockTransaction); - when(mMockDesktopUserRepositories.getCurrent()) - .thenReturn(Mockito.mock(DesktopRepository.class)); - when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(Mockito.mock(TaskInfo.class)); mPipScheduler = new PipScheduler(mMockContext, mMockPipBoundsState, mMockMainExecutor, - mMockPipTransitionState, Optional.of(mMockDesktopUserRepositories), - Optional.of(mMockDesktopWallpaperActivityTokenProvider), - mRootTaskDisplayAreaOrganizer); + mMockPipTransitionState, Optional.of(mMockSplitScreenController), + mMockPipDesktopState); mPipScheduler.setPipTransitionController(mMockPipTransitionController); mPipScheduler.setSurfaceControlTransactionFactory(mMockFactory); mPipScheduler.setPipAlphaAnimatorSupplier((context, leash, startTx, finishTx, direction) -> @@ -134,12 +128,18 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, never()).startExpandTransition(any()); + verify(mMockPipTransitionController, never()).startExpandTransition(any(), anyBoolean()); } @Test - public void scheduleExitPipViaExpand_exitTransitionCalled() { + public void scheduleExitPipViaExpand_noSplit_expandTransitionCalled() { setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 isn't in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(false); mPipScheduler.scheduleExitPipViaExpand(); @@ -147,7 +147,29 @@ public class PipSchedulerTest { assertNotNull(mRunnableArgumentCaptor.getValue()); mRunnableArgumentCaptor.getValue().run(); - verify(mMockPipTransitionController, times(1)).startExpandTransition(any()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); + } + + @Test + public void scheduleExitPipViaExpand_lastParentInSplit_prepareSplitAndExpand() { + setMockPipTaskToken(); + ActivityManager.RunningTaskInfo pipTaskInfo = getTaskInfoWithLastParentBeforePip(1); + when(mMockPipTransitionState.getPipTaskInfo()).thenReturn(pipTaskInfo); + + // Make sure task with the id = 1 is in split-screen. + when(mMockSplitScreenController.isTaskInSplitScreen( + ArgumentMatchers.eq(1))).thenReturn(true); + + mPipScheduler.scheduleExitPipViaExpand(); + + verify(mMockMainExecutor, times(1)).execute(mRunnableArgumentCaptor.capture()); + assertNotNull(mRunnableArgumentCaptor.getValue()); + mRunnableArgumentCaptor.getValue().run(); + + // We need to both prepare the split screen with the last parent and start expanding. + verify(mMockSplitScreenController, + times(1)).prepareEnterSplitScreen(any(), any(), anyInt()); + verify(mMockPipTransitionController, times(1)).startExpandTransition(any(), anyBoolean()); } @Test @@ -274,4 +296,10 @@ public class PipSchedulerTest { private void setMockPipTaskToken() { when(mMockPipTransitionState.getPipTaskToken()).thenReturn(mMockPipTaskToken); } + + private ActivityManager.RunningTaskInfo getTaskInfoWithLastParentBeforePip(int lastParentId) { + final ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.lastParentTaskIdBeforePip = lastParentId; + return taskInfo; + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java index 89cb729d17b5..1b462c30e017 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTaskListenerTest.java @@ -22,6 +22,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.when; import static org.mockito.kotlin.MatchersKt.eq; @@ -164,6 +165,25 @@ public class PipTaskListenerTest { } @Test + public void onTaskInfoChanged_withNullPipParams_doNothing() { + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + mPipTaskListener.addParamsChangedListener(mMockPipParamsChangedCallback); + Rational aspectRatio = new Rational(4, 3); + when(mMockPipBoundsState.getAspectRatio()).thenReturn(aspectRatio.toFloat()); + String action1 = "action1"; + mPipTaskListener.onTaskInfoChanged(getTaskInfo(aspectRatio, action1)); + + clearInvocations(mMockPipParamsChangedCallback); + mPipTaskListener.onTaskInfoChanged(new ActivityManager.RunningTaskInfo()); + + verifyZeroInteractions(mMockPipParamsChangedCallback); + verify(mMockPipTransitionState, times(0)) + .setOnIdlePipTransitionStateRunnable(any(Runnable.class)); + } + + @Test public void onTaskInfoChanged_withActionsChanged_callbackActionsChanged() { mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, @@ -193,6 +213,12 @@ public class PipTaskListenerTest { mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, mMockPipBoundsAlgorithm, mMockShellExecutor); mPipTaskListener.addParamsChangedListener(mMockPipParamsChangedCallback); + + // For this test case, any aspect ratio passed is considered within allowed range. + when(mMockPipBoundsAlgorithm + .isValidPictureInPictureAspectRatio(anyFloat())) + .thenReturn(true); + Rational aspectRatio = new Rational(4, 3); when(mMockPipBoundsState.getAspectRatio()).thenReturn(aspectRatio.toFloat()); String action1 = "action1"; @@ -227,6 +253,29 @@ public class PipTaskListenerTest { } @Test + public void onTaskInfoChanged_nonValidAspectRatio_doesNotCallbackAspectRatioChanged() { + mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, + mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockShellExecutor); + mPipTaskListener.addParamsChangedListener(mMockPipParamsChangedCallback); + + String action1 = "action1"; + mPipTaskListener.onTaskInfoChanged(getTaskInfo(null, action1)); + verify(mMockPipTransitionState, times(0)) + .setOnIdlePipTransitionStateRunnable(any(Runnable.class)); + + // Define an invalid aspect ratio and try and update the params with it. + Rational aspectRatio = new Rational(100, 3); + when(mMockPipBoundsAlgorithm + .isValidPictureInPictureAspectRatio(eq(aspectRatio.floatValue()))) + .thenReturn(false); + + mPipTaskListener.onTaskInfoChanged(getTaskInfo(aspectRatio, action1)); + verify(mMockPipTransitionState, times(0)) + .setOnIdlePipTransitionStateRunnable(any(Runnable.class)); + } + + @Test public void onPipTransitionStateChanged_scheduledBoundsChangeWithAspectRatioChange_schedule() { mPipTaskListener = new PipTaskListener(mMockContext, mMockShellTaskOrganizer, mMockPipTransitionState, mMockPipScheduler, mMockPipBoundsState, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java new file mode 100644 index 000000000000..2e389b7dd151 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/PipTouchStateTest.java @@ -0,0 +1,148 @@ +/* + * Copyright (C) 2017 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.pip2.phone; + +import static android.view.MotionEvent.ACTION_BUTTON_PRESS; +import static android.view.MotionEvent.ACTION_DOWN; +import static android.view.MotionEvent.ACTION_MOVE; +import static android.view.MotionEvent.ACTION_UP; + +import static org.junit.Assert.assertFalse; +import static org.junit.Assert.assertTrue; + +import android.os.SystemClock; +import android.testing.AndroidTestingRunner; +import android.view.MotionEvent; +import android.view.ViewConfiguration; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.ShellTestCase; +import com.android.wm.shell.TestShellExecutor; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; + +@RunWith(AndroidTestingRunner.class) +@SmallTest +public class PipTouchStateTest extends ShellTestCase { + + private PipTouchState mTouchState; + private CountDownLatch mDoubleTapCallbackTriggeredLatch; + private CountDownLatch mHoverExitCallbackTriggeredLatch; + private TestShellExecutor mMainExecutor; + + @Before + public void setUp() throws Exception { + mMainExecutor = new TestShellExecutor(); + mDoubleTapCallbackTriggeredLatch = new CountDownLatch(1); + mHoverExitCallbackTriggeredLatch = new CountDownLatch(1); + mTouchState = new PipTouchState(ViewConfiguration.get(getContext()), + mDoubleTapCallbackTriggeredLatch::countDown, + mHoverExitCallbackTriggeredLatch::countDown, + mMainExecutor); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + } + + @Test + public void testDoubleTapLongSingleTap_notDoubleTapAndNotWaiting() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT + 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTapTimeout_timeoutCallbackCalled() throws Exception { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertFalse(mTouchState.isDoubleTap()); + assertTrue(mTouchState.isWaitingForDoubleTap()); + + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == 10); + mTouchState.scheduleDoubleTapTimeoutCallback(); + + mMainExecutor.flushAll(); + assertTrue(mDoubleTapCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testDoubleTapDrag_doubleTapCanceled() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_MOVE, currentTime + 10, 500, 500)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 20, 500, 500)); + assertTrue(mTouchState.isDragging()); + assertFalse(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testDoubleTap_doubleTapRegistered() { + final long currentTime = SystemClock.uptimeMillis(); + + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, currentTime, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, currentTime + 10, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_DOWN, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 20, 0, 0)); + mTouchState.onTouchEvent(createMotionEvent(ACTION_UP, + currentTime + PipTouchState.DOUBLE_TAP_TIMEOUT - 10, 0, 0)); + assertTrue(mTouchState.isDoubleTap()); + assertFalse(mTouchState.isWaitingForDoubleTap()); + assertTrue(mTouchState.getDoubleTapTimeoutCallbackDelay() == -1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 0); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + @Test + public void testHoverExitTimeout_timeoutCallbackNotCalled_ifButtonPress() throws Exception { + mTouchState.scheduleHoverExitTimeoutCallback(); + mTouchState.onTouchEvent(createMotionEvent(ACTION_BUTTON_PRESS, SystemClock.uptimeMillis(), + 0, 0)); + mMainExecutor.flushAll(); + assertTrue(mHoverExitCallbackTriggeredLatch.getCount() == 1); + } + + private MotionEvent createMotionEvent(int action, long eventTime, float x, float y) { + return MotionEvent.obtain(0, eventTime, action, x, y, 0); + } + +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java new file mode 100644 index 000000000000..2a22842eda1a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/phone/transition/PipExpandHandlerTest.java @@ -0,0 +1,188 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.pip2.phone.transition; + +import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.view.WindowManager.TRANSIT_CHANGE; + +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; +import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP_TO_SPLIT; + +import static org.junit.Assert.assertNull; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.VerificationKt.times; +import static org.mockito.kotlin.VerificationKt.verify; + +import android.annotation.Nullable; +import android.app.ActivityManager; +import android.app.PictureInPictureParams; +import android.app.WindowConfiguration; +import android.content.Context; +import android.content.Intent; +import android.graphics.Rect; +import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; +import android.view.SurfaceControl; +import android.view.WindowManager; +import android.window.TransitionInfo; +import android.window.TransitionRequestInfo; +import android.window.WindowContainerToken; +import android.window.WindowContainerTransaction; + +import androidx.test.filters.SmallTest; + +import com.android.wm.shell.common.pip.PipBoundsAlgorithm; +import com.android.wm.shell.common.pip.PipBoundsState; +import com.android.wm.shell.common.pip.PipDisplayLayoutState; +import com.android.wm.shell.pip2.animation.PipExpandAnimator; +import com.android.wm.shell.pip2.phone.PipTransitionState; +import com.android.wm.shell.splitscreen.SplitScreenController; +import com.android.wm.shell.transition.TransitionInfoBuilder; +import com.android.wm.shell.util.StubTransaction; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import java.util.Optional; + +/** + * Unit test against {@link PipExpandHandler} + */ + +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipExpandHandlerTest { + @Mock private Context mMockContext; + @Mock private PipBoundsState mMockPipBoundsState; + @Mock private PipBoundsAlgorithm mMockPipBoundsAlgorithm; + @Mock private PipTransitionState mMockPipTransitionState; + @Mock private PipDisplayLayoutState mMockPipDisplayLayoutState; + @Mock private SplitScreenController mMockSplitScreenController; + + @Mock private IBinder mMockTransitionToken; + @Mock private TransitionRequestInfo mMockRequestInfo; + @Mock private StubTransaction mStartT; + @Mock private StubTransaction mFinishT; + @Mock private SurfaceControl mPipLeash; + + @Mock private PipExpandAnimator mMockPipExpandAnimator; + + @Surface.Rotation + private static final int DISPLAY_ROTATION = Surface.ROTATION_0; + + private static final float SNAP_FRACTION = 1.5f; + private static final Rect PIP_BOUNDS = new Rect(0, 0, 100, 100); + private static final Rect DISPLAY_BOUNDS = new Rect(0, 0, 1000, 1000); + private static final Rect RIGHT_HALF_DISPLAY_BOUNDS = new Rect(500, 0, 1000, 1000); + + private PipExpandHandler mPipExpandHandler; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockPipBoundsState.getBounds()).thenReturn(PIP_BOUNDS); + when(mMockPipBoundsAlgorithm.getSnapFraction(eq(PIP_BOUNDS))).thenReturn(SNAP_FRACTION); + when(mMockPipDisplayLayoutState.getRotation()).thenReturn(DISPLAY_ROTATION); + + mPipExpandHandler = new PipExpandHandler(mMockContext, mMockPipBoundsState, + mMockPipBoundsAlgorithm, mMockPipTransitionState, mMockPipDisplayLayoutState, + Optional.of(mMockSplitScreenController)); + mPipExpandHandler.setPipExpandAnimatorSupplier((context, leash, startTransaction, + finishTransaction, baseBounds, startBounds, endBounds, + sourceRectHint, rotation) -> mMockPipExpandAnimator); + } + + @Test + public void handleRequest_returnNull() { + // All expand from PiP transitions are started in Shell, so handleRequest shouldn't be + // returning any non-null WCT + WindowContainerTransaction wct = mPipExpandHandler.handleRequest( + mMockTransitionToken, mMockRequestInfo); + assertNull(wct); + } + + @Test + public void startAnimation_transitExit_startExpandAnimator() { + final ActivityManager.RunningTaskInfo pipTaskInfo = createPipTaskInfo( + 1, WINDOWING_MODE_FULLSCREEN, new PictureInPictureParams.Builder().build()); + + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP, pipTaskInfo, null /* lastParent */, false /* toSplit */); + final WindowContainerToken pipToken = pipTaskInfo.getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + @Test + public void startAnimation_transitExitToSplit_startExpandAnimator() { + // The task info of the task that was pinned while we were in PiP. + final WindowContainerToken pipToken = createPipTaskInfo(1, WINDOWING_MODE_FULLSCREEN, + new PictureInPictureParams.Builder().build()).getToken(); + when(mMockPipTransitionState.getPipTaskToken()).thenReturn(pipToken); + + // Change representing the ActivityRecord we are animating in the multi-activity PiP case; + // make sure change's taskInfo=null as this is an activity, but let lastParent be PiP token. + final TransitionInfo info = getExpandFromPipTransitionInfo( + TRANSIT_EXIT_PIP_TO_SPLIT, null /* taskInfo */, pipToken, true /* toSplit */); + + mPipExpandHandler.startAnimation(mMockTransitionToken, info, mStartT, mFinishT, + (wct) -> {}); + + verify(mMockSplitScreenController, times(1)).finishEnterSplitScreen(eq(mFinishT)); + verify(mMockPipExpandAnimator, times(1)).start(); + verify(mMockPipBoundsState, times(1)).saveReentryState(SNAP_FRACTION); + } + + private TransitionInfo getExpandFromPipTransitionInfo(@WindowManager.TransitionType int type, + @Nullable ActivityManager.RunningTaskInfo pipTaskInfo, + @Nullable WindowContainerToken lastParent, boolean toSplit) { + final TransitionInfo info = new TransitionInfoBuilder(type) + .addChange(TRANSIT_CHANGE, pipTaskInfo).build(); + final TransitionInfo.Change pipChange = info.getChanges().getFirst(); + pipChange.setRotation(DISPLAY_ROTATION, + WindowConfiguration.ROTATION_UNDEFINED); + pipChange.setStartAbsBounds(PIP_BOUNDS); + pipChange.setEndAbsBounds(toSplit ? RIGHT_HALF_DISPLAY_BOUNDS : DISPLAY_BOUNDS); + pipChange.setLeash(mPipLeash); + pipChange.setLastParent(lastParent); + return info; + } + + private static ActivityManager.RunningTaskInfo createPipTaskInfo(int taskId, + int windowingMode, PictureInPictureParams params) { + ActivityManager.RunningTaskInfo taskInfo = new ActivityManager.RunningTaskInfo(); + taskInfo.taskId = taskId; + taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); + taskInfo.token = mock(WindowContainerToken.class); + taskInfo.baseIntent = mock(Intent.class); + taskInfo.pictureInPictureParams = params; + return taskInfo; + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java index 542289db6cfc..a546b3ea7d8f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentTasksControllerTest.java @@ -63,7 +63,6 @@ import android.graphics.Rect; import android.os.Bundle; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import androidx.test.ext.junit.runners.AndroidJUnit4; @@ -89,7 +88,6 @@ import com.android.wm.shell.sysui.ShellInit; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -109,6 +107,7 @@ import java.util.function.Consumer; @RunWith(AndroidJUnit4.class) @SmallTest public class RecentTasksControllerTest extends ShellTestCase { + private static final String SYSTEM_UI_PACKAGE_NAME = "com.android.systemui"; @Mock private Context mContext; @@ -129,9 +128,6 @@ public class RecentTasksControllerTest extends ShellTestCase { @Mock private DesktopRepository mDesktopRepository; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; private RecentTasksController mRecentTasksControllerReal; @@ -587,6 +583,19 @@ public class RecentTasksControllerTest extends ShellTestCase { @Test @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void onTaskAdded_orDesktopWallpaperActivity_doesNotTriggerOnRunningTaskAppeared() + throws Exception { + RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + + mRecentTasksControllerReal.onTaskAdded(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskAppeared(any()); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) public void taskWindowingModeChanged_desktopRunningAppsEnabled_triggersOnRunningTaskChanged() throws Exception { mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); @@ -598,6 +607,19 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void taskInfoChanged_forDesktopWallpaperActivity_doesNotTriggerOnRunningTaskChanged() + throws Exception { + RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + + mRecentTasksControllerReal.onTaskRunningInfoChanged(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskChanged(any()); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS) public void @@ -624,6 +646,20 @@ public class RecentTasksControllerTest extends ShellTestCase { verify(mRecentTasksListener).onRunningTaskVanished(taskInfo); } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS}) + public void onTaskRemoved_forDesktopWallpaperActivity_doesNotTriggerOnRunningTaskVanished() + throws Exception { + RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + + mRecentTasksControllerReal.onTaskRemoved(taskInfo); + + verify(mRecentTasksListener, never()).onRunningTaskVanished(any()); + } + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS) @@ -664,6 +700,18 @@ public class RecentTasksControllerTest extends ShellTestCase { } @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + public void onDesktopWallpaperActivityMovedToFront_doesNotTriggerOnTaskMovedToFront() + throws Exception { + RunningTaskInfo taskInfo = makeDesktopWallpaperActivityTaskInfo(/* taskId= */10); + mRecentTasksControllerReal.registerRecentTasksListener(mRecentTasksListener); + + mRecentTasksControllerReal.onTaskMovedToFrontThroughTransition(taskInfo); + + verify(mRecentTasksListener, never()).onTaskMovedToFront(any()); + } + + @Test public void getNullSplitBoundsNonSplitTask() { SplitBounds sb = mRecentTasksController.getSplitBoundsForTaskId(3); assertNull("splitBounds should be null for non-split task", sb); @@ -834,16 +882,25 @@ public class RecentTasksControllerTest extends ShellTestCase { * Helper to create a running task with a given task id. */ private RunningTaskInfo makeRunningTaskInfo(int taskId) { + return makeRunningTaskInfo(taskId, new ComponentName("com." + taskId, "Activity" + taskId)); + } + + private RunningTaskInfo makeRunningTaskInfo(int taskId, ComponentName intentComponent) { RunningTaskInfo info = new RunningTaskInfo(); info.taskId = taskId; info.realActivity = new ComponentName("testPackage", "testClass"); Intent intent = new Intent(); - intent.setComponent(new ComponentName("com." + taskId, "Activity" + taskId)); + intent.setComponent(intentComponent); info.baseIntent = intent; info.lastNonFullscreenBounds = new Rect(); return info; } + private RunningTaskInfo makeDesktopWallpaperActivityTaskInfo(int taskId) { + return makeRunningTaskInfo(taskId, new ComponentName(SYSTEM_UI_PACKAGE_NAME, + DesktopWallpaperActivity.class.getName())); + } + /** * Helper to set the raw task list on the controller. */ diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java index ab43119b14c0..fd5e567f69ed 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/RecentsTransitionHandlerTest.java @@ -17,9 +17,13 @@ package com.android.wm.shell.recents; import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; +import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; +import static android.view.WindowManager.TRANSIT_CLOSE; +import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; +import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_ANIMATING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_NOT_RUNNING; import static com.android.wm.shell.recents.RecentsTransitionStateListener.TRANSITION_STATE_REQUESTED; @@ -30,6 +34,7 @@ import static com.google.common.truth.Truth.assertThat; import static org.junit.Assert.assertNull; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -44,10 +49,11 @@ import android.app.PendingIntent; import android.content.Context; import android.content.Intent; import android.content.pm.PackageManager; +import android.content.res.Resources; import android.os.Binder; import android.os.Bundle; import android.os.IBinder; -import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.annotations.EnableFlags; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -58,6 +64,7 @@ import androidx.test.runner.AndroidJUnit4; import com.android.dx.mockito.inline.extended.ExtendedMockito; import com.android.dx.mockito.inline.extended.StaticMockitoSession; import com.android.internal.os.IResultReceiver; +import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestRunningTaskInfoBuilder; @@ -77,7 +84,6 @@ import com.android.wm.shell.util.StubTransaction; import org.junit.After; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -94,9 +100,13 @@ import java.util.Optional; @SmallTest public class RecentsTransitionHandlerTest extends ShellTestCase { + private static final int FREEFORM_TASK_CORNER_RADIUS = 32; + @Mock private Context mContext; @Mock + private Resources mResources; + @Mock private TaskStackListenerImpl mTaskStackListener; @Mock private ShellCommandHandler mShellCommandHandler; @@ -115,9 +125,6 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { @Mock private DesktopRepository mDesktopRepository; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - private ShellTaskOrganizer mShellTaskOrganizer; private RecentTasksController mRecentTasksController; private RecentTasksController mRecentTasksControllerReal; @@ -139,6 +146,10 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { when(mContext.getPackageManager()).thenReturn(mock(PackageManager.class)); when(mContext.getSystemService(KeyguardManager.class)) .thenReturn(mock(KeyguardManager.class)); + when(mContext.getResources()).thenReturn(mResources); + when(mResources.getDimensionPixelSize( + R.dimen.desktop_windowing_freeform_rounded_corner_radius) + ).thenReturn(FREEFORM_TASK_CORNER_RADIUS); mShellInit = spy(new ShellInit(mMainExecutor)); mShellController = spy(new ShellController(mContext, mShellInit, mShellCommandHandler, mDisplayInsetsController, mMainExecutor)); @@ -281,6 +292,82 @@ public class RecentsTransitionHandlerTest extends ShellTestCase { assertThat(listener.getState()).isEqualTo(TRANSITION_STATE_NOT_RUNNING); } + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMerge_openingTasks_callsOnTasksAppeared() throws Exception { + final IRecentsAnimationRunner animationRunner = mock(IRecentsAnimationRunner.class); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, new TestRunningTaskInfoBuilder().build()) + .build(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false, animationRunner); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mMainExecutor.flushAll(); + + verify(animationRunner).onTasksAppeared( + /* appearedTargets= */ any(), eq(mergeTransitionInfo)); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testMergeAndFinish_openingFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo mergeTransitionInfo = new TransitionInfoBuilder(TRANSIT_OPEN) + .addChange(TRANSIT_OPEN, freeformTask) + .build(); + SurfaceControl leash = mergeTransitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, createTransitionInfo(), new StubTransaction(), new StubTransaction(), + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).merge( + mergeTransitionInfo, + new StubTransaction(), + finishT, + transition, + mock(Transitions.TransitionFinishCallback.class)); + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_RECENTS_TRANSITIONS_CORNERS_BUGFIX) + public void testFinish_returningToFreeformTasks_setsCornerRadius() { + ActivityManager.RunningTaskInfo freeformTask = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + TransitionInfo transitionInfo = new TransitionInfoBuilder(TRANSIT_CLOSE) + .addChange(TRANSIT_CLOSE, freeformTask) + .build(); + SurfaceControl leash = transitionInfo.getChanges().get(0).getLeash(); + final IBinder transition = startRecentsTransition(/* synthetic= */ false); + SurfaceControl.Transaction finishT = mock(SurfaceControl.Transaction.class); + mRecentsTransitionHandler.startAnimation( + transition, transitionInfo, new StubTransaction(), finishT, + mock(Transitions.TransitionFinishCallback.class)); + + mRecentsTransitionHandler.findController(transition).finish(/* toHome= */ false, + false /* sendUserLeaveHint */, mock(IResultReceiver.class)); + mMainExecutor.flushAll(); + + + verify(finishT).setCornerRadius(leash, FREEFORM_TASK_CORNER_RADIUS); + } + private IBinder startRecentsTransition(boolean synthetic) { return startRecentsTransition(synthetic, mock(IRecentsAnimationRunner.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt index 99194620c313..769407b370c3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/recents/TaskStackTransitionObserverTest.kt @@ -26,7 +26,6 @@ import android.content.Intent import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.SurfaceControl import android.view.WindowManager @@ -42,6 +41,7 @@ import android.window.WindowContainerToken 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.TestShellExecutor import com.android.wm.shell.TestSyncExecutor import com.android.wm.shell.common.ShellExecutor @@ -53,7 +53,6 @@ import com.android.wm.shell.windowdecor.extension.isFullscreen import com.google.common.truth.Truth.assertThat import dagger.Lazy import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentCaptor @@ -71,9 +70,7 @@ import org.mockito.kotlin.whenever */ @SmallTest @RunWith(AndroidTestingRunner::class) -class TaskStackTransitionObserverTest { - - @JvmField @Rule val setFlagsRule = SetFlagsRule() +class TaskStackTransitionObserverTest : ShellTestCase() { @Mock private lateinit var shellInit: ShellInit @Mock private lateinit var shellTaskOrganizerLazy: Lazy<ShellTaskOrganizer> diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt index c19232b6f787..4630649c75cc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt @@ -31,6 +31,7 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.any import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.ArgumentMatchers.anyLong import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -56,6 +57,7 @@ class WindowAnimatorTest { whenever(change.endAbsBounds).thenReturn(END_BOUNDS) whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction) whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction) + whenever(transaction.setFrameTimeline(anyLong())).thenReturn(transaction) whenever( transaction.setPosition( any(), diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt new file mode 100644 index 000000000000..fd22a84dee5d --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DragZoneFactoryTest.kt @@ -0,0 +1,320 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.content.Context +import android.graphics.Insets +import android.graphics.Rect +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.wm.shell.shared.bubbles.DragZoneFactory.DesktopWindowModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker +import com.android.wm.shell.shared.bubbles.DragZoneFactory.SplitScreenModeChecker.SplitScreenMode +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +private typealias DragZoneVerifier = (dragZone: DragZone) -> Unit + +@SmallTest +@RunWith(AndroidJUnit4::class) +/** Unit tests for [DragZoneFactory]. */ +class DragZoneFactoryTest { + + private val context = getApplicationContext<Context>() + private lateinit var dragZoneFactory: DragZoneFactory + private val tabletPortrait = + DeviceConfig( + windowBounds = Rect(0, 0, 1000, 2000), + isLargeScreen = true, + isSmallTablet = false, + isLandscape = false, + isRtl = false, + insets = Insets.of(0, 0, 0, 0) + ) + private val tabletLandscape = + tabletPortrait.copy(windowBounds = Rect(0, 0, 2000, 1000), isLandscape = true) + private val foldablePortrait = + tabletPortrait.copy(windowBounds = Rect(0, 0, 800, 900), isSmallTablet = true) + private val foldableLandscape = + foldablePortrait.copy(windowBounds = Rect(0, 0, 900, 800), isLandscape = true) + private val splitScreenModeChecker = SplitScreenModeChecker { SplitScreenMode.NONE } + private var isDesktopWindowModeSupported = true + private val desktopWindowModeChecker = DesktopWindowModeChecker { isDesktopWindowModeSupported } + + @Test + fun dragZonesForBubbleBar_tablet() { + dragZoneFactory = + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.BubbleBar(BubbleBarLocation.LEFT)) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForBubble_tablet_portrait() { + dragZoneFactory = + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForBubble_tablet_landscape() { + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForBubble_foldable_portrait() { + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForBubble_foldable_landscape() { + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForExpandedView_tablet_portrait() { + dragZoneFactory = + DragZoneFactory( + context, + tabletPortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForExpandedView_tablet_landscape() { + dragZoneFactory = + DragZoneFactory( + context, + tabletLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.DesktopWindow>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForExpandedView_foldable_portrait() { + dragZoneFactory = + DragZoneFactory( + context, + foldablePortrait, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Left>(), + verifyInstance<DragZone.Split.Right>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForExpandedView_foldable_landscape() { + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + val expectedZones: List<DragZoneVerifier> = + listOf( + verifyInstance<DragZone.Dismiss>(), + verifyInstance<DragZone.FullScreen>(), + verifyInstance<DragZone.Split.Top>(), + verifyInstance<DragZone.Split.Bottom>(), + verifyInstance<DragZone.Bubble.Left>(), + verifyInstance<DragZone.Bubble.Right>(), + ) + assertThat(dragZones).hasSize(expectedZones.size) + dragZones.zip(expectedZones).forEach { (zone, instanceVerifier) -> instanceVerifier(zone) } + } + + @Test + fun dragZonesForBubble_tablet_desktopModeDisabled() { + isDesktopWindowModeSupported = false + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones(DraggedObject.Bubble(BubbleBarLocation.LEFT)) + assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() + } + + @Test + fun dragZonesForExpandedView_tablet_desktopModeDisabled() { + isDesktopWindowModeSupported = false + dragZoneFactory = + DragZoneFactory( + context, + foldableLandscape, + splitScreenModeChecker, + desktopWindowModeChecker + ) + val dragZones = + dragZoneFactory.createSortedDragZones( + DraggedObject.ExpandedView(BubbleBarLocation.LEFT) + ) + assertThat(dragZones.filterIsInstance<DragZone.DesktopWindow>()).isEmpty() + } + + private inline fun <reified T> verifyInstance(): DragZoneVerifier = { dragZone -> + assertThat(dragZone).isInstanceOf(T::class.java) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt new file mode 100644 index 000000000000..180a6915b45f --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/bubbles/DropTargetManagerTest.kt @@ -0,0 +1,343 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.bubbles + +import android.content.Context +import android.graphics.Rect +import android.view.View +import android.widget.FrameLayout +import androidx.core.animation.AnimatorTestRule +import androidx.test.core.app.ApplicationProvider.getApplicationContext +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.platform.app.InstrumentationRegistry +import com.google.common.truth.Truth.assertThat +import kotlin.test.assertFails +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +/** Unit tests for [DropTargetManager]. */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class DropTargetManagerTest { + + @get:Rule val animatorTestRule = AnimatorTestRule() + + private val context = getApplicationContext<Context>() + private lateinit var dropTargetManager: DropTargetManager + private lateinit var dragZoneChangedListener: FakeDragZoneChangedListener + private lateinit var container: FrameLayout + + // create 3 drop zones that are horizontally next to each other + // ------------------------------------------------- + // | | | | + // | bubble | | bubble | + // | | dismiss | | + // | left | | right | + // | | | | + // ------------------------------------------------- + private val bubbleLeftDragZone = + DragZone.Bubble.Left(bounds = Rect(0, 0, 100, 100), dropTarget = Rect(0, 0, 50, 200)) + private val dismissDragZone = DragZone.Dismiss(bounds = Rect(100, 0, 200, 100)) + private val bubbleRightDragZone = + DragZone.Bubble.Right(bounds = Rect(200, 0, 300, 100), dropTarget = Rect(200, 0, 280, 150)) + + private val dropTargetView: View + get() = container.getChildAt(0) + + @Before + fun setUp() { + container = FrameLayout(context) + dragZoneChangedListener = FakeDragZoneChangedListener() + dropTargetManager = + DropTargetManager(context, container, isLayoutRtl = false, dragZoneChangedListener) + } + + @Test + fun onDragStarted_notifiesInitialDragZone() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(dragZoneChangedListener.initialDragZone).isEqualTo(bubbleLeftDragZone) + } + + @Test + fun onDragStarted_missingExpectedDragZone_fails() { + assertFails { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.RIGHT), + listOf(bubbleLeftDragZone) + ) + } + } + + @Test + fun onDragUpdated_notifiesDragZoneChanged() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + } + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + dismissDragZone.bounds.centerX(), + dismissDragZone.bounds.centerY() + ) + } + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_withinSameZone_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleLeftDragZone.bounds.centerX(), + bubbleLeftDragZone.bounds.centerY() + ) + } + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_outsideAllZones_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + val pointX = 200 + val pointY = 200 + assertThat(bubbleLeftDragZone.contains(pointX, pointY)).isFalse() + assertThat(bubbleRightDragZone.contains(pointX, pointY)).isFalse() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated(pointX, pointY) + } + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragUpdated_hasOverlappingZones_notifiesFirstDragZoneChanged() { + // create a drag zone that spans across the width of all 3 drag zones, but extends below + // them + val splitDragZone = DragZone.Split.Left(bounds = Rect(0, 0, 300, 200)) + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone, splitDragZone) + ) + + // drag to a point that is within both the bubble right zone and split zone + val (pointX, pointY) = + Pair(bubbleRightDragZone.bounds.centerX(), bubbleRightDragZone.bounds.centerY()) + assertThat(splitDragZone.contains(pointX, pointY)).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated(pointX, pointY) + } + // verify we dragged to the bubble right zone because that has higher priority than split + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleLeftDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(bubbleRightDragZone) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + 150 // below the bubble and dismiss drag zones but within split + ) + } + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(bubbleRightDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(splitDragZone) + + val (dismissPointX, dismissPointY) = + Pair(dismissDragZone.bounds.centerX(), dismissDragZone.bounds.centerY()) + assertThat(splitDragZone.contains(dismissPointX, dismissPointY)).isTrue() + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated(dismissPointX, dismissPointY) + } + assertThat(dragZoneChangedListener.fromDragZone).isEqualTo(splitDragZone) + assertThat(dragZoneChangedListener.toDragZone).isEqualTo(dismissDragZone) + } + + @Test + fun onDragUpdated_afterDragEnded_doesNotNotify() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone, dismissDragZone) + ) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragEnded() + } + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + assertThat(dragZoneChangedListener.fromDragZone).isNull() + assertThat(dragZoneChangedListener.toDragZone).isNull() + } + + @Test + fun onDragStarted_dropTargetAddedToContainer() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(container.childCount).isEqualTo(1) + assertThat(dropTargetView.alpha).isEqualTo(0) + } + + @Test + fun onDragEnded_dropTargetRemovedFromContainer() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(container.childCount).isEqualTo(1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragEnded() + animatorTestRule.advanceTimeBy(250) + } + assertThat(container.childCount).isEqualTo(0) + } + + @Test + fun startNewDrag_beforeDropTargetRemoved() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + assertThat(container.childCount).isEqualTo(1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragEnded() + // advance the timer by 100ms so the animation doesn't complete + animatorTestRule.advanceTimeBy(100) + } + assertThat(container.childCount).isEqualTo(1) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(bubbleLeftDragZone, bubbleRightDragZone) + ) + } + assertThat(container.childCount).isEqualTo(1) + } + + @Test + fun updateDragZone_withDropTarget_dropTargetUpdated() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1) + verifyDropTargetPosition(bubbleRightDragZone.dropTarget) + } + + @Test + fun updateDragZone_withoutDropTarget_dropTargetHidden() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + dismissDragZone.bounds.centerX(), + dismissDragZone.bounds.centerY() + ) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(0) + } + + @Test + fun updateDragZone_betweenZonesWithDropTarget_dropTargetUpdated() { + dropTargetManager.onDragStarted( + DraggedObject.Bubble(BubbleBarLocation.LEFT), + listOf(dismissDragZone, bubbleLeftDragZone, bubbleRightDragZone) + ) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleRightDragZone.bounds.centerX(), + bubbleRightDragZone.bounds.centerY() + ) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1) + verifyDropTargetPosition(bubbleRightDragZone.dropTarget) + + InstrumentationRegistry.getInstrumentation().runOnMainSync { + dropTargetManager.onDragUpdated( + bubbleLeftDragZone.bounds.centerX(), + bubbleLeftDragZone.bounds.centerY() + ) + animatorTestRule.advanceTimeBy(250) + } + + assertThat(dropTargetView.alpha).isEqualTo(1) + verifyDropTargetPosition(bubbleLeftDragZone.dropTarget) + } + + private fun verifyDropTargetPosition(rect: Rect) { + assertThat(dropTargetView.scaleX).isEqualTo(rect.width()) + assertThat(dropTargetView.scaleY).isEqualTo(rect.height()) + assertThat(dropTargetView.translationX).isEqualTo(rect.exactCenterX()) + assertThat(dropTargetView.translationY).isEqualTo(rect.exactCenterY()) + } + + private class FakeDragZoneChangedListener : DropTargetManager.DragZoneChangedListener { + var initialDragZone: DragZone? = null + var fromDragZone: DragZone? = null + var toDragZone: DragZone? = null + + override fun onInitialDragZoneSet(dragZone: DragZone) { + initialDragZone = dragZone + } + + override fun onDragZoneChanged(from: DragZone, to: DragZone) { + fromDragZone = from + toDragZone = to + } + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt index 8c78debdc19f..f69bf34ea3f7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeCompatPolicyTest.kt @@ -16,17 +16,32 @@ package com.android.wm.shell.shared.desktopmode +import android.app.TaskInfo +import android.compat.testing.PlatformCompatChangeRule import android.content.ComponentName +import android.content.pm.ActivityInfo +import android.content.pm.ApplicationInfo +import android.content.pm.PackageManager +import android.os.Process +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest import com.android.internal.R -import com.android.wm.shell.compatui.CompatUIShellTestCase +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase import com.android.wm.shell.desktopmode.DesktopTestHelpers.createFreeformTask +import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModelTestsBase.Companion.HOME_LAUNCHER_PACKAGE_NAME +import libcore.junit.util.compat.CoreCompatChangeRule.DisableCompatChanges +import libcore.junit.util.compat.CoreCompatChangeRule.EnableCompatChanges import org.junit.Assert.assertFalse import org.junit.Assert.assertTrue import org.junit.Before +import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever /** * Tests for [@link DesktopModeCompatPolicy]. @@ -35,12 +50,17 @@ import org.junit.runner.RunWith */ @RunWith(AndroidTestingRunner::class) @SmallTest -class DesktopModeCompatPolicyTest : CompatUIShellTestCase() { +class DesktopModeCompatPolicyTest : ShellTestCase() { + @get:Rule val compatRule = PlatformCompatChangeRule() private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy + private val packageManager: PackageManager = mock() + private val homeActivities = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") @Before fun setUp() { desktopModeCompatPolicy = DesktopModeCompatPolicy(mContext) + whenever(packageManager.getHomeActivities(any())).thenReturn(homeActivities) + mContext.setMockPackageManager(packageManager) } @Test @@ -110,4 +130,93 @@ class DesktopModeCompatPolicyTest : CompatUIShellTestCase() { isTopActivityNoDisplay = true })) } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage() { + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + baseActivity = homeActivities + isTopActivityNoDisplay = false + })) + } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage_notDisplayed() { + assertFalse(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + baseActivity = homeActivities + isTopActivityNoDisplay = true + })) + } + + @Test + fun testIsTopActivityExemptFromDesktopWindowing_defaultHomePackage_notYetAvailable() { + val emptyHomeActivities: ComponentName = mock() + mContext.setMockPackageManager(packageManager) + + whenever(emptyHomeActivities.packageName).thenReturn(null) + whenever(packageManager.getHomeActivities(any())).thenReturn(emptyHomeActivities) + + assertTrue(desktopModeCompatPolicy.isTopActivityExemptFromDesktopWindowing( + createFreeformTask(/* displayId */ 0) + .apply { + isTopActivityNoDisplay = false + })) + } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + fun testShouldExcludeCaptionFromAppBounds_resizeable_false() { + assertFalse(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = true }) + ) + } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + fun testShouldExcludeCaptionFromAppBounds_nonResizeable_true() { + assertTrue(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = false }) + ) + } + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @EnableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + fun testShouldExcludeCaptionFromAppBounds_nonResizeable_sdk35_false() { + assertFalse(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = false }) + ) + } + + + @Test + @EnableFlags(Flags.FLAG_EXCLUDE_CAPTION_FROM_APP_BOUNDS) + @DisableCompatChanges(ActivityInfo.INSETS_DECOUPLED_CONFIGURATION_ENFORCED) + @EnableCompatChanges(ActivityInfo.OVERRIDE_EXCLUDE_CAPTION_INSETS_FROM_APP_BOUNDS) + fun testShouldExcludeCaptionFromAppBounds_resizeable_overridden_true() { + assertTrue(desktopModeCompatPolicy.shouldExcludeCaptionFromAppBounds( + setUpFreeformTask().apply { isResizeable = true }) + ) + } + + fun setUpFreeformTask(): TaskInfo = + createFreeformTask().apply { + val componentName = + ComponentName.createRelative( + mContext, + DesktopModeCompatPolicyTest::class.java.simpleName + ) + baseActivity = componentName + topActivityInfo = ActivityInfo().apply { + applicationInfo = ApplicationInfo().apply { + packageName = componentName.packageName + uid = Process.myUid() + } + } + } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt new file mode 100644 index 000000000000..4082ffd4ac0a --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeStatusTest.kt @@ -0,0 +1,257 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.wm.shell.shared.desktopmode + +import android.content.Context +import android.content.res.Resources +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags +import android.platform.test.annotations.Presubmit +import android.platform.test.flag.junit.SetFlagsRule +import android.window.DesktopModeFlags +import androidx.test.filters.SmallTest +import com.android.internal.R +import com.android.window.flags.Flags +import com.android.wm.shell.ShellTestCase +import com.google.common.truth.Truth.assertThat +import org.junit.After +import org.junit.Before +import org.junit.Rule +import org.junit.Test +import org.mockito.kotlin.doReturn +import org.mockito.kotlin.eq +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** + * Test class for [DesktopModeStatus]. + */ +@SmallTest +@Presubmit +@EnableFlags(Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) +class DesktopModeStatusTest : ShellTestCase() { + @get:Rule + val mSetFlagsRule = SetFlagsRule() + + private val mockContext = mock<Context>() + private val mockResources = mock<Resources>() + + @Before + fun setUp() { + doReturn(mockResources).whenever(mockContext).getResources() + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(false).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + setDeviceEligibleForDesktopMode(false) + doReturn(context.contentResolver).whenever(mockContext).contentResolver + resetDesktopModeFlagsCache() + resetEnforceDeviceRestriction() + } + + @After + fun tearDown() { + resetDesktopModeFlagsCache() + resetEnforceDeviceRestriction() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_returnsFalse() { + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_deviceEligible_configDevOptionOn_returnsFalse() { + setDeviceEligibleForDesktopMode(true) + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + disableEnforceDeviceRestriction() + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_configDevOptionOn_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags( + Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE, + Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION + ) + @Test + fun canEnterDesktopMode_DWFlagDisabled_deviceNotEligible_forceUsingDevOption_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isFalse() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_deviceEligible_returnsTrue() { + setDeviceEligibleForDesktopMode(true) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @Test + fun canEnterDesktopMode_DWFlagEnabled_deviceNotEligible_forceUsingDevOption_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + setFlagOverride(DesktopModeFlags.ToggleOverride.OVERRIDE_ON) + + assertThat(DesktopModeStatus.canEnterDesktopMode(mockContext)).isTrue() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktop_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOffAndIntDispHostsDesktop_returnsFalse() { + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @Test + fun isDeviceEligibleForDesktopMode_configDEModeOnAndIntDispHostsDesktopOff_returnsFalse() { + doReturn(true).whenever(mockResources).getBoolean(eq(R.bool.config_isDesktopModeSupported)) + doReturn(false).whenever(mockResources).getBoolean(eq(R.bool.config_canInternalDisplayHostDesktops)) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) + @DisableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isInternalDisplayEligibleToHostDesktops_supportFlagOff_returnsFalse() { + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_returnsFalse() { + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_MODE_THROUGH_DEV_OPTION) + @Test + fun isInternalDisplayEligibleToHostDesktops_supportFlagOn_configDevOptModeOn_returnsTrue() { + doReturn(true).whenever(mockResources).getBoolean( + eq(R.bool.config_isDesktopModeDevOptionSupported) + ) + + assertThat(DesktopModeStatus.isDeviceEligibleForDesktopMode(mockContext)).isTrue() + } + + @DisableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagDisabled_returnsFalse() { + setDeviceEligibleForDesktopMode(true) + + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagEnabled_deviceNotEligible_returnsFalse() { + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isFalse() + } + + @EnableFlags(Flags.FLAG_SHOW_DESKTOP_EXPERIENCE_DEV_OPTION) + @Test + fun canShowDesktopExperienceDevOption_flagEnabled_deviceEligible_returnsTrue() { + setDeviceEligibleForDesktopMode(true) + + assertThat(DesktopModeStatus.canShowDesktopExperienceDevOption(mockContext)).isTrue() + } + + private fun resetEnforceDeviceRestriction() { + setEnforceDeviceRestriction(true) + } + + private fun disableEnforceDeviceRestriction() { + setEnforceDeviceRestriction(false) + } + + private fun setEnforceDeviceRestriction(value: Boolean) { + val field = DesktopModeStatus::class.java.getDeclaredField("ENFORCE_DEVICE_RESTRICTIONS") + field.isAccessible = true + field.setBoolean(null, value) + } + + private fun resetDesktopModeFlagsCache() { + val cachedToggleOverride = + DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride") + cachedToggleOverride.isAccessible = true + cachedToggleOverride.set(null, null) + } + + private fun setFlagOverride(override: DesktopModeFlags.ToggleOverride) { + val cachedToggleOverride = + DesktopModeFlags::class.java.getDeclaredField("sCachedToggleOverride") + cachedToggleOverride.isAccessible = true + cachedToggleOverride.set(null, override) + } + + private fun setDeviceEligibleForDesktopMode(eligible: Boolean) { + val deviceRestrictions = DesktopModeStatus::class.java.getDeclaredField("ENFORCE_DEVICE_RESTRICTIONS") + deviceRestrictions.isAccessible = true + deviceRestrictions.setBoolean(/* obj= */ null, /* z= */ !eligible) + } +} 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 b9d6a454694d..e5a6a6d258dd 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 @@ -360,7 +360,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(false /* returnToApp */, commitWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(commitWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(commitWCT), commitWCT, mock(SurfaceControl.Transaction.class)); } assertFalse(mStageCoordinator.isSplitScreenVisible()); @@ -430,7 +431,8 @@ public class SplitTransitionTests extends ShellTestCase { mStageCoordinator.onRecentsInSplitAnimationFinishing(true /* returnToApp */, restoreWCT, mock(SurfaceControl.Transaction.class)); } else { - mStageCoordinator.onRecentsInSplitAnimationFinish(restoreWCT, + mStageCoordinator.onRecentsInSplitAnimationFinishing( + mStageCoordinator.wctIsReorderingSplitToTop(restoreWCT), restoreWCT, mock(SurfaceControl.Transaction.class)); } assertTrue(mStageCoordinator.isSplitScreenVisible()); 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 5851cbf9b933..10c28626dc9f 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 @@ -22,6 +22,7 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; import static android.view.Display.DEFAULT_DISPLAY; +import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_50_50; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_INDEX_UNDEFINED; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT; import static com.android.wm.shell.shared.split.SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT; @@ -61,6 +62,9 @@ import android.os.Bundle; import android.os.Handler; import android.os.IBinder; import android.os.Looper; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; +import android.platform.test.flag.junit.SetFlagsRule; import android.view.SurfaceControl; import android.window.DisplayAreaInfo; import android.window.RemoteTransition; @@ -71,6 +75,8 @@ import androidx.test.annotation.UiThreadTest; import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; +import com.android.internal.logging.InstanceId; +import com.android.window.flags.Flags; import com.android.wm.shell.MockToken; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTaskOrganizer; @@ -96,6 +102,7 @@ import com.android.wm.shell.transition.HomeTransitionObserver; import com.android.wm.shell.transition.Transitions; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -111,6 +118,9 @@ import java.util.function.Consumer; @SmallTest @RunWith(AndroidJUnit4.class) public class StageCoordinatorTests extends ShellTestCase { + @Rule + public final SetFlagsRule setFlagsRule = new SetFlagsRule(); + @Mock private ShellTaskOrganizer mTaskOrganizer; @Mock @@ -141,12 +151,15 @@ public class StageCoordinatorTests extends ShellTestCase { private final Rect mBounds1 = new Rect(10, 20, 30, 40); private final Rect mBounds2 = new Rect(5, 10, 15, 20); private final Rect mRootBounds = new Rect(0, 0, 45, 60); + private final int mTaskId = 18; - private SurfaceControl mRootLeash; - private SurfaceControl mDividerLeash; private ActivityManager.RunningTaskInfo mRootTask; private StageCoordinator mStageCoordinator; - private Transitions mTransitions; + private SplitScreenTransitions mSplitScreenTransitions; + private SplitScreenListener mSplitScreenListener; + private IBinder mBinder; + private ActivityManager.RunningTaskInfo mRunningTaskInfo; + private RemoteTransition mRemoteTransition; private final TestShellExecutor mMainExecutor = new TestShellExecutor(); private final ShellExecutor mAnimExecutor = new TestShellExecutor(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); @@ -154,19 +167,35 @@ public class StageCoordinatorTests extends ShellTestCase { DEFAULT_DISPLAY, 0); private final ActivityManager.RunningTaskInfo mMainChildTaskInfo = new TestRunningTaskInfoBuilder().setVisible(true).build(); + private final ArgumentCaptor<WindowContainerTransaction> mWctCaptor = + ArgumentCaptor.forClass(WindowContainerTransaction.class); + private final WindowContainerTransaction mWct = spy(new WindowContainerTransaction()); @Before @UiThreadTest public void setup() { MockitoAnnotations.initMocks(this); - mTransitions = createTestTransitions(); + Transitions transitions = createTestTransitions(); + WindowContainerToken token = mock(WindowContainerToken.class); + SurfaceControl dividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); + mStageCoordinator = spy(new StageCoordinator(mContext, DEFAULT_DISPLAY, mSyncQueue, mTaskOrganizer, mMainStage, mSideStage, mDisplayController, mDisplayImeController, - mDisplayInsetsController, mSplitLayout, mTransitions, mTransactionPool, + mDisplayInsetsController, mSplitLayout, transitions, mTransactionPool, mMainExecutor, mMainHandler, Optional.empty(), mLaunchAdjacentController, Optional.empty(), mSplitState, Optional.empty(), mRootTDAOrganizer)); - - mDividerLeash = new SurfaceControl.Builder().setName("fakeDivider").build(); + mSplitScreenTransitions = spy(mStageCoordinator.getSplitTransitions()); + mSplitScreenListener = mock(SplitScreenListener.class); + mStageCoordinator.setSplitTransitions(mSplitScreenTransitions); + mBinder = mock(IBinder.class); + mRunningTaskInfo = mock(ActivityManager.RunningTaskInfo.class); + mRemoteTransition = mock(RemoteTransition.class); + mRunningTaskInfo.token = token; + + when(mRemoteTransition.getDebugName()).thenReturn(""); + when(token.asBinder()).thenReturn(mBinder); + when(mRunningTaskInfo.getToken()).thenReturn(token); + when(mTaskOrganizer.getRunningTaskInfo(mTaskId)).thenReturn(mRunningTaskInfo); when(mRootTDAOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(mDisplayAreaInfo); when(mSplitLayout.getTopLeftBounds()).thenReturn(mBounds1); @@ -174,11 +203,11 @@ public class StageCoordinatorTests extends ShellTestCase { when(mSplitLayout.getRootBounds()).thenReturn(mRootBounds); when(mSplitLayout.isLeftRightSplit()).thenReturn(false); when(mSplitLayout.applyTaskChanges(any(), any(), any())).thenReturn(true); - when(mSplitLayout.getDividerLeash()).thenReturn(mDividerLeash); + when(mSplitLayout.getDividerLeash()).thenReturn(dividerLeash); mRootTask = new TestRunningTaskInfoBuilder().build(); - mRootLeash = new SurfaceControl.Builder().setName("test").build(); - mStageCoordinator.onTaskAppeared(mRootTask, mRootLeash); + SurfaceControl rootLeash = new SurfaceControl.Builder().setName("test").build(); + mStageCoordinator.onTaskAppeared(mRootTask, rootLeash); mSideStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); mMainStage.mRootTaskInfo = new TestRunningTaskInfoBuilder().build(); @@ -196,14 +225,12 @@ public class StageCoordinatorTests extends ShellTestCase { public void testMoveToStage_splitActiveBackground() { when(mStageCoordinator.isSplitActive()).thenReturn(true); - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - final WindowContainerTransaction wct = spy(new WindowContainerTransaction()); + mStageCoordinator.moveToStage(mRootTask, SPLIT_POSITION_BOTTOM_OR_RIGHT, mWct); - mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); // TODO(b/349828130) Address this once we remove index_undefined called - verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + verify(mStageCoordinator).prepareEnterSplitScreen(eq(mWct), eq(mRootTask), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); - verify(mMainStage).reparentTopTask(eq(wct)); + verify(mMainStage).reparentTopTask(eq(mWct)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getMainStagePosition()); } @@ -215,25 +242,21 @@ public class StageCoordinatorTests extends ShellTestCase { // Assume current side stage is top or left. mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); + mStageCoordinator.moveToStage(mRootTask, SPLIT_POSITION_BOTTOM_OR_RIGHT, mWct); - mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); // TODO(b/349828130) Address this once we remove index_undefined called - verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + verify(mStageCoordinator).prepareEnterSplitScreen(eq(mWct), eq(mRootTask), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getMainStagePosition()); assertEquals(SPLIT_POSITION_TOP_OR_LEFT, mStageCoordinator.getSideStagePosition()); } @Test - public void testMoveToStage_splitInctive() { - final ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder().build(); - final WindowContainerTransaction wct = new WindowContainerTransaction(); + public void testMoveToStage_splitInactive() { + mStageCoordinator.moveToStage(mRootTask, SPLIT_POSITION_BOTTOM_OR_RIGHT, mWct); - mStageCoordinator.moveToStage(task, SPLIT_POSITION_BOTTOM_OR_RIGHT, wct); // TODO(b/349828130) Address this once we remove index_undefined called - verify(mStageCoordinator).prepareEnterSplitScreen(eq(wct), eq(task), + verify(mStageCoordinator).prepareEnterSplitScreen(eq(mWct), eq(mRootTask), eq(SPLIT_POSITION_BOTTOM_OR_RIGHT), eq(false), eq(SPLIT_INDEX_UNDEFINED)); assertEquals(SPLIT_POSITION_BOTTOM_OR_RIGHT, mStageCoordinator.getSideStagePosition()); } @@ -248,25 +271,23 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testLayoutChanged_topLeftSplitPosition_updatesUnfoldStageBounds() { mStageCoordinator.setSideStagePosition(SPLIT_POSITION_TOP_OR_LEFT, null); - final SplitScreenListener listener = mock(SplitScreenListener.class); - mStageCoordinator.registerSplitScreenListener(listener); - clearInvocations(listener); + mStageCoordinator.registerSplitScreenListener(mSplitScreenListener); + clearInvocations(mSplitScreenListener); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(listener).onSplitBoundsChanged(mRootBounds, mBounds2, mBounds1); + verify(mSplitScreenListener).onSplitBoundsChanged(mRootBounds, mBounds2, mBounds1); } @Test public void testLayoutChanged_bottomRightSplitPosition_updatesUnfoldStageBounds() { mStageCoordinator.setSideStagePosition(SPLIT_POSITION_BOTTOM_OR_RIGHT, null); - final SplitScreenListener listener = mock(SplitScreenListener.class); - mStageCoordinator.registerSplitScreenListener(listener); - clearInvocations(listener); + mStageCoordinator.registerSplitScreenListener(mSplitScreenListener); + clearInvocations(mSplitScreenListener); mStageCoordinator.onLayoutSizeChanged(mSplitLayout); - verify(listener).onSplitBoundsChanged(mRootBounds, mBounds1, mBounds2); + verify(mSplitScreenListener).onSplitBoundsChanged(mRootBounds, mBounds1, mBounds2); } @Test @@ -367,13 +388,8 @@ public class StageCoordinatorTests extends ShellTestCase { @Test public void testSplitIntentAndTaskWithPippedApp_launchFullscreen() { int taskId = 9; - SplitScreenTransitions splitScreenTransitions = - spy(mStageCoordinator.getSplitTransitions()); - mStageCoordinator.setSplitTransitions(splitScreenTransitions); mStageCoordinator.setMixedHandler(mDefaultMixedHandler); PendingIntent pendingIntent = mock(PendingIntent.class); - RemoteTransition remoteTransition = mock(RemoteTransition.class); - when(remoteTransition.getDebugName()).thenReturn(""); // Test launching second task full screen when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); mStageCoordinator.startIntentAndTask( @@ -384,9 +400,9 @@ public class StageCoordinatorTests extends ShellTestCase { null /*option2*/, 0 /*splitPosition*/, 1 /*snapPosition*/, - remoteTransition /*remoteTransition*/, + mRemoteTransition /*remoteTransition*/, null /*instanceId*/); - verify(splitScreenTransitions, times(1)) + verify(mSplitScreenTransitions, times(1)) .startFullscreenTransition(any(), any()); // Test launching first intent fullscreen @@ -400,22 +416,17 @@ public class StageCoordinatorTests extends ShellTestCase { null /*option2*/, 0 /*splitPosition*/, 1 /*snapPosition*/, - remoteTransition /*remoteTransition*/, + mRemoteTransition /*remoteTransition*/, null /*instanceId*/); - verify(splitScreenTransitions, times(2)) + verify(mSplitScreenTransitions, times(2)) .startFullscreenTransition(any(), any()); } @Test public void testSplitIntentsWithPippedApp_launchFullscreen() { - SplitScreenTransitions splitScreenTransitions = - spy(mStageCoordinator.getSplitTransitions()); - mStageCoordinator.setSplitTransitions(splitScreenTransitions); mStageCoordinator.setMixedHandler(mDefaultMixedHandler); PendingIntent pendingIntent = mock(PendingIntent.class); PendingIntent pendingIntent2 = mock(PendingIntent.class); - RemoteTransition remoteTransition = mock(RemoteTransition.class); - when(remoteTransition.getDebugName()).thenReturn(""); // Test launching second task full screen when(mDefaultMixedHandler.isIntentInPip(pendingIntent)).thenReturn(true); mStageCoordinator.startIntents( @@ -429,9 +440,9 @@ public class StageCoordinatorTests extends ShellTestCase { new Bundle(), 0 /*splitPosition*/, 1 /*snapPosition*/, - remoteTransition /*remoteTransition*/, + mRemoteTransition /*remoteTransition*/, null /*instanceId*/); - verify(splitScreenTransitions, times(1)) + verify(mSplitScreenTransitions, times(1)) .startFullscreenTransition(any(), any()); // Test launching first intent fullscreen @@ -448,35 +459,54 @@ public class StageCoordinatorTests extends ShellTestCase { new Bundle(), 0 /*splitPosition*/, 1 /*snapPosition*/, - remoteTransition /*remoteTransition*/, + mRemoteTransition /*remoteTransition*/, null /*instanceId*/); - verify(splitScreenTransitions, times(2)) + verify(mSplitScreenTransitions, times(2)) .startFullscreenTransition(any(), any()); } - @Test public void startTask_ensureWindowingModeCleared() { - SplitScreenTransitions splitScreenTransitions = - spy(mStageCoordinator.getSplitTransitions()); - mStageCoordinator.setSplitTransitions(splitScreenTransitions); - ArgumentCaptor<WindowContainerTransaction> wctCaptor = - ArgumentCaptor.forClass(WindowContainerTransaction.class); - int taskId = 18; - IBinder binder = mock(IBinder.class); - ActivityManager.RunningTaskInfo rti = mock(ActivityManager.RunningTaskInfo.class); - WindowContainerToken mockToken = mock(WindowContainerToken.class); - when(mockToken.asBinder()).thenReturn(binder); - when(rti.getToken()).thenReturn(mockToken); - when(mTaskOrganizer.getRunningTaskInfo(taskId)).thenReturn(rti); - mStageCoordinator.startTask(taskId, SPLIT_POSITION_TOP_OR_LEFT, null /*options*/, + mStageCoordinator.startTask(mTaskId, SPLIT_POSITION_TOP_OR_LEFT, null /*options*/, null, SPLIT_INDEX_UNDEFINED); - verify(splitScreenTransitions).startEnterTransition(anyInt(), - wctCaptor.capture(), any(), any(), anyInt(), anyBoolean()); + verify(mSplitScreenTransitions).startEnterTransition(anyInt(), + mWctCaptor.capture(), any(), any(), anyInt(), anyBoolean()); + + int windowingMode = mWctCaptor.getValue().getChanges().get(mBinder).getWindowingMode(); + assertEquals(windowingMode, WINDOWING_MODE_UNDEFINED); + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX) + public void startTasksOnSingleFreeformWindow_ensureWindowingModeClearedAndLaunchFullScreen() { + mDisplayAreaInfo.configuration.windowConfiguration.setWindowingMode( + WINDOWING_MODE_FREEFORM); + when(mRunningTaskInfo.getWindowingMode()).thenReturn(WINDOWING_MODE_FREEFORM); + + mStageCoordinator.startTasks(mTaskId, null, INVALID_TASK_ID, null, + SPLIT_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50, mRemoteTransition, + InstanceId.fakeInstanceId(0)); - int windowingMode = wctCaptor.getValue().getChanges().get(binder).getWindowingMode(); + verify(mSplitScreenTransitions).startFullscreenTransition(mWctCaptor.capture(), any()); + int windowingMode = mWctCaptor.getValue().getChanges().get(mBinder).getWindowingMode(); assertEquals(windowingMode, WINDOWING_MODE_UNDEFINED); } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_FULL_SCREEN_WINDOW_ON_REMOVING_SPLIT_SCREEN_STAGE_BUGFIX) + public void startTasksOnSingleFreeformWindow_flagDisabled_noChangeToWindowingModeInWct() { + mDisplayAreaInfo.configuration.windowConfiguration.setWindowingMode( + WINDOWING_MODE_FREEFORM); + when(mRunningTaskInfo.getWindowingMode()).thenReturn(WINDOWING_MODE_FREEFORM); + + mStageCoordinator.startTasks(mTaskId, null, INVALID_TASK_ID, null, + SPLIT_POSITION_TOP_OR_LEFT, SNAP_TO_2_50_50, mRemoteTransition, + InstanceId.fakeInstanceId(0)); + + verify(mSplitScreenTransitions).startFullscreenTransition(mWctCaptor.capture(), any()); + assertThat(mWctCaptor.getValue().getChanges()).isEmpty(); + } + @Test public void testDismiss_freeformDisplay() { mDisplayAreaInfo.configuration.windowConfiguration.setWindowingMode( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java index 6ac34d736f6f..2d454a55a51c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTest.java @@ -51,7 +51,6 @@ import android.graphics.Rect; import android.graphics.Region; import android.os.Looper; import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.view.SurfaceHolder; @@ -72,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; @@ -96,9 +94,6 @@ public class TaskViewTest extends ShellTestCase { return FlagsParameterization.allCombinationsOf(Flags.FLAG_TASK_VIEW_REPOSITORY); } - @Rule - public final SetFlagsRule mSetFlagsRule; - @Mock TaskView.Listener mViewListener; @Mock @@ -127,9 +122,7 @@ public class TaskViewTest extends ShellTestCase { TaskViewTransitions mTaskViewTransitions; TaskViewTaskController mTaskViewTaskController; - public TaskViewTest(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(flags); - } + public TaskViewTest(FlagsParameterization flags) {} @Before public void setUp() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java index 326f11e300fd..3a455ba6b5df 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/taskview/TaskViewTransitionsTest.java @@ -34,7 +34,6 @@ import android.app.ActivityManager; import android.graphics.Rect; import android.os.IBinder; import android.platform.test.flag.junit.FlagsParameterization; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.TestableLooper; import android.view.SurfaceControl; import android.window.TransitionInfo; @@ -50,7 +49,6 @@ import com.android.wm.shell.common.SyncTransactionQueue; import com.android.wm.shell.transition.Transitions; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; @@ -74,9 +72,6 @@ public class TaskViewTransitionsTest extends ShellTestCase { Flags.FLAG_ENABLE_TASK_VIEW_CONTROLLER_CLEANUP); } - @Rule - public final SetFlagsRule mSetFlagsRule; - @Mock Transitions mTransitions; @Mock @@ -95,9 +90,7 @@ public class TaskViewTransitionsTest extends ShellTestCase { TaskViewRepository mTaskViewRepository; TaskViewTransitions mTaskViewTransitions; - public TaskViewTransitionsTest(FlagsParameterization flags) { - mSetFlagsRule = new SetFlagsRule(flags); - } + public TaskViewTransitionsTest(FlagsParameterization flags) {} @Before public void setUp() { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java index e540322a96a1..18fdbeff40f4 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/DefaultTransitionHandlerTest.java @@ -48,6 +48,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import androidx.test.filters.SmallTest; import androidx.test.platform.app.InstrumentationRegistry; +import com.android.internal.jank.InteractionJankMonitor; import com.android.wm.shell.RootTaskDisplayAreaOrganizer; import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestShellExecutor; @@ -96,7 +97,7 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { mTransitionHandler = new DefaultTransitionHandler( mContext, mShellInit, mDisplayController, mTransactionPool, mMainExecutor, mMainHandler, mAnimExecutor, - mRootTaskDisplayAreaOrganizer); + mRootTaskDisplayAreaOrganizer, mock(InteractionJankMonitor.class)); mShellInit.init(); } @@ -292,6 +293,7 @@ public class DefaultTransitionHandlerTest extends ShellTestCase { new Binder(), new TransitionInfoBuilder(TRANSIT_SLEEP, FLAG_SYNC).build(), MockTransactionPool.create(), + MockTransactionPool.create(), token, mock(Transitions.TransitionFinishCallback.class)); } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java index 74c2f0e6753a..96e4f4955dc8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/FocusTransitionObserverTest.java @@ -35,8 +35,6 @@ import static org.mockito.Mockito.when; import android.app.ActivityManager.RunningTaskInfo; import android.os.RemoteException; import android.platform.test.annotations.RequiresFlagsEnabled; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; @@ -50,7 +48,6 @@ import com.android.wm.shell.TestShellExecutor; import com.android.wm.shell.shared.FocusTransitionListener; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -67,8 +64,6 @@ public class FocusTransitionObserverTest extends ShellTestCase { static final int SECONDARY_DISPLAY_ID = 1; - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private FocusTransitionListener mListener; private final TestShellExecutor mShellExecutor = new TestShellExecutor(); private FocusTransitionObserver mFocusTransitionObserver; diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java index 3e53ee5cfb9f..6f28e656d060 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/HomeTransitionObserverTest.java @@ -39,8 +39,6 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.platform.test.flag.junit.CheckFlagsRule; -import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.view.SurfaceControl; import android.window.TransitionInfo; import android.window.TransitionInfo.TransitionMode; @@ -60,7 +58,6 @@ import com.android.wm.shell.sysui.ShellController; import com.android.wm.shell.sysui.ShellInit; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -73,9 +70,6 @@ import java.util.List; @SmallTest @RunWith(AndroidJUnit4.class) public class HomeTransitionObserverTest extends ShellTestCase { - - @Rule - public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); private final ShellTaskOrganizer mOrganizer = mock(ShellTaskOrganizer.class); private final TransactionPool mTransactionPool = mock(TransactionPool.class); private final Context mContext = diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java index 0a19be4eb959..6f73db0bacc3 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/transition/ShellTransitionTests.java @@ -74,7 +74,8 @@ import android.os.Handler; import android.os.IBinder; import android.os.Looper; import android.os.RemoteException; -import android.platform.test.flag.junit.SetFlagsRule; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.util.ArraySet; import android.util.Pair; import android.view.Surface; @@ -117,7 +118,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.util.StubTransaction; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Answers; @@ -145,9 +145,6 @@ public class ShellTransitionTests extends ShellTestCase { private final TestTransitionHandler mDefaultHandler = new TestTransitionHandler(); private final Handler mMainHandler = new Handler(Looper.getMainLooper()); - @Rule - public final SetFlagsRule setFlagsRule = new SetFlagsRule(); - @Before public void setUp() { doAnswer(invocation -> new Binder()) @@ -553,7 +550,8 @@ public class ShellTransitionTests extends ShellTestCase { } @Test - public void testRegisteredRemoteTransitionTakeover() { + @DisableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + public void testRegisteredRemoteTransitionTakeover_flagDisabled() { Transitions transitions = createTestTransitions(); transitions.replaceDefaultHandlerForTest(mDefaultHandler); @@ -608,7 +606,6 @@ public class ShellTransitionTests extends ShellTestCase { mMainExecutor.flushAll(); // Takeover shouldn't happen when the flag is disabled. - setFlagsRule.disableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); @@ -621,12 +618,69 @@ public class ShellTransitionTests extends ShellTestCase { mDefaultHandler.finishAll(); mMainExecutor.flushAll(); verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); + } + + @Test + @EnableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED) + public void testRegisteredRemoteTransitionTakeover_flagEnabled() { + Transitions transitions = createTestTransitions(); + transitions.replaceDefaultHandlerForTest(mDefaultHandler); + + IRemoteTransition testRemote = new RemoteTransitionStub() { + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) throws RemoteException { + final Transitions.TransitionHandler takeoverHandler = + transitions.getHandlerForTakeover(token, info); + + if (takeoverHandler == null) { + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); + return; + } + + takeoverHandler.takeOverAnimation(token, info, new SurfaceControl.Transaction(), + wct -> { + try { + finishCallback.onTransitionFinished(wct, null /* sct */); + } catch (RemoteException e) { + // Fail + } + }, new WindowAnimationState[info.getChanges().size()]); + } + }; + final boolean[] takeoverRemoteCalled = new boolean[]{false}; + IRemoteTransition testTakeoverRemote = new RemoteTransitionStub() { + @Override + public void startAnimation(IBinder token, TransitionInfo info, + SurfaceControl.Transaction t, + IRemoteTransitionFinishedCallback finishCallback) {} + + @Override + public void takeOverAnimation(IBinder transition, TransitionInfo info, + SurfaceControl.Transaction startTransaction, + IRemoteTransitionFinishedCallback finishCallback, WindowAnimationState[] states) + throws RemoteException { + takeoverRemoteCalled[0] = true; + finishCallback.onTransitionFinished(null /* wct */, null /* sct */); + } + }; + + TransitionFilter filter = new TransitionFilter(); + filter.mRequirements = + new TransitionFilter.Requirement[]{new TransitionFilter.Requirement()}; + filter.mRequirements[0].mModes = new int[]{TRANSIT_OPEN, TRANSIT_TO_FRONT}; + + transitions.registerRemote(filter, new RemoteTransition(testRemote, "Test")); + transitions.registerRemoteForTakeover( + filter, new RemoteTransition(testTakeoverRemote, "Test")); + mMainExecutor.flushAll(); // Takeover should happen when the flag is enabled. - setFlagsRule.enableFlags(Flags.FLAG_RETURN_ANIMATION_FRAMEWORK_LONG_LIVED); + IBinder transitToken = new Binder(); transitions.requestStartTransition(transitToken, new TransitionRequestInfo(TRANSIT_OPEN, null /* trigger */, null /* remote */)); - info = new TransitionInfoBuilder(TRANSIT_OPEN) + TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN) .addChange(TRANSIT_OPEN).addChange(TRANSIT_CLOSE).build(); transitions.onTransitionReady(transitToken, info, new StubTransaction(), new StubTransaction()); @@ -634,7 +688,7 @@ public class ShellTransitionTests extends ShellTestCase { assertTrue(takeoverRemoteCalled[0]); mDefaultHandler.finishAll(); mMainExecutor.flushAll(); - verify(mOrganizer, times(2)).finishTransition(eq(transitToken), any()); + verify(mOrganizer, times(1)).finishTransition(eq(transitToken), any()); } @Test @@ -1705,7 +1759,9 @@ public class ShellTransitionTests extends ShellTestCase { @Override public void mergeAnimation(@NonNull IBinder transition, @NonNull TransitionInfo info, - @NonNull SurfaceControl.Transaction t, @NonNull IBinder mergeTarget, + @NonNull SurfaceControl.Transaction startT, + @NonNull SurfaceControl.Transaction finishT, + @NonNull IBinder mergeTarget, @NonNull Transitions.TransitionFinishCallback finishCallback) { if (mFinishOnSync && info.getType() == TRANSIT_SLEEP) { for (int i = 0; i < mFinishes.size(); ++i) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java index 71af97e5add3..aad18cba4436 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/unfold/UnfoldTransitionHandlerTest.java @@ -43,6 +43,7 @@ import android.window.TransitionInfo; import android.window.TransitionRequestInfo; import android.window.WindowContainerTransaction; +import com.android.wm.shell.ShellTestCase; import com.android.wm.shell.TestSyncExecutor; import com.android.wm.shell.common.ShellExecutor; import com.android.wm.shell.shared.TransactionPool; @@ -61,7 +62,7 @@ import java.util.ArrayList; import java.util.List; import java.util.concurrent.Executor; -public class UnfoldTransitionHandlerTest { +public class UnfoldTransitionHandlerTest extends ShellTestCase { private UnfoldTransitionHandler mUnfoldTransitionHandler; @@ -169,7 +170,8 @@ public class UnfoldTransitionHandlerTest { // Send fold transition request TransitionFinishCallback mergeFinishCallback = mock(TransitionFinishCallback.class); mUnfoldTransitionHandler.mergeAnimation(new Binder(), createFoldTransitionInfo(), - mock(SurfaceControl.Transaction.class), mTransition, mergeFinishCallback); + mock(SurfaceControl.Transaction.class), mock(SurfaceControl.Transaction.class), + mTransition, mergeFinishCallback); mTestLooper.dispatchAll(); // Verify that fold transition is merged into unfold and that unfold is finished @@ -387,6 +389,7 @@ public class UnfoldTransitionHandlerTest { new Binder(), new TransitionInfoBuilder(TRANSIT_CHANGE, TRANSIT_FLAG_KEYGUARD_GOING_AWAY).build(), mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mTransition, mergeCallback); verify(finishCallback, never()).onTransitionFinished(any()); @@ -396,6 +399,7 @@ public class UnfoldTransitionHandlerTest { new Binder(), new TransitionInfoBuilder(TRANSIT_CHANGE).build(), mock(SurfaceControl.Transaction.class), + mock(SurfaceControl.Transaction.class), mTransition, mergeCallback); verify(mergeCallback).onTransitionFinished(any()); diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt index cf6c3a5e03a0..257bbb5603a7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopHeaderManageWindowsMenuTest.kt @@ -19,7 +19,6 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.SurfaceControl @@ -35,7 +34,6 @@ import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystem import com.google.common.truth.Truth.assertThat import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock @@ -51,10 +49,6 @@ import org.mockito.kotlin.mock @RunWith(AndroidTestingRunner::class) class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - private lateinit var userRepositories: DesktopUserRepositories private lateinit var menu: DesktopHeaderManageWindowsMenu diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt index 8b4cf6d1fabe..067dcec5d65d 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelAppHandleOnlyTest.kt @@ -25,6 +25,7 @@ import android.content.pm.ActivityInfo import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper +import android.view.Display import android.view.Display.DEFAULT_DISPLAY import android.view.SurfaceControl import androidx.test.filters.SmallTest @@ -38,10 +39,13 @@ import junit.framework.Assert.assertTrue import org.junit.Before import org.junit.Test import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.anyInt import org.mockito.Mockito.times import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever import org.mockito.quality.Strictness /** @@ -59,6 +63,8 @@ import org.mockito.quality.Strictness class DesktopModeWindowDecorViewModelAppHandleOnlyTest : DesktopModeWindowDecorViewModelTestsBase() { + protected val mockDisplay = mock<Display>() + @Before fun setUp() { mockitoSession = @@ -67,9 +73,10 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : .spyStatic(DesktopModeStatus::class.java) .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(false).`when` { DesktopModeStatus.canEnterDesktopMode(any()) } doReturn(true).`when` { DesktopModeStatus.overridesShowAppHandle(any())} setUpCommon() + whenever(mockDisplayController.getDisplay(anyInt())).thenReturn(mockDisplay) } @Test @@ -156,7 +163,7 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) - task.setOnLargeScreen(false) + setLargeScreen(false) setUpMockDecorationForTask(task) onTaskChanging(task, taskSurface) assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) @@ -172,11 +179,12 @@ class DesktopModeWindowDecorViewModelAppHandleOnlyTest : ): RunningTaskInfo { val task = createTask( displayId, windowingMode, activityType, activityInfo, requestingImmersive) - task.setOnLargeScreen(shouldShowAspectRatioButton) + setLargeScreen(shouldShowAspectRatioButton) return task } - private fun RunningTaskInfo.setOnLargeScreen(large: Boolean) { - configuration.smallestScreenWidthDp = if (large) 1000 else 100 + private fun setLargeScreen(large: Boolean) { + val size: Float = if (large) 1000f else 100f + whenever(mockDisplay.getMinSizeDimensionDp()).thenReturn(size) } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt index baccbee0893d..d8d45c02b364 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt @@ -96,7 +96,6 @@ import org.mockito.kotlin.whenever import org.mockito.quality.Strictness import java.util.function.Consumer - /** * Tests of [DesktopModeWindowDecorViewModel] * Usage: atest WMShellUnitTests:DesktopModeWindowDecorViewModelTests @@ -116,7 +115,8 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest .spyStatic(DragPositioningCallbackUtility::class.java) .startMocking() - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(Mockito.any()) } + doReturn(true).`when` { DesktopModeStatus.canEnterDesktopMode(Mockito.any()) } doReturn(false).`when` { DesktopModeStatus.overridesShowAppHandle(Mockito.any()) } setUpCommon() @@ -307,6 +307,19 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY) + fun testDecorationIsNotCreatedForDefaultHomePackage() { + val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN).apply { + baseActivity = homeComponentName + isTopActivityNoDisplay = false + } + + onTaskOpening(task) + + assertFalse(windowDecorByTaskIdSpy.contains(task.taskId)) + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_IMMERSIVE_HANDLE_HIDING) fun testInsetsStateChanged_notifiesAllDecorsInDisplay() { val task1 = createTask(windowingMode = WINDOWING_MODE_FREEFORM, displayId = 1) @@ -362,37 +375,21 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest @Test @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_desktopModeUnsupportedOnDevice_deviceRestrictionsOverridden_decorCreated() { - // Simulate enforce device restrictions system property overridden to false - whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(false) - // Simulate device that doesn't support desktop mode - doReturn(false).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } - - val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - setUpMockDecorationsForTasks(task) - - onTaskOpening(task) - assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) - } - - @Test - @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODE) - fun testWindowDecor_deviceSupportsDesktopMode_decorCreated() { + fun testWindowDecor_deviceEligibleForDesktopMode_decorCreated() { // Simulate default enforce device restrictions system property whenever(DesktopModeStatus.enforceDeviceRestrictions()).thenReturn(true) val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN) - doReturn(true).`when` { DesktopModeStatus.isDesktopModeSupported(any()) } + doReturn(true).`when` { DesktopModeStatus.canInternalDisplayHostDesktops(any()) } setUpMockDecorationsForTasks(task) onTaskOpening(task) - assertTrue(windowDecorByTaskIdSpy.contains(task.taskId)) + assertTrue(task.taskId in windowDecorByTaskIdSpy) } @Test fun testOnDecorMaximizedOrRestored_togglesTaskSize_maximize() { - val maxOrRestoreListenerCaptor = forClass(Function0::class.java) - as ArgumentCaptor<Function0<Unit>> + val maxOrRestoreListenerCaptor = forClass(Function0::class.java as Class<Function0<Unit>>) val decor = createOpenTaskDecoration( windowingMode = WINDOWING_MODE_FREEFORM, onMaxOrRestoreListenerCaptor = maxOrRestoreListenerCaptor @@ -487,7 +484,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor) ) } @@ -523,7 +519,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.LEFT), eq(ResizeTrigger.SNAP_LEFT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -545,7 +540,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.LEFT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -565,7 +559,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -601,7 +594,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(SnapPosition.RIGHT), eq(ResizeTrigger.SNAP_RIGHT_MENU), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -623,7 +615,6 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest eq(decor.mTaskInfo), any(), eq(currentBounds), eq(SnapPosition.RIGHT), eq(ResizeTrigger.MAXIMIZE_BUTTON), eq(InputMethod.UNKNOWN_INPUT_METHOD), - eq(decor), ) } @@ -638,7 +629,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest toDesktopListenerCaptor.value.accept(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON) - verify(mockDesktopTasksController).moveTaskToDesktop( + verify(mockDesktopTasksController).moveTaskToDefaultDeskAndActivate( eq(decor.mTaskInfo.taskId), any(), eq(DesktopModeTransitionSource.APP_HANDLE_MENU_BUTTON), @@ -876,7 +867,7 @@ class DesktopModeWindowDecorViewModelTests : DesktopModeWindowDecorViewModelTest ) verify(mockDesktopTasksController, times(1)) - .moveTaskToDesktop(any(), any(), any(), anyOrNull(), anyOrNull()) + .moveTaskToDefaultDeskAndActivate(any(), any(), any(), anyOrNull(), anyOrNull()) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt index c5c827467c75..8cccdb2b6120 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTestsBase.kt @@ -20,14 +20,13 @@ import android.app.ActivityManager.RunningTaskInfo import android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD import android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW import android.app.WindowConfiguration.WindowingMode +import android.content.ComponentName import android.content.pm.ActivityInfo +import android.content.pm.PackageManager import android.graphics.Rect import android.hardware.input.InputManager import android.os.Handler import android.os.UserHandle -import android.platform.test.flag.junit.CheckFlagsRule -import android.platform.test.flag.junit.DeviceFlagsValueProvider -import android.platform.test.flag.junit.SetFlagsRule import android.testing.TestableContext import android.util.SparseArray import android.view.Choreographer @@ -82,9 +81,9 @@ import com.android.wm.shell.windowdecor.DesktopModeWindowDecorViewModel.DesktopM import com.android.wm.shell.windowdecor.common.WindowDecorTaskResourceLoader import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier +import com.android.wm.shell.windowdecor.tiling.DesktopTilingDecorViewModel import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder import org.junit.After -import org.junit.Rule import org.mockito.Mockito import org.mockito.Mockito.anyInt import org.mockito.kotlin.any @@ -105,14 +104,6 @@ import kotlinx.coroutines.MainCoroutineDispatcher */ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class) open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() - - @JvmField - @Rule - val checkFlagsRule: CheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule() - private val mockDesktopModeWindowDecorFactory = mock<DesktopModeWindowDecoration.Factory>() protected val mockMainHandler = mock<Handler>() protected val mockMainChoreographer = mock<Choreographer>() @@ -157,12 +148,14 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { protected val mockCaptionHandleRepository = mock<WindowDecorCaptionHandleRepository>() protected val mockDesktopRepository: DesktopRepository = mock<DesktopRepository>() protected val mockRecentsTransitionHandler = mock<RecentsTransitionHandler>() + protected val mockTilingWindowDecoration = mock<DesktopTilingDecorViewModel>() protected val motionEvent = mock<MotionEvent>() - val displayLayout = mock<DisplayLayout>() - val display = mock<Display>() + private val displayLayout = mock<DisplayLayout>() + private val display = mock<Display>() + private val packageManager = mock<PackageManager>() + protected val homeComponentName = ComponentName(HOME_LAUNCHER_PACKAGE_NAME, /* class */ "") protected lateinit var spyContext: TestableContext private lateinit var desktopModeEventLogger: DesktopModeEventLogger - private lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy private val transactionFactory = Supplier<SurfaceControl.Transaction> { SurfaceControl.Transaction() @@ -177,6 +170,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { DisplayChangeController.OnDisplayChangingListener internal lateinit var desktopModeOnKeyguardChangedListener: DesktopModeKeyguardChangeListener protected lateinit var desktopModeWindowDecorViewModel: DesktopModeWindowDecorViewModel + protected lateinit var desktopModeCompatPolicy: DesktopModeCompatPolicy fun setUpCommon() { spyContext = spy(mContext) @@ -190,7 +184,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { whenever(mockDisplayController.getDisplay(any())).thenReturn(display) whenever(mockDesktopUserRepositories.getProfile(anyInt())) .thenReturn(mockDesktopRepository) - desktopModeCompatPolicy = DesktopModeCompatPolicy(context) + desktopModeCompatPolicy = DesktopModeCompatPolicy(spyContext) desktopModeWindowDecorViewModel = DesktopModeWindowDecorViewModel( spyContext, testShellExecutor, @@ -234,6 +228,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mock<WindowDecorTaskResourceLoader>(), mockRecentsTransitionHandler, desktopModeCompatPolicy, + mockTilingWindowDecoration, ) desktopModeWindowDecorViewModel.setSplitScreenController(mockSplitScreenController) whenever(mockDisplayController.getDisplayLayout(any())).thenReturn(mockDisplayLayout) @@ -285,6 +280,8 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { whenever(displayLayout.getStableBounds(any())).thenAnswer { i -> (i.arguments.first() as Rect).set(STABLE_BOUNDS) } + spyContext.setMockPackageManager(packageManager) + whenever(packageManager.getHomeActivities(ArrayList())).thenReturn(homeComponentName) } @After @@ -322,7 +319,7 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { mockDesktopModeWindowDecorFactory.create( any(), any(), any(), any(), any(), any(), any(), eq(task), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), any(), - any(), any()) + any(), any(), any()) ).thenReturn(decoration) decoration.mTaskInfo = task whenever(decoration.user).thenReturn(mockUserHandle) @@ -366,5 +363,6 @@ open class DesktopModeWindowDecorViewModelTestsBase : ShellTestCase() { val STABLE_INSETS = Rect(0, 100, 0, 0) val INITIAL_BOUNDS = Rect(0, 0, 100, 100) val STABLE_BOUNDS = Rect(0, 0, 1000, 1000) + val HOME_LAUNCHER_PACKAGE_NAME = "com.android.launcher" } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java index 87198d14c839..c4f70ac2297f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java @@ -20,7 +20,6 @@ import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW; import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; @@ -69,7 +68,6 @@ import android.os.Handler; import android.os.SystemProperties; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.testing.TestableContext; import android.testing.TestableLooper; @@ -110,6 +108,7 @@ import com.android.wm.shell.desktopmode.DesktopModeEventLogger; import com.android.wm.shell.desktopmode.DesktopRepository; import com.android.wm.shell.desktopmode.DesktopUserRepositories; import com.android.wm.shell.desktopmode.WindowDecorCaptionHandleRepository; +import com.android.wm.shell.shared.desktopmode.DesktopModeCompatPolicy; import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.splitscreen.SplitScreenController; import com.android.wm.shell.windowdecor.WindowDecoration.RelayoutParams; @@ -121,6 +120,7 @@ import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; import kotlin.Unit; import kotlin.jvm.functions.Function0; import kotlin.jvm.functions.Function1; +import kotlin.jvm.functions.Function2; import kotlinx.coroutines.CoroutineScope; import kotlinx.coroutines.MainCoroutineDispatcher; @@ -129,7 +129,6 @@ import org.junit.After; import org.junit.Before; import org.junit.BeforeClass; import org.junit.Ignore; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -170,8 +169,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private static final boolean DEFAULT_IS_IN_FULL_IMMERSIVE_MODE = false; private static final boolean DEFAULT_HAS_GLOBAL_FOCUS = true; private static final boolean DEFAULT_SHOULD_IGNORE_CORNER_RADIUS = false; - - @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); + private static final boolean DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS = false; @Mock private DisplayController mMockDisplayController; @@ -241,6 +239,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { private DesktopRepository mDesktopRepository; @Mock private WindowDecorTaskResourceLoader mMockTaskResourceLoader; + @Mock + private DesktopModeCompatPolicy mDesktopModeCompatPolicy; @Captor private ArgumentCaptor<Function1<Boolean, Unit>> mOnMaxMenuHoverChangeListener; @Captor @@ -297,7 +297,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { .thenReturn(mMockHandleMenu); when(mMockMultiInstanceHelper.supportsMultiInstanceSplit(any(), anyInt())) .thenReturn(false); - when(mMockAppHeaderViewHolderFactory.create(any(), any(), any(), any(), any(), any())) + when(mMockAppHeaderViewHolderFactory + .create(any(), any(), any(), any(), any(), any(), any(), any(), any())) .thenReturn(mMockAppHeaderViewHolder); when(mMockDesktopUserRepositories.getCurrent()).thenReturn(mDesktopRepository); when(mMockDesktopUserRepositories.getProfile(anyInt())).thenReturn(mDesktopRepository); @@ -417,7 +418,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - /* shouldIgnoreCornerRadius= */ true); + /* shouldIgnoreCornerRadius= */ true, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS); } @@ -596,6 +598,73 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { } @Test + @EnableFlags({Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS}) + public void updateRelayoutParams_excludeCaptionTrue_forceConsumptionFalse() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + DEFAULT_IS_STATUSBAR_VISIBLE, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + /* shouldExcludeCaptionFromAppBounds */ true); + + // Force consuming flags are disabled. + assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) == 0).isTrue(); + assertThat( + (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) == 0) + .isTrue(); + // Exclude caption from app bounds is true. + assertThat(relayoutParams.mShouldSetAppBounds).isTrue(); + } + + @Test + @EnableFlags({Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION, + Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS}) + public void updateRelayoutParams_excludeCaptionFalse_forceConsumptionTrue() { + final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); + taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM); + taskInfo.taskDescription.setTopOpaqueSystemBarsAppearance(0); + final RelayoutParams relayoutParams = new RelayoutParams(); + + DesktopModeWindowDecoration.updateRelayoutParams( + relayoutParams, + mTestableContext, + taskInfo, + mMockSplitScreenController, + DEFAULT_APPLY_START_TRANSACTION_ON_DRAW, + DEFAULT_SHOULD_SET_TASK_POSITIONING_AND_CROP, + DEFAULT_IS_STATUSBAR_VISIBLE, + DEFAULT_IS_KEYGUARD_VISIBLE_AND_OCCLUDED, + DEFAULT_IS_IN_FULL_IMMERSIVE_MODE, + new InsetsState(), + DEFAULT_HAS_GLOBAL_FOCUS, + mExclusionRegion, + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); + + assertThat((relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING) != 0).isTrue(); + assertThat( + (relayoutParams.mInsetSourceFlags & FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR) != 0) + .isTrue(); + // Exclude caption from app bounds is false. + assertThat(relayoutParams.mShouldSetAppBounds).isFalse(); + } + + @Test @EnableFlags(Flags.FLAG_ENABLE_CAPTION_COMPAT_INSET_FORCE_CONSUMPTION_ALWAYS) public void updateRelayoutParams_header_addsForceConsumingCaptionBar() { final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true); @@ -662,7 +731,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { insetsState, DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); // Takes status bar inset as padding, ignores caption bar inset. assertThat(relayoutParams.mCaptionTopPadding).isEqualTo(50); @@ -688,7 +758,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsInsetSource).isFalse(); } @@ -713,7 +784,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); // Header is always shown because it's assumed the status bar is always visible. assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -738,7 +810,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); } @@ -762,7 +835,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -786,7 +860,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -811,7 +886,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isTrue(); @@ -828,7 +904,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -853,7 +930,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); assertThat(relayoutParams.mIsCaptionVisible).isFalse(); } @@ -921,8 +999,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); assertTrue(decoration.isMaximizeMenuActive()); } @@ -934,8 +1012,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new FakeMaximizeMenuFactory(menu)); decoration.setAppHeaderMaximizeButtonHovered(false); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(false); @@ -973,8 +1051,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); mOnMaxMenuHoverChangeListener.getValue().invoke(true); @@ -988,8 +1066,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { final DesktopModeWindowDecoration decoration = createWindowDecoration(taskInfo, new FakeMaximizeMenuFactory(menu)); createMaximizeMenu(decoration); - verify(menu).show(anyBoolean(), anyInt(), anyBoolean(), anyBoolean(), any(), any(), any(), - any(), mOnMaxMenuHoverChangeListener.capture(), any()); + verify(menu).show(anyBoolean(), anyBoolean(), anyBoolean(), any(), any(), any(), any(), + mOnMaxMenuHoverChangeListener.capture(), any()); decoration.setAppHeaderMaximizeButtonHovered(true); @@ -1009,7 +1087,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(true), anyBoolean(), any(), @@ -1034,7 +1111,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), /* showImmersiveOption= */ eq(false), anyBoolean(), any(), @@ -1059,7 +1135,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(true), any(), @@ -1084,7 +1159,6 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { verify(menu).show( anyBoolean(), - anyInt(), anyBoolean(), /* showSnapOptions= */ eq(false), any(), @@ -1517,7 +1591,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { new InsetsState(), DEFAULT_HAS_GLOBAL_FOCUS, mExclusionRegion, - DEFAULT_SHOULD_IGNORE_CORNER_RADIUS); + DEFAULT_SHOULD_IGNORE_CORNER_RADIUS, + DEFAULT_SHOULD_EXCLUDE_CAPTION_FROM_APP_BOUNDS); } private DesktopModeWindowDecoration createWindowDecoration( @@ -1565,7 +1640,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { WindowContainerTransaction::new, SurfaceControl::new, new WindowManagerWrapper(mMockWindowManager), mMockSurfaceControlViewHostFactory, mMockWindowDecorViewHostSupplier, maximizeMenuFactory, mMockHandleMenuFactory, - mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger); + mMockMultiInstanceHelper, mMockCaptionHandleRepository, mDesktopModeEventLogger, + mDesktopModeCompatPolicy); windowDecor.setCaptionListeners(mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener, mMockTouchEventListener); windowDecor.setExclusionRegionListener(mMockExclusionRegionListener); @@ -1596,6 +1672,7 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { "DesktopModeWindowDecorationTests"); taskInfo.baseActivity = new ComponentName("com.android.wm.shell.windowdecor", "DesktopModeWindowDecorationTests"); + taskInfo.topActivityInfo = createActivityInfo(); return taskInfo; } @@ -1686,7 +1763,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { @NonNull RootTaskDisplayAreaOrganizer rootTdaOrganizer, @NonNull DisplayController displayController, @NonNull ActivityManager.RunningTaskInfo taskInfo, - @NonNull Context decorWindowContext, @NonNull PointF menuPosition, + @NonNull Context decorWindowContext, + @NonNull Function2<? super Integer,? super Integer,? extends PointF> + positionSupplier, @NonNull Supplier<SurfaceControl.Transaction> transactionSupplier) { return mMaximizeMenu; } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt index a20a89c644ed..ab9dbc98e15f 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragPositioningCallbackUtilityTest.kt @@ -23,7 +23,6 @@ import android.graphics.Rect import android.os.IBinder import android.platform.test.annotations.DisableFlags import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.view.Display import android.window.WindowContainerToken @@ -44,7 +43,6 @@ import junit.framework.Assert.assertFalse import junit.framework.Assert.assertTrue import org.junit.After import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mock @@ -85,10 +83,6 @@ class DragPositioningCallbackUtilityTest { @Mock private lateinit var mockResources: Resources - @JvmField - @Rule - val setFlagsRule = SetFlagsRule() - private lateinit var mockitoSession: StaticMockitoSession @Before diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt new file mode 100644 index 000000000000..7341e098add5 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeInputListenerTest.kt @@ -0,0 +1,207 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.android.wm.shell.windowdecor + +import android.app.ActivityManager +import android.content.Context +import android.graphics.Region +import android.os.Handler +import android.os.Looper +import android.testing.AndroidTestingRunner +import android.testing.TestableLooper +import android.util.Size +import android.view.Choreographer +import android.view.Display +import android.view.IWindowSession +import android.view.InputChannel +import android.view.SurfaceControl +import androidx.test.filters.SmallTest +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.TestHandler +import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.TestShellExecutor +import com.android.wm.shell.common.DisplayController +import com.android.wm.shell.desktopmode.DesktopModeEventLogger +import com.android.wm.shell.util.StubTransaction +import com.android.wm.shell.windowdecor.DragResizeInputListener.TaskResizeInputEventReceiver +import com.google.common.truth.Truth.assertThat +import java.util.function.Consumer +import java.util.function.Supplier +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyInt +import org.mockito.kotlin.anyOrNull +import org.mockito.kotlin.mock +import org.mockito.kotlin.never +import org.mockito.kotlin.verify + +/** + * Tests for [DragResizeInputListener]. + * + * Build/Install/Run: + * atest WMShellUnitTests:DragResizeInputListenerTest + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner::class) +class DragResizeInputListenerTest : ShellTestCase() { + private val testMainExecutor = TestShellExecutor() + private val testBgExecutor = TestShellExecutor() + private val mockWindowSession = mock<IWindowSession>() + private val mockInputEventReceiver = mock<TaskResizeInputEventReceiver>() + + @Test + fun testGrantInputChannelOffMainThread() { + create() + testMainExecutor.flushAll() + + verifyNoInputChannelGrantRequests() + } + + @Test + fun testInitializationCallback_waitsForBgSetup() { + val inputListener = create() + + val callback = TestInitializationCallback() + inputListener.addInitializedCallback(callback) + assertThat(callback.initialized).isFalse() + + testBgExecutor.flushAll() + testMainExecutor.flushAll() + + assertThat(callback.initialized).isTrue() + } + + @Test + fun testInitializationCallback_alreadyInitialized_callsBackImmediately() { + val inputListener = create() + testBgExecutor.flushAll() + testMainExecutor.flushAll() + + val callback = TestInitializationCallback() + inputListener.addInitializedCallback(callback) + + assertThat(callback.initialized).isTrue() + } + + @Test + fun testClose_beforeBgSetup_cancelsBgSetup() { + val inputListener = create() + + inputListener.close() + testBgExecutor.flushAll() + + verifyNoInputChannelGrantRequests() + } + + @Test + fun testClose_beforeBgSetupResultSet_cancelsInit() { + val inputListener = create() + val callback = TestInitializationCallback() + inputListener.addInitializedCallback(callback) + + testBgExecutor.flushAll() + inputListener.close() + testMainExecutor.flushAll() + + assertThat(callback.initialized).isFalse() + } + + @Test + fun testClose_afterInit_disposesOfReceiver() { + val inputListener = create() + + testBgExecutor.flushAll() + testMainExecutor.flushAll() + inputListener.close() + + verify(mockInputEventReceiver).dispose() + } + + @Test + fun testClose_afterInit_removesTokens() { + val inputListener = create() + + inputListener.close() + testBgExecutor.flushAll() + + verify(mockWindowSession).remove(inputListener.mClientToken) + verify(mockWindowSession).remove(inputListener.mSinkClientToken) + } + + private fun verifyNoInputChannelGrantRequests() { + verify(mockWindowSession, never()) + .grantInputChannel( + anyInt(), + any(), + any(), + anyOrNull(), + anyInt(), + anyInt(), + anyInt(), + anyInt(), + anyOrNull(), + any(), + any(), + any(), + ) + } + + private fun create(): DragResizeInputListener = + DragResizeInputListener( + context, + mockWindowSession, + testMainExecutor, + testBgExecutor, + TestTaskResizeInputEventReceiverFactory(mockInputEventReceiver), + TestRunningTaskInfoBuilder().build(), + TestHandler(Looper.getMainLooper()), + mock<Choreographer>(), + Display.DEFAULT_DISPLAY, + mock<SurfaceControl>(), + mock<DragPositioningCallback>(), + { SurfaceControl.Builder() }, + { StubTransaction() }, + mock<DisplayController>(), + mock<DesktopModeEventLogger>(), + ) + + private class TestInitializationCallback : Runnable { + var initialized: Boolean = false + private set + + override fun run() { + initialized = true + } + } + + private class TestTaskResizeInputEventReceiverFactory( + private val mockInputEventReceiver: TaskResizeInputEventReceiver + ) : DragResizeInputListener.TaskResizeInputEventReceiverFactory { + override fun create( + context: Context, + taskInfo: ActivityManager.RunningTaskInfo, + inputChannel: InputChannel, + callback: DragPositioningCallback, + handler: Handler, + choreographer: Choreographer, + displayLayoutSizeSupplier: Supplier<Size?>, + touchRegionConsumer: Consumer<Region?>, + desktopModeEventLogger: DesktopModeEventLogger, + ): TaskResizeInputEventReceiver = mockInputEventReceiver + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java index 479f1567ed31..3389ec11f9d0 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DragResizeWindowGeometryTests.java @@ -30,7 +30,6 @@ import android.graphics.Point; import android.graphics.Region; import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.Size; @@ -41,7 +40,6 @@ import com.android.wm.shell.ShellTestCase; import com.google.common.testing.EqualsTester; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; @@ -87,9 +85,6 @@ public class DragResizeWindowGeometryTests extends ShellTestCase { private static final Point BOTTOM_INSET_POINT = new Point(TASK_SIZE.getWidth() / 2, TASK_SIZE.getHeight() - EDGE_RESIZE_HANDLE_INSET / 2); - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - /** * Check that both groups of objects satisfy equals/hashcode within each group, and that each * group is distinct from the next. diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt index f90988e90b22..f984f6db13fc 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/HandleMenuTest.kt @@ -26,7 +26,6 @@ import android.graphics.Point import android.graphics.Rect import android.graphics.drawable.BitmapDrawable import android.platform.test.annotations.EnableFlags -import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.testing.TestableLooper import android.view.Display @@ -60,7 +59,6 @@ import kotlinx.coroutines.test.runTest import org.junit.Assert.assertEquals import org.junit.Assert.assertTrue import org.junit.Before -import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.ArgumentMatchers.anyInt @@ -81,10 +79,6 @@ import org.mockito.kotlin.whenever @TestableLooper.RunWithLooper @RunWith(AndroidTestingRunner::class) class HandleMenuTest : ShellTestCase() { - @JvmField - @Rule - val setFlagsRule: SetFlagsRule = SetFlagsRule() - @Mock private lateinit var mockDesktopWindowDecoration: DesktopModeWindowDecoration @Mock diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt index 0615c1d677ba..937938df82c8 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/MultiDisplayVeiledResizeTaskPositionerTest.kt @@ -51,11 +51,10 @@ import com.android.wm.shell.windowdecor.DragPositioningCallback.CTRL_TYPE_UNDEFI import java.util.function.Supplier import junit.framework.Assert import org.junit.Before -import org.junit.Ignore import org.junit.Test import org.junit.runner.RunWith +import org.mockito.kotlin.any import org.mockito.Mock -import org.mockito.Mockito.any import org.mockito.Mockito.argThat import org.mockito.Mockito.doAnswer import org.mockito.Mockito.eq @@ -93,6 +92,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { @Mock private lateinit var mockFinishCallback: TransitionFinishCallback @Mock private lateinit var mockTransitions: Transitions @Mock private lateinit var mockInteractionJankMonitor: InteractionJankMonitor + @Mock private lateinit var mockSurfaceControl: SurfaceControl + private lateinit var resources: TestableResources private lateinit var spyDisplayLayout0: DisplayLayout private lateinit var spyDisplayLayout1: DisplayLayout @@ -145,6 +146,8 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { .`when`(spyDisplayLayout0) .getStableBounds(any()) `when`(mockTransactionFactory.get()).thenReturn(mockTransaction) + `when`(mockDesktopWindowDecoration.leash).thenReturn(mockSurfaceControl) + `when`(mockTransaction.setPosition(any(), any(), any())).thenReturn(mockTransaction) mockDesktopWindowDecoration.mTaskInfo = ActivityManager.RunningTaskInfo().apply { taskId = TASK_ID @@ -207,7 +210,6 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - @Ignore("Causing presubmit failure b/391717499") fun testDragResize_movesTask_doesNotShowResizeVeil() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, @@ -247,7 +249,25 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - @Ignore("Causing presubmit failure b/391717499") + fun testDragResize_movesTaskOnSameDisplay_noPxDpConversion() = runOnUiThread { + taskPositioner.onDragPositioningStart( + CTRL_TYPE_UNDEFINED, + DISPLAY_ID_0, + STARTING_BOUNDS.left.toFloat(), + STARTING_BOUNDS.top.toFloat(), + ) + + taskPositioner.onDragPositioningEnd( + DISPLAY_ID_0, + STARTING_BOUNDS.left.toFloat() + 70, + STARTING_BOUNDS.top.toFloat() + 20, + ) + + verify(spyDisplayLayout0, never()).localPxToGlobalDp(any(), any()) + verify(spyDisplayLayout0, never()).globalDpToLocalPx(any(), any()) + } + + @Test fun testDragResize_movesTaskToNewDisplay() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, @@ -373,7 +393,6 @@ class MultiDisplayVeiledResizeTaskPositionerTest : ShellTestCase() { } @Test - @Ignore("Causing presubmit failure b/391717499") fun testDragResize_drag_setBoundsNotRunIfDragEndsInDisallowedEndArea() = runOnUiThread { taskPositioner.onDragPositioningStart( CTRL_TYPE_UNDEFINED, // drag diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java index d9693460008f..af0162334440 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java @@ -18,7 +18,6 @@ package com.android.wm.shell.windowdecor; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static android.platform.test.flag.junit.SetFlagsRule.DefaultInitValueType.DEVICE_DEFAULT; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING; import static android.view.InsetsSource.FLAG_FORCE_CONSUMING_OPAQUE_CAPTION_BAR; import static android.view.WindowInsets.Type.captionBar; @@ -41,6 +40,7 @@ import static org.mockito.ArgumentMatchers.anyFloat; import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.Mockito.any; import static org.mockito.Mockito.argThat; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.eq; import static org.mockito.Mockito.inOrder; @@ -62,7 +62,6 @@ import android.graphics.Point; import android.graphics.Rect; import android.graphics.Region; import android.os.Handler; -import android.platform.test.flag.junit.SetFlagsRule; import android.testing.AndroidTestingRunner; import android.util.DisplayMetrics; import android.view.AttachedSurfaceControl; @@ -93,7 +92,6 @@ import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHost; import com.android.wm.shell.windowdecor.common.viewhost.WindowDecorViewHostSupplier; import org.junit.Before; -import org.junit.Rule; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.ArgumentCaptor; @@ -120,9 +118,6 @@ public class WindowDecorationTests extends ShellTestCase { private static final int SHADOW_RADIUS = 10; private static final int STATUS_BAR_INSET_SOURCE_ID = 0; - @Rule - public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(DEVICE_DEFAULT); - private final WindowDecoration.RelayoutResult<TestView> mRelayoutResult = new WindowDecoration.RelayoutResult<>(); @@ -392,6 +387,49 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockWindowDecorViewHost).updateView(same(mMockView), any(), any(), any(), any()); } + + @Test + public void testReinflateViewsOnFontScaleChange() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + final ActivityManager.RunningTaskInfo taskInfo2 = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + taskInfo2.configuration.fontScale = taskInfo.configuration.fontScale + 1; + windowDecor.relayout(taskInfo2, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since the font scale has changed. + verify(windowDecor).releaseViews(any()); + } + + @Test + public void testViewNotReinflatedWhenFontScaleNotChanged() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController) + .getDisplay(Display.DEFAULT_DISPLAY); + + final ActivityManager.RunningTaskInfo taskInfo = new TestRunningTaskInfoBuilder() + .setVisible(true) + .setDisplayId(Display.DEFAULT_DISPLAY) + .build(); + final TestWindowDecoration windowDecor = spy(createWindowDecoration(taskInfo)); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + clearInvocations(windowDecor); + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */, Region.obtain()); + // WindowDecoration#releaseViews should be called since task info (and therefore the + // fontScale) has not changed. + verify(windowDecor, never()).releaseViews(any()); + } + @Test public void testAddViewHostViewContainer() { final Display defaultDisplay = mock(Display.class); @@ -837,6 +875,54 @@ public class WindowDecorationTests extends ShellTestCase { } @Test + public void testRelayout_setAppBoundsIfNeeded() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + final ActivityManager.RunningTaskInfo taskInfo = + builder.setToken(token).setBounds(TASK_BOUNDS).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + mRelayoutParams.mIsCaptionVisible = true; + mRelayoutParams.mShouldSetAppBounds = true; + + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + final Rect appBounds = new Rect(TASK_BOUNDS); + appBounds.top += WindowDecoration.loadDimensionPixelSize( + windowDecor.mDecorWindowContext.getResources(), mRelayoutParams.mCaptionHeightId); + verify(mMockWindowContainerTransaction).setAppBounds(eq(token), eq(appBounds)); + } + + @Test + public void testRelayout_setAppBoundsIfNeeded_reset() { + final Display defaultDisplay = mock(Display.class); + doReturn(defaultDisplay).when(mMockDisplayController).getDisplay(Display.DEFAULT_DISPLAY); + final WindowContainerToken token = TestRunningTaskInfoBuilder.createMockWCToken(); + final TestRunningTaskInfoBuilder builder = new TestRunningTaskInfoBuilder() + .setDisplayId(Display.DEFAULT_DISPLAY) + .setVisible(true); + + final ActivityManager.RunningTaskInfo taskInfo = + builder.setToken(token).setBounds(TASK_BOUNDS).build(); + final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); + + mRelayoutParams.mIsCaptionVisible = true; + mRelayoutParams.mIsInsetSource = true; + mRelayoutParams.mShouldSetAppBounds = true; + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + + mRelayoutParams.mIsCaptionVisible = true; + mRelayoutParams.mIsInsetSource = false; + mRelayoutParams.mShouldSetAppBounds = false; + windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); + + verify(mMockWindowContainerTransaction).setAppBounds(eq(token), eq(new Rect())); + } + + @Test public void testTaskPositionAndCropNotSetWhenFalse() { final Display defaultDisplay = mock(Display.class); doReturn(defaultDisplay).when(mMockDisplayController) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt index 431de896f433..c61e0eb3b5af 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/WindowDecorTaskResourceLoaderTest.kt @@ -86,6 +86,7 @@ class WindowDecorTaskResourceLoaderTest : ShellTestCase() { spyContext.setMockPackageManager(mockPackageManager) doReturn(spyContext).whenever(spyContext).createContextAsUser(any(), anyInt()) doReturn(spyContext).whenever(mMockUserProfileContexts)[anyInt()] + doReturn(spyContext).whenever(mMockUserProfileContexts).getOrCreate(anyInt()) loader = WindowDecorTaskResourceLoader( shellInit = shellInit, diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt index d99a4825e580..c86730ed1dc7 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/common/viewhost/ReusableWindowDecorViewHostTest.kt @@ -20,6 +20,7 @@ import android.testing.TestableLooper import android.view.SurfaceControl import android.view.View import android.view.WindowManager +import android.widget.FrameLayout import androidx.test.filters.SmallTest import com.android.wm.shell.ShellTestCase import com.google.common.truth.Truth.assertThat @@ -30,6 +31,9 @@ import kotlinx.coroutines.test.runTest import org.junit.Test import org.junit.runner.RunWith import org.mockito.Mockito.mock +import org.mockito.Mockito.times +import org.mockito.kotlin.clearInvocations +import org.mockito.kotlin.never import org.mockito.kotlin.spy import org.mockito.kotlin.verify @@ -47,24 +51,46 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { fun update_differentView_replacesView() = runTest { val view = View(context) val lp = WindowManager.LayoutParams() - val reusableVH = createReusableViewHost() - reusableVH.updateView(view, lp, context.resources.configuration, null) + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) + reusableVH.updateView(view, lp, context.resources.configuration) - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(view) + assertThat(rootView.childCount).isEqualTo(1) + assertThat(rootView.getChildAt(0)).isEqualTo(view) val newView = View(context) val newLp = WindowManager.LayoutParams() - reusableVH.updateView(newView, newLp, context.resources.configuration, null) + reusableVH.updateView(newView, newLp, context.resources.configuration) - assertThat(reusableVH.rootView.childCount).isEqualTo(1) - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(newView) + assertThat(rootView.childCount).isEqualTo(1) + assertThat(rootView.getChildAt(0)).isEqualTo(newView) + } + + @Test + fun update_sameView_doesNotReplaceView() = runTest { + val view = View(context) + val lp = WindowManager.LayoutParams() + val spyRootView = spy(FrameLayout(context)) + val reusableVH = createReusableViewHost(spyRootView) + reusableVH.updateView(view, lp, context.resources.configuration) + + verify(spyRootView, times(1)).removeAllViews() + assertThat(spyRootView.childCount).isEqualTo(1) + assertThat(spyRootView.getChildAt(0)).isEqualTo(view) + + reusableVH.updateView(view, lp, context.resources.configuration) + + clearInvocations(spyRootView) + verify(spyRootView, never()).removeAllViews() + assertThat(spyRootView.childCount).isEqualTo(1) + assertThat(spyRootView.getChildAt(0)).isEqualTo(view) } @OptIn(ExperimentalCoroutinesApi::class) @Test fun updateView_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) val asyncView = View(context) val syncView = View(context) val asyncAttrs = WindowManager.LayoutParams(100, 100) @@ -83,7 +109,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { view = syncView, attrs = syncAttrs, configuration = context.resources.configuration, - onDrawTransaction = null, ) // Would run coroutine if it hadn't been cancelled. @@ -91,7 +116,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() // View host view/attrs should match the ones from the sync call. - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(syncView) + assertThat(rootView.getChildAt(0)).isEqualTo(syncView) assertThat(reusableVH.view()!!.layoutParams.width).isEqualTo(syncAttrs.width) } @@ -118,7 +143,8 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { @OptIn(ExperimentalCoroutinesApi::class) @Test fun updateViewAsync_clearsPendingAsyncJob() = runTest { - val reusableVH = createReusableViewHost() + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView) val view = View(context) reusableVH.updateViewAsync( @@ -136,7 +162,7 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { advanceUntilIdle() assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.rootView.getChildAt(0)).isEqualTo(otherView) + assertThat(rootView.getChildAt(0)).isEqualTo(otherView) } @Test @@ -148,7 +174,6 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { view = view, attrs = WindowManager.LayoutParams(100, 100), configuration = context.resources.configuration, - onDrawTransaction = null, ) val t = mock(SurfaceControl.Transaction::class.java) @@ -159,19 +184,23 @@ class ReusableWindowDecorViewHostTest : ShellTestCase() { @Test fun warmUp_addsRootView() = runTest { - val reusableVH = createReusableViewHost().apply { warmUp() } + val rootView = FrameLayout(context) + val reusableVH = createReusableViewHost(rootView).apply { warmUp() } assertThat(reusableVH.viewHostAdapter.isInitialized()).isTrue() - assertThat(reusableVH.view()).isEqualTo(reusableVH.rootView) + assertThat(reusableVH.view()).isEqualTo(rootView) } - private fun CoroutineScope.createReusableViewHost() = + private fun CoroutineScope.createReusableViewHost( + rootView: FrameLayout = FrameLayout(context) + ) = ReusableWindowDecorViewHost( context = context, mainScope = this, display = context.display, id = 1, viewHostAdapter = spy(SurfaceControlViewHostAdapter(context, context.display)), + rootView ) private fun ReusableWindowDecorViewHost.view(): View? = viewHostAdapter.viewHost?.view diff --git a/libs/androidfw/Android.bp b/libs/androidfw/Android.bp index 12b1dd794a03..3dc53c5051e9 100644 --- a/libs/androidfw/Android.bp +++ b/libs/androidfw/Android.bp @@ -288,6 +288,7 @@ cc_benchmark { "tests/AttributeResolution_bench.cpp", "tests/CursorWindow_bench.cpp", "tests/Generic_bench.cpp", + "tests/LocaleDataLookup_bench.cpp", "tests/SparseEntry_bench.cpp", "tests/Theme_bench.cpp", ], diff --git a/libs/androidfw/ApkAssets.cpp b/libs/androidfw/ApkAssets.cpp index dbb891455ddd..e693fcfd3918 100644 --- a/libs/androidfw/ApkAssets.cpp +++ b/libs/androidfw/ApkAssets.cpp @@ -162,10 +162,13 @@ const std::string& ApkAssets::GetDebugName() const { return assets_provider_->GetDebugName(); } -bool ApkAssets::IsUpToDate() const { +UpToDate ApkAssets::IsUpToDate() const { // Loaders are invalidated by the app, not the system, so assume they are up to date. - return IsLoader() || ((!loaded_idmap_ || loaded_idmap_->IsUpToDate()) - && assets_provider_->IsUpToDate()); + if (IsLoader()) { + return UpToDate::Always; + } + const auto idmap_res = loaded_idmap_ ? loaded_idmap_->IsUpToDate() : UpToDate::Always; + return combine(idmap_res, [this] { return assets_provider_->IsUpToDate(); }); } } // namespace android diff --git a/libs/androidfw/AssetManager2.cpp b/libs/androidfw/AssetManager2.cpp index 0fa31c7a832e..f5e10d94452f 100644 --- a/libs/androidfw/AssetManager2.cpp +++ b/libs/androidfw/AssetManager2.cpp @@ -1467,8 +1467,6 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( } const StringPiece16 kAttr16 = u"attr"; - const static std::u16string kAttrPrivate16 = u"^attr-private"; - for (const PackageGroup& package_group : package_groups_) { for (const ConfiguredPackage& package_impl : package_group.packages_) { const LoadedPackage* package = package_impl.loaded_package_; @@ -1480,12 +1478,13 @@ base::expected<uint32_t, NullOrIOError> AssetManager2::GetResourceId( base::expected<uint32_t, NullOrIOError> resid = package->FindEntryByName(type16, entry16); if (UNLIKELY(IsIOError(resid))) { return base::unexpected(resid.error()); - } + } if (!resid.has_value() && kAttr16 == type16) { // Private attributes in libraries (such as the framework) are sometimes encoded // under the type '^attr-private' in order to leave the ID space of public 'attr' // free for future additions. Check '^attr-private' for the same name. + const static std::u16string kAttrPrivate16 = u"^attr-private"; resid = package->FindEntryByName(kAttrPrivate16, entry16); } diff --git a/libs/androidfw/AssetsProvider.cpp b/libs/androidfw/AssetsProvider.cpp index 2d3c06506a1f..808509120462 100644 --- a/libs/androidfw/AssetsProvider.cpp +++ b/libs/androidfw/AssetsProvider.cpp @@ -24,9 +24,27 @@ #include <ziparchive/zip_archive.h> namespace android { -namespace { -constexpr const char* kEmptyDebugString = "<empty>"; -} // namespace + +static constexpr std::string_view kEmptyDebugString = "<empty>"; + +std::unique_ptr<AssetsProvider> AssetsProvider::CreateWithOverride( + std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override) { + if (provider == nullptr) { + return {}; + } + if (override == nullptr) { + return provider; + } + return MultiAssetsProvider::Create(std::move(override), std::move(provider)); +} + +std::unique_ptr<AssetsProvider> AssetsProvider::CreateFromNullable( + std::unique_ptr<AssetsProvider> nullable) { + if (nullable) { + return nullable; + } + return EmptyAssetsProvider::Create(); +} std::unique_ptr<Asset> AssetsProvider::Open(const std::string& path, Asset::AccessMode mode, bool* file_exists) const { @@ -86,11 +104,9 @@ void ZipAssetsProvider::ZipCloser::operator()(ZipArchive* a) const { } ZipAssetsProvider::ZipAssetsProvider(ZipArchiveHandle handle, PathOrDebugName&& path, - package_property_t flags, time_t last_mod_time) - : zip_handle_(handle), - name_(std::move(path)), - flags_(flags), - last_mod_time_(last_mod_time) {} + package_property_t flags, ModDate last_mod_time) + : zip_handle_(handle), name_(std::move(path)), flags_(flags), last_mod_time_(last_mod_time) { +} std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, package_property_t flags, @@ -104,10 +120,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. - if (!isReadonlyFilesystem(path.c_str())) { - if ((released_fd < 0 ? stat(path.c_str(), &sb) : fstat(released_fd, &sb)) < 0) { + if (isKnownWritablePath(path.c_str()) || !isReadonlyFilesystem(GetFileDescriptor(handle))) { + if (mod_date = getFileModDate(GetFileDescriptor(handle)); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -116,7 +132,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(std::string path, } return std::unique_ptr<ZipAssetsProvider>( - new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, sb.st_mtime)); + new ZipAssetsProvider(handle, PathOrDebugName::Path(std::move(path)), flags, mod_date)); } std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, @@ -137,10 +153,10 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, return {}; } - struct stat sb{.st_mtime = -1}; + ModDate mod_date = kInvalidModDate; // Skip all up-to-date checks if the file won't ever change. if (!isReadonlyFilesystem(released_fd)) { - if (fstat(released_fd, &sb) < 0) { + if (mod_date = getFileModDate(released_fd); mod_date == kInvalidModDate) { // Stat requires execute permissions on all directories path to the file. If the process does // not have execute permissions on this file, allow the zip to be opened but IsUpToDate() will // always have to return true. @@ -150,7 +166,7 @@ std::unique_ptr<ZipAssetsProvider> ZipAssetsProvider::Create(base::unique_fd fd, } return std::unique_ptr<ZipAssetsProvider>(new ZipAssetsProvider( - handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, sb.st_mtime)); + handle, PathOrDebugName::DebugName(std::move(friendly_name)), flags, mod_date)); } std::unique_ptr<Asset> ZipAssetsProvider::OpenInternal(const std::string& path, @@ -282,21 +298,16 @@ const std::string& ZipAssetsProvider::GetDebugName() const { return name_.GetDebugName(); } -bool ZipAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; +UpToDate ZipAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - struct stat sb{}; - if (fstat(GetFileDescriptor(zip_handle_.get()), &sb) < 0) { - // If fstat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; - } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(GetFileDescriptor(zip_handle_.get()))); } -DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, time_t last_mod_time) - : dir_(std::move(path)), last_mod_time_(last_mod_time) {} +DirectoryAssetsProvider::DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time) + : dir_(std::move(path)), last_mod_time_(last_mod_time) { +} std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::string path) { struct stat sb; @@ -317,7 +328,7 @@ std::unique_ptr<DirectoryAssetsProvider> DirectoryAssetsProvider::Create(std::st const bool isReadonly = isReadonlyFilesystem(path.c_str()); return std::unique_ptr<DirectoryAssetsProvider>( - new DirectoryAssetsProvider(std::move(path), isReadonly ? -1 : sb.st_mtime)); + new DirectoryAssetsProvider(std::move(path), isReadonly ? kInvalidModDate : getModDate(sb))); } std::unique_ptr<Asset> DirectoryAssetsProvider::OpenInternal(const std::string& path, @@ -346,17 +357,11 @@ const std::string& DirectoryAssetsProvider::GetDebugName() const { return dir_; } -bool DirectoryAssetsProvider::IsUpToDate() const { - if (last_mod_time_ == -1) { - return true; +UpToDate DirectoryAssetsProvider::IsUpToDate() const { + if (last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; } - struct stat sb; - if (stat(dir_.c_str(), &sb) < 0) { - // If stat fails on the zip archive, return true so the zip archive the resource system does - // attempt to refresh the ApkAsset. - return true; - } - return last_mod_time_ == sb.st_mtime; + return fromBool(last_mod_time_ == getFileModDate(dir_.c_str())); } MultiAssetsProvider::MultiAssetsProvider(std::unique_ptr<AssetsProvider>&& primary, @@ -397,8 +402,8 @@ const std::string& MultiAssetsProvider::GetDebugName() const { return debug_name_; } -bool MultiAssetsProvider::IsUpToDate() const { - return primary_->IsUpToDate() && secondary_->IsUpToDate(); +UpToDate MultiAssetsProvider::IsUpToDate() const { + return combine(primary_->IsUpToDate(), [this] { return secondary_->IsUpToDate(); }); } EmptyAssetsProvider::EmptyAssetsProvider(std::optional<std::string>&& path) : @@ -438,12 +443,12 @@ const std::string& EmptyAssetsProvider::GetDebugName() const { if (path_.has_value()) { return *path_; } - const static std::string kEmpty = kEmptyDebugString; + constexpr static std::string kEmpty{kEmptyDebugString}; return kEmpty; } -bool EmptyAssetsProvider::IsUpToDate() const { - return true; +UpToDate EmptyAssetsProvider::IsUpToDate() const { + return UpToDate::Always; } } // namespace android diff --git a/libs/androidfw/Idmap.cpp b/libs/androidfw/Idmap.cpp index 3ecd82b074a1..f0ef97e5bdcc 100644 --- a/libs/androidfw/Idmap.cpp +++ b/libs/androidfw/Idmap.cpp @@ -22,9 +22,10 @@ #include "android-base/logging.h" #include "android-base/stringprintf.h" #include "android-base/utf8.h" -#include "androidfw/misc.h" +#include "androidfw/AssetManager.h" #include "androidfw/ResourceTypes.h" #include "androidfw/Util.h" +#include "androidfw/misc.h" #include "utils/ByteOrder.h" #include "utils/Trace.h" @@ -55,6 +56,13 @@ struct Idmap_header { // without having to read/store each header entry separately. }; +struct Idmap_constraint { + // Constraint type can be TYPE_DISPLAY_ID or TYP_DEVICE_ID, please refer + // to ConstraintType in OverlayConstraint.java + uint32_t constraint_type; + uint32_t constraint_value; +}; + struct Idmap_data_header { uint32_t target_entry_count; uint32_t target_inline_entry_count; @@ -254,13 +262,18 @@ std::optional<std::string_view> ReadString(const uint8_t** in_out_data_ptr, size #endif LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* header, - const Idmap_data_header* data_header, Idmap_target_entries target_entries, + const Idmap_constraint* constraints, + uint32_t constraints_count, + const Idmap_data_header* data_header, + Idmap_target_entries target_entries, Idmap_target_inline_entries target_inline_entries, const Idmap_target_entry_inline_value* inline_entry_values, const ConfigDescription* configs, Idmap_overlay_entries overlay_entries, std::unique_ptr<ResStringPool>&& string_pool, std::string_view overlay_apk_path, std::string_view target_apk_path) : header_(header), + constraints_(constraints), + constraints_count_(constraints_count), data_header_(data_header), target_entries_(target_entries), target_inline_entries_(target_inline_entries), @@ -268,11 +281,16 @@ LoadedIdmap::LoadedIdmap(const std::string& idmap_path, const Idmap_header* head configurations_(configs), overlay_entries_(overlay_entries), string_pool_(std::move(string_pool)), - idmap_fd_( - android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)), overlay_apk_path_(overlay_apk_path), target_apk_path_(target_apk_path), - idmap_last_mod_time_(getFileModDate(idmap_fd_.get())) { + idmap_last_mod_time_(kInvalidModDate) { + if (!isReadonlyFilesystem(std::string(overlay_apk_path_).c_str()) || + !(target_apk_path_ == AssetManager::TARGET_APK_PATH || + isReadonlyFilesystem(std::string(target_apk_path_).c_str()))) { + idmap_fd_.reset( + android::base::utf8::open(idmap_path.c_str(), O_RDONLY | O_CLOEXEC | O_BINARY | O_PATH)); + idmap_last_mod_time_ = getFileModDate(idmap_fd_); + } } std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPiece idmap_data) { @@ -298,9 +316,9 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie return {}; } std::optional<std::string_view> target_path = ReadString(&data_ptr, &data_size, "target path"); - if (!target_path) { - return {}; - } + if (!target_path) { + return {}; + } std::optional<std::string_view> overlay_path = ReadString(&data_ptr, &data_size, "overlay path"); if (!overlay_path) { return {}; @@ -310,6 +328,17 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie return {}; } + auto constraints_count = ReadType<uint32_t>(&data_ptr, &data_size, "constraints count"); + if (!constraints_count) { + return {}; + } + auto constraints = *constraints_count > 0 ? + ReadType<Idmap_constraint>(&data_ptr, &data_size, "constraints", *constraints_count) + : nullptr; + if (*constraints_count > 0 && !constraints) { + return {}; + } + // Parse the idmap data blocks. Currently idmap2 can only generate one data block. auto data_header = ReadType<Idmap_data_header>(&data_ptr, &data_size, "data header"); if (data_header == nullptr) { @@ -376,13 +405,17 @@ std::unique_ptr<LoadedIdmap> LoadedIdmap::Load(StringPiece idmap_path, StringPie // Can't use make_unique because LoadedIdmap constructor is private. return std::unique_ptr<LoadedIdmap>( - new LoadedIdmap(std::string(idmap_path), header, data_header, target_entries, - target_inline_entries, target_inline_entry_values, configurations, - overlay_entries, std::move(idmap_string_pool), *overlay_path, *target_path)); + new LoadedIdmap(std::string(idmap_path), header, constraints, *constraints_count, + data_header, target_entries, target_inline_entries, + target_inline_entry_values,configurations, overlay_entries, + std::move(idmap_string_pool),*overlay_path, *target_path)); } -bool LoadedIdmap::IsUpToDate() const { - return idmap_last_mod_time_ == getFileModDate(idmap_fd_.get()); +UpToDate LoadedIdmap::IsUpToDate() const { + if (idmap_last_mod_time_ == kInvalidModDate) { + return UpToDate::Always; + } + return fromBool(idmap_last_mod_time_ == getFileModDate(idmap_fd_.get())); } } // namespace android diff --git a/libs/androidfw/LocaleDataLookup.cpp b/libs/androidfw/LocaleDataLookup.cpp index 6e751a77f355..9aacdcb9ca92 100644 --- a/libs/androidfw/LocaleDataLookup.cpp +++ b/libs/androidfw/LocaleDataLookup.cpp @@ -7518,6 +7518,13 @@ const char* lookupLikelyScript(uint32_t packed_lang_region) { } } +/* + * TODO: Consider turning the below switch statement into binary search + * to save the disk space when the table is larger in the future. + * Disassembled code shows that the jump table emitted by clang can be + * 4x larger than the data in disk size, but it depends on the optimization option. + * However, a switch statement will benefit from the future of compiler improvement. + */ bool isLocaleRepresentative(uint32_t language_and_region, const char* script) { const uint64_t packed_locale = ((static_cast<uint64_t>(language_and_region)) << 32u) | @@ -14864,12 +14871,22 @@ static uint32_t findLatnParent(uint32_t packed_lang_region) { case 0x656E4154u: // en-AT -> en-150 case 0x656E4245u: // en-BE -> en-150 case 0x656E4348u: // en-CH -> en-150 + case 0x656E435Au: // en-CZ -> en-150 case 0x656E4445u: // en-DE -> en-150 case 0x656E444Bu: // en-DK -> en-150 + case 0x656E4553u: // en-ES -> en-150 case 0x656E4649u: // en-FI -> en-150 + case 0x656E4652u: // en-FR -> en-150 + case 0x656E4855u: // en-HU -> en-150 + case 0x656E4954u: // en-IT -> en-150 case 0x656E4E4Cu: // en-NL -> en-150 + case 0x656E4E4Fu: // en-NO -> en-150 + case 0x656E504Cu: // en-PL -> en-150 + case 0x656E5054u: // en-PT -> en-150 + case 0x656E524Fu: // en-RO -> en-150 case 0x656E5345u: // en-SE -> en-150 case 0x656E5349u: // en-SI -> en-150 + case 0x656E534Bu: // en-SK -> en-150 return 0x656E80A1u; case 0x65734152u: // es-AR -> es-419 case 0x6573424Fu: // es-BO -> es-419 diff --git a/libs/androidfw/ResourceTypes.cpp b/libs/androidfw/ResourceTypes.cpp index de9991a8be5e..a18c5f5f92f6 100644 --- a/libs/androidfw/ResourceTypes.cpp +++ b/libs/androidfw/ResourceTypes.cpp @@ -152,12 +152,11 @@ static void fill9patchOffsets(Res_png_9patch* patch) { patch->colorsOffset = patch->yDivsOffset + (patch->numYDivs * sizeof(int32_t)); } -void Res_value::copyFrom_dtoh(const Res_value& src) -{ - size = dtohs(src.size); - res0 = src.res0; - dataType = src.dataType; - data = dtohl(src.data); +void Res_value::copyFrom_dtoh_slow(const Res_value& src) { + size = dtohs(src.size); + res0 = src.res0; + dataType = src.dataType; + data = dtohl(src.data); } void Res_png_9patch::deviceToFile() @@ -290,11 +289,11 @@ static bool assertIdmapHeader(const void* idmap, size_t size) { } const uint32_t version = htodl(*(reinterpret_cast<const uint32_t*>(idmap) + 1)); - if (version != ResTable::IDMAP_CURRENT_VERSION) { + if (version != kIdmapCurrentVersion) { // We are strict about versions because files with this format are // auto-generated and don't need backwards compatibility. ALOGW("idmap: version mismatch in header (is 0x%08x, expected 0x%08x)", - version, ResTable::IDMAP_CURRENT_VERSION); + version, kIdmapCurrentVersion); return false; } return true; @@ -400,14 +399,18 @@ status_t parseIdmap(const void* idmap, size_t size, uint8_t* outPackageId, Keyed return UNKNOWN_ERROR; } - size -= ResTable::IDMAP_HEADER_SIZE_BYTES; + size_t sizeOfHeaderAndConstraints = ResTable::IDMAP_HEADER_SIZE_BYTES + + // This accounts for zero constraints, and hence takes only 4 bytes for + // the constraints count. + ResTable::IDMAP_CONSTRAINTS_COUNT_SIZE_BYTES; + size -= sizeOfHeaderAndConstraints; if (size < sizeof(uint16_t) * 2) { ALOGE("idmap: too small to contain any mapping"); return UNKNOWN_ERROR; } const uint16_t* data = reinterpret_cast<const uint16_t*>( - reinterpret_cast<const uint8_t*>(idmap) + ResTable::IDMAP_HEADER_SIZE_BYTES); + reinterpret_cast<const uint8_t*>(idmap) + sizeOfHeaderAndConstraints); uint16_t targetPackageId = dtohs(*(data++)); if (targetPackageId == 0 || targetPackageId > 255) { @@ -2031,16 +2034,6 @@ status_t ResXMLTree::validateNode(const ResXMLTree_node* node) const // -------------------------------------------------------------------- // -------------------------------------------------------------------- -void ResTable_config::copyFromDeviceNoSwap(const ResTable_config& o) { - const size_t size = dtohl(o.size); - if (size >= sizeof(ResTable_config)) { - *this = o; - } else { - memcpy(this, &o, size); - memset(((uint8_t*)this)+size, 0, sizeof(ResTable_config)-size); - } -} - /* static */ size_t unpackLanguageOrRegion(const char in[2], const char base, char out[4]) { if (in[0] & 0x80) { @@ -2105,34 +2098,33 @@ size_t ResTable_config::unpackRegion(char region[4]) const { return unpackLanguageOrRegion(this->country, '0', region); } - -void ResTable_config::copyFromDtoH(const ResTable_config& o) { - copyFromDeviceNoSwap(o); - size = sizeof(ResTable_config); - mcc = dtohs(mcc); - mnc = dtohs(mnc); - density = dtohs(density); - screenWidth = dtohs(screenWidth); - screenHeight = dtohs(screenHeight); - sdkVersion = dtohs(sdkVersion); - minorVersion = dtohs(minorVersion); - smallestScreenWidthDp = dtohs(smallestScreenWidthDp); - screenWidthDp = dtohs(screenWidthDp); - screenHeightDp = dtohs(screenHeightDp); -} - -void ResTable_config::swapHtoD() { - size = htodl(size); - mcc = htods(mcc); - mnc = htods(mnc); - density = htods(density); - screenWidth = htods(screenWidth); - screenHeight = htods(screenHeight); - sdkVersion = htods(sdkVersion); - minorVersion = htods(minorVersion); - smallestScreenWidthDp = htods(smallestScreenWidthDp); - screenWidthDp = htods(screenWidthDp); - screenHeightDp = htods(screenHeightDp); +void ResTable_config::copyFromDtoH_slow(const ResTable_config& o) { + copyFromDeviceNoSwap(o); + size = sizeof(ResTable_config); + mcc = dtohs(mcc); + mnc = dtohs(mnc); + density = dtohs(density); + screenWidth = dtohs(screenWidth); + screenHeight = dtohs(screenHeight); + sdkVersion = dtohs(sdkVersion); + minorVersion = dtohs(minorVersion); + smallestScreenWidthDp = dtohs(smallestScreenWidthDp); + screenWidthDp = dtohs(screenWidthDp); + screenHeightDp = dtohs(screenHeightDp); +} + +void ResTable_config::swapHtoD_slow() { + size = htodl(size); + mcc = htods(mcc); + mnc = htods(mnc); + density = htods(density); + screenWidth = htods(screenWidth); + screenHeight = htods(screenHeight); + sdkVersion = htods(sdkVersion); + minorVersion = htods(minorVersion); + smallestScreenWidthDp = htods(smallestScreenWidthDp); + screenWidthDp = htods(screenWidthDp); + screenHeightDp = htods(screenHeightDp); } /* static */ inline int compareLocales(const ResTable_config &l, const ResTable_config &r) { @@ -2145,7 +2137,7 @@ void ResTable_config::swapHtoD() { // systems should happen very infrequently (if at all.) // The comparison code relies on memcmp low-level optimizations that make it // more efficient than strncmp. - const char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; + static constexpr char emptyScript[sizeof(l.localeScript)] = {'\0', '\0', '\0', '\0'}; const char *lScript = l.localeScriptWasComputed ? emptyScript : l.localeScript; const char *rScript = r.localeScriptWasComputed ? emptyScript : r.localeScript; @@ -7492,7 +7484,7 @@ status_t ResTable::createIdmap(const ResTable& targetResTable, // write idmap header uint32_t* data = reinterpret_cast<uint32_t*>(*outData); *data++ = htodl(IDMAP_MAGIC); // write: magic - *data++ = htodl(ResTable::IDMAP_CURRENT_VERSION); // write: version + *data++ = htodl(kIdmapCurrentVersion); // write: version *data++ = htodl(targetCrc); // write: target crc *data++ = htodl(overlayCrc); // write: overlay crc @@ -7507,6 +7499,9 @@ status_t ResTable::createIdmap(const ResTable& targetResTable, } data += (2 * 256) / sizeof(uint32_t); + // write zero constraints count (no constraints) + *data++ = htodl(0); + // write idmap data header uint16_t* typeData = reinterpret_cast<uint16_t*>(data); *typeData++ = htods(targetPackageStruct->id); // write: target package id diff --git a/libs/androidfw/TypeWrappers.cpp b/libs/androidfw/TypeWrappers.cpp index 970463492c1a..2a20106b6ee1 100644 --- a/libs/androidfw/TypeWrappers.cpp +++ b/libs/androidfw/TypeWrappers.cpp @@ -18,8 +18,11 @@ namespace android { -TypeVariant::TypeVariant(const ResTable_type* data) : data(data), mLength(dtohl(data->entryCount)) { - if (data->flags & ResTable_type::FLAG_SPARSE) { +TypeVariant::TypeVariant(const ResTable_type* data) + : data(data) + , mLength(dtohl(data->entryCount)) + , mSparse(data->flags & ResTable_type::FLAG_SPARSE) { + if (mSparse) { const uint32_t entryCount = dtohl(data->entryCount); const uintptr_t containerEnd = reinterpret_cast<uintptr_t>(data) + dtohl(data->header.size); const uint32_t* const entryIndices = reinterpret_cast<const uint32_t*>( @@ -40,18 +43,18 @@ TypeVariant::iterator& TypeVariant::iterator::operator++() { mIndex = mTypeVariant->mLength; } - const ResTable_type* type = mTypeVariant->data; - if ((type->flags & ResTable_type::FLAG_SPARSE) == 0) { + if (!mTypeVariant->mSparse) { return *this; } // Need to adjust |mSparseIndex| as well if we've passed its current element. + const ResTable_type* type = mTypeVariant->data; const uint32_t entryCount = dtohl(type->entryCount); - const auto entryIndices = reinterpret_cast<const uint32_t*>( - reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); if (mSparseIndex >= entryCount) { return *this; // done } + const auto entryIndices = reinterpret_cast<const uint32_t*>( + reinterpret_cast<uintptr_t>(type) + dtohs(type->header.headerSize)); const auto element = (const ResTable_sparseTypeEntry*)(entryIndices + mSparseIndex); if (mIndex > dtohs(element->idx)) { ++mSparseIndex; @@ -79,7 +82,7 @@ const ResTable_entry* TypeVariant::iterator::operator*() const { } uint32_t entryOffset; - if (type->flags & ResTable_type::FLAG_SPARSE) { + if (mTypeVariant->mSparse) { if (mSparseIndex >= entryCount) { return nullptr; } diff --git a/libs/androidfw/Util.cpp b/libs/androidfw/Util.cpp index be55fe8b4bb6..86c459fb4647 100644 --- a/libs/androidfw/Util.cpp +++ b/libs/androidfw/Util.cpp @@ -32,13 +32,18 @@ namespace android { namespace util { void ReadUtf16StringFromDevice(const uint16_t* src, size_t len, std::string* out) { - char buf[5]; - while (*src && len != 0) { - char16_t c = static_cast<char16_t>(dtohs(*src)); - utf16_to_utf8(&c, 1, buf, sizeof(buf)); - out->append(buf, strlen(buf)); - ++src; - --len; + static constexpr bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + if constexpr (kDeviceEndiannessSame) { + *out = Utf16ToUtf8({(const char16_t*)src, strnlen16((const char16_t*)src, len)}); + } else { + char buf[5]; + while (*src && len != 0) { + char16_t c = static_cast<char16_t>(dtohs(*src)); + utf16_to_utf8(&c, 1, buf, sizeof(buf)); + out->append(buf, strlen(buf)); + ++src; + --len; + } } } @@ -63,8 +68,10 @@ std::string Utf16ToUtf8(StringPiece16 utf16) { } std::string utf8; - utf8.resize(utf8_length); - utf16_to_utf8(utf16.data(), utf16.length(), &*utf8.begin(), utf8_length + 1); + utf8.resize_and_overwrite(utf8_length, [&utf16](char* data, size_t size) { + utf16_to_utf8(utf16.data(), utf16.length(), data, size + 1); + return size; + }); return utf8; } diff --git a/libs/androidfw/include/androidfw/ApkAssets.h b/libs/androidfw/include/androidfw/ApkAssets.h index 231808beb718..3f6f4661f2f7 100644 --- a/libs/androidfw/include/androidfw/ApkAssets.h +++ b/libs/androidfw/include/androidfw/ApkAssets.h @@ -116,7 +116,7 @@ class ApkAssets : public RefBase { return resources_asset_ != nullptr && resources_asset_->isAllocated(); } - bool IsUpToDate() const; + UpToDate IsUpToDate() const; // DANGER! // This is a destructive method that rips the assets provider out of ApkAssets object. diff --git a/libs/androidfw/include/androidfw/AssetsProvider.h b/libs/androidfw/include/androidfw/AssetsProvider.h index d33c325ff369..037f684f5b78 100644 --- a/libs/androidfw/include/androidfw/AssetsProvider.h +++ b/libs/androidfw/include/androidfw/AssetsProvider.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef ANDROIDFW_ASSETSPROVIDER_H -#define ANDROIDFW_ASSETSPROVIDER_H +#pragma once #include <memory> #include <string> @@ -37,6 +36,12 @@ namespace android { struct AssetsProvider { static constexpr off64_t kUnknownLength = -1; + static std::unique_ptr<AssetsProvider> CreateWithOverride( + std::unique_ptr<AssetsProvider> provider, std::unique_ptr<AssetsProvider> override); + + static std::unique_ptr<AssetsProvider> CreateFromNullable( + std::unique_ptr<AssetsProvider> nullable); + // Opens a file for reading. If `file_exists` is not null, it will be set to `true` if the file // exists. This is useful for determining if the file exists but was unable to be opened due to // an I/O error. @@ -58,7 +63,7 @@ struct AssetsProvider { WARN_UNUSED virtual const std::string& GetDebugName() const = 0; // Returns whether the interface provides the most recent version of its files. - WARN_UNUSED virtual bool IsUpToDate() const = 0; + WARN_UNUSED virtual UpToDate IsUpToDate() const = 0; // Creates an Asset from a file on disk. static std::unique_ptr<Asset> CreateAssetFromFile(const std::string& path); @@ -95,7 +100,7 @@ struct ZipAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; WARN_UNUSED std::optional<uint32_t> GetCrc(std::string_view path) const; ~ZipAssetsProvider() override = default; @@ -106,7 +111,7 @@ struct ZipAssetsProvider : public AssetsProvider { private: struct PathOrDebugName; ZipAssetsProvider(ZipArchive* handle, PathOrDebugName&& path, package_property_t flags, - time_t last_mod_time); + ModDate last_mod_time); struct PathOrDebugName { static PathOrDebugName Path(std::string value) { @@ -135,7 +140,7 @@ struct ZipAssetsProvider : public AssetsProvider { std::unique_ptr<ZipArchive, ZipCloser> zip_handle_; PathOrDebugName name_; package_property_t flags_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a root directory. @@ -147,7 +152,7 @@ struct DirectoryAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~DirectoryAssetsProvider() override = default; protected: @@ -156,9 +161,9 @@ struct DirectoryAssetsProvider : public AssetsProvider { bool* file_exists) const override; private: - explicit DirectoryAssetsProvider(std::string&& path, time_t last_mod_time); + explicit DirectoryAssetsProvider(std::string&& path, ModDate last_mod_time); std::string dir_; - time_t last_mod_time_; + ModDate last_mod_time_; }; // Supplies assets from a `primary` asset provider and falls back to supplying assets from the @@ -172,7 +177,7 @@ struct MultiAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~MultiAssetsProvider() override = default; protected: @@ -199,7 +204,7 @@ struct EmptyAssetsProvider : public AssetsProvider { WARN_UNUSED std::optional<std::string_view> GetPath() const override; WARN_UNUSED const std::string& GetDebugName() const override; - WARN_UNUSED bool IsUpToDate() const override; + WARN_UNUSED UpToDate IsUpToDate() const override; ~EmptyAssetsProvider() override = default; protected: @@ -212,5 +217,3 @@ struct EmptyAssetsProvider : public AssetsProvider { }; } // namespace android - -#endif /* ANDROIDFW_ASSETSPROVIDER_H */ diff --git a/libs/androidfw/include/androidfw/Idmap.h b/libs/androidfw/include/androidfw/Idmap.h index ac75eb3bb98c..0c0856315d8f 100644 --- a/libs/androidfw/include/androidfw/Idmap.h +++ b/libs/androidfw/include/androidfw/Idmap.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef IDMAP_H_ -#define IDMAP_H_ +#pragma once #include <memory> #include <string> @@ -32,9 +31,35 @@ namespace android { +// An enum that tracks more states than just 'up to date' or 'not' for a resources container: +// there are several cases where we know for sure that the object can't change and won't get +// out of date. Reporting those states to the managed layer allows it to stop checking here +// completely, speeding up the cache lookups by dozens of milliseconds. +enum class UpToDate : int { False, True, Always }; + +// Combines two UpToDate values, and only accesses the second one if it matters to the result. +template <class Getter> +UpToDate combine(UpToDate first, Getter secondGetter) { + switch (first) { + case UpToDate::False: + return UpToDate::False; + case UpToDate::True: { + const auto second = secondGetter(); + return second == UpToDate::False ? UpToDate::False : UpToDate::True; + } + case UpToDate::Always: + return secondGetter(); + } +} + +inline UpToDate fromBool(bool value) { + return value ? UpToDate::True : UpToDate::False; +} + class LoadedIdmap; class IdmapResMap; struct Idmap_header; +struct Idmap_constraint; struct Idmap_data_header; struct Idmap_target_entry; struct Idmap_target_entry_inline; @@ -196,13 +221,15 @@ class LoadedIdmap { // Returns whether the idmap file on disk has not been modified since the construction of this // LoadedIdmap. - bool IsUpToDate() const; + UpToDate IsUpToDate() const; protected: // Exposed as protected so that tests can subclass and mock this class out. LoadedIdmap() = default; const Idmap_header* header_; + const Idmap_constraint* constraints_; + uint32_t constraints_count_; const Idmap_data_header* data_header_; Idmap_target_entries target_entries_; Idmap_target_inline_entries target_inline_entries_; @@ -220,7 +247,10 @@ class LoadedIdmap { DISALLOW_COPY_AND_ASSIGN(LoadedIdmap); explicit LoadedIdmap(const std::string& idmap_path, const Idmap_header* header, - const Idmap_data_header* data_header, Idmap_target_entries target_entries, + const Idmap_constraint* constraints, + uint32_t constraints_count, + const Idmap_data_header* data_header, + Idmap_target_entries target_entries, Idmap_target_inline_entries target_inline_entries, const Idmap_target_entry_inline_value* inline_entry_values_, const ConfigDescription* configs, Idmap_overlay_entries overlay_entries, @@ -231,5 +261,3 @@ class LoadedIdmap { }; } // namespace android - -#endif // IDMAP_H_ diff --git a/libs/androidfw/include/androidfw/ResourceTypes.h b/libs/androidfw/include/androidfw/ResourceTypes.h index e330410ed1a0..30594dcfa939 100644 --- a/libs/androidfw/include/androidfw/ResourceTypes.h +++ b/libs/androidfw/include/androidfw/ResourceTypes.h @@ -47,8 +47,10 @@ namespace android { +constexpr const bool kDeviceEndiannessSame = dtohs(0x1001) == 0x1001; + constexpr const uint32_t kIdmapMagic = 0x504D4449u; -constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Au; +constexpr const uint32_t kIdmapCurrentVersion = 0x0000000Bu; // This must never change. constexpr const uint32_t kFabricatedOverlayMagic = 0x4f525246; // FRRO (big endian) @@ -408,7 +410,16 @@ struct Res_value typedef uint32_t data_type; data_type data; - void copyFrom_dtoh(const Res_value& src); + void copyFrom_dtoh(const Res_value& src) { + if constexpr (kDeviceEndiannessSame) { + *this = src; + } else { + copyFrom_dtoh_slow(src); + } + } + + private: + void copyFrom_dtoh_slow(const Res_value& src); }; /** @@ -1254,11 +1265,32 @@ struct ResTable_config // Varies in length from 3 to 8 chars. Zero-filled value. char localeNumberingSystem[8]; - void copyFromDeviceNoSwap(const ResTable_config& o); - - void copyFromDtoH(const ResTable_config& o); - - void swapHtoD(); + void copyFromDeviceNoSwap(const ResTable_config& o) { + const auto o_size = dtohl(o.size); + if (o_size >= sizeof(ResTable_config)) [[likely]] { + *this = o; + } else { + memcpy(this, &o, o_size); + memset(((uint8_t*)this) + o_size, 0, sizeof(ResTable_config) - o_size); + } + this->size = sizeof(*this); + } + + void copyFromDtoH(const ResTable_config& o) { + if constexpr (kDeviceEndiannessSame) { + copyFromDeviceNoSwap(o); + } else { + copyFromDtoH_slow(o); + } + } + + void swapHtoD() { + if constexpr (kDeviceEndiannessSame) { + ; // noop + } else { + swapHtoD_slow(); + } + } int compare(const ResTable_config& o) const; int compareLogical(const ResTable_config& o) const; @@ -1384,6 +1416,10 @@ struct ResTable_config bool isBetterThanBeforeLocale(const ResTable_config& o, const ResTable_config* requested) const; String8 toString() const; + + private: + void copyFromDtoH_slow(const ResTable_config& o); + void swapHtoD_slow(); }; /** @@ -2267,7 +2303,7 @@ public: void** outData, size_t* outSize) const; static const size_t IDMAP_HEADER_SIZE_BYTES = 4 * sizeof(uint32_t) + 2 * 256; - static const uint32_t IDMAP_CURRENT_VERSION = 0x00000001; + static const size_t IDMAP_CONSTRAINTS_COUNT_SIZE_BYTES = sizeof(uint32_t); // Retrieve idmap meta-data. // diff --git a/libs/androidfw/include/androidfw/TypeWrappers.h b/libs/androidfw/include/androidfw/TypeWrappers.h index db641b78a4e4..d901af3c908b 100644 --- a/libs/androidfw/include/androidfw/TypeWrappers.h +++ b/libs/androidfw/include/androidfw/TypeWrappers.h @@ -14,8 +14,7 @@ * limitations under the License. */ -#ifndef __TYPE_WRAPPERS_H -#define __TYPE_WRAPPERS_H +#pragma once #include <androidfw/ResourceTypes.h> #include <utils/ByteOrder.h> @@ -54,7 +53,7 @@ struct TypeVariant { enum class Kind { Begin, End }; iterator(const TypeVariant* tv, Kind kind) : mTypeVariant(tv) { - mSparseIndex = mIndex = kind == Kind::Begin ? 0 : tv->mLength; + mSparseIndex = mIndex = (kind == Kind::Begin ? 0 : tv->mLength); // mSparseIndex here is technically past the number of sparse entries, but it is still // ok as it is enough to infer that this is the end iterator. } @@ -75,9 +74,11 @@ struct TypeVariant { const ResTable_type* data; private: - size_t mLength; + // For a dense table, this is the number of the elements. + // For a sparse table, this is the index of the last element + 1. + // In both cases, it can be used for iteration as the upper loop bound as in |i < mLength|. + uint32_t mLength; + bool mSparse; }; } // namespace android - -#endif // __TYPE_WRAPPERS_H diff --git a/libs/androidfw/include/androidfw/misc.h b/libs/androidfw/include/androidfw/misc.h index c9ba8a01a5e9..d8ca64a174a2 100644 --- a/libs/androidfw/include/androidfw/misc.h +++ b/libs/androidfw/include/androidfw/misc.h @@ -15,6 +15,7 @@ */ #pragma once +#include <sys/stat.h> #include <time.h> // @@ -64,10 +65,15 @@ ModDate getFileModDate(const char* fileName); /* same, but also returns -1 if the file has already been deleted */ ModDate getFileModDate(int fd); +// Extract the modification date from the stat structure. +ModDate getModDate(const struct ::stat& st); + // Check if |path| or |fd| resides on a readonly filesystem. bool isReadonlyFilesystem(const char* path); bool isReadonlyFilesystem(int fd); +bool isKnownWritablePath(const char* path); + } // namespace android // Whoever uses getFileModDate() will need this as well diff --git a/libs/androidfw/misc.cpp b/libs/androidfw/misc.cpp index 32f3624a3aee..26eb320805c9 100644 --- a/libs/androidfw/misc.cpp +++ b/libs/androidfw/misc.cpp @@ -16,10 +16,10 @@ #define LOG_TAG "misc" -// -// Miscellaneous utility functions. -// -#include <androidfw/misc.h> +#include "androidfw/misc.h" + +#include <errno.h> +#include <sys/stat.h> #include "android-base/logging.h" @@ -28,9 +28,7 @@ #include <sys/vfs.h> #endif // __linux__ -#include <errno.h> -#include <sys/stat.h> - +#include <array> #include <cstdio> #include <cstring> #include <tuple> @@ -40,28 +38,26 @@ namespace android { /* * Get a file's type. */ -FileType getFileType(const char* fileName) -{ - struct stat sb; - - if (stat(fileName, &sb) < 0) { - if (errno == ENOENT || errno == ENOTDIR) - return kFileTypeNonexistent; - else { - PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; - return kFileTypeUnknown; - } - } else { - if (S_ISREG(sb.st_mode)) - return kFileTypeRegular; - else if (S_ISDIR(sb.st_mode)) - return kFileTypeDirectory; - else if (S_ISCHR(sb.st_mode)) - return kFileTypeCharDev; - else if (S_ISBLK(sb.st_mode)) - return kFileTypeBlockDev; - else if (S_ISFIFO(sb.st_mode)) - return kFileTypeFifo; +FileType getFileType(const char* fileName) { + struct stat sb; + if (stat(fileName, &sb) < 0) { + if (errno == ENOENT || errno == ENOTDIR) + return kFileTypeNonexistent; + else { + PLOG(ERROR) << "getFileType(): stat(" << fileName << ") failed"; + return kFileTypeUnknown; + } + } else { + if (S_ISREG(sb.st_mode)) + return kFileTypeRegular; + else if (S_ISDIR(sb.st_mode)) + return kFileTypeDirectory; + else if (S_ISCHR(sb.st_mode)) + return kFileTypeCharDev; + else if (S_ISBLK(sb.st_mode)) + return kFileTypeBlockDev; + else if (S_ISFIFO(sb.st_mode)) + return kFileTypeFifo; #if defined(S_ISLNK) else if (S_ISLNK(sb.st_mode)) return kFileTypeSymlink; @@ -75,7 +71,7 @@ FileType getFileType(const char* fileName) } } -static ModDate getModDate(const struct stat& st) { +ModDate getModDate(const struct stat& st) { #ifdef _WIN32 return st.st_mtime; #elif defined(__APPLE__) @@ -113,8 +109,14 @@ bool isReadonlyFilesystem(const char*) { bool isReadonlyFilesystem(int) { return false; } +bool isKnownWritablePath(const char*) { + return false; +} #else // __linux__ bool isReadonlyFilesystem(const char* path) { + if (isKnownWritablePath(path)) { + return false; + } struct statfs sfs; if (::statfs(path, &sfs)) { PLOG(ERROR) << "isReadonlyFilesystem(): statfs(" << path << ") failed"; @@ -131,6 +133,13 @@ bool isReadonlyFilesystem(int fd) { } return (sfs.f_flags & ST_RDONLY) != 0; } + +bool isKnownWritablePath(const char* path) { + // We know that all paths in /data/ are writable. + static constexpr char kRwPrefix[] = "/data/"; + return strncmp(kRwPrefix, path, std::size(kRwPrefix) - 1) == 0; +} + #endif // __linux__ } // namespace android diff --git a/libs/androidfw/tests/Idmap_test.cpp b/libs/androidfw/tests/Idmap_test.cpp index cb2e56f5f5e4..22b9e69500d9 100644 --- a/libs/androidfw/tests/Idmap_test.cpp +++ b/libs/androidfw/tests/Idmap_test.cpp @@ -218,10 +218,11 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { auto apk_assets = ApkAssets::LoadOverlay(temp_file.path); ASSERT_NE(nullptr, apk_assets); - ASSERT_TRUE(apk_assets->IsUpToDate()); + ASSERT_TRUE(apk_assets->IsOverlay()); + ASSERT_EQ(UpToDate::True, apk_assets->IsUpToDate()); unlink(temp_file.path); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); const auto sleep_duration = std::chrono::nanoseconds(std::max(kModDateResolutionNs, 1'000'000ull)); @@ -230,7 +231,27 @@ TEST_F(IdmapTest, OverlayAssetsIsUpToDate) { base::WriteStringToFile("hello", temp_file.path); std::this_thread::sleep_for(sleep_duration); - ASSERT_FALSE(apk_assets->IsUpToDate()); + ASSERT_EQ(UpToDate::False, apk_assets->IsUpToDate()); +} + +TEST(IdmapTestUpToDate, Combine) { + ASSERT_EQ(UpToDate::False, combine(UpToDate::False, [] { + ADD_FAILURE(); // Shouldn't get called at all. + return UpToDate::False; + })); + + ASSERT_EQ(UpToDate::False, combine(UpToDate::True, [] { return UpToDate::False; })); + + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::True; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::True, [] { return UpToDate::Always; })); + ASSERT_EQ(UpToDate::True, combine(UpToDate::Always, [] { return UpToDate::True; })); + + ASSERT_EQ(UpToDate::Always, combine(UpToDate::Always, [] { return UpToDate::Always; })); +} + +TEST(IdmapTestUpToDate, FromBool) { + ASSERT_EQ(UpToDate::False, fromBool(false)); + ASSERT_EQ(UpToDate::True, fromBool(true)); } } // namespace diff --git a/libs/androidfw/tests/LocaleDataLookup_bench.cpp b/libs/androidfw/tests/LocaleDataLookup_bench.cpp new file mode 100644 index 000000000000..60ce3b944551 --- /dev/null +++ b/libs/androidfw/tests/LocaleDataLookup_bench.cpp @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include "benchmark/benchmark.h" + +#include "androidfw/LocaleDataLookup.h" + +namespace android { + +static void BM_LocaleDataLookupIsLocaleRepresentative(benchmark::State& state) { + for (auto&& _ : state) { + isLocaleRepresentative(packLocale("en", "US"), "Latn"); + isLocaleRepresentative(packLocale("es", "ES"), "Latn"); + isLocaleRepresentative(packLocale("zh", "CN"), "Hans"); + isLocaleRepresentative(packLocale("pt", "BR"), "Latn"); + isLocaleRepresentative(packLocale("ar", "EG"), "Arab"); + isLocaleRepresentative(packLocale("hi", "IN"), "Deva"); + isLocaleRepresentative(packLocale("jp", "JP"), "Jpan"); + } +} +BENCHMARK(BM_LocaleDataLookupIsLocaleRepresentative); + +static void BM_LocaleDataLookupLikelyScript(benchmark::State& state) { + for (auto&& _ : state) { + lookupLikelyScript(packLocale("en", "")); + lookupLikelyScript(packLocale("es", "")); + lookupLikelyScript(packLocale("zh", "")); + lookupLikelyScript(packLocale("pt", "")); + lookupLikelyScript(packLocale("ar", "")); + lookupLikelyScript(packLocale("hi", "")); + lookupLikelyScript(packLocale("jp", "")); + lookupLikelyScript(packLocale("en", "US")); + lookupLikelyScript(packLocale("es", "ES")); + lookupLikelyScript(packLocale("zh", "CN")); + lookupLikelyScript(packLocale("pt", "BR")); + lookupLikelyScript(packLocale("ar", "EG")); + lookupLikelyScript(packLocale("hi", "IN")); + lookupLikelyScript(packLocale("jp", "JP")); + } +} +BENCHMARK(BM_LocaleDataLookupLikelyScript); + + +} // namespace android diff --git a/libs/androidfw/tests/TypeWrappers_test.cpp b/libs/androidfw/tests/TypeWrappers_test.cpp index d66e05805484..69c24c5d8956 100644 --- a/libs/androidfw/tests/TypeWrappers_test.cpp +++ b/libs/androidfw/tests/TypeWrappers_test.cpp @@ -121,6 +121,7 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { values.push_back(std::nullopt); values.push_back(std::nullopt); values.push_back(Res_value{ sizeof(Res_value), 0, Res_value::TYPE_STRING, 0x87654321}); + values.push_back(std::nullopt); // test for combinations of compact_entry and short_offsets for (size_t i = 0; i < 8; i++) { @@ -191,6 +192,17 @@ TEST(TypeVariantIteratorTest, shouldIterateOverTypeWithoutErrors) { ++iter; + ASSERT_EQ(uint32_t(9), iter.index()); + ASSERT_TRUE(NULL == *iter); + if (sparse) { + // Sparse iterator doesn't know anything beyond the last entry. + ASSERT_EQ(v.endEntries(), iter); + } else { + ASSERT_NE(v.endEntries(), iter); + } + + ++iter; + ASSERT_EQ(v.endEntries(), iter); } } diff --git a/libs/androidfw/tests/data/overlay/overlay.idmap b/libs/androidfw/tests/data/overlay/overlay.idmap Binary files differindex 7e4b261cf109..6bd57c8d517c 100644 --- a/libs/androidfw/tests/data/overlay/overlay.idmap +++ b/libs/androidfw/tests/data/overlay/overlay.idmap diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 139ccfd22b0e..7280b12aaca9 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -24,8 +24,8 @@ package com.android.extensions.appfunctions { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); - method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(android.Manifest.permission.EXECUTE_APP_FUNCTIONS) public void executeAppFunction(@NonNull com.android.extensions.appfunctions.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull android.os.OutcomeReceiver<com.android.extensions.appfunctions.ExecuteAppFunctionResponse,com.android.extensions.appfunctions.AppFunctionException>); + method @RequiresPermission(android.Manifest.permission.EXECUTE_APP_FUNCTIONS) public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void isAppFunctionEnabled(@NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); method public void setAppFunctionEnabled(@NonNull String, int, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Void,java.lang.Exception>); field public static final int APP_FUNCTION_STATE_DEFAULT = 0; // 0x0 diff --git a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java index 9eb66a33fedc..1e31390854b8 100644 --- a/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java +++ b/libs/appfunctions/java/com/android/extensions/appfunctions/AppFunctionManager.java @@ -104,12 +104,7 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#executeAppFunction} for the * documented behaviour of this method. */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(Manifest.permission.EXECUTE_APP_FUNCTIONS) public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @@ -150,12 +145,7 @@ public final class AppFunctionManager { * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the * documented behaviour of this method. */ - @RequiresPermission( - anyOf = { - Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, - Manifest.permission.EXECUTE_APP_FUNCTIONS - }, - conditional = true) + @RequiresPermission(Manifest.permission.EXECUTE_APP_FUNCTIONS) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 53d3b77f1ba2..bb2a53bc04d6 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -779,6 +779,7 @@ cc_test { "tests/unit/CommonPoolTests.cpp", "tests/unit/DamageAccumulatorTests.cpp", "tests/unit/DeferredLayerUpdaterTests.cpp", + "tests/unit/DrawTextFunctorTest.cpp", "tests/unit/EglManagerTests.cpp", "tests/unit/FatVectorTests.cpp", "tests/unit/GraphicsStatsServiceTests.cpp", diff --git a/libs/hwui/FrameInfo.cpp b/libs/hwui/FrameInfo.cpp index a958a091a830..36feabde07eb 100644 --- a/libs/hwui/FrameInfo.cpp +++ b/libs/hwui/FrameInfo.cpp @@ -32,8 +32,9 @@ const std::array FrameInfoNames{"Flags", "PerformTraversalsStart", "DrawStart", "FrameDeadline", - "FrameInterval", "FrameStartTime", + "FrameInterval", + "WorkloadTarget", "SyncQueued", "SyncStart", "IssueDrawCommandsStart", @@ -48,7 +49,7 @@ const std::array FrameInfoNames{"Flags", }; -static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 23, +static_assert(static_cast<int>(FrameInfoIndex::NumIndexes) == 24, "Must update value in FrameMetrics.java#FRAME_STATS_COUNT (and here)"); void FrameInfo::importUiThreadInfo(int64_t* info) { diff --git a/libs/hwui/FrameInfo.h b/libs/hwui/FrameInfo.h index f7ad13978a30..61c30b803b00 100644 --- a/libs/hwui/FrameInfo.h +++ b/libs/hwui/FrameInfo.h @@ -30,7 +30,8 @@ namespace android { namespace uirenderer { -static constexpr size_t UI_THREAD_FRAME_INFO_SIZE = 12; +// This value must be in sync with `FRAME_INFO_SIZE` in FrameInfo.Java +static constexpr size_t UI_THREAD_FRAME_INFO_SIZE = 13; enum class FrameInfoIndex { Flags = 0, @@ -47,6 +48,11 @@ enum class FrameInfoIndex { FrameInterval, // End of UI frame info + // The target workload duration based on the original frame deadline and + // and intended vsync. Counted in UI_THREAD_FRAME_INFO_SIZE so its value + // can be set in setVsync(). + WorkloadTarget, + SyncQueued, SyncStart, @@ -109,6 +115,7 @@ public: set(FrameInfoIndex::FrameStartTime) = vsyncTime; set(FrameInfoIndex::FrameDeadline) = frameDeadline; set(FrameInfoIndex::FrameInterval) = frameInterval; + set(FrameInfoIndex::WorkloadTarget) = frameDeadline - intendedVsync; return *this; } diff --git a/libs/hwui/JankTracker.cpp b/libs/hwui/JankTracker.cpp index 638a060bdb1c..80eb6bc986d6 100644 --- a/libs/hwui/JankTracker.cpp +++ b/libs/hwui/JankTracker.cpp @@ -201,7 +201,7 @@ void JankTracker::finishFrame(FrameInfo& frame, std::unique_ptr<FrameMetricsRepo // If we are in triple buffering, we have enough buffers in queue to sustain a single frame // drop without jank, so adjust the frame interval to the deadline. if (isTripleBuffered) { - int64_t originalDeadlineDuration = deadline - frame[FrameInfoIndex::IntendedVsync]; + int64_t originalDeadlineDuration = frame[FrameInfoIndex::WorkloadTarget]; deadline = mNextFrameStartUnstuffed + originalDeadlineDuration; frame.set(FrameInfoIndex::FrameDeadline) = deadline; } diff --git a/libs/hwui/OWNERS b/libs/hwui/OWNERS index bc174599a4d3..70d13ab8b3e5 100644 --- a/libs/hwui/OWNERS +++ b/libs/hwui/OWNERS @@ -4,7 +4,6 @@ alecmouri@google.com djsollen@google.com jreck@google.com njawad@google.com -scroggo@google.com sumir@google.com # For text, e.g. Typeface, Font, Minikin, etc. diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp index 7d01dfbb446f..21430f7e6777 100644 --- a/libs/hwui/Properties.cpp +++ b/libs/hwui/Properties.cpp @@ -57,6 +57,9 @@ constexpr bool query_global_priority() { constexpr bool early_preload_gl_context() { return false; } +constexpr bool calc_workload_orig_deadline() { + return false; +} } // namespace hwui_flags #endif @@ -299,5 +302,10 @@ bool Properties::earlyPreloadGlContext() { hwui_flags::early_preload_gl_context()); } +bool Properties::calcWorkloadOrigDeadline() { + static bool sCalcWorkloadOrigDeadline = base::GetBoolProperty( + "debug.hwui.calc_workload_orig_deadline", hwui_flags::calc_workload_orig_deadline()); + return sCalcWorkloadOrigDeadline; +} } // namespace uirenderer } // namespace android diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h index 280a75a28e65..a7a564428636 100644 --- a/libs/hwui/Properties.h +++ b/libs/hwui/Properties.h @@ -384,6 +384,7 @@ public: static bool initializeGlAlways(); static bool resampleGainmapRegions(); static bool earlyPreloadGlContext(); + static bool calcWorkloadOrigDeadline(); private: static StretchEffectBehavior stretchEffectBehavior; diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 2851dd8b1003..d3fc91b65829 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -137,6 +137,14 @@ flag { } flag { + name: "shader_color_space" + is_exported: true + namespace: "core_graphics" + description: "API to set the working colorspace of a Shader or ColorFilter" + bug: "299670828" +} + +flag { name: "query_global_priority" namespace: "core_graphics" description: "Attempt to query whether the vulkan driver supports the requested global priority before queue creation." @@ -174,6 +182,25 @@ flag { flag { name: "early_preload_gl_context" namespace: "core_graphics" - description: "Initialize GL context and GraphicBufferAllocater init on renderThread preload. This improves app startup time for apps using GL." + description: "Preload GL context on renderThread preload. This improves app startup time for apps using GL." bug: "383612849" +} + +flag { + name: "calc_workload_orig_deadline" + namespace: "window_surfaces" + description: "Use original frame deadline to calculate the workload target deadline for jank tracking" + bug: "389939827" + is_fixed_read_only: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "early_preinit_buffer_allocator" + namespace: "core_graphics" + description: "Initialize GraphicBufferAllocater on ViewRootImpl init, to avoid blocking on init during buffer allocation, improving app launch latency." + bug: "389908734" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b1550b0b6888..63a024b8e780 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -260,7 +260,7 @@ sk_sp<Bitmap> Bitmap::createFrom(AHardwareBuffer* hardwareBuffer, const SkImageI #endif sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly) { + size_t size, bool readOnly, int64_t id) { #ifdef _WIN32 // ashmem not implemented on Windows return nullptr; #else @@ -279,7 +279,7 @@ sk_sp<Bitmap> Bitmap::createFrom(const SkImageInfo& info, size_t rowBytes, int f } } - sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes)); + sk_sp<Bitmap> bitmap(new Bitmap(addr, fd, size, info, rowBytes, id)); if (readOnly) { bitmap->setImmutable(); } @@ -334,7 +334,7 @@ Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Ashmem) - , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { + , mId(id != UNDEFINED_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 8abe6a8c445a..4e9bcf27c0ef 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -97,7 +97,7 @@ public: BitmapPalette palette); #endif static sk_sp<Bitmap> createFrom(const SkImageInfo& info, size_t rowBytes, int fd, void* addr, - size_t size, bool readOnly); + size_t size, bool readOnly, int64_t id); static sk_sp<Bitmap> createFrom(const SkImageInfo&, SkPixelRef&); int rowBytesAsPixels() const { return rowBytes() >> mInfo.shiftPerPixel(); } @@ -183,15 +183,15 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); -private: - static constexpr uint64_t INVALID_BITMAP_ID = 0u; + static constexpr uint64_t UNDEFINED_BITMAP_ID = 0u; +private: static sk_sp<Bitmap> allocateAshmemBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); Bitmap(void* address, size_t allocSize, const SkImageInfo& info, size_t rowBytes); Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info); Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, - uint64_t id = INVALID_BITMAP_ID); + uint64_t id = UNDEFINED_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); diff --git a/libs/hwui/hwui/DrawTextFunctor.h b/libs/hwui/hwui/DrawTextFunctor.h index 008b693edf02..b782837bb4ee 100644 --- a/libs/hwui/hwui/DrawTextFunctor.h +++ b/libs/hwui/hwui/DrawTextFunctor.h @@ -76,6 +76,41 @@ static void simplifyPaint(int color, Paint* paint) { paint->setBlendMode(SkBlendMode::kSrcOver); } +namespace { + +static bool shouldDarkenTextForHighContrast(const uirenderer::Lab& lab) { + // LINT.IfChange(hct_darken) + return lab.L <= 50; + // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) +} + +} // namespace + +static void adjustHighContrastInnerTextColor(uirenderer::Lab* lab) { + bool darken = shouldDarkenTextForHighContrast(*lab); + bool isGrayscale = abs(lab->a) < 10 && abs(lab->b) < 10; + if (isGrayscale) { + // For near-grayscale text we first remove all color. + lab->a = lab->b = 0; + if (lab->L > 40 && lab->L < 60) { + // Text near "middle gray" is pushed to a more contrasty gray. + lab->L = darken ? 20 : 80; + } else { + // Other grayscale text is pushed completely white or black. + lab->L = darken ? 0 : 100; + } + } else { + // For color text we ensure the text is bright enough (for light text) + // or dark enough (for dark text) to stand out against the background, + // without touching the A and B components so we retain color. + if (darken && lab->L > 20.f) { + lab->L = 20.0f; + } else if (!darken && lab->L < 90.f) { + lab->L = 90.0f; + } + } +} + class DrawTextFunctor { public: /** @@ -114,10 +149,8 @@ public: if (CC_UNLIKELY(canvas->isHighContrastText() && paint.getAlpha() != 0)) { // high contrast draw path int color = paint.getColor(); - // LINT.IfChange(hct_darken) uirenderer::Lab lab = uirenderer::sRGBToLab(color); - bool darken = lab.L <= 50; - // LINT.ThenChange(/core/java/android/text/Layout.java:hct_darken) + bool darken = shouldDarkenTextForHighContrast(lab); // outline gDrawTextBlobMode = DrawTextBlobMode::HctOutline; @@ -130,20 +163,7 @@ public: gDrawTextBlobMode = DrawTextBlobMode::HctInner; Paint innerPaint(paint); if (flags::high_contrast_text_inner_text_color()) { - // Preserve some color information while still ensuring sufficient contrast. - // Thus we increase the lightness to make the color stand out against a black - // background, and vice-versa. For grayscale, we retain some gray to indicate - // states like disabled or to distinguish links. - bool isGrayscale = abs(lab.a) < 1 && abs(lab.b) < 1; - if (isGrayscale) { - if (darken) { - lab.L = lab.L < 40 ? 0 : 20; - } else { - lab.L = lab.L > 60 ? 100 : 80; - } - } else { - lab.L = darken ? 20 : 90; - } + adjustHighContrastInnerTextColor(&lab); simplifyPaint(uirenderer::LabToSRGB(lab, SK_AlphaOPAQUE), &innerPaint); } else { simplifyPaint(darken ? SK_ColorBLACK : SK_ColorWHITE, &innerPaint); diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 29efd98b41d0..cfde0b28c0d5 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -191,9 +191,8 @@ void reinitBitmap(JNIEnv* env, jobject javaBitmap, const SkImageInfo& info, info.width(), info.height(), isPremultiplied); } -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk, jobject ninePatchInsets, - int density) { +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, jbyteArray ninePatchChunk, + jobject ninePatchInsets, int density, int64_t id) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); @@ -208,10 +207,12 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, if (!isMutable) { bitmapWrapper->bitmap().setImmutable(); } + int64_t bitmapId = id != Bitmap::UNDEFINED_BITMAP_ID ? id : bitmap->getId(); jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), - bitmap->width(), bitmap->height(), density, - isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); + static_cast<jlong>(bitmapId), + reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), + bitmap->height(), density, isPremultiplied, ninePatchChunk, + ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { ALOGE("*** Uncaught exception returned from Java call!\n"); @@ -759,6 +760,7 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { const int32_t height = p.readInt32(); const int32_t rowBytes = p.readInt32(); const int32_t density = p.readInt32(); + const int64_t sourceId = p.readInt64(); if (kN32_SkColorType != colorType && kRGBA_F16_SkColorType != colorType && @@ -815,7 +817,8 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { return STATUS_NO_MEMORY; } nativeBitmap = - Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, !isMutable); + Bitmap::createFrom(imageInfo, rowBytes, fd.release(), addr, size, + !isMutable, sourceId); return STATUS_OK; }); @@ -831,15 +834,15 @@ static jobject Bitmap_createFromParcel(JNIEnv* env, jobject, jobject parcel) { } return createBitmap(env, nativeBitmap.release(), getPremulBitmapCreateFlags(isMutable), nullptr, - nullptr, density); + nullptr, density, sourceId); #else jniThrowRuntimeException(env, "Cannot use parcels outside of Android"); return NULL; #endif } -static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, - jlong bitmapHandle, jint density, jobject parcel) { +static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, jlong bitmapHandle, jint density, + jobject parcel) { #ifdef __ANDROID__ // Layoutlib does not support parcel if (parcel == NULL) { ALOGD("------- writeToParcel null parcel\n"); @@ -870,6 +873,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, binder_status_t status; int fd = bitmapWrapper->bitmap().getAshmemFd(); if (fd >= 0 && p.allowFds() && bitmap.isImmutable()) { + p.writeInt64(bitmapWrapper->bitmap().getId()); #if DEBUG_PARCEL ALOGD("Bitmap.writeToParcel: transferring immutable bitmap's ashmem fd as " "immutable blob (fds %s)", @@ -889,7 +893,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, ALOGD("Bitmap.writeToParcel: copying bitmap into new blob (fds %s)", p.allowFds() ? "allowed" : "forbidden"); #endif - + p.writeInt64(Bitmap::UNDEFINED_BITMAP_ID); status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); diff --git a/libs/hwui/jni/Bitmap.h b/libs/hwui/jni/Bitmap.h index 21a93f066d9b..c93246a972b6 100644 --- a/libs/hwui/jni/Bitmap.h +++ b/libs/hwui/jni/Bitmap.h @@ -18,6 +18,7 @@ #include <jni.h> #include <android/bitmap.h> +#include <hwui/Bitmap.h> struct SkImageInfo; @@ -33,9 +34,9 @@ enum BitmapCreateFlags { kBitmapCreateFlag_Premultiplied = 0x2, }; -jobject createBitmap(JNIEnv* env, Bitmap* bitmap, - int bitmapCreateFlags, jbyteArray ninePatchChunk = nullptr, - jobject ninePatchInsets = nullptr, int density = -1); +jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int bitmapCreateFlags, + jbyteArray ninePatchChunk = nullptr, jobject ninePatchInsets = nullptr, + int density = -1, int64_t id = Bitmap::UNDEFINED_BITMAP_ID); Bitmap& toBitmap(jlong bitmapHandle); diff --git a/libs/hwui/jni/ScopedParcel.cpp b/libs/hwui/jni/ScopedParcel.cpp index b0f5423813b7..95e4e01d8df8 100644 --- a/libs/hwui/jni/ScopedParcel.cpp +++ b/libs/hwui/jni/ScopedParcel.cpp @@ -39,6 +39,16 @@ uint32_t ScopedParcel::readUint32() { return temp; } +int64_t ScopedParcel::readInt64() { + int64_t temp = 0; + // TODO: This behavior-matches what android::Parcel does + // but this should probably be better + if (AParcel_readInt64(mParcel, &temp) != STATUS_OK) { + temp = 0; + } + return temp; +} + float ScopedParcel::readFloat() { float temp = 0.; if (AParcel_readFloat(mParcel, &temp) != STATUS_OK) { diff --git a/libs/hwui/jni/ScopedParcel.h b/libs/hwui/jni/ScopedParcel.h index fd8d6a210f0f..f2f138fda43c 100644 --- a/libs/hwui/jni/ScopedParcel.h +++ b/libs/hwui/jni/ScopedParcel.h @@ -35,12 +35,16 @@ public: uint32_t readUint32(); + int64_t readInt64(); + float readFloat(); void writeInt32(int32_t value) { AParcel_writeInt32(mParcel, value); } void writeUint32(uint32_t value) { AParcel_writeUint32(mParcel, value); } + void writeInt64(int64_t value) { AParcel_writeInt64(mParcel, value); } + void writeFloat(float value) { AParcel_writeFloat(mParcel, value); } bool allowFds() const { return AParcel_getAllowFds(mParcel); } diff --git a/libs/hwui/jni/Shader.cpp b/libs/hwui/jni/Shader.cpp index eadb9dea566f..45f0fe0288a4 100644 --- a/libs/hwui/jni/Shader.cpp +++ b/libs/hwui/jni/Shader.cpp @@ -266,11 +266,17 @@ static jlong RuntimeShader_getNativeFinalizer(JNIEnv*, jobject) { return static_cast<jlong>(reinterpret_cast<uintptr_t>(&SkRuntimeShaderBuilder_delete)); } -static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr) { +static jlong RuntimeShader_create(JNIEnv* env, jobject, jlong shaderBuilder, jlong matrixPtr, + jlong colorSpacePtr) { SkRuntimeShaderBuilder* builder = reinterpret_cast<SkRuntimeShaderBuilder*>(shaderBuilder); const SkMatrix* matrix = reinterpret_cast<const SkMatrix*>(matrixPtr); + auto colorSpace = GraphicsJNI::getNativeColorSpace(colorSpacePtr); sk_sp<SkShader> shader = builder->makeShader(matrix); ThrowIAE_IfNull(env, shader); + if (colorSpace) { + shader = shader->makeWithWorkingColorSpace(colorSpace); + ThrowIAE_IfNull(env, shader); + } return reinterpret_cast<jlong>(shader.release()); } @@ -350,6 +356,10 @@ static void RuntimeShader_updateChild(JNIEnv* env, jobject, jlong shaderBuilder, UpdateChild(env, builder, name.c_str(), childEffect); } +static void RuntimeShader_no(JNIEnv* env) { + jniThrowRuntimeException(env, "Not supported"); +} + /////////////////////////////////////////////////////////////////////////////////////////////// static const JNINativeMethod gShaderMethods[] = { @@ -379,7 +389,8 @@ static const JNINativeMethod gComposeShaderMethods[] = { static const JNINativeMethod gRuntimeShaderMethods[] = { {"nativeGetFinalizer", "()J", (void*)RuntimeShader_getNativeFinalizer}, - {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_create}, + {"nativeCreateShader", "(JJ)J", (void*)RuntimeShader_no}, + {"nativeCreateShader", "(JJJ)J", (void*)RuntimeShader_create}, {"nativeCreateBuilder", "(Ljava/lang/String;)J", (void*)RuntimeShader_createShaderBuilder}, {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", (void*)RuntimeShader_updateFloatArrayUniforms}, diff --git a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp index df9f83036709..99e7740d66d2 100644 --- a/libs/hwui/jni/android_graphics_HardwareRenderer.cpp +++ b/libs/hwui/jni/android_graphics_HardwareRenderer.cpp @@ -52,6 +52,9 @@ #include <renderthread/RenderThread.h> #include <src/image/SkImage_Base.h> #include <thread/CommonPool.h> +#ifdef __ANDROID__ +#include <ui/GraphicBufferAllocator.h> +#endif #include <utils/Color.h> #include <utils/RefBase.h> #include <utils/StrongPointer.h> @@ -849,6 +852,17 @@ static void android_view_ThreadedRenderer_preload(JNIEnv*, jclass) { RenderProxy::preload(); } +static void android_view_ThreadedRenderer_preInitBufferAllocator(JNIEnv*, jclass) { +#ifdef __ANDROID__ + CommonPool::async([] { + ATRACE_NAME("preInitBufferAllocator:GraphicBufferAllocator"); + // This involves several binder calls which we do not want blocking + // critical path of the activity that is launching. + GraphicBufferAllocator::getInstance(); + }); +#endif +} + static void android_view_ThreadedRenderer_setRtAnimationsEnabled(JNIEnv* env, jobject clazz, jboolean enabled) { RenderProxy::setRtAnimationsEnabled(enabled); @@ -1040,6 +1054,8 @@ static const JNINativeMethod gMethods[] = { (void*)android_view_ThreadedRenderer_setDisplayDensityDpi}, {"nInitDisplayInfo", "(IIFIJJZZZ)V", (void*)android_view_ThreadedRenderer_initDisplayInfo}, {"preload", "()V", (void*)android_view_ThreadedRenderer_preload}, + {"preInitBufferAllocator", "()V", + (void*)android_view_ThreadedRenderer_preInitBufferAllocator}, {"isWebViewOverlaysEnabled", "()Z", (void*)android_view_ThreadedRenderer_isWebViewOverlaysEnabled}, {"nSetDrawingEnabled", "(Z)V", (void*)android_view_ThreadedRenderer_setDrawingEnabled}, diff --git a/libs/hwui/renderthread/CanvasContext.cpp b/libs/hwui/renderthread/CanvasContext.cpp index b36b8be10779..e3e393c4fdfb 100644 --- a/libs/hwui/renderthread/CanvasContext.cpp +++ b/libs/hwui/renderthread/CanvasContext.cpp @@ -789,7 +789,13 @@ void CanvasContext::draw(bool solelyTextureViewUpdates) { int64_t frameDeadline = mCurrentFrameInfo->get(FrameInfoIndex::FrameDeadline); int64_t dequeueBufferDuration = mCurrentFrameInfo->get(FrameInfoIndex::DequeueBufferDuration); - mHintSessionWrapper->updateTargetWorkDuration(frameDeadline - intendedVsync); + if (Properties::calcWorkloadOrigDeadline()) { + // Uses the unmodified frame deadline in calculating workload target duration + mHintSessionWrapper->updateTargetWorkDuration( + mCurrentFrameInfo->get(FrameInfoIndex::WorkloadTarget)); + } else { + mHintSessionWrapper->updateTargetWorkDuration(frameDeadline - intendedVsync); + } if (didDraw) { int64_t frameStartTime = mCurrentFrameInfo->get(FrameInfoIndex::FrameStartTime); diff --git a/libs/hwui/tests/unit/DrawTextFunctorTest.cpp b/libs/hwui/tests/unit/DrawTextFunctorTest.cpp new file mode 100644 index 000000000000..c5361a0833c4 --- /dev/null +++ b/libs/hwui/tests/unit/DrawTextFunctorTest.cpp @@ -0,0 +1,88 @@ +/* + * Copyright (C) 2025 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +#include <gtest/gtest.h> + +#include "hwui/DrawTextFunctor.h" + +using namespace android; + +namespace { + +void testHighContrastInnerTextColor(float originalL, float originalA, float originalB, + float expectedL, float expectedA, float expectedB) { + uirenderer::Lab color = {originalL, originalA, originalB}; + adjustHighContrastInnerTextColor(&color); + EXPECT_FLOAT_EQ(color.L, expectedL); + EXPECT_FLOAT_EQ(color.a, expectedA); + EXPECT_FLOAT_EQ(color.b, expectedB); +} + +TEST(DrawTextFunctorTest, BlackUnaffected) { + testHighContrastInnerTextColor(0, 0, 0, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, WhiteUnaffected) { + testHighContrastInnerTextColor(100, 0, 0, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, DarkGrayPushedToWhite) { + testHighContrastInnerTextColor(10, 0, 0, 0, 0, 0); + testHighContrastInnerTextColor(20, 0, 0, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, LightGrayPushedToWhite) { + testHighContrastInnerTextColor(80, 0, 0, 100, 0, 0); + testHighContrastInnerTextColor(90, 0, 0, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, MiddleDarkGrayPushedToDarkGray) { + testHighContrastInnerTextColor(41, 0, 0, 20, 0, 0); + testHighContrastInnerTextColor(49, 0, 0, 20, 0, 0); +} + +TEST(DrawTextFunctorTest, MiddleLightGrayPushedToLightGray) { + testHighContrastInnerTextColor(51, 0, 0, 80, 0, 0); + testHighContrastInnerTextColor(59, 0, 0, 80, 0, 0); +} + +TEST(DrawTextFunctorTest, PaleColorTreatedAsGrayscaleAndPushedToWhite) { + testHighContrastInnerTextColor(75, 5, -5, 100, 0, 0); + testHighContrastInnerTextColor(85, -6, 8, 100, 0, 0); +} + +TEST(DrawTextFunctorTest, PaleColorTreatedAsGrayscaleAndPushedToBlack) { + testHighContrastInnerTextColor(25, 5, -5, 0, 0, 0); + testHighContrastInnerTextColor(35, -6, 8, 0, 0, 0); +} + +TEST(DrawTextFunctorTest, ColorfulColorIsLightened) { + testHighContrastInnerTextColor(70, 100, -100, 90, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulLightColorIsUntouched) { + testHighContrastInnerTextColor(95, 100, -100, 95, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulColorIsDarkened) { + testHighContrastInnerTextColor(30, 100, -100, 20, 100, -100); +} + +TEST(DrawTextFunctorTest, ColorfulDarkColorIsUntouched) { + testHighContrastInnerTextColor(5, 100, -100, 5, 100, -100); +} + +} // namespace diff --git a/libs/hwui/tests/unit/JankTrackerTests.cpp b/libs/hwui/tests/unit/JankTrackerTests.cpp index b67e419e7d4a..c289d67fbef6 100644 --- a/libs/hwui/tests/unit/JankTrackerTests.cpp +++ b/libs/hwui/tests/unit/JankTrackerTests.cpp @@ -45,6 +45,7 @@ TEST(JankTracker, noJank) { info->set(FrameInfoIndex::FrameCompleted) = 115_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); info = jankTracker.startFrame(); @@ -55,6 +56,7 @@ TEST(JankTracker, noJank) { info->set(FrameInfoIndex::FrameCompleted) = 131_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->totalFrameCount()); @@ -79,6 +81,7 @@ TEST(JankTracker, jank) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->totalFrameCount()); @@ -102,6 +105,7 @@ TEST(JankTracker, legacyJankButNoRealJank) { info->set(FrameInfoIndex::FrameCompleted) = 118_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->totalFrameCount()); @@ -127,6 +131,7 @@ TEST(JankTracker, doubleStuffed) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -140,6 +145,7 @@ TEST(JankTracker, doubleStuffed) { info->set(FrameInfoIndex::FrameCompleted) = 137_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->totalFrameCount()); @@ -164,6 +170,7 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 121_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 120_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -177,6 +184,7 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 137_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 136_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -190,6 +198,7 @@ TEST(JankTracker, doubleStuffedThenPauseThenJank) { info->set(FrameInfoIndex::FrameCompleted) = 169_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 168_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 20_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(3, container.get()->totalFrameCount()); @@ -214,6 +223,7 @@ TEST(JankTracker, doubleStuffedTwoIntervalBehind) { info->set(FrameInfoIndex::FrameCompleted) = 117_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 116_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 16_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -228,6 +238,7 @@ TEST(JankTracker, doubleStuffedTwoIntervalBehind) { info->set(FrameInfoIndex::FrameCompleted) = 133_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 132_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 16_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(1, container.get()->jankFrameCount()); @@ -242,6 +253,7 @@ TEST(JankTracker, doubleStuffedTwoIntervalBehind) { info->set(FrameInfoIndex::FrameCompleted) = 165_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 148_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 16_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->jankFrameCount()); @@ -256,6 +268,7 @@ TEST(JankTracker, doubleStuffedTwoIntervalBehind) { info->set(FrameInfoIndex::FrameCompleted) = 181_ms; info->set(FrameInfoIndex::FrameInterval) = 16_ms; info->set(FrameInfoIndex::FrameDeadline) = 164_ms; + info->set(FrameInfoIndex::WorkloadTarget) = 16_ms; jankTracker.finishFrame(*info, reporter, frameNumber, surfaceId); ASSERT_EQ(2, container.get()->jankFrameCount()); diff --git a/libs/input/PointerControllerContext.cpp b/libs/input/PointerControllerContext.cpp index 747eb8e5ad1b..5406de8602d6 100644 --- a/libs/input/PointerControllerContext.cpp +++ b/libs/input/PointerControllerContext.cpp @@ -15,6 +15,7 @@ */ #include "PointerControllerContext.h" + #include "PointerController.h" namespace { @@ -184,7 +185,7 @@ void PointerControllerContext::PointerAnimator::handleVsyncEvents() { DisplayEventReceiver::Event buf[EVENT_BUFFER_SIZE]; while ((n = mDisplayEventReceiver.getEvents(buf, EVENT_BUFFER_SIZE)) > 0) { for (size_t i = 0; i < static_cast<size_t>(n); ++i) { - if (buf[i].header.type == DisplayEventReceiver::DISPLAY_EVENT_VSYNC) { + if (buf[i].header.type == DisplayEventType::DISPLAY_EVENT_VSYNC) { timestamp = buf[i].header.timestamp; gotVsync = true; } |