diff options
Diffstat (limited to 'libs')
237 files changed, 6321 insertions, 1756 deletions
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java index f466d603bda3..19f837c8bd13 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/common/ExtensionHelper.java @@ -23,7 +23,6 @@ import android.annotation.SuppressLint; import android.app.WindowConfiguration; import android.content.Context; import android.graphics.Rect; -import android.hardware.display.DisplayManagerGlobal; import android.util.RotationUtils; import android.view.DisplayInfo; import android.view.Surface; @@ -31,7 +30,6 @@ import android.view.WindowManager; import androidx.annotation.NonNull; import androidx.annotation.UiContext; -import androidx.annotation.VisibleForTesting; /** * Util class for both Sidecar and Extensions. @@ -46,24 +44,15 @@ public final class ExtensionHelper { * Rotates the input rectangle specified in default display orientation to the current display * rotation. * - * @param displayId the display id. + * @param displayInfo the display information. * @param rotation the target rotation relative to the default display orientation. * @param inOutRect the input/output Rect as specified in the default display orientation. */ - public static void rotateRectToDisplayRotation( - int displayId, @Surface.Rotation int rotation, @NonNull Rect inOutRect) { - final DisplayManagerGlobal dmGlobal = DisplayManagerGlobal.getInstance(); - final DisplayInfo displayInfo = dmGlobal.getDisplayInfo(displayId); - - rotateRectToDisplayRotation(displayInfo, rotation, inOutRect); - } - // We suppress the Lint error CheckResult for Rect#intersect because in case the displayInfo and // folding features are out of sync, e.g. when a foldable devices is unfolding, it is acceptable // to provide the original folding feature Rect even if they don't intersect. @SuppressLint("RectIntersectReturnValueIgnored") - @VisibleForTesting - static void rotateRectToDisplayRotation(@NonNull DisplayInfo displayInfo, + public static void rotateRectToDisplayRotation(@NonNull DisplayInfo displayInfo, @Surface.Rotation int rotation, @NonNull Rect inOutRect) { // The inOutRect is specified in the default display orientation, so here we need to get // the display width and height in the default orientation to perform the intersection and diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java index ad194f707cf3..6398c7a2f498 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/DividerPresenter.java @@ -39,6 +39,7 @@ import static androidx.window.extensions.embedding.SplitPresenter.CONTAINER_POSI import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; +import android.annotation.ColorInt; import android.annotation.Nullable; import android.app.Activity; import android.app.ActivityThread; @@ -1394,10 +1395,14 @@ class DividerPresenter implements View.OnTouchListener { } private void showVeils(@NonNull SurfaceControl.Transaction t) { - final Color primaryVeilColor = getContainerBackgroundColor( - mProperties.mPrimaryContainer, DEFAULT_PRIMARY_VEIL_COLOR); - final Color secondaryVeilColor = getContainerBackgroundColor( - mProperties.mSecondaryContainer, DEFAULT_SECONDARY_VEIL_COLOR); + final Color primaryVeilColor = getVeilColor( + mProperties.mDividerAttributes.getPrimaryVeilColor(), + mProperties.mPrimaryContainer, + DEFAULT_PRIMARY_VEIL_COLOR); + final Color secondaryVeilColor = getVeilColor( + mProperties.mDividerAttributes.getSecondaryVeilColor(), + mProperties.mSecondaryContainer, + DEFAULT_SECONDARY_VEIL_COLOR); t.setColor(mPrimaryVeil, colorToFloatArray(primaryVeilColor)) .setColor(mSecondaryVeil, colorToFloatArray(secondaryVeilColor)) .setLayer(mDividerSurface, DIVIDER_LAYER) @@ -1444,6 +1449,21 @@ class DividerPresenter implements View.OnTouchListener { } } + /** + * Returns the veil color. + * + * If the configured color is not transparent, we use the configured color, otherwise we use + * the window background color of the top activity. If the background color of the top + * activity is unavailable, the default color is used. + */ + @NonNull + private static Color getVeilColor(@ColorInt int configuredColor, + @NonNull TaskFragmentContainer container, @NonNull Color defaultColor) { + return configuredColor != Color.TRANSPARENT + ? Color.valueOf(configuredColor) + : getContainerBackgroundColor(container, defaultColor); + } + private static float[] colorToFloatArray(@NonNull Color color) { return new float[]{color.red(), color.green(), color.blue()}; } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java index 556da3798df5..5ce73b908c4c 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/layout/WindowLayoutComponentImpl.java @@ -34,11 +34,14 @@ import android.content.Context; import android.content.ContextWrapper; import android.content.res.Configuration; import android.graphics.Rect; +import android.hardware.display.DisplayManagerGlobal; import android.os.Bundle; import android.os.IBinder; import android.os.StrictMode; import android.util.ArrayMap; import android.util.Log; +import android.view.DisplayInfo; +import android.view.Surface; import androidx.annotation.GuardedBy; import androidx.annotation.NonNull; @@ -96,8 +99,29 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { private final SupportedWindowFeatures mSupportedWindowFeatures; + private final DisplayStateProvider mDisplayStateProvider; + public WindowLayoutComponentImpl(@NonNull Context context, @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer) { + this(context, foldingFeatureProducer, new DisplayStateProvider() { + @Override + public int getDisplayRotation(@NonNull WindowConfiguration windowConfiguration) { + return windowConfiguration.getDisplayRotation(); + } + + @NonNull + @Override + public DisplayInfo getDisplayInfo(int displayId) { + return DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); + } + }); + } + + @VisibleForTesting + WindowLayoutComponentImpl(@NonNull Context context, + @NonNull DeviceStateManagerFoldingFeatureProducer foldingFeatureProducer, + @NonNull DisplayStateProvider displayStateProvider) { + mDisplayStateProvider = displayStateProvider; ((Application) context.getApplicationContext()) .registerActivityLifecycleCallbacks(new NotifyOnConfigurationChanged()); mFoldingFeatureProducer = foldingFeatureProducer; @@ -145,21 +169,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { || containsConsumer(consumer)) { return; } - final IllegalArgumentException exception = new IllegalArgumentException( - "Context must be a UI Context with display association, which should be" - + " an Activity, WindowContext or InputMethodService"); - if (!context.isUiContext()) { - throw exception; - } - if (context.getAssociatedDisplayId() == INVALID_DISPLAY) { - // This is to identify if #isUiContext of a non-UI Context is overridden. - // #isUiContext is more likely to be overridden than #getAssociatedDisplayId - // since #isUiContext is a public API. - StrictMode.onIncorrectContextUsed("The registered Context is a UI Context " - + "but not associated with any display. " - + "This Context may not receive any WindowLayoutInfo update. " - + dumpAllBaseContextToString(context), exception); - } + assertUiContext(context); Log.d(TAG, "Register WindowLayoutInfoListener on " + dumpAllBaseContextToString(context)); mFoldingFeatureProducer.getData((features) -> { @@ -339,6 +349,7 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { @Override @NonNull public WindowLayoutInfo getCurrentWindowLayoutInfo(@NonNull @UiContext Context context) { + assertUiContext(context); synchronized (mLock) { return getWindowLayoutInfo(context, mLastReportedFoldingFeatures); } @@ -353,6 +364,25 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { return mSupportedWindowFeatures; } + private void assertUiContext(@NonNull Context context) { + final IllegalArgumentException exception = new IllegalArgumentException( + "Context must be a UI Context with display association, which should be " + + "an Activity, WindowContext or InputMethodService"); + if (!context.isUiContext()) { + throw exception; + } + if (context.getAssociatedDisplayId() == INVALID_DISPLAY) { + // This is to identify if #isUiContext of a non-UI Context is overridden. + // #isUiContext is more likely to be overridden than #getAssociatedDisplayId + // since #isUiContext is a public API. + StrictMode.onIncorrectContextUsed("The given context is a UI context, " + + "but it is not associated with any display. " + + "This context may not receive WindowLayoutInfo updates and " + + "may get an empty WindowLayoutInfo return value. " + + dumpAllBaseContextToString(context), exception); + } + } + /** @see #getWindowLayoutInfo(Context, List) */ private WindowLayoutInfo getWindowLayoutInfo(int displayId, @NonNull WindowConfiguration windowConfiguration, @@ -401,15 +431,16 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { // We will transform the feature bounds to the Activity window, so using the rotation // from the same source (WindowConfiguration) to make sure they are synchronized. - final int rotation = windowConfiguration.getDisplayRotation(); + final int rotation = mDisplayStateProvider.getDisplayRotation(windowConfiguration); + final DisplayInfo displayInfo = mDisplayStateProvider.getDisplayInfo(displayId); for (CommonFoldingFeature baseFeature : storedFeatures) { Integer state = convertToExtensionState(baseFeature.getState()); if (state == null) { continue; } - Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, rotation, featureRect); + final Rect featureRect = baseFeature.getRect(); + rotateRectToDisplayRotation(displayInfo, rotation, featureRect); transformToWindowSpaceRect(windowConfiguration, featureRect); if (isZero(featureRect)) { @@ -530,4 +561,13 @@ public class WindowLayoutComponentImpl implements WindowLayoutComponent { public void onLowMemory() { } } + + @VisibleForTesting + interface DisplayStateProvider { + @Surface.Rotation + int getDisplayRotation(@NonNull WindowConfiguration windowConfiguration); + + @NonNull + DisplayInfo getDisplayInfo(int displayId); + } } diff --git a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java index 6e0e7115cfb1..3b30f1b591c0 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/sidecar/SidecarHelper.java @@ -24,7 +24,9 @@ import android.annotation.NonNull; import android.app.Activity; import android.app.ActivityThread; import android.graphics.Rect; +import android.hardware.display.DisplayManagerGlobal; import android.os.IBinder; +import android.view.DisplayInfo; import androidx.window.common.layout.CommonFoldingFeature; @@ -96,13 +98,18 @@ class SidecarHelper { return Collections.emptyList(); } - final List<SidecarDisplayFeature> features = new ArrayList<>(); + // We will transform the feature bounds to the Activity window, so using the rotation + // from the same source (WindowConfiguration) to make sure they are synchronized. final int rotation = activity.getResources().getConfiguration().windowConfiguration .getDisplayRotation(); + final DisplayInfo displayInfo = + DisplayManagerGlobal.getInstance().getDisplayInfo(displayId); + + final List<SidecarDisplayFeature> features = new ArrayList<>(); for (CommonFoldingFeature baseFeature : featureList) { final SidecarDisplayFeature feature = new SidecarDisplayFeature(); final Rect featureRect = baseFeature.getRect(); - rotateRectToDisplayRotation(displayId, rotation, featureRect); + rotateRectToDisplayRotation(displayInfo, rotation, featureRect); transformToWindowSpaceRect(activity, featureRect); feature.setRect(featureRect); feature.setType(baseFeature.getType()); diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java index ed4eddf7c209..0643febb79bf 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/layout/WindowLayoutComponentImplTest.java @@ -26,6 +26,8 @@ import android.content.ContextWrapper; import android.graphics.Rect; import android.platform.test.annotations.Presubmit; import android.view.Display; +import android.view.DisplayInfo; +import android.view.Surface; import androidx.annotation.NonNull; import androidx.test.core.app.ApplicationProvider; @@ -72,6 +74,11 @@ public class WindowLayoutComponentImplTest { mWindowLayoutComponent.onDisplayFeaturesChanged(Collections.emptyList()); } + @Test(expected = IllegalArgumentException.class) + public void testAddWindowLayoutListener_nonUiContext_throwsError() { + mWindowLayoutComponent.addWindowLayoutInfoListener(mAppContext, info -> {}); + } + @Test public void testGetCurrentWindowLayoutInfo_noFoldingFeature_returnsEmptyList() { final Context testUiContext = new TestUiContext(mAppContext); @@ -88,6 +95,29 @@ public class WindowLayoutComponentImplTest { final WindowConfiguration windowConfiguration = testUiContext.getResources().getConfiguration().windowConfiguration; final Rect featureRect = windowConfiguration.getBounds(); + // Mock DisplayStateProvider to control rotation and DisplayInfo, preventing dependency on + // the real device orientation or display configuration. This improves test reliability on + // devices like foldables or tablets that might have varying configurations. + final WindowLayoutComponentImpl.DisplayStateProvider displayStateProvider = + new WindowLayoutComponentImpl.DisplayStateProvider() { + @Override + public int getDisplayRotation( + @NonNull WindowConfiguration windowConfiguration) { + return Surface.ROTATION_0; + } + + @NonNull + @Override + public DisplayInfo getDisplayInfo(int displayId) { + final DisplayInfo displayInfo = new DisplayInfo(); + displayInfo.logicalWidth = featureRect.width(); + displayInfo.logicalHeight = featureRect.height(); + return displayInfo; + } + }; + mWindowLayoutComponent = new WindowLayoutComponentImpl(mAppContext, + mock(DeviceStateManagerFoldingFeatureProducer.class), + displayStateProvider); final CommonFoldingFeature foldingFeature = new CommonFoldingFeature( CommonFoldingFeature.COMMON_TYPE_HINGE, CommonFoldingFeature.COMMON_STATE_FLAT, @@ -102,12 +132,9 @@ public class WindowLayoutComponentImplTest { featureRect, FoldingFeature.TYPE_HINGE, FoldingFeature.STATE_FLAT)); } - @Test - public void testGetCurrentWindowLayoutInfo_nonUiContext_returnsEmptyList() { - final WindowLayoutInfo layoutInfo = - mWindowLayoutComponent.getCurrentWindowLayoutInfo(mAppContext); - - assertThat(layoutInfo.getDisplayFeatures()).isEmpty(); + @Test(expected = IllegalArgumentException.class) + public void testGetCurrentWindowLayoutInfo_nonUiContext_throwsError() { + mWindowLayoutComponent.getCurrentWindowLayoutInfo(mAppContext); } /** diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp index 61c09f2c396b..7f54c75929fb 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/Android.bp @@ -22,42 +22,6 @@ package { default_team: "trendy_team_multitasking_windowing", } -android_app { - name: "WMShellRobolectricScreenshotTestApp", - platform_apis: true, - certificate: "platform", - static_libs: [ - "WindowManager-Shell", - "platform-screenshot-diff-core", - "ScreenshotComposeUtilsLib", // ComposableScreenshotTestRule & Theme.PlatformUi.Screenshot - "SystemUI-res", // Theme.SystemUI (dragged in by ScreenshotComposeUtilsLib) - ], - asset_dirs: ["goldens/robolectric"], - manifest: "AndroidManifestRobolectric.xml", - use_resource_processor: true, -} - -android_robolectric_test { - name: "WMShellRobolectricScreenshotTests", - instrumentation_for: "WMShellRobolectricScreenshotTestApp", - upstream: true, - java_resource_dirs: [ - "robolectric/config", - ], - srcs: [ - "src/**/*.kt", - ], - static_libs: [ - "junit", - "androidx.test.runner", - "androidx.test.rules", - "androidx.test.ext.junit", - "truth", - "platform-parametric-runner-lib", - ], - auto_gen_config: true, -} - android_test { name: "WMShellMultivalentScreenshotTestsOnDevice", srcs: [ diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png Binary files differindex 5b429c0eaf7c..15af624b8609 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png Binary files differindex 6028fa21a8fd..85ce24bea51a 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/onDevice/phone/light_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png Binary files differindex e540b455028b..a1d0e7ba9453 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/dark_portrait_bubbles_education.png diff --git a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png Binary files differindex e540b455028b..3bc2ae7dfe73 100644 --- a/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png +++ b/libs/WindowManager/Shell/multivalentScreenshotTests/goldens/robolectric/phone/light_portrait_bubbles_education.png 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 52ce8cb5c0dc..0b515f590f98 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 @@ -23,14 +23,15 @@ import android.content.res.Resources import android.graphics.Color import android.graphics.drawable.Icon import android.os.UserHandle +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.platform.test.flag.junit.SetFlagsRule -import android.view.IWindowManager import android.view.WindowManager -import android.view.WindowManagerGlobal import androidx.test.core.app.ApplicationProvider import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import androidx.test.platform.app.InstrumentationRegistry +import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn import com.android.internal.logging.testing.UiEventLoggerFake import com.android.internal.protolog.ProtoLog import com.android.launcher3.icons.BubbleIconFactory @@ -41,6 +42,7 @@ import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix import com.android.wm.shell.common.FloatingContentCoordinator import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.shared.animation.PhysicsAnimatorTestUtils +import com.android.wm.shell.shared.bubbles.BubbleBarLocation import com.android.wm.shell.taskview.TaskView import com.android.wm.shell.taskview.TaskViewTaskController import com.google.common.truth.Truth.assertThat @@ -51,9 +53,7 @@ import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.mock -import android.platform.test.annotations.DisableFlags -import android.platform.test.annotations.EnableFlags -import com.android.wm.shell.shared.bubbles.BubbleBarLocation +import org.mockito.kotlin.verify import java.util.concurrent.Semaphore import java.util.concurrent.TimeUnit import java.util.function.Consumer @@ -72,7 +72,7 @@ class BubbleStackViewTest { private lateinit var expandedViewManager: FakeBubbleExpandedViewManager private lateinit var bubbleStackView: BubbleStackView private lateinit var shellExecutor: ShellExecutor - private lateinit var windowManager: IWindowManager + private lateinit var windowManager: WindowManager private lateinit var bubbleTaskViewFactory: BubbleTaskViewFactory private lateinit var bubbleData: BubbleData private lateinit var bubbleStackViewManager: FakeBubbleStackViewManager @@ -83,9 +83,8 @@ class BubbleStackViewTest { PhysicsAnimatorTestUtils.prepareForTest() // Disable protolog tool when running the tests from studio ProtoLog.REQUIRE_PROTOLOGTOOL = false - windowManager = WindowManagerGlobal.getWindowManagerService()!! shellExecutor = TestShellExecutor() - val windowManager = context.getSystemService(WindowManager::class.java) + windowManager = context.getSystemService(WindowManager::class.java) iconFactory = BubbleIconFactory( context, @@ -354,6 +353,16 @@ class BubbleStackViewTest { assertThat(bubbleStackView.getBubbleIndex(bubbleOverflow)).isGreaterThan(-1) } + @Test + fun removeFromWindow_stopMonitoringSwipeUpGesture() { + spyOn(bubbleStackView) + InstrumentationRegistry.getInstrumentation().runOnMainSync { + // No way to add to window in the test environment right now so just pretend + bubbleStackView.onDetachedFromWindow() + } + verify(bubbleStackView).stopMonitoringSwipeUpGesture() + } + private fun createAndInflateChatBubble(key: String): Bubble { val icon = Icon.createWithResource(context.resources, R.drawable.bubble_ic_overflow_button) val shortcutInfo = ShortcutInfo.Builder(context, "fakeId").setIcon(icon).build() 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 new file mode 100644 index 000000000000..cb6fb62bf89b --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/FakeBubbleFactory.kt @@ -0,0 +1,57 @@ +/* + * 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.bubbles + +import android.content.Context +import android.content.pm.ShortcutInfo +import android.content.res.Resources +import com.android.wm.shell.bubbles.BubbleViewInfoTask.BubbleViewInfo +import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView +import com.google.common.util.concurrent.MoreExecutors.directExecutor + +/** Helper to create a [Bubble] instance */ +class FakeBubbleFactory { + + companion object { + + fun createViewInfo(bubbleExpandedView: BubbleBarExpandedView): BubbleViewInfo { + return BubbleViewInfo().apply { bubbleBarExpandedView = bubbleExpandedView } + } + + fun createChatBubbleWithViewInfo( + context: Context, + key: String = "key", + viewInfo: BubbleViewInfo, + ): Bubble { + val bubble = + Bubble( + key, + ShortcutInfo.Builder(context, "id").build(), + 100, /* desiredHeight */ + Resources.ID_NULL, /* desiredHeightResId */ + "title", + 0, /* taskId */ + null, /* locus */ + true, /* isDismissable */ + directExecutor(), + directExecutor(), + ) {} + bubble.setViewInfo(viewInfo) + return bubble + } + } +} diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrixTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrixTest.kt new file mode 100644 index 000000000000..3ed360461c25 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrixTest.kt @@ -0,0 +1,53 @@ +/* + * 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.bubbles.animation + +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.google.common.truth.Truth.assertThat +import org.junit.Test +import org.junit.runner.RunWith + +/** Tests for [AnimatableScaleMatrix] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class AnimatableScaleMatrixTest { + + @Test + fun test_equals_matricesWithSameValuesAreNotEqual() { + val matrix1 = AnimatableScaleMatrix().apply { setScale(0.5f, 0.5f) } + val matrix2 = AnimatableScaleMatrix().apply { setScale(0.5f, 0.5f) } + assertThat(matrix1).isNotEqualTo(matrix2) + } + + @Test + fun test_hashCode_remainsSameIfMatrixUpdates() { + val matrix = AnimatableScaleMatrix().apply { setScale(0.5f, 0.5f) } + val hash1 = matrix.hashCode() + matrix.setScale(0.75f, 0.75f) + val hash2 = matrix.hashCode() + + assertThat(hash1).isEqualTo(hash2) + } + + @Test + fun test_hashCode_matricesWithSameValuesHaveDiffHashCode() { + val matrix1 = AnimatableScaleMatrix().apply { setScale(0.5f, 0.5f) } + val matrix2 = AnimatableScaleMatrix().apply { setScale(0.5f, 0.5f) } + assertThat(matrix1.hashCode()).isNotEqualTo(matrix2.hashCode()) + } +} 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 new file mode 100644 index 000000000000..2fbf089d99d6 --- /dev/null +++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt @@ -0,0 +1,293 @@ +/* + * 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.bubbles.bar + +import android.app.ActivityManager +import android.content.Context +import android.content.pm.LauncherApps +import android.os.Handler +import android.os.UserManager +import android.view.IWindowManager +import android.view.LayoutInflater +import android.view.View +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.WindowManagerShellWrapper +import com.android.wm.shell.bubbles.Bubble +import com.android.wm.shell.bubbles.BubbleController +import com.android.wm.shell.bubbles.BubbleData +import com.android.wm.shell.bubbles.BubbleDataRepository +import com.android.wm.shell.bubbles.BubbleEducationController +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.BubbleTaskView +import com.android.wm.shell.bubbles.BubbleTaskViewFactory +import com.android.wm.shell.bubbles.Bubbles.SysuiProxy +import com.android.wm.shell.bubbles.FakeBubbleFactory +import com.android.wm.shell.bubbles.UiEventSubject.Companion.assertThat +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.DisplayInsetsController +import com.android.wm.shell.common.FloatingContentCoordinator +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.common.SyncTransactionQueue +import com.android.wm.shell.common.TaskStackListenerImpl +import com.android.wm.shell.shared.TransactionPool +import com.android.wm.shell.shared.bubbles.BubbleBarLocation +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.TaskView +import com.android.wm.shell.taskview.TaskViewTaskController +import com.android.wm.shell.taskview.TaskViewTransitions +import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat +import java.util.Collections +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.mock +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +/** Tests for [BubbleBarLayerView] */ +@SmallTest +@RunWith(AndroidJUnit4::class) +class BubbleBarLayerViewTest { + + private val context = ApplicationProvider.getApplicationContext<Context>() + + private lateinit var bubbleBarLayerView: BubbleBarLayerView + + private lateinit var uiEventLoggerFake: UiEventLoggerFake + + private lateinit var bubble: Bubble + + @Before + fun setUp() { + ProtoLog.REQUIRE_PROTOLOGTOOL = false + ProtoLog.init() + + uiEventLoggerFake = UiEventLoggerFake() + val bubbleLogger = BubbleLogger(uiEventLoggerFake) + + val mainExecutor = TestExecutor() + val bgExecutor = TestExecutor() + + val windowManager = context.getSystemService(WindowManager::class.java) + + val bubblePositioner = BubblePositioner(context, windowManager) + bubblePositioner.setShowingInBubbleBar(true) + + val bubbleData = + BubbleData( + context, + bubbleLogger, + bubblePositioner, + BubbleEducationController(context), + mainExecutor, + bgExecutor, + ) + + val bubbleController = + createBubbleController( + bubbleData, + windowManager, + bubbleLogger, + bubblePositioner, + mainExecutor, + bgExecutor, + ) + bubbleController.asBubbles().setSysuiProxy(mock(SysuiProxy::class.java)) + // Flush so that proxy gets set + mainExecutor.flushAll() + + bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData, bubbleLogger) + + val expandedViewManager = createExpandedViewManager() + val bubbleTaskView = FakeBubbleTaskViewFactory(mainExecutor).create() + val bubbleBarExpandedView = + (LayoutInflater.from(context) + .inflate(R.layout.bubble_bar_expanded_view, null, false /* attachToRoot */) + as BubbleBarExpandedView) + .apply { + initialize( + expandedViewManager, + bubblePositioner, + bubbleLogger, + false /* isOverflow */, + bubbleTaskView, + mainExecutor, + bgExecutor, + null, /* regionSamplingProvider */ + ) + } + + val viewInfo = FakeBubbleFactory.createViewInfo(bubbleBarExpandedView) + bubble = FakeBubbleFactory.createChatBubbleWithViewInfo(context, viewInfo = viewInfo) + } + + private fun createBubbleController( + bubbleData: BubbleData, + windowManager: WindowManager?, + bubbleLogger: BubbleLogger, + bubblePositioner: BubblePositioner, + mainExecutor: TestExecutor, + bgExecutor: TestExecutor, + ): BubbleController { + val shellInit = ShellInit(mainExecutor) + val shellCommandHandler = ShellCommandHandler() + val shellController = + ShellController( + context, + shellInit, + shellCommandHandler, + mock<DisplayInsetsController>(), + mainExecutor, + ) + val surfaceSynchronizer = { obj: Runnable -> obj.run() } + + val bubbleDataRepository = + BubbleDataRepository( + mock<LauncherApps>(), + mainExecutor, + bgExecutor, + BubblePersistentRepository(context), + ) + + return BubbleController( + context, + shellInit, + shellCommandHandler, + shellController, + bubbleData, + surfaceSynchronizer, + FloatingContentCoordinator(), + bubbleDataRepository, + mock<IStatusBarService>(), + windowManager, + WindowManagerShellWrapper(mainExecutor), + mock<UserManager>(), + mock<LauncherApps>(), + bubbleLogger, + mock<TaskStackListenerImpl>(), + mock<ShellTaskOrganizer>(), + bubblePositioner, + mock<DisplayController>(), + null, + null, + mainExecutor, + mock<Handler>(), + bgExecutor, + mock<TaskViewTransitions>(), + mock<Transitions>(), + SyncTransactionQueue(TransactionPool(), mainExecutor), + mock<IWindowManager>(), + mock<BubbleProperties>(), + ) + } + + @Test + fun testEventLogging_dismissExpandedViewViaDrag() { + getInstrumentation().runOnMainSync { bubbleBarLayerView.showExpandedView(bubble) } + assertThat(bubbleBarLayerView.findViewById<View>(R.id.bubble_bar_handle_view)).isNotNull() + + bubbleBarLayerView.dragController?.dragListener?.onReleased(true) + + assertThat(uiEventLoggerFake.numLogs()).isEqualTo(1) + assertThat(uiEventLoggerFake.logs[0].eventId) + .isEqualTo(BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_DRAG_EXP_VIEW.id) + assertThat(uiEventLoggerFake.logs[0]).hasBubbleInfo(bubble) + } + + private inner class FakeBubbleTaskViewFactory(private val mainExecutor: ShellExecutor) : + BubbleTaskViewFactory { + override fun create(): BubbleTaskView { + val taskViewTaskController = mock<TaskViewTaskController>() + val taskView = TaskView(context, taskViewTaskController) + val taskInfo = mock<ActivityManager.RunningTaskInfo>() + whenever(taskViewTaskController.taskInfo).thenReturn(taskInfo) + return BubbleTaskView(taskView, mainExecutor) + } + } + + private fun createExpandedViewManager(): BubbleExpandedViewManager { + return object : BubbleExpandedViewManager { + override val overflowBubbles: List<Bubble> + get() = Collections.emptyList() + + override fun setOverflowListener(listener: BubbleData.Listener) {} + + override fun collapseStack() {} + + override fun updateWindowFlagsForBackpress(intercept: Boolean) {} + + override fun promoteBubbleFromOverflow(bubble: Bubble) {} + + override fun removeBubble(key: String, reason: Int) {} + + override fun dismissBubble(bubble: Bubble, reason: Int) {} + + override fun setAppBubbleTaskId(key: String, taskId: Int) {} + + override fun isStackExpanded(): Boolean { + return true + } + + override fun isShowingAsBubbleBar(): Boolean { + return true + } + + override fun hideCurrentInputMethod() {} + + override fun updateBubbleBarLocation(location: BubbleBarLocation) {} + } + } + + private class TestExecutor : ShellExecutor { + + private val runnables: MutableList<Runnable> = mutableListOf() + + override fun execute(runnable: Runnable) { + runnables.add(runnable) + } + + override fun executeDelayed(runnable: Runnable, delayMillis: Long) { + execute(runnable) + } + + override fun removeCallbacks(runnable: Runnable?) {} + + override fun hasCallback(runnable: Runnable?): Boolean = false + + fun flushAll() { + while (runnables.isNotEmpty()) { + runnables.removeAt(0).run() + } + } + } +} diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml index 4a42616a45ec..4c7d1c7339fb 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_manage_education.xml @@ -22,7 +22,6 @@ android:layout_gravity="center_horizontal" android:layout_marginTop="@dimen/bubble_popup_margin_top" android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal" - android:layout_marginBottom="@dimen/bubble_popup_margin_bottom" android:elevation="@dimen/bubble_popup_elevation" android:gravity="center_horizontal" android:orientation="vertical"> @@ -30,7 +29,7 @@ <ImageView android:layout_width="@dimen/bubble_popup_icon_size" android:layout_height="@dimen/bubble_popup_icon_size" - android:tint="?androidprv:attr/materialColorPrimary" + android:tint="?androidprv:attr/materialColorOutline" android:contentDescription="@null" android:src="@drawable/pip_ic_settings"/> @@ -49,6 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" + android:paddingBottom="@dimen/bubble_popup_padding_bottom" android:maxWidth="@dimen/bubble_popup_content_max_width" android:textAppearance="@android:style/TextAppearance.DeviceDefault" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" diff --git a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml index f19c3c762d9d..345c399652f9 100644 --- a/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml +++ b/libs/WindowManager/Shell/res/layout/bubble_bar_stack_education.xml @@ -22,7 +22,6 @@ android:layout_gravity="bottom|end" android:layout_marginTop="@dimen/bubble_popup_margin_top" android:layout_marginHorizontal="@dimen/bubble_popup_margin_horizontal" - android:layout_marginBottom="@dimen/bubble_popup_margin_bottom" android:elevation="@dimen/bubble_popup_elevation" android:gravity="center_horizontal" android:orientation="vertical"> @@ -49,6 +48,7 @@ android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginTop="@dimen/bubble_popup_text_margin" + android:paddingBottom="@dimen/bubble_popup_padding_bottom" android:maxWidth="@dimen/bubble_popup_content_max_width" android:textAppearance="@android:style/TextAppearance.DeviceDefault" android:textColor="?androidprv:attr/materialColorOnSurfaceVariant" diff --git a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml index 62782a784db9..e7ead63a8df6 100644 --- a/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml +++ b/libs/WindowManager/Shell/res/layout/compat_ui_layout.xml @@ -22,14 +22,14 @@ android:gravity="bottom|end"> <include android:id="@+id/size_compat_hint" - android:visibility="gone" + android:visibility="invisible" android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/size_compat_restart_button" - android:visibility="gone" + android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/compat_button_margin" diff --git a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml index 433d8546ece0..b5f04c3b815a 100644 --- a/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml +++ b/libs/WindowManager/Shell/res/layout/user_aspect_ratio_settings_layout.xml @@ -22,14 +22,14 @@ android:gravity="bottom|end"> <include android:id="@+id/user_aspect_ratio_settings_hint" - android:visibility="gone" + android:visibility="invisible" android:layout_width="@dimen/compat_hint_width" android:layout_height="wrap_content" layout="@layout/compat_mode_hint"/> <ImageButton android:id="@+id/user_aspect_ratio_settings_button" - android:visibility="gone" + android:visibility="invisible" android:layout_width="wrap_content" android:layout_height="wrap_content" android:layout_marginEnd="@dimen/compat_button_margin" diff --git a/libs/WindowManager/Shell/res/values-af/strings.xml b/libs/WindowManager/Shell/res/values-af/strings.xml index b29e8bf5e922..95c2bb59d202 100644 --- a/libs/WindowManager/Shell/res/values-af/strings.xml +++ b/libs/WindowManager/Shell/res/values-af/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimeer skerm"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gryp skerm vas"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App kan nie hierheen geskuif word nie"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimeer"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-am/strings.xml b/libs/WindowManager/Shell/res/values-am/strings.xml index d96033f286f8..ba74e342f353 100644 --- a/libs/WindowManager/Shell/res/values-am/strings.xml +++ b/libs/WindowManager/Shell/res/values-am/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"የማያ ገጹ መጠን አሳድግ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ማያ ገጹን አሳድግ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"መተግበሪያ ወደዚህ መንቀሳቀስ አይችልም"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-ar/strings.xml b/libs/WindowManager/Shell/res/values-ar/strings.xml index 42ef4c307b1e..a8febc80ffc1 100644 --- a/libs/WindowManager/Shell/res/values-ar/strings.xml +++ b/libs/WindowManager/Shell/res/values-ar/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"تكبير الشاشة إلى أقصى حدّ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"التقاط صورة للشاشة"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-as/strings.xml b/libs/WindowManager/Shell/res/values-as/strings.xml index 8e9c7045118f..8c924e342875 100644 --- a/libs/WindowManager/Shell/res/values-as/strings.xml +++ b/libs/WindowManager/Shell/res/values-as/strings.xml @@ -133,9 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্ৰীন মেক্সিমাইজ কৰক"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্ৰীন স্নেপ কৰক"</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> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"মেক্সিমাইজ কৰক"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> - <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"ডিফ’ল্ট ছেটিং খোলক"</string> diff --git a/libs/WindowManager/Shell/res/values-az/strings.xml b/libs/WindowManager/Shell/res/values-az/strings.xml index 1310f6f743f4..aa232e330604 100644 --- a/libs/WindowManager/Shell/res/values-az/strings.xml +++ b/libs/WindowManager/Shell/res/values-az/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı maksimum böyüdün"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranı çəkin"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Tətbiqi bura köçürmək mümkün deyil"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Böyüdün"</string> <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> 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 b9c42397f8c7..256344a4cb31 100644 --- a/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml +++ b/libs/WindowManager/Shell/res/values-b+sr+Latn/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Povećaj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Uklopi ekran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacija ne može da se premesti ovde"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Uvećajte"</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> diff --git a/libs/WindowManager/Shell/res/values-be/strings.xml b/libs/WindowManager/Shell/res/values-be/strings.xml index 5d389d849433..701c51091aa4 100644 --- a/libs/WindowManager/Shell/res/values-be/strings.xml +++ b/libs/WindowManager/Shell/res/values-be/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Разгарнуць на ўвесь экран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Размясціць на палавіне экрана"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Нельга перамясціць сюды праграму"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-bg/strings.xml b/libs/WindowManager/Shell/res/values-bg/strings.xml index 807878620d90..9ab86f4cbc56 100644 --- a/libs/WindowManager/Shell/res/values-bg/strings.xml +++ b/libs/WindowManager/Shell/res/values-bg/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Увеличаване на екрана"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Прилепване на екрана"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Приложението не може да бъде преместено тук"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-bn/strings.xml b/libs/WindowManager/Shell/res/values-bn/strings.xml index 8db7144e6db6..22a445f1754c 100644 --- a/libs/WindowManager/Shell/res/values-bn/strings.xml +++ b/libs/WindowManager/Shell/res/values-bn/strings.xml @@ -133,9 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"স্ক্রিন বড় করুন"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"স্ক্রিনে অ্যাপ মানানসই হিসেবে ছোট বড় করুন"</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> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"বড় করুন"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> - <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"ডিফল্ট হিসেবে থাকা সেটিংস খুলুন"</string> diff --git a/libs/WindowManager/Shell/res/values-bs/strings.xml b/libs/WindowManager/Shell/res/values-bs/strings.xml index 3d922d8334c8..73f30d797883 100644 --- a/libs/WindowManager/Shell/res/values-bs/strings.xml +++ b/libs/WindowManager/Shell/res/values-bs/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiziraj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snimi ekran"</string> <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">"Interaktivno"</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">"Maksimiziranje"</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> diff --git a/libs/WindowManager/Shell/res/values-ca/strings.xml b/libs/WindowManager/Shell/res/values-ca/strings.xml index dc96cd0f72ec..499ed329e511 100644 --- a/libs/WindowManager/Shell/res/values-ca/strings.xml +++ b/libs/WindowManager/Shell/res/values-ca/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximitza la pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajusta la pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"L\'aplicació no es pot moure aquí"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximitza"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-cs/strings.xml b/libs/WindowManager/Shell/res/values-cs/strings.xml index 30ba78ad8e06..6a5780e01822 100644 --- a/libs/WindowManager/Shell/res/values-cs/strings.xml +++ b/libs/WindowManager/Shell/res/values-cs/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovat obrazovku"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Rozpůlit obrazovku"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikaci sem nelze přesunout"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Pohlcující"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Obnovit"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximalizovat"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-da/strings.xml b/libs/WindowManager/Shell/res/values-da/strings.xml index 433b858cfd84..430cf96cd72f 100644 --- a/libs/WindowManager/Shell/res/values-da/strings.xml +++ b/libs/WindowManager/Shell/res/values-da/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimér skærm"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tilpas skærm"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Apps kan ikke flyttes hertil"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimér"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-de/strings.xml b/libs/WindowManager/Shell/res/values-de/strings.xml index 0b4d189716c2..cafaa89f57e3 100644 --- a/libs/WindowManager/Shell/res/values-de/strings.xml +++ b/libs/WindowManager/Shell/res/values-de/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Bildschirm maximieren"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Bildschirm teilen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Die App kann nicht hierher verschoben werden"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximieren"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximieren"</string> + <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="open_by_default_settings_text" msgid="2526548548598185500">"Einstellungen für die Option „Standardmäßig öffnen“"</string> diff --git a/libs/WindowManager/Shell/res/values-el/strings.xml b/libs/WindowManager/Shell/res/values-el/strings.xml index beff019e6897..d02fae2a986d 100644 --- a/libs/WindowManager/Shell/res/values-el/strings.xml +++ b/libs/WindowManager/Shell/res/values-el/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Μεγιστοποίηση οθόνης"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Προβολή στο μισό της οθόνης"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml index 01d3d25e7e4f..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rAU/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rAU/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml index 20ec076a33d1..2d123ec3a3d4 100644 --- a/libs/WindowManager/Shell/res/values-en-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rCA/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximize Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap Screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximize"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml index 01d3d25e7e4f..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rGB/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rGB/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml index 01d3d25e7e4f..f9911451f4b5 100644 --- a/libs/WindowManager/Shell/res/values-en-rIN/strings.xml +++ b/libs/WindowManager/Shell/res/values-en-rIN/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximise screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Snap screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"App can\'t be moved here"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximise"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml index c9e7ecbb056e..210b708b49af 100644 --- a/libs/WindowManager/Shell/res/values-es-rUS/strings.xml +++ b/libs/WindowManager/Shell/res/values-es-rUS/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"No se puede mover la app aquí"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-es/strings.xml b/libs/WindowManager/Shell/res/values-es/strings.xml index 903294bbd1cf..3c7bfe5a3cb4 100644 --- a/libs/WindowManager/Shell/res/values-es/strings.xml +++ b/libs/WindowManager/Shell/res/values-es/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"La aplicación no se puede mover aquí"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-et/strings.xml b/libs/WindowManager/Shell/res/values-et/strings.xml index f6bebff9a4f3..d17ee02a3a7f 100644 --- a/libs/WindowManager/Shell/res/values-et/strings.xml +++ b/libs/WindowManager/Shell/res/values-et/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Kuva täisekraanil"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Kuva poolel ekraanil"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Rakendust ei saa siia teisaldada"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimeeri"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-eu/strings.xml b/libs/WindowManager/Shell/res/values-eu/strings.xml index 267452f2e6e1..f9419bc4614b 100644 --- a/libs/WindowManager/Shell/res/values-eu/strings.xml +++ b/libs/WindowManager/Shell/res/values-eu/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Handitu pantaila"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zatitu pantaila"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikazioa ezin da hona ekarri"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizatu"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-fa/strings.xml b/libs/WindowManager/Shell/res/values-fa/strings.xml index ec4779b0a129..a3d3cbc872fd 100644 --- a/libs/WindowManager/Shell/res/values-fa/strings.xml +++ b/libs/WindowManager/Shell/res/values-fa/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"بزرگ کردن صفحه"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"بزرگ کردن صفحه"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"برنامه را نمیتوان به اینجا منتقل کرد"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-fi/strings.xml b/libs/WindowManager/Shell/res/values-fi/strings.xml index 6dcd20cc0227..ee5dd6516098 100644 --- a/libs/WindowManager/Shell/res/values-fi/strings.xml +++ b/libs/WindowManager/Shell/res/values-fi/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Suurenna näyttö"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Jaa näyttö"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Sovellusta ei voi siirtää tänne"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Suurenna"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml index df6d503c2121..dc4789169146 100644 --- a/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr-rCA/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Agrandir l\'écran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aligner l\'écran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Impossible de déplacer l\'appli ici"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Agrandir"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-fr/strings.xml b/libs/WindowManager/Shell/res/values-fr/strings.xml index 3a7f2f0bd4fa..a52ab49da3ab 100644 --- a/libs/WindowManager/Shell/res/values-fr/strings.xml +++ b/libs/WindowManager/Shell/res/values-fr/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mettre en plein écran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fractionner l\'écran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Impossible de déplacer l\'appli ici"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Agrandir"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-gl/strings.xml b/libs/WindowManager/Shell/res/values-gl/strings.xml index cb7bb324de27..97d5e51e5b98 100644 --- a/libs/WindowManager/Shell/res/values-gl/strings.xml +++ b/libs/WindowManager/Shell/res/values-gl/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar pantalla"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar pantalla"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Non se pode mover aquí a aplicación"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-gu/strings.xml b/libs/WindowManager/Shell/res/values-gu/strings.xml index bee06fed5fd1..362ff8d874bb 100644 --- a/libs/WindowManager/Shell/res/values-gu/strings.xml +++ b/libs/WindowManager/Shell/res/values-gu/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"સ્ક્રીન કરો મોટી કરો"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"સ્ક્રીન સ્નૅપ કરો"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ઍપ અહીં ખસેડી શકાતી નથી"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"મોટું કરો"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"\'ડિફૉલ્ટ તરીકે ખોલો\' સેટિંગ"</string> diff --git a/libs/WindowManager/Shell/res/values-hi/strings.xml b/libs/WindowManager/Shell/res/values-hi/strings.xml index 2c141996b109..527793eac9c3 100644 --- a/libs/WindowManager/Shell/res/values-hi/strings.xml +++ b/libs/WindowManager/Shell/res/values-hi/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन को बड़ा करें"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्नैप स्क्रीन"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-hr/strings.xml b/libs/WindowManager/Shell/res/values-hr/strings.xml index a2a52d13b463..659d1ec39b73 100644 --- a/libs/WindowManager/Shell/res/values-hr/strings.xml +++ b/libs/WindowManager/Shell/res/values-hr/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimalno povećaj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Izradi snimku zaslona"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacija se ne može premjestiti ovdje"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Interaktivno"</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">"Maksimiziraj"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-hu/strings.xml b/libs/WindowManager/Shell/res/values-hu/strings.xml index 03cc9f569d29..943b5eb30768 100644 --- a/libs/WindowManager/Shell/res/values-hu/strings.xml +++ b/libs/WindowManager/Shell/res/values-hu/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Képernyő méretének maximalizálása"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Igazodás a képernyő adott részéhez"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Az alkalmazás nem helyezhető át ide"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Teljes méret"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-hy/strings.xml b/libs/WindowManager/Shell/res/values-hy/strings.xml index 28c762eb0752..6bcfc9a22d6e 100644 --- a/libs/WindowManager/Shell/res/values-hy/strings.xml +++ b/libs/WindowManager/Shell/res/values-hy/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ծավալել էկրանը"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ծալել էկրանը"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Հավելվածը հնարավոր չէ տեղափոխել այստեղ"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-in/strings.xml b/libs/WindowManager/Shell/res/values-in/strings.xml index 4929b79875a0..96a3ebce9a45 100644 --- a/libs/WindowManager/Shell/res/values-in/strings.xml +++ b/libs/WindowManager/Shell/res/values-in/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Perbesar Layar"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Gabungkan Layar"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikasi tidak dapat dipindahkan ke sini"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimalkan"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-is/strings.xml b/libs/WindowManager/Shell/res/values-is/strings.xml index d7ba53f73926..ca1bc15edf33 100644 --- a/libs/WindowManager/Shell/res/values-is/strings.xml +++ b/libs/WindowManager/Shell/res/values-is/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Stækka skjá"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Smelluskjár"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ekki er hægt að færa forritið hingað"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Umlykjandi"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Endurheimta"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Stækka"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-it/strings.xml b/libs/WindowManager/Shell/res/values-it/strings.xml index 4522d37d289a..87919b5dc1ff 100644 --- a/libs/WindowManager/Shell/res/values-it/strings.xml +++ b/libs/WindowManager/Shell/res/values-it/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Massimizza schermo"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Aggancia schermo"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Impossibile spostare l\'app qui"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersivo"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Ripristina"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ingrandisci"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-iw/strings.xml b/libs/WindowManager/Shell/res/values-iw/strings.xml index 110d357d0cb2..17ffe8ee7600 100644 --- a/libs/WindowManager/Shell/res/values-iw/strings.xml +++ b/libs/WindowManager/Shell/res/values-iw/strings.xml @@ -133,12 +133,16 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"הגדלת המסך"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"כיווץ המסך"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"לא ניתן להעביר את האפליקציה לכאן"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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="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_subheader_text" msgid="1729599730664063881">"כאן בוחרים איך לפתוח באפליקציה הזו קישורים לדפי אינטרנט"</string> <string name="open_by_default_dialog_in_app_text" msgid="6978022419634199806">"באפליקציה"</string> <string name="open_by_default_dialog_in_browser_text" msgid="8042769465958497081">"בדפדפן"</string> <string name="open_by_default_dialog_dismiss_button_text" msgid="3487238795534582291">"אישור"</string> diff --git a/libs/WindowManager/Shell/res/values-ja/strings.xml b/libs/WindowManager/Shell/res/values-ja/strings.xml index 69421da5b450..c7a77d9b9214 100644 --- a/libs/WindowManager/Shell/res/values-ja/strings.xml +++ b/libs/WindowManager/Shell/res/values-ja/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"画面の最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"画面のスナップ"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-ka/strings.xml b/libs/WindowManager/Shell/res/values-ka/strings.xml index 46859889304a..39362ef4ca57 100644 --- a/libs/WindowManager/Shell/res/values-ka/strings.xml +++ b/libs/WindowManager/Shell/res/values-ka/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"აპლიკაციის გაშლა სრულ ეკრანზე"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"აპლიკაციის დაპატარავება ეკრანზე"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-kk/strings.xml b/libs/WindowManager/Shell/res/values-kk/strings.xml index 84e7ea5b7af3..45f85b9e9a70 100644 --- a/libs/WindowManager/Shell/res/values-kk/strings.xml +++ b/libs/WindowManager/Shell/res/values-kk/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды ұлғайту"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды бөлу"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Қолданба бұл жерге қойылмайды."</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-km/strings.xml b/libs/WindowManager/Shell/res/values-km/strings.xml index 7eb81e158513..9c4ae05f3a36 100644 --- a/libs/WindowManager/Shell/res/values-km/strings.xml +++ b/libs/WindowManager/Shell/res/values-km/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ពង្រីកអេក្រង់"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ថតអេក្រង់"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-kn/strings.xml b/libs/WindowManager/Shell/res/values-kn/strings.xml index 2b43f573e1ab..f365cfb34412 100644 --- a/libs/WindowManager/Shell/res/values-kn/strings.xml +++ b/libs/WindowManager/Shell/res/values-kn/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ಸ್ಕ್ರೀನ್ ಅನ್ನು ಮ್ಯಾಕ್ಸಿಮೈಸ್ ಮಾಡಿ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ಸ್ನ್ಯಾಪ್ ಸ್ಕ್ರೀನ್"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-ko/strings.xml b/libs/WindowManager/Shell/res/values-ko/strings.xml index 8f36aab7ce8a..2bf1b05a919b 100644 --- a/libs/WindowManager/Shell/res/values-ko/strings.xml +++ b/libs/WindowManager/Shell/res/values-ko/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"화면 최대화"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"화면 분할"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"앱을 여기로 이동할 수 없음"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-ky/strings.xml b/libs/WindowManager/Shell/res/values-ky/strings.xml index c1219ba86249..392ae4cab107 100644 --- a/libs/WindowManager/Shell/res/values-ky/strings.xml +++ b/libs/WindowManager/Shell/res/values-ky/strings.xml @@ -133,12 +133,16 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Экранды чоңойтуу"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Экранды сүрөткө тартып алуу"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Колдонмону бул жерге жылдырууга болбойт"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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="open_by_default_settings_text" msgid="2526548548598185500">"Демейки шартта ачуу параметрлери"</string> - <string name="open_by_default_dialog_subheader_text" msgid="1729599730664063881">"Колдонмодо шилтемелер кантип ачылышы керек экенин тандаңыз"</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> <string name="open_by_default_dialog_in_browser_text" msgid="8042769465958497081">"Серепчиңизде"</string> <string name="open_by_default_dialog_dismiss_button_text" msgid="3487238795534582291">"Жарайт"</string> diff --git a/libs/WindowManager/Shell/res/values-lo/strings.xml b/libs/WindowManager/Shell/res/values-lo/strings.xml index 375fc6435935..4e4b678755b2 100644 --- a/libs/WindowManager/Shell/res/values-lo/strings.xml +++ b/libs/WindowManager/Shell/res/values-lo/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ປັບຈໍໃຫຍ່ສຸດ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ສະແນັບໜ້າຈໍ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ບໍ່ສາມາດຍ້າຍແອັບມາບ່ອນນີ້ໄດ້"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ຂະຫຍາຍໃຫຍ່ສຸດ"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"ເປີດຕາມການຕັ້ງຄ່າເລີ່ມຕົ້ນ"</string> diff --git a/libs/WindowManager/Shell/res/values-lt/strings.xml b/libs/WindowManager/Shell/res/values-lt/strings.xml index dfc3b45786de..5a7f58e5781a 100644 --- a/libs/WindowManager/Shell/res/values-lt/strings.xml +++ b/libs/WindowManager/Shell/res/values-lt/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Išskleisti ekraną"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Sutraukti ekraną"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Programos negalima perkelti čia"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Įtraukiantis"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Atkurti"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Padidinti"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-lv/strings.xml b/libs/WindowManager/Shell/res/values-lv/strings.xml index 87818524cabe..60912f627841 100644 --- a/libs/WindowManager/Shell/res/values-lv/strings.xml +++ b/libs/WindowManager/Shell/res/values-lv/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizēt ekrānu"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fiksēt ekrānu"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Lietotni nevar pārvietot šeit."</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimizēt"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-mk/strings.xml b/libs/WindowManager/Shell/res/values-mk/strings.xml index 88fed7414758..7c0c856ed1a7 100644 --- a/libs/WindowManager/Shell/res/values-mk/strings.xml +++ b/libs/WindowManager/Shell/res/values-mk/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Максимизирај го екранот"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Подели го екранот на половина"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Апликацијата не може да се премести овде"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-ml/strings.xml b/libs/WindowManager/Shell/res/values-ml/strings.xml index 71fb78eca0f3..e14ab8b0161c 100644 --- a/libs/WindowManager/Shell/res/values-ml/strings.xml +++ b/libs/WindowManager/Shell/res/values-ml/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"സ്ക്രീൻ വലുതാക്കുക"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"സ്ക്രീൻ സ്നാപ്പ് ചെയ്യുക"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ആപ്പ് ഇവിടേക്ക് നീക്കാനാകില്ല"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-mn/strings.xml b/libs/WindowManager/Shell/res/values-mn/strings.xml index 04f2f8202961..d406b99e80b3 100644 --- a/libs/WindowManager/Shell/res/values-mn/strings.xml +++ b/libs/WindowManager/Shell/res/values-mn/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Дэлгэцийг томруулах"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Дэлгэцийг таллах"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Аппыг ийш зөөх боломжгүй"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Томруулах"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"Өгөгдмөл тохиргоогоор нээх"</string> diff --git a/libs/WindowManager/Shell/res/values-mr/strings.xml b/libs/WindowManager/Shell/res/values-mr/strings.xml index be1df3273b21..871bc3fcc8e7 100644 --- a/libs/WindowManager/Shell/res/values-mr/strings.xml +++ b/libs/WindowManager/Shell/res/values-mr/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रीन मोठी करा"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रीन स्नॅप करा"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-ms/strings.xml b/libs/WindowManager/Shell/res/values-ms/strings.xml index 04da8869d5a7..71666cae93c8 100644 --- a/libs/WindowManager/Shell/res/values-ms/strings.xml +++ b/libs/WindowManager/Shell/res/values-ms/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimumkan Skrin"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Tangkap Skrin"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Apl tidak boleh dialihkan ke sini"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Mengasyikkan"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Pulihkan"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimumkan"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-my/strings.xml b/libs/WindowManager/Shell/res/values-my/strings.xml index 915a7cd3d67a..ae34624c98a0 100644 --- a/libs/WindowManager/Shell/res/values-my/strings.xml +++ b/libs/WindowManager/Shell/res/values-my/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"စခရင်ကို ချဲ့မည်"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"စခရင်ကို ချုံ့မည်"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"အက်ပ်ကို ဤနေရာသို့ ရွှေ့၍မရပါ"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-nb/strings.xml b/libs/WindowManager/Shell/res/values-nb/strings.xml index 8e5aee1eb49b..9270dc859728 100644 --- a/libs/WindowManager/Shell/res/values-nb/strings.xml +++ b/libs/WindowManager/Shell/res/values-nb/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimer skjermen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fest skjermen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Appen kan ikke flyttes hit"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimer"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-ne/strings.xml b/libs/WindowManager/Shell/res/values-ne/strings.xml index 42f2336c63ff..7015b2c11b32 100644 --- a/libs/WindowManager/Shell/res/values-ne/strings.xml +++ b/libs/WindowManager/Shell/res/values-ne/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"स्क्रिन ठुलो बनाउनुहोस्"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"स्क्रिन स्न्याप गर्नुहोस्"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-nl/strings.xml b/libs/WindowManager/Shell/res/values-nl/strings.xml index d19a4d44d6f9..45305d62a69b 100644 --- a/libs/WindowManager/Shell/res/values-nl/strings.xml +++ b/libs/WindowManager/Shell/res/values-nl/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Scherm maximaliseren"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Scherm halveren"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Kan de app niet hierheen verplaatsen"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximaliseren"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-or/strings.xml b/libs/WindowManager/Shell/res/values-or/strings.xml index f67a6b2b7d38..2d30441ab6f4 100644 --- a/libs/WindowManager/Shell/res/values-or/strings.xml +++ b/libs/WindowManager/Shell/res/values-or/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ସ୍କ୍ରିନକୁ ବଡ଼ କରନ୍ତୁ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ସ୍କ୍ରିନକୁ ସ୍ନାପ କରନ୍ତୁ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ଆପକୁ ଏଠାକୁ ମୁଭ କରାଯାଇପାରିବ ନାହିଁ"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ବଡ଼ କରନ୍ତୁ"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"ଡିଫଲ୍ଟ ସେଟିଂସକୁ ଖୋଲନ୍ତୁ"</string> diff --git a/libs/WindowManager/Shell/res/values-pa/strings.xml b/libs/WindowManager/Shell/res/values-pa/strings.xml index 76d59af75bfb..26ba461cba5d 100644 --- a/libs/WindowManager/Shell/res/values-pa/strings.xml +++ b/libs/WindowManager/Shell/res/values-pa/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ਸਕ੍ਰੀਨ ਦਾ ਆਕਾਰ ਵਧਾਓ"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ਸਕ੍ਰੀਨ ਨੂੰ ਸਨੈਪ ਕਰੋ"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ਐਪ ਨੂੰ ਇੱਥੇ ਨਹੀਂ ਲਿਜਾਇਆ ਜਾ ਸਕਦਾ"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"ਵੱਡਾ ਕਰੋ"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"ਪੂਰਵ-ਨਿਰਧਾਰਿਤ ਸੈਟਿੰਗਾਂ ਮੁਤਾਬਕ ਖੋਲ੍ਹੋ"</string> diff --git a/libs/WindowManager/Shell/res/values-pl/strings.xml b/libs/WindowManager/Shell/res/values-pl/strings.xml index 502e53abdcd3..5f78b134ad22 100644 --- a/libs/WindowManager/Shell/res/values-pl/strings.xml +++ b/libs/WindowManager/Shell/res/values-pl/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksymalizuj ekran"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Przyciągnij ekran"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Nie można przenieść aplikacji tutaj"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Tryb immersyjny"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Przywróć"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksymalizuj"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml index 3ec5e76bc5b4..8c7f9e73296d 100644 --- a/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rBR/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover o app para cá"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml index 184a5b336fd8..cd78ef95d88e 100644 --- a/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt-rPT/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizar ecrã"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Encaixar ecrã"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover a app para aqui"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Envolvente"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Restaurar"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-pt/strings.xml b/libs/WindowManager/Shell/res/values-pt/strings.xml index 3ec5e76bc5b4..8c7f9e73296d 100644 --- a/libs/WindowManager/Shell/res/values-pt/strings.xml +++ b/libs/WindowManager/Shell/res/values-pt/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ampliar tela"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ajustar tela"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Não é possível mover o app para cá"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizar"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-ro/strings.xml b/libs/WindowManager/Shell/res/values-ro/strings.xml index 9703328d4d98..e3fe2804bdcd 100644 --- a/libs/WindowManager/Shell/res/values-ro/strings.xml +++ b/libs/WindowManager/Shell/res/values-ro/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximizează fereastra"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Micșorează fereastra și fixeaz-o"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplicația nu poate fi mutată aici"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximizează"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-ru/strings.xml b/libs/WindowManager/Shell/res/values-ru/strings.xml index 401a8aab2576..442fca3ef0a7 100644 --- a/libs/WindowManager/Shell/res/values-ru/strings.xml +++ b/libs/WindowManager/Shell/res/values-ru/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Развернуть на весь экран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Свернуть"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Приложение нельзя сюда переместить"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-si/strings.xml b/libs/WindowManager/Shell/res/values-si/strings.xml index c101b4cd757c..8a7ad3b9f80c 100644 --- a/libs/WindowManager/Shell/res/values-si/strings.xml +++ b/libs/WindowManager/Shell/res/values-si/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"තිරය උපරිම කරන්න"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"ස්නැප් තිරය"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"යෙදුම මෙතැනට ගෙන යා නොහැක"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"විහිදන්න"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <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="open_by_default_settings_text" msgid="2526548548598185500">"පෙරනිමි සැකසීම් මඟින් විවෘත කරන්න"</string> diff --git a/libs/WindowManager/Shell/res/values-sk/strings.xml b/libs/WindowManager/Shell/res/values-sk/strings.xml index 7214300f1eb7..4234e8073bc8 100644 --- a/libs/WindowManager/Shell/res/values-sk/strings.xml +++ b/libs/WindowManager/Shell/res/values-sk/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximalizovať obrazovku"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Zobraziť polovicu obrazovky"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikácia sa sem nedá presunúť"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maximalizovať"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-sl/strings.xml b/libs/WindowManager/Shell/res/values-sl/strings.xml index 04fe7e89c16e..ae7e524da6cc 100644 --- a/libs/WindowManager/Shell/res/values-sl/strings.xml +++ b/libs/WindowManager/Shell/res/values-sl/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimiraj zaslon"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Pripni zaslon"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacije ni mogoče premakniti sem"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Poglobljeno"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"Obnovi"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimiraj"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-sq/strings.xml b/libs/WindowManager/Shell/res/values-sq/strings.xml index 6662ca1a059d..de6f681cfe74 100644 --- a/libs/WindowManager/Shell/res/values-sq/strings.xml +++ b/libs/WindowManager/Shell/res/values-sq/strings.xml @@ -133,9 +133,12 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maksimizo ekranin"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Regjistro ekranin"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Aplikacioni nuk mund të zhvendoset këtu"</string> - <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimizo"</string> - <!-- no translation found for desktop_mode_maximize_menu_restore_button_text (4234449220944704387) --> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> <skip /> + <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Maksimizo"</string> + <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="open_by_default_settings_text" msgid="2526548548598185500">"Hap sipas cilësimeve të parazgjedhura"</string> diff --git a/libs/WindowManager/Shell/res/values-sr/strings.xml b/libs/WindowManager/Shell/res/values-sr/strings.xml index d0a4ae684af7..901d6d967a7d 100644 --- a/libs/WindowManager/Shell/res/values-sr/strings.xml +++ b/libs/WindowManager/Shell/res/values-sr/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Повећај екран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Уклопи екран"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Апликација не може да се премести овде"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-sv/strings.xml b/libs/WindowManager/Shell/res/values-sv/strings.xml index 6ce2a9a1cb4d..6566801b7c63 100644 --- a/libs/WindowManager/Shell/res/values-sv/strings.xml +++ b/libs/WindowManager/Shell/res/values-sv/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Maximera skärmen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Fäst skärmen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Det går inte att flytta appen hit"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Utöka"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-sw/strings.xml b/libs/WindowManager/Shell/res/values-sw/strings.xml index 40967f02be45..a952011385de 100644 --- a/libs/WindowManager/Shell/res/values-sw/strings.xml +++ b/libs/WindowManager/Shell/res/values-sw/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Panua Dirisha kwenye Skrini"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Panga Madirisha kwenye Skrini"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Imeshindwa kuhamishia programu hapa"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Panua"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-ta/strings.xml b/libs/WindowManager/Shell/res/values-ta/strings.xml index 3140c2c77fae..2c73d3a14620 100644 --- a/libs/WindowManager/Shell/res/values-ta/strings.xml +++ b/libs/WindowManager/Shell/res/values-ta/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"திரையைப் பெரிதாக்கு"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"திரையை ஸ்னாப் செய்"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-te/strings.xml b/libs/WindowManager/Shell/res/values-te/strings.xml index 62e62c7a1e25..b17d4d1afaf7 100644 --- a/libs/WindowManager/Shell/res/values-te/strings.xml +++ b/libs/WindowManager/Shell/res/values-te/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"స్క్రీన్ సైజ్ను పెంచండి"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"స్క్రీన్ను స్నాప్ చేయండి"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-th/strings.xml b/libs/WindowManager/Shell/res/values-th/strings.xml index e6386a20e492..43cee41f5a15 100644 --- a/libs/WindowManager/Shell/res/values-th/strings.xml +++ b/libs/WindowManager/Shell/res/values-th/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"ขยายหน้าจอให้ใหญ่สุด"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"สแนปหน้าจอ"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-tl/strings.xml b/libs/WindowManager/Shell/res/values-tl/strings.xml index 176be336117d..428499532005 100644 --- a/libs/WindowManager/Shell/res/values-tl/strings.xml +++ b/libs/WindowManager/Shell/res/values-tl/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"I-maximize ang Screen"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"I-snap ang Screen"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Hindi mailipat dito ang app"</string> + <string name="desktop_mode_maximize_menu_immersive_button_text" msgid="559492223133829481">"Immersive"</string> + <string name="desktop_mode_maximize_menu_immersive_restore_button_text" msgid="4900114367354709257">"I-restore"</string> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"I-maximize"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-tr/strings.xml b/libs/WindowManager/Shell/res/values-tr/strings.xml index 73248e3d0c96..7eac4a8e4ffb 100644 --- a/libs/WindowManager/Shell/res/values-tr/strings.xml +++ b/libs/WindowManager/Shell/res/values-tr/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranı Büyüt"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranın Yarısına Tuttur"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Uygulama buraya taşınamıyor"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Ekranı kapla"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-uk/strings.xml b/libs/WindowManager/Shell/res/values-uk/strings.xml index a655a3eb452c..5fb14bf50e10 100644 --- a/libs/WindowManager/Shell/res/values-uk/strings.xml +++ b/libs/WindowManager/Shell/res/values-uk/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Розгорнути екран"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Зафіксувати екран"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Сюди не можна перемістити додаток"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-ur/strings.xml b/libs/WindowManager/Shell/res/values-ur/strings.xml index 4bdea83e59e3..bb0358f12b7a 100644 --- a/libs/WindowManager/Shell/res/values-ur/strings.xml +++ b/libs/WindowManager/Shell/res/values-ur/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"اسکرین کو بڑا کریں"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"اسکرین کا اسناپ شاٹ لیں"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"ایپ کو یہاں منتقل نہیں کیا جا سکتا"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-uz/strings.xml b/libs/WindowManager/Shell/res/values-uz/strings.xml index b3a496f27582..0648dd1c1bb8 100644 --- a/libs/WindowManager/Shell/res/values-uz/strings.xml +++ b/libs/WindowManager/Shell/res/values-uz/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Ekranni yoyish"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Ekranni biriktirish"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Ilova bu yerga surilmaydi"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Yoyish"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-vi/strings.xml b/libs/WindowManager/Shell/res/values-vi/strings.xml index 36d66e432590..dda2225b5f3e 100644 --- a/libs/WindowManager/Shell/res/values-vi/strings.xml +++ b/libs/WindowManager/Shell/res/values-vi/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Mở rộng màn hình"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Điều chỉnh kích thước màn hình"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"Không di chuyển được ứng dụng đến đây"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Phóng to tối đa"</string> <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> diff --git a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml index 64446cd4b342..2fb3f5ab5ea4 100644 --- a/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rCN/strings.xml @@ -133,6 +133,8 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"最大化屏幕"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"屏幕快照"</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> <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> diff --git a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml index 4970e8b92afb..1d7fb4c4c6e2 100644 --- a/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rHK/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"應用程式無法移至這裡"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml index fcdcccaee5f5..8083e378bf29 100644 --- a/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml +++ b/libs/WindowManager/Shell/res/values-zh-rTW/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"畫面最大化"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"貼齊畫面"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"應用程式無法移至此處"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <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> diff --git a/libs/WindowManager/Shell/res/values-zu/strings.xml b/libs/WindowManager/Shell/res/values-zu/strings.xml index cbc6c022a3c6..092efd6593fc 100644 --- a/libs/WindowManager/Shell/res/values-zu/strings.xml +++ b/libs/WindowManager/Shell/res/values-zu/strings.xml @@ -133,6 +133,10 @@ <string name="desktop_mode_maximize_menu_maximize_text" msgid="3275717276171114411">"Khulisa Isikrini Sifike Ekugcineni"</string> <string name="desktop_mode_maximize_menu_snap_text" msgid="2065251022783880154">"Thwebula Isikrini"</string> <string name="desktop_mode_non_resizable_snap_text" msgid="3771776422751387878">"I-app ayikwazi ukuhanjiswa lapha"</string> + <!-- no translation found for desktop_mode_maximize_menu_immersive_button_text (559492223133829481) --> + <skip /> + <!-- no translation found for desktop_mode_maximize_menu_immersive_restore_button_text (4900114367354709257) --> + <skip /> <string name="desktop_mode_maximize_menu_maximize_button_text" msgid="3090199175564175845">"Khulisa"</string> <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> diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml index 6cd380d1eed9..fa1aa193e1e3 100644 --- a/libs/WindowManager/Shell/res/values/dimen.xml +++ b/libs/WindowManager/Shell/res/values/dimen.xml @@ -239,11 +239,11 @@ <!-- Max width for the bubble popup view. --> <dimen name="bubble_popup_content_max_width">300dp</dimen> <!-- Horizontal margin for the bubble popup view. --> - <dimen name="bubble_popup_margin_horizontal">32dp</dimen> + <dimen name="bubble_popup_margin_horizontal">24dp</dimen> <!-- Top margin for the bubble bar education views. --> <dimen name="bubble_popup_margin_top">24dp</dimen> - <!-- Bottom margin for the bubble bar education views. --> - <dimen name="bubble_popup_margin_bottom">32dp</dimen> + <!-- Bottom padding for the bubble bar education views. --> + <dimen name="bubble_popup_padding_bottom">8dp</dimen> <!-- Text margin for the bubble bar education views. --> <dimen name="bubble_popup_text_margin">16dp</dimen> <!-- Size of icons in the bubble bar education views. --> diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/MinimizeAnimator.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/MinimizeAnimator.kt new file mode 100644 index 000000000000..0586e265eced --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/MinimizeAnimator.kt @@ -0,0 +1,74 @@ +/* + * 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.shared.animation + +import android.animation.Animator +import android.animation.AnimatorSet +import android.animation.ValueAnimator +import android.util.DisplayMetrics +import android.view.SurfaceControl.Transaction +import android.view.animation.LinearInterpolator +import android.view.animation.PathInterpolator +import android.window.TransitionInfo.Change + +/** Creates minimization animation */ +object MinimizeAnimator { + + private const val MINIMIZE_ANIM_ALPHA_DURATION_MS = 100L + + private val STANDARD_ACCELERATE = PathInterpolator(0.3f, 0f, 1f, 1f) + + private val minimizeBoundsAnimationDef = + WindowAnimator.BoundsAnimationParams( + durationMs = 200, + endOffsetYDp = 12f, + endScale = 0.97f, + interpolator = STANDARD_ACCELERATE, + ) + + @JvmStatic + fun create( + displayMetrics: DisplayMetrics, + change: Change, + transaction: Transaction, + onAnimFinish: (Animator) -> Unit, + ): Animator { + val boundsAnimator = WindowAnimator.createBoundsAnimator( + displayMetrics, + minimizeBoundsAnimationDef, + change, + transaction, + ) + val alphaAnimator = ValueAnimator.ofFloat(1f, 0f).apply { + duration = MINIMIZE_ANIM_ALPHA_DURATION_MS + interpolator = LinearInterpolator() + addUpdateListener { animation -> + transaction.setAlpha(change.leash, animation.animatedValue as Float).apply() + } + } + val listener = object : Animator.AnimatorListener { + override fun onAnimationEnd(animator: Animator) = onAnimFinish(animator) + override fun onAnimationCancel(animator: Animator) = Unit + override fun onAnimationRepeat(animator: Animator) = Unit + override fun onAnimationStart(animator: Animator) = Unit + } + return AnimatorSet().apply { + playTogether(boundsAnimator, alphaAnimator) + addListener(listener) + } + } +} 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 new file mode 100644 index 000000000000..91d66eaeb088 --- /dev/null +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/animation/WindowAnimator.kt @@ -0,0 +1,116 @@ +/* + * 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.shared.animation + +import android.animation.PointFEvaluator +import android.animation.ValueAnimator +import android.graphics.PointF +import android.graphics.Rect +import android.util.DisplayMetrics +import android.util.TypedValue +import android.view.SurfaceControl +import android.view.animation.Interpolator +import android.window.TransitionInfo + +/** Creates animations that can be applied to windows/surfaces. */ +object WindowAnimator { + + /** Parameters defining a window bounds animation. */ + data class BoundsAnimationParams( + val durationMs: Long, + val startOffsetYDp: Float = 0f, + val endOffsetYDp: Float = 0f, + val startScale: Float = 1f, + val endScale: Float = 1f, + val interpolator: Interpolator, + ) + + /** + * Creates an animator to reposition and scale the bounds of the leash of the given change. + * + * @param displayMetrics the metrics of the display where the animation plays in + * @param boundsAnimDef the parameters for the animation itself (duration, scale, position) + * @param change the change to which the animation should be applied + * @param transaction the transaction to apply the animation to + */ + fun createBoundsAnimator( + displayMetrics: DisplayMetrics, + boundsAnimDef: BoundsAnimationParams, + change: TransitionInfo.Change, + transaction: SurfaceControl.Transaction, + ): ValueAnimator { + val startPos = + getPosition( + displayMetrics, + change.endAbsBounds, + boundsAnimDef.startScale, + boundsAnimDef.startOffsetYDp, + ) + val leash = change.leash + val endPos = + getPosition( + displayMetrics, + change.endAbsBounds, + boundsAnimDef.endScale, + boundsAnimDef.endOffsetYDp, + ) + return ValueAnimator.ofObject(PointFEvaluator(), startPos, endPos).apply { + duration = boundsAnimDef.durationMs + interpolator = boundsAnimDef.interpolator + addUpdateListener { animation -> + val animPos = animation.animatedValue as PointF + val animScale = + interpolate( + boundsAnimDef.startScale, + boundsAnimDef.endScale, + animation.animatedFraction + ) + transaction + .setPosition(leash, animPos.x, animPos.y) + .setScale(leash, animScale, animScale) + .apply() + } + } + } + + private fun interpolate(startVal: Float, endVal: Float, fraction: Float): Float { + require(fraction in 0.0f..1.0f) + return startVal + (endVal - startVal) * fraction + } + + private fun getPosition( + displayMetrics: DisplayMetrics, + bounds: Rect, + scale: Float, + offsetYDp: Float + ) = PointF(bounds.left.toFloat(), bounds.top.toFloat()).apply { + check(scale in 0.0f..1.0f) + // Scale the bounds down with an anchor in the center + offset( + (bounds.width().toFloat() * (1 - scale) / 2), + (bounds.height().toFloat() * (1 - scale) / 2), + ) + val offsetYPx = + TypedValue.applyDimension( + TypedValue.COMPLEX_UNIT_DIP, + offsetYDp, + displayMetrics, + ) + .toInt() + offset(/* dx= */ 0f, offsetYPx.toFloat()) + } +} 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 0150bcdbd412..6bc995f14d44 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 @@ -102,6 +102,15 @@ public class DesktopModeStatus { "persist.wm.debug.enter_desktop_by_default_on_freeform_display"; /** + * Sysprop declaring whether to enable drag-to-maximize for desktop windows. + * + * <p>If it is not defined, then {@code R.integer.config_dragToMaximizeInDesktopMode} + * is used. + */ + public static final String ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP = + "persist.wm.debug.enable_drag_to_maximize"; + + /** * Sysprop declaring the maximum number of Tasks to show in Desktop Mode at any one time. * * <p>If it is not defined, then {@code R.integer.config_maxDesktopWindowingActiveTasks} is @@ -230,6 +239,18 @@ public class DesktopModeStatus { R.bool.config_enterDesktopByDefaultOnFreeformDisplay)); } + /** + * Return {@code true} if a window should be maximized when it's dragged to the top edge of the + * screen. + */ + public static boolean shouldMaximizeWhenDragToTopEdge(@NonNull Context context) { + if (!Flags.enableDragToMaximize()) { + return false; + } + return SystemProperties.getBoolean(ENABLE_DRAG_TO_MAXIMIZE_SYS_PROP, + context.getResources().getBoolean(R.bool.config_dragToMaximizeInDesktopMode)); + } + /** Dumps DesktopModeStatus flags and configs. */ public static void dump(PrintWriter pw, String prefix, Context context) { String innerPrefix = prefix + " "; diff --git a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt index 0e8e90467745..23e7441ff86b 100644 --- a/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt +++ b/libs/WindowManager/Shell/shared/src/com/android/wm/shell/shared/desktopmode/ManageWindowsViewContainer.kt @@ -15,6 +15,10 @@ */ package com.android.wm.shell.shared.desktopmode +import android.animation.Animator +import android.animation.AnimatorListenerAdapter +import android.animation.AnimatorSet +import android.animation.ObjectAnimator import android.annotation.ColorInt import android.content.Context import android.graphics.Bitmap @@ -23,6 +27,10 @@ import android.graphics.drawable.shapes.RoundRectShape import android.util.TypedValue import android.view.MotionEvent.ACTION_OUTSIDE import android.view.SurfaceView +import android.view.View +import android.view.View.ALPHA +import android.view.View.SCALE_X +import android.view.View.SCALE_Y import android.view.ViewGroup.MarginLayoutParams import android.widget.LinearLayout import android.window.TaskSnapshot @@ -39,7 +47,7 @@ abstract class ManageWindowsViewContainer( lateinit var menuView: ManageWindowsView /** Creates the base menu view and fills it with icon views. */ - fun show(snapshotList: List<Pair<Int, TaskSnapshot>>, + fun createMenu(snapshotList: List<Pair<Int, TaskSnapshot>>, onIconClickListener: ((Int) -> Unit), onOutsideClickListener: (() -> Unit)): ManageWindowsView { menuView = ManageWindowsView(context, menuBackgroundColor).apply { @@ -51,11 +59,24 @@ abstract class ManageWindowsViewContainer( return menuView } + /** Play the animation for opening the menu. */ + fun animateOpen() { + menuView.animateOpen() + } + + /** + * Play the animation for closing the menu. On finish, will run the provided callback, + * which will be responsible for removing the view from the container used in [addToContainer]. + */ + fun animateClose() { + menuView.animateClose { removeFromContainer() } + } + /** Adds the menu view to the container responsible for displaying it. */ abstract fun addToContainer(menuView: ManageWindowsView) - /** Dispose of the menu, perform needed cleanup. */ - abstract fun close() + /** Removes the menu view from the container used in the method above */ + abstract fun removeFromContainer() companion object { const val MANAGE_WINDOWS_MINIMUM_INSTANCES = 2 @@ -65,6 +86,8 @@ abstract class ManageWindowsViewContainer( private val context: Context, menuBackgroundColor: Int ) { + private val animators = mutableListOf<Animator>() + private val iconViews = mutableListOf<SurfaceView>() val rootView: LinearLayout = LinearLayout(context) var menuHeight = 0 var menuWidth = 0 @@ -147,6 +170,7 @@ abstract class ManageWindowsViewContainer( menuWidth += (instanceIconWidth + iconMargin).toInt() } rowLayout?.addView(appSnapshotButton) + iconViews += appSnapshotButton appSnapshotButton.requestLayout() rowLayout?.post { appSnapshotButton.holder.surface @@ -190,6 +214,78 @@ abstract class ManageWindowsViewContainer( } } + /** Play the animation for opening the menu. */ + fun animateOpen() { + animateView(rootView, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE, + MENU_START_ALPHA, MENU_FULL_ALPHA) + for (view in iconViews) { + animateView(view, MENU_BOUNDS_SHRUNK_SCALE, MENU_BOUNDS_FULL_SCALE, + MENU_START_ALPHA, MENU_FULL_ALPHA) + } + createAnimatorSet().start() + } + + /** Play the animation for closing the menu. */ + fun animateClose(callback: () -> Unit) { + animateView(rootView, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE, + MENU_FULL_ALPHA, MENU_START_ALPHA) + for (view in iconViews) { + animateView(view, MENU_BOUNDS_FULL_SCALE, MENU_BOUNDS_SHRUNK_SCALE, + MENU_FULL_ALPHA, MENU_START_ALPHA) + } + createAnimatorSet().apply { + addListener( + object : AnimatorListenerAdapter() { + override fun onAnimationEnd(animation: Animator) { + callback.invoke() + } + } + ) + start() + } + } + + private fun animateView( + view: View, + startBoundsScale: Float, + endBoundsScale: Float, + startAlpha: Float, + endAlpha: Float) { + animators += ObjectAnimator.ofFloat( + view, + SCALE_X, + startBoundsScale, + endBoundsScale + ).apply { + duration = MENU_BOUNDS_ANIM_DURATION + } + animators += ObjectAnimator.ofFloat( + view, + SCALE_Y, + startBoundsScale, + endBoundsScale + ).apply { + duration = MENU_BOUNDS_ANIM_DURATION + } + animators += ObjectAnimator.ofFloat( + view, + ALPHA, + startAlpha, + endAlpha + ).apply { + duration = MENU_ALPHA_ANIM_DURATION + startDelay = MENU_ALPHA_ANIM_DELAY + } + } + + private fun createAnimatorSet(): AnimatorSet { + val animatorSet = AnimatorSet().apply { + playTogether(animators) + } + animators.clear() + return animatorSet + } + companion object { private const val MENU_RADIUS_DP = 26f private const val ICON_WIDTH_DP = 204f @@ -198,6 +294,13 @@ abstract class ManageWindowsViewContainer( private const val ICON_MARGIN_DP = 16f private const val MENU_ELEVATION_DP = 1f private const val MENU_MAX_ICONS_PER_ROW = 3 + private const val MENU_BOUNDS_ANIM_DURATION = 200L + private const val MENU_BOUNDS_SHRUNK_SCALE = 0.8f + private const val MENU_BOUNDS_FULL_SCALE = 1f + private const val MENU_ALPHA_ANIM_DURATION = 100L + private const val MENU_ALPHA_ANIM_DELAY = 50L + private const val MENU_START_ALPHA = 0f + private const val MENU_FULL_ALPHA = 1f } } } 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 eb7ef1478a90..62ca5c687a2a 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.shared.pip; import static android.util.TypedValue.COMPLEX_UNIT_DIP; +import android.annotation.NonNull; import android.annotation.Nullable; import android.content.Context; import android.content.res.TypedArray; @@ -170,26 +171,34 @@ public abstract class PipContentOverlay { private final Context mContext; private final int mAppIconSizePx; - private final Rect mAppBounds; + /** + * The bounds of the application window relative to the task leash. + */ + private final Rect mRelativeAppBounds; private final int mOverlayHalfSize; private final Matrix mTmpTransform = new Matrix(); private final float[] mTmpFloat9 = new float[9]; private Bitmap mBitmap; - public PipAppIconOverlay(Context context, Rect appBounds, Rect destinationBounds, - Drawable appIcon, int appIconSizePx) { + // TODO(b/356277166): add non-match_parent support on PIP2. + /** + * @param context the {@link Context} that contains the icon information + * @param relativeAppBounds the bounds of the app window frame relative to the task leash + * @param destinationBounds the bounds for rhe PIP task + * @param appIcon the app icon {@link Drawable} + * @param appIconSizePx the icon dimension in pixel + */ + public PipAppIconOverlay(@NonNull Context context, @NonNull Rect relativeAppBounds, + @NonNull Rect destinationBounds, @NonNull Drawable appIcon, int appIconSizePx) { mContext = context; final int maxAppIconSizePx = (int) TypedValue.applyDimension(COMPLEX_UNIT_DIP, MAX_APP_ICON_SIZE_DP, context.getResources().getDisplayMetrics()); mAppIconSizePx = Math.min(maxAppIconSizePx, appIconSizePx); - final int overlaySize = getOverlaySize(appBounds, destinationBounds); + final int overlaySize = getOverlaySize(relativeAppBounds, 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()); + mRelativeAppBounds = relativeAppBounds; mBitmap = Bitmap.createBitmap(overlaySize, overlaySize, Bitmap.Config.ARGB_8888); prepareAppIconOverlay(appIcon); @@ -206,9 +215,9 @@ public abstract class PipContentOverlay { * the overlay will be drawn with the max size of the start and end bounds in different * rotation. */ - public static int getOverlaySize(Rect appBounds, Rect destinationBounds) { - final int appWidth = appBounds.width(); - final int appHeight = appBounds.height(); + public static int getOverlaySize(Rect overlayBounds, Rect destinationBounds) { + final int appWidth = overlayBounds.width(); + final int appHeight = overlayBounds.height(); return Math.max(Math.max(appWidth, appHeight), Math.max(destinationBounds.width(), destinationBounds.height())) + 1; @@ -230,15 +239,15 @@ public abstract class PipContentOverlay { mTmpTransform.reset(); // In order for the overlay to always cover the pip window, the overlay may have a // size larger than the pip window. Make sure that app icon is at the center. - final int appBoundsCenterX = mAppBounds.centerX(); - final int appBoundsCenterY = mAppBounds.centerY(); + final int appBoundsCenterX = mRelativeAppBounds.centerX(); + final int appBoundsCenterY = mRelativeAppBounds.centerY(); mTmpTransform.setTranslate( appBoundsCenterX - mOverlayHalfSize, appBoundsCenterY - mOverlayHalfSize); // Scale back the bitmap with the pivot point at center. final float scale = Math.min( - (float) mAppBounds.width() / currentBounds.width(), - (float) mAppBounds.height() / currentBounds.height()); + (float) mRelativeAppBounds.width() / currentBounds.width(), + (float) mRelativeAppBounds.height() / currentBounds.height()); mTmpTransform.postScale(scale, scale, appBoundsCenterX, appBoundsCenterY); atomicTx.setMatrix(mLeash, mTmpTransform, mTmpFloat9) .setAlpha(mLeash, fraction < 0.5f ? 0 : (fraction - 0.5f) * 2); 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 5eb5d8962b55..f296c710f9a0 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 @@ -19,6 +19,7 @@ package com.android.wm.shell.back; import static android.app.ActivityTaskManager.INVALID_TASK_ID; import static android.view.RemoteAnimationTarget.MODE_CLOSING; import static android.view.RemoteAnimationTarget.MODE_OPENING; +import static android.view.WindowManager.TRANSIT_CHANGE; import static android.view.WindowManager.TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IS_WALLPAPER; @@ -190,6 +191,16 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont @Override public void onResult(@Nullable Bundle result) { mShellExecutor.execute(() -> { + if (mBackGestureStarted && result != null && result.getBoolean( + BackNavigationInfo.KEY_TOUCH_GESTURE_TRANSFERRED)) { + // Host app won't able to process motion event anymore, so pilfer + // pointers anyway. + if (mBackNavigationInfo != null) { + mBackNavigationInfo.disableAppProgressGenerationAllowed(); + } + tryPilferPointers(); + return; + } if (!mBackGestureStarted || mPostCommitAnimationInProgress) { // If an uninterruptible animation is already in progress, we should // ignore this due to it may cause focus lost. (alpha = 0) @@ -1262,6 +1273,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return handleCloseTransition(info, st, ft, finishCallback); } + @Override + public void onTransitionConsumed(@NonNull IBinder transition, boolean aborted, + @Nullable SurfaceControl.Transaction finishTransaction) { + if (transition == mClosePrepareTransition && aborted) { + mClosePrepareTransition = null; + applyFinishOpenTransition(); + } + } + void createClosePrepareTransition() { if (mClosePrepareTransition != null) { Log.e(TAG, "Re-create close prepare transition"); @@ -1280,7 +1300,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont final TransitionInfo init = mOpenTransitionInfo; // Find prepare open target boolean openShowWallpaper = false; - final ArrayList<OpenChangeInfo> targets = new ArrayList<>(); + final ArrayList<SurfaceControl> openSurfaces = new ArrayList<>(); int tmpSize; for (int j = init.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = init.getChanges().get(j); @@ -1293,13 +1313,13 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont && openToken == null) { continue; } - targets.add(new OpenChangeInfo(openComponent, openTaskId, openToken)); + openSurfaces.add(change.getLeash()); if (change.hasFlags(FLAG_SHOW_WALLPAPER)) { openShowWallpaper = true; } } } - if (targets.isEmpty()) { + if (openSurfaces.isEmpty()) { // This shouldn't happen, but if that happen, consume the initial transition anyway. Log.e(TAG, "Unable to merge following transition, cannot find the gesture " + "animated target from the open transition=" + mOpenTransitionInfo); @@ -1311,7 +1331,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont tmpSize = info.getChanges().size(); for (int j = 0; j < tmpSize; ++j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { // This is original close target, potential be close, but cannot determine // from it. if (change.hasFlags(FLAG_BACK_GESTURE_ANIMATED)) { @@ -1324,15 +1344,15 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } if (!isOpen) { // Close transition, the transition info should be: - // init info(open A & wallpaper) - // current info(close B target) + // init info(open A & wallpaper) => init info(open A & change B & wallpaper) + // current info(close B target) => current info(change A & close B) // remove init info(open/change A target & wallpaper) boolean moveToTop = false; boolean excludeOpenTarget = false; boolean mergePredictive = false; for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { if (TransitionUtil.isClosingMode(change.getMode())) { excludeOpenTarget = true; } @@ -1353,7 +1373,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (change.hasFlags(FLAG_IS_WALLPAPER)) { continue; } - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { if (excludeOpenTarget) { // App has triggered another change during predictive back // transition, filter out predictive back target. @@ -1388,7 +1408,7 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont if (nonBackClose && nonBackOpen) { for (int j = info.getChanges().size() - 1; j >= 0; --j) { final TransitionInfo.Change change = info.getChanges().get(j); - if (isOpenChangeMatched(targets, change)) { + if (isOpenSurfaceMatched(openSurfaces, change)) { info.getChanges().remove(j); } else if ((openShowWallpaper && change.hasFlags(FLAG_IS_WALLPAPER))) { info.getChanges().remove(j); @@ -1515,14 +1535,17 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return false; } SurfaceControl openingLeash = null; + SurfaceControl closingLeash = null; if (mApps != null) { for (int i = mApps.length - 1; i >= 0; --i) { if (mApps[i].mode == MODE_OPENING) { openingLeash = mApps[i].leash; + } else if (mApps[i].mode == MODE_CLOSING) { + closingLeash = mApps[i].leash; } } } - if (openingLeash != null) { + if (openingLeash != null && closingLeash != null) { int rootIdx = -1; for (int i = info.getChanges().size() - 1; i >= 0; --i) { final TransitionInfo.Change c = info.getChanges().get(i); @@ -1532,6 +1555,9 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont st.reparent(c.getLeash(), openingLeash); st.setAlpha(c.getLeash(), 1.0f); rootIdx = TransitionUtil.rootIndexFor(c, info); + } else if (c.hasFlags(FLAG_BACK_GESTURE_ANIMATED) + && c.getMode() == TRANSIT_CHANGE) { + st.reparent(c.getLeash(), closingLeash); } } // The root leash and the leash of opening target should actually in the same level, @@ -1666,22 +1692,10 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont return INVALID_TASK_ID; } - private static boolean isSameChangeTarget(ComponentName topActivity, int taskId, - WindowContainerToken token, TransitionInfo.Change change) { - final ComponentName openChange = findComponentName(change); - final int firstTaskId = findTaskId(change); - final WindowContainerToken openToken = findToken(change); - return (openChange != null && openChange.equals(topActivity)) - || (firstTaskId != INVALID_TASK_ID && firstTaskId == taskId) - || (openToken != null && openToken.equals(token)); - } - - static boolean isOpenChangeMatched(@NonNull ArrayList<OpenChangeInfo> targets, + static boolean isOpenSurfaceMatched(@NonNull ArrayList<SurfaceControl> openSurfaces, TransitionInfo.Change change) { - for (int i = targets.size() - 1; i >= 0; --i) { - final OpenChangeInfo next = targets.get(i); - if (isSameChangeTarget(next.mOpenComponent, next.mOpenTaskId, next.mOpenToken, - change)) { + for (int i = openSurfaces.size() - 1; i >= 0; --i) { + if (openSurfaces.get(i).isSameSurface(change.getLeash())) { return true; } } @@ -1745,16 +1759,4 @@ public class BackAnimationController implements RemoteCallable<BackAnimationCont } } } - - static class OpenChangeInfo { - final ComponentName mOpenComponent; - final int mOpenTaskId; - final WindowContainerToken mOpenToken; - OpenChangeInfo(ComponentName openComponent, int openTaskId, - WindowContainerToken openToken) { - mOpenComponent = openComponent; - mOpenTaskId = openTaskId; - mOpenToken = openToken; - } - } } 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 37e8ead4fc78..5f0eed9daa1a 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 @@ -833,7 +833,7 @@ public class BubbleController implements ConfigurationChangeListener, // window to show this in, but we use a separate code path. // TODO(b/273312602): consider foldables where we do need a stack view when folded if (mLayerView == null) { - mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData); + mLayerView = new BubbleBarLayerView(mContext, this, mBubbleData, mLogger); mLayerView.setUnBubbleConversationCallback(mSysuiProxy::onUnbubbleConversation); } } else { @@ -1212,7 +1212,7 @@ public class BubbleController implements ConfigurationChangeListener, */ public void startBubbleDrag(String bubbleKey) { if (mBubbleData.getSelectedBubble() != null) { - mBubbleBarViewCallback.expansionChanged(/* isExpanded = */ false); + collapseExpandedViewForBubbleBar(); } if (mBubbleStateListener != null) { boolean overflow = BubbleOverflow.KEY.equals(bubbleKey); @@ -1304,6 +1304,7 @@ public class BubbleController implements ConfigurationChangeListener, if (BubbleOverflow.KEY.equals(key)) { mBubbleData.setSelectedBubbleFromLauncher(mBubbleData.getOverflow()); mLayerView.showExpandedView(mBubbleData.getOverflow()); + mLogger.log(BubbleLogger.Event.BUBBLE_BAR_EXPANDED); return; } @@ -1315,6 +1316,7 @@ public class BubbleController implements ConfigurationChangeListener, // already in the stack mBubbleData.setSelectedBubbleFromLauncher(b); mLayerView.showExpandedView(b); + mLogger.log(b, BubbleLogger.Event.BUBBLE_BAR_EXPANDED); } else if (mBubbleData.hasOverflowBubbleWithKey(b.getKey())) { // TODO: (b/271468319) handle overflow } else { @@ -1759,6 +1761,9 @@ public class BubbleController implements ConfigurationChangeListener, @MainThread public void removeAllBubbles(@Bubbles.DismissReason int reason) { mBubbleData.dismissAll(reason); + if (reason == Bubbles.DISMISS_USER_GESTURE) { + mLogger.log(BubbleLogger.Event.BUBBLE_BAR_DISMISSED_DRAG_BAR); + } } private void onEntryAdded(BubbleEntry entry) { @@ -2021,12 +2026,16 @@ public class BubbleController implements ConfigurationChangeListener, public void expansionChanged(boolean isExpanded) { // in bubble bar mode, let the request to show the expanded view come from launcher. // only collapse here if we're collapsing. - if (mLayerView != null && !isExpanded) { - if (mBubblePositioner.isImeVisible()) { - // If we're collapsing, hide the IME - hideCurrentInputMethod(); - } - mLayerView.collapse(); + if (!isExpanded) { + collapseExpandedViewForBubbleBar(); + } + + BubbleLogger.Event event = isExpanded ? BubbleLogger.Event.BUBBLE_BAR_EXPANDED + : BubbleLogger.Event.BUBBLE_BAR_COLLAPSED; + if (mBubbleData.getSelectedBubble() instanceof Bubble bubble) { + mLogger.log(bubble, event); + } else { + mLogger.log(event); } } @@ -2179,6 +2188,16 @@ public class BubbleController implements ConfigurationChangeListener, } } + private void collapseExpandedViewForBubbleBar() { + if (mLayerView != null && mLayerView.isExpanded()) { + if (mBubblePositioner.isImeVisible()) { + // If we're collapsing, hide the IME + hideCurrentInputMethod(); + } + mLayerView.collapse(); + } + } + private void updateOverflowButtonDot() { BubbleOverflow overflow = mBubbleData.getOverflow(); if (overflow == null) return; 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 6d757d26a9bd..36630733e1da 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 @@ -145,6 +145,9 @@ public class BubbleLogger { @UiEvent(doc = "bubble promoted from overflow back to bubble bar") BUBBLE_BAR_OVERFLOW_REMOVE_BACK_TO_BAR(1949), + @UiEvent(doc = "while bubble bar is expanded, switch to another/existing bubble") + BUBBLE_BAR_BUBBLE_SWITCHED(1977) + // endregion ; @@ -165,8 +168,14 @@ public class BubbleLogger { } /** - * @param b Bubble involved in this UI event - * @param e UI event + * Log an UIEvent + */ + public void log(UiEventLogger.UiEventEnum e) { + mUiEventLogger.log(e); + } + + /** + * Log an UIEvent with the given bubble info */ public void log(Bubble b, UiEventLogger.UiEventEnum e) { mUiEventLogger.logWithInstanceId(e, b.getAppUid(), b.getPackageName(), b.getInstanceId()); 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 35a0d07a63b2..88f55b8af8f7 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 @@ -1704,6 +1704,7 @@ public class BubbleStackView extends FrameLayout getViewTreeObserver().removeOnPreDrawListener(mViewUpdater); getViewTreeObserver().removeOnDrawListener(mSystemGestureExcludeUpdater); getViewTreeObserver().removeOnComputeInternalInsetsListener(this); + stopMonitoringSwipeUpGesture(); } @Override @@ -2313,7 +2314,8 @@ public class BubbleStackView extends FrameLayout /** * Stop monitoring for swipe up gesture */ - void stopMonitoringSwipeUpGesture() { + @VisibleForTesting + public void stopMonitoringSwipeUpGesture() { stopMonitoringSwipeUpGestureInternal(); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java index 2612b81aae00..e577c3e0b1b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/animation/AnimatableScaleMatrix.java @@ -141,4 +141,10 @@ public class AnimatableScaleMatrix extends Matrix { // PhysicsAnimator's animator caching). return obj == this; } + + @Override + public int hashCode() { + // Make sure equals and hashCode work in a similar way. Rely on object identity for both. + return System.identityHashCode(this); + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt index 07463bb024a2..34259bfb7aaa 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedViewDragController.kt @@ -19,6 +19,7 @@ package com.android.wm.shell.bubbles.bar import android.annotation.SuppressLint import android.view.MotionEvent import android.view.View +import androidx.annotation.VisibleForTesting import com.android.wm.shell.bubbles.BubblePositioner import com.android.wm.shell.shared.bubbles.DismissView import com.android.wm.shell.shared.bubbles.RelativeTouchListener @@ -32,7 +33,7 @@ class BubbleBarExpandedViewDragController( private val animationHelper: BubbleBarAnimationHelper, private val bubblePositioner: BubblePositioner, private val pinController: BubbleExpandedViewPinController, - private val dragListener: DragListener + @get:VisibleForTesting val dragListener: DragListener, ) { var isStuckToDismiss: Boolean = false @@ -107,7 +108,7 @@ class BubbleBarExpandedViewDragController( viewInitialX: Float, viewInitialY: Float, dx: Float, - dy: Float + dy: Float, ) { if (!isMoving) { isMoving = true @@ -127,7 +128,7 @@ class BubbleBarExpandedViewDragController( dx: Float, dy: Float, velX: Float, - velY: Float + velY: Float, ) { finishDrag() } @@ -152,7 +153,7 @@ class BubbleBarExpandedViewDragController( private inner class MagnetListener : MagnetizedObject.MagnetListener { override fun onStuckToTarget( target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*> + draggedObject: MagnetizedObject<*>, ) { isStuckToDismiss = true pinController.onStuckToDismissTarget() @@ -163,7 +164,7 @@ class BubbleBarExpandedViewDragController( draggedObject: MagnetizedObject<*>, velX: Float, velY: Float, - wasFlungOut: Boolean + wasFlungOut: Boolean, ) { isStuckToDismiss = false animationHelper.animateUnstuckFromDismissView(target) @@ -171,7 +172,7 @@ class BubbleBarExpandedViewDragController( override fun onReleasedInTarget( target: MagnetizedObject.MagneticTarget, - draggedObject: MagnetizedObject<*> + draggedObject: MagnetizedObject<*>, ) { dragListener.onReleased(inDismiss = true) pinController.onDragEnd() 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 1367b7e24bc7..402818c80b01 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 @@ -34,10 +34,12 @@ import android.view.WindowManager; import android.widget.FrameLayout; import androidx.annotation.NonNull; +import androidx.annotation.VisibleForTesting; import com.android.wm.shell.bubbles.Bubble; import com.android.wm.shell.bubbles.BubbleController; import com.android.wm.shell.bubbles.BubbleData; +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; @@ -69,6 +71,7 @@ public class BubbleBarLayerView extends FrameLayout private final BubbleController mBubbleController; private final BubbleData mBubbleData; private final BubblePositioner mPositioner; + private final BubbleLogger mBubbleLogger; private final BubbleBarAnimationHelper mAnimationHelper; private final BubbleEducationViewController mEducationViewController; private final View mScrimView; @@ -93,11 +96,13 @@ public class BubbleBarLayerView extends FrameLayout private TouchDelegate mHandleTouchDelegate; private final Rect mHandleTouchBounds = new Rect(); - public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData) { + public BubbleBarLayerView(Context context, BubbleController controller, BubbleData bubbleData, + BubbleLogger bubbleLogger) { super(context); mBubbleController = controller; mBubbleData = bubbleData; mPositioner = mBubbleController.getPositioner(); + mBubbleLogger = bubbleLogger; mAnimationHelper = new BubbleBarAnimationHelper(context, this, mPositioner); @@ -233,6 +238,11 @@ public class BubbleBarLayerView extends FrameLayout DragListener dragListener = inDismiss -> { if (inDismiss && mExpandedBubble != null) { mBubbleController.dismissBubble(mExpandedBubble.getKey(), DISMISS_USER_GESTURE); + if (mExpandedBubble instanceof Bubble) { + // Only a bubble can be dragged to dismiss + mBubbleLogger.log((Bubble) mExpandedBubble, + BubbleLogger.Event.BUBBLE_BAR_BUBBLE_DISMISSED_DRAG_EXP_VIEW); + } } }; mDragController = new BubbleBarExpandedViewDragController( @@ -413,4 +423,10 @@ public class BubbleBarLayerView extends FrameLayout } } + @Nullable + @VisibleForTesting + public BubbleBarExpandedViewDragController getDragController() { + return mDragController; + } + } 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 85921703d559..813772f20a8a 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 @@ -71,6 +71,11 @@ public class DividerSnapAlgorithm { */ private static final int SNAP_MODE_MINIMIZED = 3; + /** + * A mode where apps can be "flexibly offscreen" on smaller displays. + */ + private static final int SNAP_FLEXIBLE_SPLIT = 4; + private final float mMinFlingVelocityPxPerSecond; private final float mMinDismissVelocityPxPerSecond; private final int mDisplayWidth; @@ -78,6 +83,7 @@ public class DividerSnapAlgorithm { private final int mDividerSize; private final ArrayList<SnapTarget> mTargets = new ArrayList<>(); private final Rect mInsets = new Rect(); + private final Rect mPinnedTaskbarInsets = new Rect(); private final int mSnapMode; private final boolean mFreeSnapMode; private final int mMinimalSizeResizableTask; @@ -88,6 +94,8 @@ public class DividerSnapAlgorithm { /** Allows split ratios that go offscreen (a.k.a. "flexible split") */ private final boolean mAllowOffscreenRatios; private final boolean mIsLeftRightSplit; + /** In SNAP_MODE_MINIMIZED, the side of the screen on which an app will "dock" when minimized */ + private final int mDockSide; /** The first target which is still splitting the screen */ private final SnapTarget mFirstSplitTarget; @@ -101,14 +109,14 @@ public class DividerSnapAlgorithm { public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isLeftRightSplit, Rect insets, int dockSide) { + boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide) { this(res, displayWidth, displayHeight, dividerSize, isLeftRightSplit, insets, - dockSide, false /* minimized */, true /* resizable */); + pinnedTaskbarInsets, dockSide, false /* minimized */, true /* resizable */); } public DividerSnapAlgorithm(Resources res, int displayWidth, int displayHeight, int dividerSize, - boolean isLeftRightSplit, Rect insets, int dockSide, boolean isMinimizedMode, - boolean isHomeResizable) { + boolean isLeftRightSplit, Rect insets, Rect pinnedTaskbarInsets, int dockSide, + boolean isMinimizedMode, boolean isHomeResizable) { mMinFlingVelocityPxPerSecond = MIN_FLING_VELOCITY_DP_PER_SECOND * res.getDisplayMetrics().density; mMinDismissVelocityPxPerSecond = @@ -117,9 +125,18 @@ public class DividerSnapAlgorithm { mDisplayWidth = displayWidth; mDisplayHeight = displayHeight; mIsLeftRightSplit = isLeftRightSplit; + mDockSide = dockSide; mInsets.set(insets); - mSnapMode = isMinimizedMode ? SNAP_MODE_MINIMIZED : - res.getInteger(com.android.internal.R.integer.config_dockedStackDividerSnapMode); + mPinnedTaskbarInsets.set(pinnedTaskbarInsets); + if (Flags.enableFlexibleTwoAppSplit()) { + mSnapMode = SNAP_FLEXIBLE_SPLIT; + } else { + // Set SNAP_MODE_MINIMIZED, SNAP_MODE_16_9, or SNAP_FIXED_RATIO depending on config + mSnapMode = isMinimizedMode + ? SNAP_MODE_MINIMIZED + : res.getInteger( + com.android.internal.R.integer.config_dockedStackDividerSnapMode); + } mFreeSnapMode = res.getBoolean( com.android.internal.R.bool.config_dockedStackDividerFreeSnapMode); mFixedRatio = res.getFraction( @@ -129,11 +146,10 @@ public class DividerSnapAlgorithm { mCalculateRatiosBasedOnAvailableSpace = res.getBoolean( com.android.internal.R.bool.config_flexibleSplitRatios); // If this is a small screen or a foldable, use offscreen ratios - mAllowOffscreenRatios = - Flags.enableFlexibleTwoAppSplit() && SplitScreenUtils.allowOffscreenRatios(res); + mAllowOffscreenRatios = SplitScreenUtils.allowOffscreenRatios(res); mTaskHeightInMinimizedMode = isHomeResizable ? res.getDimensionPixelSize( com.android.internal.R.dimen.task_height_of_minimized_mode) : 0; - calculateTargets(isLeftRightSplit, dockSide); + calculateTargets(); mFirstSplitTarget = mTargets.get(1); mLastSplitTarget = mTargets.get(mTargets.size() - 2); mDismissStartTarget = mTargets.get(0); @@ -269,28 +285,31 @@ public class DividerSnapAlgorithm { return mTargets.get(minIndex); } - private void calculateTargets(boolean isLeftRightSplit, int dockedSide) { + private void calculateTargets() { mTargets.clear(); - int dividerMax = isLeftRightSplit + int dividerMax = mIsLeftRightSplit ? mDisplayWidth : mDisplayHeight; int startPos = -mDividerSize; - if (dockedSide == DOCKED_RIGHT) { + if (mDockSide == DOCKED_RIGHT) { startPos += mInsets.left; } mTargets.add(new SnapTarget(startPos, SNAP_TO_START_AND_DISMISS, 0.35f)); switch (mSnapMode) { case SNAP_MODE_16_9: - addRatio16_9Targets(isLeftRightSplit, dividerMax); + addRatio16_9Targets(mIsLeftRightSplit, dividerMax); break; case SNAP_FIXED_RATIO: - addFixedDivisionTargets(isLeftRightSplit, dividerMax); + addFixedDivisionTargets(mIsLeftRightSplit, dividerMax); break; case SNAP_ONLY_1_1: - addMiddleTarget(isLeftRightSplit); + addMiddleTarget(mIsLeftRightSplit); break; case SNAP_MODE_MINIMIZED: - addMinimizedTarget(isLeftRightSplit, dockedSide); + addMinimizedTarget(mIsLeftRightSplit, mDockSide); + break; + case SNAP_FLEXIBLE_SPLIT: + addFlexSplitTargets(mIsLeftRightSplit, dividerMax); break; } mTargets.add(new SnapTarget(dividerMax, SNAP_TO_END_AND_DISMISS, 0.35f)); @@ -299,9 +318,9 @@ public class DividerSnapAlgorithm { private void addNonDismissingTargets(boolean isLeftRightSplit, int topPosition, int bottomPosition, int dividerMax) { @PersistentSnapPosition int firstTarget = - mAllowOffscreenRatios ? SNAP_TO_2_10_90 : SNAP_TO_2_33_66; + areOffscreenRatiosSupported() ? SNAP_TO_2_10_90 : SNAP_TO_2_33_66; @PersistentSnapPosition int lastTarget = - mAllowOffscreenRatios ? SNAP_TO_2_90_10 : SNAP_TO_2_66_33; + areOffscreenRatiosSupported() ? SNAP_TO_2_90_10 : SNAP_TO_2_66_33; maybeAddTarget(topPosition, topPosition - getStartInset(), firstTarget); addMiddleTarget(isLeftRightSplit); maybeAddTarget(bottomPosition, @@ -313,19 +332,35 @@ public class DividerSnapAlgorithm { int end = isLeftRightSplit ? mDisplayWidth - mInsets.right : mDisplayHeight - mInsets.bottom; - int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; - if (mAllowOffscreenRatios) { - // TODO (b/349828130): This is a placeholder value, real measurements to come - size = (int) (0.3f * (end - start)) - mDividerSize / 2; - } else if (mCalculateRatiosBasedOnAvailableSpace) { + int size = (int) (mFixedRatio * (end - start)) - mDividerSize / 2; + if (mCalculateRatiosBasedOnAvailableSpace) { size = Math.max(size, mMinimalSizeResizableTask); } + int topPosition = start + size; int bottomPosition = end - size - mDividerSize; addNonDismissingTargets(isLeftRightSplit, topPosition, bottomPosition, dividerMax); } + private void addFlexSplitTargets(boolean isLeftRightSplit, int dividerMax) { + int start = 0; + int end = isLeftRightSplit ? mDisplayWidth : mDisplayHeight; + int pinnedTaskbarShiftStart = isLeftRightSplit + ? mPinnedTaskbarInsets.left : mPinnedTaskbarInsets.top; + int pinnedTaskbarShiftEnd = isLeftRightSplit + ? mPinnedTaskbarInsets.right : mPinnedTaskbarInsets.bottom; + + float ratio = areOffscreenRatiosSupported() + ? SplitLayout.OFFSCREEN_ASYMMETRIC_RATIO + : SplitLayout.ONSCREEN_ONLY_ASYMMETRIC_RATIO; + int size = (int) (ratio * (end - start)) - mDividerSize / 2; + + int leftTopPosition = start + pinnedTaskbarShiftStart + size; + int rightBottomPosition = end - pinnedTaskbarShiftEnd - size - mDividerSize; + addNonDismissingTargets(isLeftRightSplit, leftTopPosition, rightBottomPosition, dividerMax); + } + private void addRatio16_9Targets(boolean isLeftRightSplit, int dividerMax) { int start = isLeftRightSplit ? mInsets.left : mInsets.top; int end = isLeftRightSplit @@ -347,7 +382,7 @@ public class DividerSnapAlgorithm { * meets the minimal size requirement. */ private void maybeAddTarget(int position, int smallerSize, @SnapPosition int snapPosition) { - if (smallerSize >= mMinimalSizeResizableTask || mAllowOffscreenRatios) { + if (smallerSize >= mMinimalSizeResizableTask || areOffscreenRatiosSupported()) { mTargets.add(new SnapTarget(position, snapPosition)); } } 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 e7848e27d7ed..cf858deb0327 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 @@ -480,6 +480,7 @@ public class DividerView extends FrameLayout implements View.OnTouchListener { mLastDraggingPosition, position, mSplitLayout.FLING_RESIZE_DURATION, + Interpolators.FAST_OUT_SLOW_IN, () -> mSplitLayout.setDividerPosition(position, true /* applyLayoutChange */)); mMoving = 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 f73065ea8eb8..dab30b0f0b96 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 @@ -28,6 +28,7 @@ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_ import static com.android.internal.jank.InteractionJankMonitor.CUJ_SPLIT_SCREEN_RESIZE; import static com.android.wm.shell.shared.animation.Interpolators.DIM_INTERPOLATOR; import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED; +import static com.android.wm.shell.shared.animation.Interpolators.FAST_OUT_SLOW_IN; import static com.android.wm.shell.shared.animation.Interpolators.LINEAR; import static com.android.wm.shell.shared.animation.Interpolators.SLOWDOWN_INTERPOLATOR; import static com.android.wm.shell.shared.split.SplitScreenConstants.SNAP_TO_2_10_90; @@ -48,10 +49,13 @@ import android.app.ActivityManager; import android.content.Context; import android.content.res.Configuration; import android.content.res.Resources; +import android.graphics.Insets; import android.graphics.Point; import android.graphics.Rect; import android.os.Handler; import android.view.Display; +import android.view.InsetsController; +import android.view.InsetsSource; import android.view.InsetsSourceControl; import android.view.InsetsState; import android.view.RoundedCorner; @@ -77,7 +81,6 @@ import com.android.wm.shell.common.DisplayLayout; import com.android.wm.shell.common.pip.PipUtils; import com.android.wm.shell.common.split.DividerSnapAlgorithm.SnapTarget; import com.android.wm.shell.protolog.ShellProtoLogGroup; -import com.android.wm.shell.shared.animation.Interpolators; import com.android.wm.shell.shared.annotations.ShellMainThread; import com.android.wm.shell.shared.split.SplitScreenConstants.PersistentSnapPosition; import com.android.wm.shell.shared.split.SplitScreenConstants.SnapPosition; @@ -100,6 +103,12 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public static final int FLING_RESIZE_DURATION = 250; private static final int FLING_ENTER_DURATION = 450; private static final int FLING_EXIT_DURATION = 450; + private static final int FLING_OFFSCREEN_DURATION = 500; + + /** A split ratio used on larger screens, where we can fit both apps onscreen. */ + public static final float ONSCREEN_ONLY_ASYMMETRIC_RATIO = 0.33f; + /** A split ratio used on smaller screens, where we place one app mostly offscreen. */ + public static final float OFFSCREEN_ASYMMETRIC_RATIO = 0.1f; // Here are some (arbitrarily decided) layer definitions used during animations to make sure the // layers stay in order. Note: This does not affect any other layer numbering systems because @@ -147,6 +156,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange private final ResizingEffectPolicy mSurfaceEffectPolicy; private final ShellTaskOrganizer mTaskOrganizer; private final InsetsState mInsetsState = new InsetsState(); + private Insets mPinnedTaskbarInsets = Insets.NONE; private Context mContext; @VisibleForTesting DividerSnapAlgorithm mDividerSnapAlgorithm; @@ -517,6 +527,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange @Override public void insetsChanged(InsetsState insetsState) { mInsetsState.set(insetsState); + if (!mInitialized) { return; } @@ -525,9 +536,51 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange // flicker. return; } + + // Check to see if insets changed in such a way that the divider algorithm needs to be + // recalculated. + Insets pinnedTaskbarInsets = calculatePinnedTaskbarInsets(insetsState); + if (!mPinnedTaskbarInsets.equals(pinnedTaskbarInsets)) { + mPinnedTaskbarInsets = pinnedTaskbarInsets; + // Refresh the DividerSnapAlgorithm. + mDividerSnapAlgorithm = getSnapAlgorithm(mContext, mRootBounds); + // If the divider is no longer placed on a snap point, animate it to the nearest one. + DividerSnapAlgorithm.SnapTarget snapTarget = + findSnapTarget(mDividerPosition, 0, false /* hardDismiss */); + if (snapTarget.position != mDividerPosition) { + snapToTarget(mDividerPosition, snapTarget, + InsetsController.ANIMATION_DURATION_RESIZE, + InsetsController.RESIZE_INTERPOLATOR); + } + } + mSplitWindowManager.onInsetsChanged(insetsState); } + /** + * Calculates the insets that might trigger a divider algorithm recalculation. Currently, only + * pinned Taskbar does this, and only when the IME is not showing. + */ + private Insets calculatePinnedTaskbarInsets(InsetsState insetsState) { + if (insetsState.isSourceOrDefaultVisible(InsetsSource.ID_IME, WindowInsets.Type.ime())) { + return Insets.NONE; + } + + // If IME is not showing... + for (int i = insetsState.sourceSize() - 1; i >= 0; i--) { + final InsetsSource source = insetsState.sourceAt(i); + // and Taskbar is pinned... + if (source.getType() == WindowInsets.Type.navigationBars() + && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)) { + // Return Insets representing the pinned taskbar state. + return source.calculateVisibleInsets(mRootBounds); + } + } + + // Else, divider can calculate based on the full display. + return Insets.NONE; + } + @Override public void insetsControlChanged(InsetsState insetsState, InsetsSourceControl[] activeControls) { @@ -604,25 +657,35 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange * Sets new divider position and updates bounds correspondingly. Notifies listener if the new * target indicates dismissing split. */ - public void snapToTarget(int currentPosition, SnapTarget snapTarget) { + public void snapToTarget(int currentPosition, SnapTarget snapTarget, int duration, + Interpolator interpolator) { switch (snapTarget.snapPosition) { case SNAP_TO_START_AND_DISMISS: - flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, () -> mSplitLayoutHandler.onSnappedToDismiss(false /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; case SNAP_TO_END_AND_DISMISS: - flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, () -> mSplitLayoutHandler.onSnappedToDismiss(true /* bottomOrRight */, EXIT_REASON_DRAG_DIVIDER)); break; default: - flingDividerPosition(currentPosition, snapTarget.position, FLING_RESIZE_DURATION, + flingDividerPosition(currentPosition, snapTarget.position, duration, interpolator, () -> setDividerPosition(snapTarget.position, true /* applyLayoutChange */)); break; } } + /** + * Same as {@link #snapToTarget(int, SnapTarget, int, Interpolator)}, with default animation + * duration and interpolator. + */ + public void snapToTarget(int currentPosition, SnapTarget snapTarget) { + snapToTarget(currentPosition, snapTarget, FLING_RESIZE_DURATION, + FAST_OUT_SLOW_IN); + } + void onStartDragging() { mInteractionJankMonitor.begin(getDividerLeash(), mContext, mHandler, CUJ_SPLIT_SCREEN_RESIZE); @@ -667,6 +730,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerSize, mIsLeftRightSplit, insets, + mPinnedTaskbarInsets.toRect(), mIsLeftRightSplit ? DOCKED_LEFT : DOCKED_TOP /* dockSide */); } @@ -674,14 +738,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void flingDividerToDismiss(boolean toEnd, int reason) { final int target = toEnd ? mDividerSnapAlgorithm.getDismissEndTarget().position : mDividerSnapAlgorithm.getDismissStartTarget().position; - flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION, + flingDividerPosition(getDividerPosition(), target, FLING_EXIT_DURATION, FAST_OUT_SLOW_IN, () -> mSplitLayoutHandler.onSnappedToDismiss(toEnd, reason)); } /** Fling divider from current position to center position. */ public void flingDividerToCenter(@Nullable Runnable finishCallback) { final int pos = mDividerSnapAlgorithm.getMiddleTarget().position; - flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION, + flingDividerPosition(getDividerPosition(), pos, FLING_ENTER_DURATION, FAST_OUT_SLOW_IN, () -> { setDividerPosition(pos, true /* applyLayoutChange */); if (finishCallback != null) { @@ -699,14 +763,16 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange public void flingDividerToOtherSide(@PersistentSnapPosition int currentSnapPosition) { switch (currentSnapPosition) { case SNAP_TO_2_10_90 -> - snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget()); + snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getLastSplitTarget(), + FLING_OFFSCREEN_DURATION, EMPHASIZED); case SNAP_TO_2_90_10 -> - snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getFirstSplitTarget()); + snapToTarget(mDividerPosition, mDividerSnapAlgorithm.getFirstSplitTarget(), + FLING_OFFSCREEN_DURATION, EMPHASIZED); } } @VisibleForTesting - void flingDividerPosition(int from, int to, int duration, + void flingDividerPosition(int from, int to, int duration, Interpolator interpolator, @Nullable Runnable flingFinishedCallback) { if (from == to) { if (flingFinishedCallback != null) { @@ -724,7 +790,7 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange mDividerFlingAnimator = ValueAnimator .ofInt(from, to) .setDuration(duration); - mDividerFlingAnimator.setInterpolator(Interpolators.FAST_OUT_SLOW_IN); + mDividerFlingAnimator.setInterpolator(interpolator); // If the divider is being physically controlled by the user, we use a cool parallax effect // on the task windows. So if this "snap" animation is an extension of a user-controlled @@ -1048,6 +1114,14 @@ public final class SplitLayout implements DisplayInsetsController.OnInsetsChange return (int) (minWidth / density); } + public int getDisplayWidth() { + return mRootBounds.width(); + } + + public int getDisplayHeight() { + return mRootBounds.height(); + } + /** * Shift configuration bounds to prevent client apps get configuration changed or relaunch. And * restore shifted configuration bounds if it's no longer shifted. diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java index 65bf389f3819..9113c0a53178 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/split/SplitScreenUtils.java @@ -142,13 +142,25 @@ public class SplitScreenUtils { } /** + * Convenience function for {@link #isLargeScreen(Configuration)}. + */ + public static boolean isLargeScreen(Resources res) { + return isLargeScreen(res.getConfiguration()); + } + + /** + * Returns whether the current device is a foldable + */ + public static boolean isFoldable(Resources res) { + return res.getIntArray(com.android.internal.R.array.config_foldedDeviceStates).length != 0; + } + + /** * Returns whether we should allow split ratios to go offscreen or not. If the device is a phone * or a foldable (either screen), we allow it. */ public static boolean allowOffscreenRatios(Resources res) { - return !isLargeScreen(res.getConfiguration()) - || - res.getIntArray(com.android.internal.R.array.config_foldedDeviceStates).length != 0; + return Flags.enableFlexibleTwoAppSplit() && (!isLargeScreen(res) || isFoldable(res)); } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java index 688f8ca2dc75..49c2785f1ecd 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/CompatUILayout.java @@ -68,7 +68,7 @@ class CompatUILayout extends LinearLayout { private void setViewVisibility(@IdRes int resId, boolean show) { final View view = findViewById(resId); - int visibility = show ? View.VISIBLE : View.GONE; + int visibility = show ? View.VISIBLE : View.INVISIBLE; if (view.getVisibility() == visibility) { return; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java index b141bebbe8b1..fd1bbc477cf4 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/UserAspectRatioSettingsLayout.java @@ -100,7 +100,7 @@ public class UserAspectRatioSettingsLayout extends LinearLayout { private void setViewVisibility(@IdRes int resId, boolean show) { final View view = findViewById(resId); - int visibility = show ? View.VISIBLE : View.GONE; + int visibility = show ? View.VISIBLE : View.INVISIBLE; if (view.getVisibility() == visibility) { return; } @@ -171,7 +171,7 @@ public class UserAspectRatioSettingsLayout extends LinearLayout { fadeOut.addListener(new AnimatorListenerAdapter() { @Override public void onAnimationEnd(Animator animation) { - view.setVisibility(View.GONE); + view.setVisibility(View.INVISIBLE); } }); fadeOut.start(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS new file mode 100644 index 000000000000..752d2fd721a5 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/OWNERS @@ -0,0 +1,2 @@ +# WM Shell sub-module dagger owners +jorgegil@google.com
\ No newline at end of file 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 0942e058bbdc..817be3b1fe0d 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.dagger; import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS; import static android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_TASK_LIMIT; +import static android.window.DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS; import android.annotation.Nullable; import android.app.KeyguardManager; @@ -87,6 +88,8 @@ import com.android.wm.shell.desktopmode.education.AppHandleEducationController; import com.android.wm.shell.desktopmode.education.AppHandleEducationFilter; import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository; import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository; +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer; +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializerImpl; import com.android.wm.shell.draganddrop.DragAndDropController; import com.android.wm.shell.draganddrop.GlobalDragListener; import com.android.wm.shell.freeform.FreeformComponents; @@ -153,12 +156,7 @@ import java.util.Optional; * <p>This module only defines Shell dependencies for handheld SystemUI implementation. Common * dependencies should go into {@link WMShellBaseModule}. */ -@Module( - includes = { - WMShellBaseModule.class, - PipModule.class, - ShellBackAnimationModule.class - }) +@Module(includes = {WMShellBaseModule.class, PipModule.class, ShellBackAnimationModule.class}) public abstract class WMShellModule { // @@ -173,8 +171,7 @@ public abstract class WMShellModule { @WMSingleton @Provides - static BubblePositioner provideBubblePositioner(Context context, - WindowManager windowManager) { + static BubblePositioner provideBubblePositioner(Context context, WindowManager windowManager) { return new BubblePositioner(context, windowManager); } @@ -186,20 +183,22 @@ public abstract class WMShellModule { @WMSingleton @Provides - static BubbleData provideBubbleData(Context context, + static BubbleData provideBubbleData( + Context context, BubbleLogger logger, BubblePositioner positioner, BubbleEducationController educationController, @ShellMainThread ShellExecutor mainExecutor, @ShellBackgroundThread ShellExecutor bgExecutor) { - return new BubbleData(context, logger, positioner, educationController, mainExecutor, - bgExecutor); + return new BubbleData( + context, logger, positioner, educationController, mainExecutor, bgExecutor); } // Note: Handler needed for LauncherApps.register @WMSingleton @Provides - static BubbleController provideBubbleController(Context context, + static BubbleController provideBubbleController( + Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -224,14 +223,38 @@ public abstract class WMShellModule { Transitions transitions, SyncTransactionQueue syncQueue, IWindowManager wmService) { - return new BubbleController(context, shellInit, shellCommandHandler, shellController, data, - null /* synchronizer */, floatingContentCoordinator, - new BubbleDataRepository(launcherApps, mainExecutor, bgExecutor, + return new BubbleController( + context, + shellInit, + shellCommandHandler, + shellController, + data, + null /* synchronizer */, + floatingContentCoordinator, + new BubbleDataRepository( + launcherApps, + mainExecutor, + bgExecutor, new BubblePersistentRepository(context)), - statusBarService, windowManager, windowManagerShellWrapper, userManager, - launcherApps, logger, taskStackListener, organizer, positioner, displayController, - oneHandedOptional, dragAndDropController, mainExecutor, mainHandler, bgExecutor, - taskViewTransitions, transitions, syncQueue, wmService, + statusBarService, + windowManager, + windowManagerShellWrapper, + userManager, + launcherApps, + logger, + taskStackListener, + organizer, + positioner, + displayController, + oneHandedOptional, + dragAndDropController, + mainExecutor, + mainHandler, + bgExecutor, + taskViewTransitions, + transitions, + syncQueue, + wmService, ProdBubbleProperties.INSTANCE); } @@ -318,9 +341,7 @@ public abstract class WMShellModule { @WMSingleton @Provides static AppToWebGenericLinksParser provideGenericLinksParser( - Context context, - @ShellMainThread ShellExecutor mainExecutor - ) { + Context context, @ShellMainThread ShellExecutor mainExecutor) { return new AppToWebGenericLinksParser(context, mainExecutor); } @@ -328,8 +349,7 @@ public abstract class WMShellModule { static AssistContentRequester provideAssistContentRequester( Context context, @ShellMainThread ShellExecutor shellExecutor, - @ShellBackgroundThread ShellExecutor bgExecutor - ) { + @ShellBackgroundThread ShellExecutor bgExecutor) { return new AssistContentRequester(context, shellExecutor, bgExecutor); } @@ -366,15 +386,20 @@ public abstract class WMShellModule { Optional<DesktopRepository> desktopRepository, Optional<DesktopTasksController> desktopTasksController, LaunchAdjacentController launchAdjacentController, - WindowDecorViewModel windowDecorViewModel) { + WindowDecorViewModel windowDecorViewModel, + Optional<TaskChangeListener> taskChangeListener) { // TODO(b/238217847): Temporarily add this check here until we can remove the dynamic // override for this controller from the base module - ShellInit init = FreeformComponents.isFreeformEnabled(context) - ? shellInit - : null; - return new FreeformTaskListener(context, init, shellTaskOrganizer, - desktopRepository, desktopTasksController, launchAdjacentController, - windowDecorViewModel); + ShellInit init = FreeformComponents.isFreeformEnabled(context) ? shellInit : null; + return new FreeformTaskListener( + context, + init, + shellTaskOrganizer, + desktopRepository, + desktopTasksController, + launchAdjacentController, + windowDecorViewModel, + taskChangeListener); } @WMSingleton @@ -385,10 +410,7 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor mainExecutor, @ShellAnimationThread ShellExecutor animExecutor) { return new FreeformTaskTransitionHandler( - transitions, - displayController, - mainExecutor, - animExecutor); + transitions, displayController, mainExecutor, animExecutor); } @WMSingleton @@ -402,8 +424,13 @@ public abstract class WMShellModule { Optional<TaskChangeListener> taskChangeListener, FocusTransitionObserver focusTransitionObserver) { return new FreeformTaskTransitionObserver( - context, shellInit, transitions, desktopImmersiveController, - windowDecorViewModel, taskChangeListener, focusTransitionObserver); + context, + shellInit, + transitions, + desktopImmersiveController, + windowDecorViewModel, + taskChangeListener, + focusTransitionObserver); } @WMSingleton @@ -419,8 +446,8 @@ public abstract class WMShellModule { } else { transitionStarter = freeformTaskTransitionHandler; } - return new FreeformTaskTransitionStarterInitializer(shellInit, windowDecorViewModel, - transitionStarter); + return new FreeformTaskTransitionStarterInitializer( + shellInit, windowDecorViewModel, transitionStarter); } // @@ -431,7 +458,8 @@ public abstract class WMShellModule { @WMSingleton @Provides @DynamicOverride - static OneHandedController provideOneHandedController(Context context, + static OneHandedController provideOneHandedController( + Context context, ShellInit shellInit, ShellCommandHandler shellCommandHandler, ShellController shellController, @@ -443,9 +471,19 @@ public abstract class WMShellModule { InteractionJankMonitor jankMonitor, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { - return OneHandedController.create(context, shellInit, shellCommandHandler, shellController, - windowManager, displayController, displayLayout, taskStackListener, jankMonitor, - uiEventLogger, mainExecutor, mainHandler); + return OneHandedController.create( + context, + shellInit, + shellCommandHandler, + shellController, + windowManager, + displayController, + displayLayout, + taskStackListener, + jankMonitor, + uiEventLogger, + mainExecutor, + mainHandler); } // @@ -477,12 +515,29 @@ public abstract class WMShellModule { MultiInstanceHelper multiInstanceHelper, @ShellMainThread ShellExecutor mainExecutor, @ShellMainThread Handler mainHandler) { - return new SplitScreenController(context, shellInit, shellCommandHandler, shellController, - shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, displayController, - displayImeController, displayInsetsController, dragAndDropController, transitions, - transactionPool, iconProvider, recentTasks, launchAdjacentController, - windowDecorViewModel, desktopTasksController, null /* stageCoordinator */, - multiInstanceHelper, mainExecutor, mainHandler); + return new SplitScreenController( + context, + shellInit, + shellCommandHandler, + shellController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + displayController, + displayImeController, + displayInsetsController, + dragAndDropController, + transitions, + transactionPool, + iconProvider, + recentTasks, + launchAdjacentController, + windowDecorViewModel, + desktopTasksController, + null /* stageCoordinator */, + multiInstanceHelper, + mainExecutor, + mainHandler); } // @@ -502,10 +557,16 @@ public abstract class WMShellModule { Optional<UnfoldTransitionHandler> unfoldHandler, Optional<ActivityEmbeddingController> activityEmbeddingController, Transitions transitions) { - return new DefaultMixedHandler(shellInit, transitions, splitScreenOptional, - pipTransitionController, recentsTransitionHandler, - keyguardTransitionHandler, desktopTasksController, - unfoldHandler, activityEmbeddingController); + return new DefaultMixedHandler( + shellInit, + transitions, + splitScreenOptional, + pipTransitionController, + recentsTransitionHandler, + keyguardTransitionHandler, + desktopTasksController, + unfoldHandler, + activityEmbeddingController); } @WMSingleton @@ -516,8 +577,12 @@ public abstract class WMShellModule { Transitions transitions, Optional<RecentTasksController> recentTasksController, HomeTransitionObserver homeTransitionObserver) { - return new RecentsTransitionHandler(shellInit, shellTaskOrganizer, transitions, - recentTasksController.orElse(null), homeTransitionObserver); + return new RecentsTransitionHandler( + shellInit, + shellTaskOrganizer, + transitions, + recentTasksController.orElse(null), + homeTransitionObserver); } // @@ -534,8 +599,7 @@ public abstract class WMShellModule { FullscreenUnfoldTaskAnimator fullscreenAnimator, Lazy<Optional<UnfoldTransitionHandler>> unfoldTransitionHandler, ShellInit shellInit, - @ShellMainThread ShellExecutor mainExecutor - ) { + @ShellMainThread ShellExecutor mainExecutor) { final List<UnfoldTaskAnimator> animators = new ArrayList<>(); animators.add(splitAnimator); animators.add(fullscreenAnimator); @@ -546,8 +610,7 @@ public abstract class WMShellModule { progressProvider.get(), animators, unfoldTransitionHandler, - mainExecutor - ); + mainExecutor); } @Provides @@ -555,10 +618,9 @@ public abstract class WMShellModule { Context context, UnfoldBackgroundController unfoldBackgroundController, ShellController shellController, - DisplayInsetsController displayInsetsController - ) { - return new FullscreenUnfoldTaskAnimator(context, unfoldBackgroundController, - shellController, displayInsetsController); + DisplayInsetsController displayInsetsController) { + return new FullscreenUnfoldTaskAnimator( + context, unfoldBackgroundController, shellController, displayInsetsController); } @Provides @@ -568,14 +630,18 @@ public abstract class WMShellModule { ShellController shellController, @ShellMainThread ShellExecutor executor, Lazy<Optional<SplitScreenController>> splitScreenOptional, - DisplayInsetsController displayInsetsController - ) { + DisplayInsetsController displayInsetsController) { // TODO(b/238217847): The lazy reference here causes some dependency issues since it // immediately registers a listener on that controller on init. We should reference the // controller directly once we refactor ShellTaskOrganizer to not depend on the unfold // animation controller directly. - return new SplitTaskUnfoldAnimator(context, executor, splitScreenOptional, - shellController, backgroundController, displayInsetsController); + return new SplitTaskUnfoldAnimator( + context, + executor, + splitScreenOptional, + shellController, + backgroundController, + displayInsetsController); } @WMSingleton @@ -602,8 +668,15 @@ public abstract class WMShellModule { @ShellMainThread ShellExecutor executor, @ShellMainThread Handler handler, ShellInit shellInit) { - return new UnfoldTransitionHandler(shellInit, progressProvider.get(), animator, - unfoldAnimator, transactionPool, executor, handler, transitions); + return new UnfoldTransitionHandler( + shellInit, + progressProvider.get(), + animator, + unfoldAnimator, + transactionPool, + executor, + handler, + transitions); } @WMSingleton @@ -632,6 +705,7 @@ public abstract class WMShellModule { Transitions transitions, KeyguardManager keyguardManager, ReturnToDragStartAnimator returnToDragStartAnimator, + Optional<DesktopMixedTransitionHandler> desktopMixedTransitionHandler, EnterDesktopTaskTransitionHandler enterDesktopTransitionHandler, ExitDesktopTaskTransitionHandler exitDesktopTransitionHandler, DesktopModeDragAndDropTransitionHandler desktopModeDragAndDropTransitionHandler, @@ -652,18 +726,39 @@ public abstract class WMShellModule { FocusTransitionObserver focusTransitionObserver, DesktopModeEventLogger desktopModeEventLogger, DesktopTilingDecorViewModel desktopTilingDecorViewModel) { - return new DesktopTasksController(context, shellInit, shellCommandHandler, shellController, - displayController, shellTaskOrganizer, syncQueue, rootTaskDisplayAreaOrganizer, - dragAndDropController, transitions, keyguardManager, - returnToDragStartAnimator, enterDesktopTransitionHandler, - exitDesktopTransitionHandler, desktopModeDragAndDropTransitionHandler, + return new DesktopTasksController( + context, + shellInit, + shellCommandHandler, + shellController, + displayController, + shellTaskOrganizer, + syncQueue, + rootTaskDisplayAreaOrganizer, + dragAndDropController, + transitions, + keyguardManager, + returnToDragStartAnimator, + desktopMixedTransitionHandler.get(), + enterDesktopTransitionHandler, + exitDesktopTransitionHandler, + desktopModeDragAndDropTransitionHandler, toggleResizeDesktopTaskTransitionHandler, - dragToDesktopTransitionHandler, desktopImmersiveController.get(), + dragToDesktopTransitionHandler, + desktopImmersiveController.get(), desktopRepository, - desktopModeLoggerTransitionObserver, launchAdjacentController, - recentsTransitionHandler, multiInstanceHelper, mainExecutor, desktopTasksLimiter, - recentTasksController.orElse(null), interactionJankMonitor, mainHandler, - inputManager, focusTransitionObserver, desktopModeEventLogger, + desktopModeLoggerTransitionObserver, + launchAdjacentController, + recentsTransitionHandler, + multiInstanceHelper, + mainExecutor, + desktopTasksLimiter, + recentTasksController.orElse(null), + interactionJankMonitor, + mainHandler, + inputManager, + focusTransitionObserver, + desktopModeEventLogger, desktopTilingDecorViewModel); } @@ -693,10 +788,11 @@ public abstract class WMShellModule { @WMSingleton @Provides - static Optional<TaskChangeListener> provideDesktopTaskChangeListener(Context context) { - if (Flags.enableWindowingTransitionHandlersObservers() && - DesktopModeStatus.canEnterDesktopMode(context)) { - return Optional.of(new DesktopTaskChangeListener()); + static Optional<TaskChangeListener> provideDesktopTaskChangeListener( + Context context, @DynamicOverride DesktopRepository desktopRepository) { + if (ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS.isTrue() + && DesktopModeStatus.canEnterDesktopMode(context)) { + return Optional.of(new DesktopTaskChangeListener(desktopRepository)); } return Optional.empty(); } @@ -724,8 +820,7 @@ public abstract class WMShellModule { maxTaskLimit, interactionJankMonitor, context, - handler) - ); + handler)); } @WMSingleton @@ -739,10 +834,7 @@ public abstract class WMShellModule { if (DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.of( new DesktopImmersiveController( - transitions, - desktopRepository, - displayController, - shellTaskOrganizer)); + transitions, desktopRepository, displayController, shellTaskOrganizer)); } return Optional.empty(); } @@ -750,11 +842,10 @@ public abstract class WMShellModule { @WMSingleton @Provides static ReturnToDragStartAnimator provideReturnToDragStartAnimator( - Context context, InteractionJankMonitor interactionJankMonitor) { - return new ReturnToDragStartAnimator(context, interactionJankMonitor); + InteractionJankMonitor interactionJankMonitor) { + return new ReturnToDragStartAnimator(interactionJankMonitor); } - @WMSingleton @Provides static DragToDesktopTransitionHandler provideDragToDesktopTransitionHandler( @@ -762,12 +853,12 @@ public abstract class WMShellModule { Transitions transitions, RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer, InteractionJankMonitor interactionJankMonitor) { - return (Flags.enableDesktopWindowingTransitions() || - ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS.isTrue()) - ? new SpringDragToDesktopTransitionHandler(context, transitions, - rootTaskDisplayAreaOrganizer, interactionJankMonitor) - : new DefaultDragToDesktopTransitionHandler(context, transitions, - rootTaskDisplayAreaOrganizer, interactionJankMonitor); + return (Flags.enableDesktopWindowingTransitions() + || ENABLE_DESKTOP_WINDOWING_ENTER_TRANSITIONS.isTrue()) + ? new SpringDragToDesktopTransitionHandler( + context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor) + : new DefaultDragToDesktopTransitionHandler( + context, transitions, rootTaskDisplayAreaOrganizer, interactionJankMonitor); } @WMSingleton @@ -802,30 +893,29 @@ public abstract class WMShellModule { static CloseDesktopTaskTransitionHandler provideCloseDesktopTaskTransitionHandler( Context context, @ShellMainThread ShellExecutor mainExecutor, - @ShellAnimationThread ShellExecutor animExecutor - ) { + @ShellAnimationThread ShellExecutor animExecutor) { return new CloseDesktopTaskTransitionHandler(context, mainExecutor, animExecutor); } @WMSingleton @Provides static DesktopModeDragAndDropTransitionHandler provideDesktopModeDragAndDropTransitionHandler( - Transitions transitions - ) { + Transitions transitions) { return new DesktopModeDragAndDropTransitionHandler(transitions); } @WMSingleton @Provides @DynamicOverride - static DesktopRepository provideDesktopRepository( Context context, ShellInit shellInit, DesktopPersistentRepository desktopPersistentRepository, + DesktopRepositoryInitializer desktopRepositoryInitializer, @ShellMainThread CoroutineScope mainScope ) { return new DesktopRepository(context, shellInit, desktopPersistentRepository, + desktopRepositoryInitializer, mainScope); } @@ -837,12 +927,16 @@ public abstract class WMShellModule { ShellTaskOrganizer shellTaskOrganizer, TaskStackListenerImpl taskStackListener, ToggleResizeDesktopTaskTransitionHandler toggleResizeDesktopTaskTransitionHandler, - @DynamicOverride DesktopRepository desktopRepository - ) { + @DynamicOverride DesktopRepository desktopRepository) { if (DesktopModeStatus.canEnterDesktopMode(context)) { - return Optional.of(new DesktopActivityOrientationChangeHandler( - context, shellInit, shellTaskOrganizer, taskStackListener, - toggleResizeDesktopTaskTransitionHandler, desktopRepository)); + return Optional.of( + new DesktopActivityOrientationChangeHandler( + context, + shellInit, + shellTaskOrganizer, + taskStackListener, + toggleResizeDesktopTaskTransitionHandler, + desktopRepository)); } return Optional.empty(); } @@ -854,12 +948,16 @@ public abstract class WMShellModule { Optional<DesktopRepository> desktopRepository, Transitions transitions, ShellTaskOrganizer shellTaskOrganizer, - ShellInit shellInit - ) { - return desktopRepository.flatMap(repository -> - Optional.of(new DesktopTasksTransitionObserver( - context, repository, transitions, shellTaskOrganizer, shellInit)) - ); + ShellInit shellInit) { + return desktopRepository.flatMap( + repository -> + Optional.of( + new DesktopTasksTransitionObserver( + context, + repository, + transitions, + shellTaskOrganizer, + shellInit))); } @WMSingleton @@ -870,8 +968,11 @@ public abstract class WMShellModule { @DynamicOverride DesktopRepository desktopRepository, FreeformTaskTransitionHandler freeformTaskTransitionHandler, CloseDesktopTaskTransitionHandler closeDesktopTaskTransitionHandler, + Optional<DesktopImmersiveController> desktopImmersiveController, InteractionJankMonitor interactionJankMonitor, - @ShellMainThread Handler handler + @ShellMainThread Handler handler, + ShellInit shellInit, + RootTaskDisplayAreaOrganizer rootTaskDisplayAreaOrganizer ) { if (!DesktopModeStatus.canEnterDesktopMode(context)) { return Optional.empty(); @@ -883,8 +984,11 @@ public abstract class WMShellModule { desktopRepository, freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, + desktopImmersiveController.get(), interactionJankMonitor, - handler)); + handler, + shellInit, + rootTaskDisplayAreaOrganizer)); } @WMSingleton @@ -929,12 +1033,11 @@ public abstract class WMShellModule { @Provides static DesktopWindowingEducationTooltipController provideDesktopWindowingEducationTooltipController( - Context context, - AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, - DisplayController displayController - ) { - return new DesktopWindowingEducationTooltipController(context, - additionalSystemViewContainerFactory, displayController); + Context context, + AdditionalSystemViewContainer.Factory additionalSystemViewContainerFactory, + DisplayController displayController) { + return new DesktopWindowingEducationTooltipController( + context, additionalSystemViewContainerFactory, displayController); } @OptIn(markerClass = ExperimentalCoroutinesApi.class) @@ -946,22 +1049,35 @@ public abstract class WMShellModule { AppHandleEducationDatastoreRepository appHandleEducationDatastoreRepository, WindowDecorCaptionHandleRepository windowDecorCaptionHandleRepository, DesktopWindowingEducationTooltipController desktopWindowingEducationTooltipController, - @ShellMainThread CoroutineScope applicationScope, @ShellBackgroundThread - MainCoroutineDispatcher backgroundDispatcher) { - return new AppHandleEducationController(context, appHandleEducationFilter, - appHandleEducationDatastoreRepository, windowDecorCaptionHandleRepository, - desktopWindowingEducationTooltipController, applicationScope, + @ShellMainThread CoroutineScope applicationScope, + @ShellBackgroundThread MainCoroutineDispatcher backgroundDispatcher) { + return new AppHandleEducationController( + context, + appHandleEducationFilter, + appHandleEducationDatastoreRepository, + windowDecorCaptionHandleRepository, + desktopWindowingEducationTooltipController, + applicationScope, backgroundDispatcher); } @WMSingleton @Provides static DesktopPersistentRepository provideDesktopPersistentRepository( - Context context, - @ShellBackgroundThread CoroutineScope bgScope) { + Context context, @ShellBackgroundThread CoroutineScope bgScope) { return new DesktopPersistentRepository(context, bgScope); } + @WMSingleton + @Provides + static DesktopRepositoryInitializer provideDesktopRepositoryInitializer( + Context context, + DesktopPersistentRepository desktopPersistentRepository, + @ShellMainThread CoroutineScope mainScope) { + return new DesktopRepositoryInitializerImpl(context, desktopPersistentRepository, + mainScope); + } + // // Drag and drop // @@ -969,14 +1085,14 @@ public abstract class WMShellModule { @WMSingleton @Provides static GlobalDragListener provideGlobalDragListener( - IWindowManager wmService, - @ShellMainThread ShellExecutor mainExecutor) { + IWindowManager wmService, @ShellMainThread ShellExecutor mainExecutor) { return new GlobalDragListener(wmService, mainExecutor); } @WMSingleton @Provides - static DragAndDropController provideDragAndDropController(Context context, + static DragAndDropController provideDragAndDropController( + Context context, ShellInit shellInit, ShellController shellController, ShellCommandHandler shellCommandHandler, @@ -987,9 +1103,18 @@ public abstract class WMShellModule { GlobalDragListener globalDragListener, Transitions transitions, @ShellMainThread ShellExecutor mainExecutor) { - return new DragAndDropController(context, shellInit, shellController, shellCommandHandler, - shellTaskOrganizer, displayController, uiEventLogger, iconProvider, - globalDragListener, transitions, mainExecutor); + return new DragAndDropController( + context, + shellInit, + shellController, + shellCommandHandler, + shellTaskOrganizer, + displayController, + uiEventLogger, + iconProvider, + globalDragListener, + transitions, + mainExecutor); } // @@ -1003,8 +1128,7 @@ public abstract class WMShellModule { @Provides static Object provideIndependentShellComponentsToCreate( DragAndDropController dragAndDropController, - Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional - ) { + Optional<DesktopTasksTransitionObserver> desktopTasksTransitionObserverOptional) { return new Object(); } } 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 d0bc5f0955f7..f69aa6df6a1d 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 @@ -130,7 +130,8 @@ class DesktopImmersiveController( displayId: Int ) { if (!Flags.enableFullyImmersiveInDesktop()) return - exitImmersiveIfApplicable(wct, displayId)?.invoke(transition) + val result = exitImmersiveIfApplicable(wct, displayId) + result.asExit()?.runOnTransitionStart?.invoke(transition) } /** @@ -145,16 +146,23 @@ class DesktopImmersiveController( wct: WindowContainerTransaction, displayId: Int, excludeTaskId: Int? = null, - ): ((IBinder) -> Unit)? { - if (!Flags.enableFullyImmersiveInDesktop()) return null - val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) ?: return null + ): ExitResult { + if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit + val immersiveTask = desktopRepository.getTaskInFullImmersiveState(displayId) + ?: return ExitResult.NoExit if (immersiveTask == excludeTaskId) { - return null + return ExitResult.NoExit } - val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) ?: return null + val taskInfo = shellTaskOrganizer.getRunningTaskInfo(immersiveTask) + ?: return ExitResult.NoExit logV("Appending immersive exit for task: $immersiveTask in display: $displayId") wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo)) - return { transition -> addPendingImmersiveExit(immersiveTask, displayId, transition) } + return ExitResult.Exit( + exitingTask = immersiveTask, + runOnTransitionStart = { transition -> + addPendingImmersiveExit(immersiveTask, displayId, transition) + } + ) } /** @@ -167,22 +175,25 @@ class DesktopImmersiveController( fun exitImmersiveIfApplicable( wct: WindowContainerTransaction, taskInfo: RunningTaskInfo - ): ((IBinder) -> Unit)? { - if (!Flags.enableFullyImmersiveInDesktop()) return null + ): ExitResult { + if (!Flags.enableFullyImmersiveInDesktop()) return ExitResult.NoExit if (desktopRepository.isTaskInFullImmersiveState(taskInfo.taskId)) { // A full immersive task is being minimized, make sure the immersive state is broken // (i.e. resize back to max bounds). wct.setBounds(taskInfo.token, getExitDestinationBounds(taskInfo)) logV("Appending immersive exit for task: ${taskInfo.taskId}") - return { transition -> - addPendingImmersiveExit( - taskId = taskInfo.taskId, - displayId = taskInfo.displayId, - transition = transition - ) - } + return ExitResult.Exit( + exitingTask = taskInfo.taskId, + runOnTransitionStart = { transition -> + addPendingImmersiveExit( + taskId = taskInfo.taskId, + displayId = taskInfo.displayId, + transition = transition + ) + } + ) } - return null + return ExitResult.NoExit } @@ -213,9 +224,9 @@ class DesktopImmersiveController( finishTransaction: SurfaceControl.Transaction, finishCallback: Transitions.TransitionFinishCallback ): Boolean { - logD("startAnimation transition=%s", transition) val state = requireState() if (transition != state.transition) return false + logD("startAnimation transition=%s", transition) animateResize( targetTaskId = state.taskId, info = info, @@ -323,7 +334,6 @@ class DesktopImmersiveController( startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, ) { - logD("onTransitionReady transition=%s", transition) // Check if this is a pending external exit transition. val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == transition } @@ -391,7 +401,6 @@ class DesktopImmersiveController( } override fun onTransitionMerged(merged: IBinder, playing: IBinder) { - logD("onTransitionMerged merged=%s playing=%s", merged, playing) val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == merged } if (pendingExit != null) { @@ -404,7 +413,6 @@ class DesktopImmersiveController( } override fun onTransitionFinished(transition: IBinder, aborted: Boolean) { - logD("onTransitionFinished transition=%s aborted=%b", transition, aborted) val pendingExit = pendingExternalExitTransitions .firstOrNull { pendingExit -> pendingExit.transition == transition } if (pendingExit != null) { @@ -461,6 +469,20 @@ class DesktopImmersiveController( var transition: IBinder, ) + /** The result of an external exit request. */ + sealed class ExitResult { + /** An immersive task exit (meaning, resize) was appended to the request. */ + data class Exit( + val exitingTask: Int, + val runOnTransitionStart: ((IBinder) -> Unit) + ) : ExitResult() + /** There was no exit appended to the request. */ + data object NoExit : ExitResult() + + /** Returns the result as an [Exit] or null if it isn't of that type. */ + fun asExit(): Exit? = if (this is Exit) this else null + } + private enum class Direction { ENTER, EXIT } 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 54a07f20fd84..48bb2a8b4a74 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 @@ -25,17 +25,24 @@ import android.view.SurfaceControl import android.view.WindowManager import android.window.DesktopModeFlags import android.window.TransitionInfo +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 import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE +import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.MixedTransitionHandler import com.android.wm.shell.transition.Transitions +import com.android.wm.shell.transition.Transitions.TransitionFinishCallback /** The [Transitions.TransitionHandler] coordinates transition handlers in desktop windowing. */ class DesktopMixedTransitionHandler( @@ -44,10 +51,20 @@ class DesktopMixedTransitionHandler( private val desktopRepository: DesktopRepository, private val freeformTaskTransitionHandler: FreeformTaskTransitionHandler, private val closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler, + private val desktopImmersiveController: DesktopImmersiveController, private val interactionJankMonitor: InteractionJankMonitor, @ShellMainThread private val handler: Handler, + shellInit: ShellInit, + private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer, ) : MixedTransitionHandler, FreeformTaskTransitionStarter { + init { + shellInit.addInitCallback ({ transitions.addHandler(this) }, this) + } + + @VisibleForTesting + val pendingMixedTransitions = mutableListOf<PendingMixedTransition>() + /** Delegates starting transition to [FreeformTaskTransitionHandler]. */ override fun startWindowingModeTransition( targetWindowingMode: Int, @@ -65,6 +82,48 @@ class DesktopMixedTransitionHandler( } requireNotNull(wct) return transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, /* handler= */ this) + .also { transition -> + pendingMixedTransitions.add(PendingMixedTransition.Close(transition)) + } + } + + /** + * Starts a launch transition for [taskId], with an optional [exitingImmersiveTask] if it was + * included in the [wct] and is expected to be animated by this handler. + */ + fun startLaunchTransition( + @WindowManager.TransitionType transitionType: Int, + wct: WindowContainerTransaction, + taskId: Int, + minimizingTaskId: Int? = null, + exitingImmersiveTask: Int? = null, + ): IBinder { + if (!Flags.enableFullyImmersiveInDesktop() && + !DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { + return transitions.startTransition(transitionType, wct, /* handler= */ null) + } + if (exitingImmersiveTask == null) { + logV("Starting mixed launch transition for task#%d", taskId) + } else { + logV( + "Starting mixed launch transition for task#%d with immersive exit of task#%d", + taskId, exitingImmersiveTask + ) + } + return transitions.startTransition(transitionType, wct, /* handler= */ this) + .also { transition -> + pendingMixedTransitions.add(PendingMixedTransition.Launch( + transition = transition, + launchingTask = taskId, + minimizingTask = minimizingTaskId, + exitingImmersiveTask = exitingImmersiveTask, + )) + } + } + + /** Notifies this handler that there is a pending transition for it to handle. */ + fun addPendingMixedTransition(pendingMixedTransition: PendingMixedTransition) { + pendingMixedTransitions.add(pendingMixedTransition) } /** Returns null, as it only handles transitions started from Shell. */ @@ -78,11 +137,43 @@ class DesktopMixedTransitionHandler( info: TransitionInfo, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, - finishCallback: Transitions.TransitionFinishCallback, + finishCallback: TransitionFinishCallback, + ): Boolean { + val pending = pendingMixedTransitions.find { pending -> pending.transition == transition } + ?: return false.also { + logV("No pending desktop transition") + } + pendingMixedTransitions.remove(pending) + logV("Animating pending mixed transition: %s", pending) + return when (pending) { + is PendingMixedTransition.Close -> animateCloseTransition( + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) + is PendingMixedTransition.Launch -> animateLaunchTransition( + pending, + transition, + info, + startTransaction, + finishTransaction, + finishCallback + ) + } + } + + private fun animateCloseTransition( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback, ): Boolean { val closeChange = findCloseDesktopTaskChange(info) if (closeChange == null) { - ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: Should have closing desktop task", TAG) + logW("Should have closing desktop task") return false } if (isLastDesktopTask(closeChange)) { @@ -106,6 +197,85 @@ class DesktopMixedTransitionHandler( ) } + private fun animateLaunchTransition( + pending: PendingMixedTransition.Launch, + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback, + ): Boolean { + // Check if there's also an immersive change during this launch. + val immersiveExitChange = pending.exitingImmersiveTask?.let { exitingTask -> + findDesktopTaskChange(info, exitingTask) + } + val minimizeChange = pending.minimizingTask?.let { minimizingTask -> + findDesktopTaskChange(info, minimizingTask) + } + val launchChange = findDesktopTaskChange(info, pending.launchingTask) + ?: error("Should have pending launching task change") + + var subAnimationCount = -1 + var combinedWct: WindowContainerTransaction? = null + val finishCb = TransitionFinishCallback { wct -> + --subAnimationCount + combinedWct = combinedWct.merge(wct) + if (subAnimationCount > 0) return@TransitionFinishCallback + finishCallback.onTransitionFinished(combinedWct) + } + + logV( + "Animating mixed launch transition task#%d, minimizingTask#%s immersiveExitTask#%s", + launchChange.taskInfo!!.taskId, minimizeChange?.taskInfo?.taskId, + immersiveExitChange?.taskInfo?.taskId + ) + if (DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { + // Only apply minimize change reparenting here if we implement the new app launch + // transitions, otherwise this reparenting is handled in the default handler. + minimizeChange?.let { + applyMinimizeChangeReparenting(info, minimizeChange, startTransaction) + } + } + if (immersiveExitChange != null) { + subAnimationCount = 2 + // Animate the immersive exit change separately. + info.changes.remove(immersiveExitChange) + desktopImmersiveController.animateResizeChange( + immersiveExitChange, + startTransaction, + finishTransaction, + finishCb + ) + // Let the leftover/default handler animate the remaining changes. + return dispatchToLeftoverHandler( + transition, + info, + startTransaction, + finishTransaction, + finishCb + ) + } + // There's nothing to animate separately, so let the left over handler animate + // the entire transition. + subAnimationCount = 1 + return dispatchToLeftoverHandler( + transition, + info, + startTransaction, + finishTransaction, + finishCb + ) + } + + override fun onTransitionConsumed( + transition: IBinder, + aborted: Boolean, + finishTransaction: SurfaceControl.Transaction? + ) { + pendingMixedTransitions.removeAll { pending -> pending.transition == transition } + super.onTransitionConsumed(transition, aborted, finishTransaction) + } + /** * Dispatch close desktop task animation to the default transition handlers. Allows delegating * it to Launcher to animate in sync with show Home transition. @@ -116,7 +286,7 @@ class DesktopMixedTransitionHandler( change: TransitionInfo.Change, startTransaction: SurfaceControl.Transaction, finishTransaction: SurfaceControl.Transaction, - finishCallback: Transitions.TransitionFinishCallback, + finishCallback: TransitionFinishCallback, ): Boolean { // Starting the jank trace if closing the last window in desktop mode. interactionJankMonitor.begin( @@ -126,14 +296,56 @@ class DesktopMixedTransitionHandler( CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE, ) // Dispatch the last desktop task closing animation. + return dispatchToLeftoverHandler( + transition = transition, + info = info, + 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) + } + ) + } + + /** + * Reparent the minimizing task back to its root display area. + * + * During the launch/minimize animation the all animated tasks will be reparented to a + * transition leash shown in front of other desktop tasks. Reparenting the minimizing task back + * to its root display area ensures that task stays behind other desktop tasks during the + * animation. + */ + private fun applyMinimizeChangeReparenting( + info: TransitionInfo, + minimizeChange: Change, + startTransaction: SurfaceControl.Transaction, + ) { + require(TransitionUtil.isOpeningMode(info.type)) + require(minimizeChange.taskInfo != null) + val taskInfo = minimizeChange.taskInfo!! + require(taskInfo.isFreeform) + logV("Reparenting minimizing task#%d", taskInfo.taskId) + rootTaskDisplayAreaOrganizer.reparentToDisplayArea( + taskInfo.displayId, minimizeChange.leash, startTransaction) + } + + private fun dispatchToLeftoverHandler( + transition: IBinder, + info: TransitionInfo, + startTransaction: SurfaceControl.Transaction, + finishTransaction: SurfaceControl.Transaction, + finishCallback: TransitionFinishCallback, + doOnFinishCallback: (() -> Unit)? = null, + ): Boolean { return transitions.dispatchTransition( transition, info, startTransaction, finishTransaction, { wct -> - // Finish the jank trace when closing the last window in desktop mode. - interactionJankMonitor.end(CUJ_DESKTOP_MODE_EXIT_MODE_ON_LAST_WINDOW_CLOSE) + doOnFinishCallback?.invoke() finishCallback.onTransitionFinished(wct) }, /* skip= */ this @@ -155,6 +367,44 @@ class DesktopMixedTransitionHandler( } } + private fun findDesktopTaskChange(info: TransitionInfo, taskId: Int): TransitionInfo.Change? { + return info.changes.firstOrNull { change -> change.taskInfo?.taskId == taskId } + } + + private fun WindowContainerTransaction?.merge( + wct: WindowContainerTransaction? + ): WindowContainerTransaction? { + if (wct == null) return this + if (this == null) return wct + return this.merge(wct) + } + + /** A scheduled transition that will potentially be animated by more than one handler */ + sealed class PendingMixedTransition { + abstract val transition: IBinder + + /** A task is closing. */ + data class Close( + override val transition: IBinder, + ) : PendingMixedTransition() + + /** A task is opening or moving to front. */ + data class Launch( + override val transition: IBinder, + val launchingTask: Int, + val minimizingTask: Int?, + val exitingImmersiveTask: Int?, + ) : PendingMixedTransition() + } + + private fun logV(msg: String, vararg arguments: Any?) { + ProtoLog.v(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + + private fun logW(msg: String, vararg arguments: Any?) { + ProtoLog.w(WM_SHELL_DESKTOP_MODE, "%s: $msg", TAG, *arguments) + } + companion object { private const val TAG = "DesktopMixedTransitionHandler" } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt index 255ca6e2511f..bed484c7a532 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt @@ -541,7 +541,8 @@ class DesktopModeEventLogger { FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__RETURN_HOME_OR_OVERVIEW ), TASK_FINISHED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_FINISHED), - SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF) + SCREEN_OFF(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__SCREEN_OFF), + TASK_MINIMIZED(FrameworkStatsLog.DESKTOP_MODE_UICHANGED__EXIT_REASON__TASK_MINIMIZED), } /** diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt index ed03982d191d..41febdfb3c2e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt @@ -382,6 +382,7 @@ class DesktopModeLoggerTransitionObserver( transitionInfo.type == TRANSIT_EXIT_DESKTOP_MODE_KEYBOARD_SHORTCUT -> ExitReason.KEYBOARD_SHORTCUT_EXIT transitionInfo.isExitToRecentsTransition() -> ExitReason.RETURN_HOME_OR_OVERVIEW + transitionInfo.type == Transitions.TRANSIT_MINIMIZE -> ExitReason.TASK_MINIMIZED else -> { ProtoLog.w( WM_SHELL_DESKTOP_MODE, 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 61de0777ed05..09e77fee7527 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 @@ -19,6 +19,7 @@ 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; @@ -259,6 +260,7 @@ public class DesktopModeVisualIndicator { 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, null /* hostInputToken */); 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 6f7b7162d2ec..fda709a4a2d7 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 @@ -23,14 +23,14 @@ import android.util.ArrayMap import android.util.ArraySet import android.util.SparseArray import android.view.Display.INVALID_DISPLAY +import android.window.DesktopModeFlags import android.window.WindowContainerToken import androidx.core.util.forEach import androidx.core.util.keyIterator import androidx.core.util.valueIterator import com.android.internal.protolog.ProtoLog -import com.android.window.flags.Flags import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository -import com.android.wm.shell.desktopmode.persistence.DesktopTaskState +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE import com.android.wm.shell.shared.annotations.ShellMainThread import com.android.wm.shell.shared.desktopmode.DesktopModeStatus @@ -46,6 +46,7 @@ class DesktopRepository ( private val context: Context, shellInit: ShellInit, private val persistentRepository: DesktopPersistentRepository, + private val repositoryInitializer: DesktopRepositoryInitializer, @ShellMainThread private val mainCoroutineScope: CoroutineScope, ){ @@ -120,35 +121,7 @@ class DesktopRepository ( } private fun initRepoFromPersistentStorage() { - if (!Flags.enableDesktopWindowingPersistence()) return - // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized - mainCoroutineScope.launch { - val desktop = persistentRepository.readDesktop() ?: return@launch - - val maxTasks = - DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } - ?: desktop.zOrderedTasksCount - - var visibleTasksCount = 0 - desktop.zOrderedTasksList - // Reverse it so we initialize the repo from bottom to top. - .reversed() - .mapNotNull { taskId -> desktop.tasksByTaskIdMap[taskId] } - .forEach { task -> - if (task.desktopTaskState == DesktopTaskState.VISIBLE - && visibleTasksCount < maxTasks - ) { - visibleTasksCount++ - addOrMoveFreeformTaskToTop(desktop.displayId, task.taskId) - addActiveTask(desktop.displayId, task.taskId) - updateTaskVisibility(desktop.displayId, task.taskId, visible = false) - } else { - addActiveTask(desktop.displayId, task.taskId) - updateTaskVisibility(desktop.displayId, task.taskId, visible = false) - minimizeTask(desktop.displayId, task.taskId) - } - } - } + repositoryInitializer.initialize(this) } /** Adds [activeTasksListener] to be notified of updates to active tasks. */ @@ -204,8 +177,15 @@ class DesktopRepository ( visibleTasksListeners.remove(visibleTasksListener) } + /** Adds task with [taskId] to the list of freeform tasks on [displayId]. */ + fun addTask(displayId: Int, taskId: Int, isVisible: Boolean) { + addOrMoveFreeformTaskToTop(displayId, taskId) + addActiveTask(displayId, taskId) + updateTask(displayId, taskId, isVisible) + } + /** Adds task with [taskId] to the list of active tasks on [displayId]. */ - fun addActiveTask(displayId: Int, taskId: Int) { + private fun addActiveTask(displayId: Int, taskId: Int) { // Removes task if it is active on another display excluding [displayId]. removeActiveTask(taskId, excludedDisplayId = displayId) @@ -219,7 +199,7 @@ class DesktopRepository ( fun removeActiveTask(taskId: Int, excludedDisplayId: Int? = null) { desktopTaskDataByDisplayId.forEach { displayId, desktopTaskData -> if ((displayId != excludedDisplayId) - && desktopTaskData.activeTasks.remove(taskId)) { + && desktopTaskData.activeTasks.remove(taskId)) { logD("Removed active task=%d displayId=%d", taskId, displayId) updateActiveTasksListeners(displayId) } @@ -292,8 +272,8 @@ class DesktopRepository ( * If task was visible on a different display with a different [displayId], removes from * the set of visible tasks on that display and notifies listeners. */ - fun updateTaskVisibility(displayId: Int, taskId: Int, visible: Boolean) { - if (visible) { + fun updateTask(displayId: Int, taskId: Int, isVisible: Boolean) { + if (isVisible) { // If task is visible, remove it from any other display besides [displayId]. removeVisibleTask(taskId, excludedDisplayId = displayId) } else if (displayId == INVALID_DISPLAY) { @@ -302,7 +282,7 @@ class DesktopRepository ( return } val prevCount = getVisibleTaskCount(displayId) - if (visible) { + if (isVisible) { desktopTaskDataByDisplayId.getOrCreate(displayId).visibleTasks.add(taskId) unminimizeTask(displayId, taskId) } else { @@ -311,9 +291,12 @@ class DesktopRepository ( val newCount = getVisibleTaskCount(displayId) if (prevCount != newCount) { logD("Update task visibility taskId=%d visible=%b displayId=%d", - taskId, visible, displayId) + taskId, isVisible, displayId) logD("VisibleTaskCount has changed from %d to %d", prevCount, newCount) notifyVisibleTaskListeners(displayId, newCount) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { + updatePersistentRepository(displayId) + } } } @@ -355,13 +338,13 @@ class DesktopRepository ( * * Unminimizes the task if it is minimized. */ - fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { + private fun addOrMoveFreeformTaskToTop(displayId: Int, taskId: Int) { logD("Add or move task to top: display=%d taskId=%d", taskId, displayId) desktopTaskDataByDisplayId[displayId]?.freeformTasksInZOrder?.remove(taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).freeformTasksInZOrder.add(0, taskId) // Unminimize the task if it is minimized. unminimizeTask(displayId, taskId) - if (Flags.enableDesktopWindowingPersistence()) { + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } @@ -378,8 +361,8 @@ class DesktopRepository ( logD("Minimize Task: display=%d, task=%d", displayId, taskId) desktopTaskDataByDisplayId.getOrCreate(displayId).minimizedTasks.add(taskId) } - - if (Flags.enableDesktopWindowingPersistence()) { + updateTask(displayId, taskId, isVisible = false) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } @@ -426,8 +409,8 @@ class DesktopRepository ( // Remove task from unminimized task if it is minimized. unminimizeTask(displayId, taskId) removeActiveTask(taskId) - updateTaskVisibility(displayId, taskId, visible = false) - if (Flags.enableDesktopWindowingPersistence()) { + removeVisibleTask(taskId) + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { updatePersistentRepository(displayId) } } 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 1ee2de958e55..94ac2e665f61 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 @@ -17,21 +17,56 @@ package com.android.wm.shell.desktopmode import android.app.ActivityManager.RunningTaskInfo +import android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM +import android.window.DesktopModeFlags import com.android.wm.shell.freeform.TaskChangeListener /** Manages tasks handling specific to Android Desktop Mode. */ -class DesktopTaskChangeListener: TaskChangeListener { +class DesktopTaskChangeListener( + private val desktopRepository: DesktopRepository, +) : TaskChangeListener { override fun onTaskOpening(taskInfo: RunningTaskInfo) { - // TODO: b/367268953 - Connect this with DesktopRepository. + if (!isFreeformTask(taskInfo) && desktopRepository.isActiveTask(taskInfo.taskId)) { + desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + return + } + if (isFreeformTask(taskInfo)) { + desktopRepository.addTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible) + } } override fun onTaskChanging(taskInfo: RunningTaskInfo) { - // TODO: b/367268953 - Connect this with DesktopRepository. + if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + + // Case 1: Freeform task is changed in Desktop Mode. + if (isFreeformTask(taskInfo)) { + if (taskInfo.isVisible) { + desktopRepository.addTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible) + } + desktopRepository.updateTask( + taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible) + } else { + // Case 2: Freeform task is changed outside Desktop Mode. + desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + } + } + + // This method should only be used for scenarios where the task info changes are not propagated to + // [DesktopTaskChangeListener#onTaskChanging] via [TransitionsObserver]. + // Any changes to [DesktopRepository] from this method should be made carefully to minimize risk + // of race conditions and possible duplications with [onTaskChanging]. + override fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) { + // TODO: b/367268953 - Propapagate usages from FreeformTaskListener to this method. } override fun onTaskMovingToFront(taskInfo: RunningTaskInfo) { - // TODO: b/367268953 - Connect this with DesktopRepository. + if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + if (!isFreeformTask(taskInfo)) { + desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + } + // TODO: b/367268953 - Connect this with DesktopRepository for handling + // task moving to front for tasks in windowing mode. } override fun onTaskMovingToBack(taskInfo: RunningTaskInfo) { @@ -39,6 +74,20 @@ class DesktopTaskChangeListener: TaskChangeListener { } override fun onTaskClosing(taskInfo: RunningTaskInfo) { - // TODO: b/367268953 - Connect this with DesktopRepository. + if (!desktopRepository.isActiveTask(taskInfo.taskId)) return + // TODO: b/370038902 - Handle Activity#finishAndRemoveTask. + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() || + desktopRepository.isClosingTask(taskInfo.taskId)) { + // A task that's vanishing should be removed: + // - If it's closed by the X button which means it's marked as a closing task. + desktopRepository.removeClosingTask(taskInfo.taskId) + desktopRepository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId) + } else { + desktopRepository.updateTask(taskInfo.displayId, taskInfo.taskId, isVisible = false) + desktopRepository.minimizeTask(taskInfo.displayId, taskInfo.taskId) + } } + + private fun isFreeformTask(taskInfo: RunningTaskInfo): Boolean = + taskInfo.windowingMode == WINDOWING_MODE_FREEFORM } 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 eec2ba5f41cd..75f8839692a2 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 @@ -54,6 +54,7 @@ import android.view.WindowManager.TRANSIT_CLOSE import android.view.WindowManager.TRANSIT_NONE import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_FRONT +import android.widget.Toast import android.window.DesktopModeFlags import android.window.DesktopModeFlags.DISABLE_NON_RESIZABLE_APP_SNAP_RESIZE import android.window.DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_WALLPAPER_ACTIVITY @@ -87,6 +88,7 @@ import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.common.SingleInstanceRemoteListener import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.compatui.isTopActivityExemptFromDesktopWindowing +import com.android.wm.shell.desktopmode.DesktopImmersiveController.ExitResult import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.DragStartState import com.android.wm.shell.desktopmode.DesktopModeVisualIndicator.IndicatorType import com.android.wm.shell.desktopmode.DesktopRepository.VisibleTasksListener @@ -146,6 +148,7 @@ class DesktopTasksController( private val transitions: Transitions, private val keyguardManager: KeyguardManager, private val returnToDragStartAnimator: ReturnToDragStartAnimator, + private val desktopMixedTransitionHandler: DesktopMixedTransitionHandler, private val enterDesktopTaskTransitionHandler: EnterDesktopTaskTransitionHandler, private val exitDesktopTaskTransitionHandler: ExitDesktopTaskTransitionHandler, private val desktopModeDragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler, @@ -379,7 +382,7 @@ class DesktopTasksController( // TODO(342378842): Instead of using default display, support multiple displays val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( DEFAULT_DISPLAY, wct, taskId) - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, displayId = DEFAULT_DISPLAY, excludeTaskId = taskId, @@ -393,7 +396,7 @@ class DesktopTasksController( // TODO(343149901): Add DPI changes for task launch val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } - runOnTransit?.invoke(transition) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) return true } @@ -410,7 +413,7 @@ class DesktopTasksController( } logV("moveRunningTaskToDesktop taskId=%d", task.taskId) exitSplitIfApplicable(wct, task) - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, displayId = task.displayId, excludeTaskId = task.taskId, @@ -422,7 +425,7 @@ class DesktopTasksController( val transition = enterDesktopTaskTransitionHandler.moveToDesktop(wct, transitionSource) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } - runOnTransit?.invoke(transition) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } /** @@ -459,12 +462,12 @@ class DesktopTasksController( val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask(taskInfo.displayId, wct, taskInfo.taskId) addMoveToDesktopChanges(wct, taskInfo) - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct, taskInfo.displayId) val transition = dragToDesktopTransitionHandler.finishDragToDesktopTransition(wct) transition?.let { taskIdToMinimize?.let { taskId -> addPendingMinimizeTransition(it, taskId) } - runOnTransit?.invoke(transition) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } } @@ -507,7 +510,8 @@ class DesktopTasksController( taskId ) ) - return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo) + return desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo).asExit() + ?.runOnTransitionStart } fun minimizeTask(taskInfo: RunningTaskInfo) { @@ -520,7 +524,7 @@ class DesktopTasksController( removeWallpaperActivity(wct) } // Notify immersive handler as it might need to exit immersive state. - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo) + val exitResult = desktopImmersiveController.exitImmersiveIfApplicable(wct, taskInfo) wct.reorder(taskInfo.token, false) val transition = freeformTaskTransitionStarter.startMinimizedModeTransition(wct) @@ -531,7 +535,7 @@ class DesktopTasksController( taskId = taskId ) } - runOnTransit?.invoke(transition) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } /** Move a task with given `taskId` to fullscreen */ @@ -627,7 +631,7 @@ class DesktopTasksController( logV("moveBackgroundTaskToFront taskId=%s", taskId) val wct = WindowContainerTransaction() // TODO: b/342378842 - Instead of using default display, support multiple displays - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + val exitResult = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, displayId = DEFAULT_DISPLAY, excludeTaskId = taskId, @@ -638,8 +642,13 @@ class DesktopTasksController( launchWindowingMode = WINDOWING_MODE_FREEFORM }.toBundle(), ) - val transition = startLaunchTransition(TRANSIT_OPEN, wct, taskId, remoteTransition) - runOnTransit?.invoke(transition) + val transition = startLaunchTransition( + TRANSIT_OPEN, + wct, + taskId, + remoteTransition = remoteTransition + ) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } /** @@ -658,32 +667,40 @@ class DesktopTasksController( } val wct = WindowContainerTransaction() wct.reorder(taskInfo.token, true /* onTop */, true /* includingParents */) - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( + val result = desktopImmersiveController.exitImmersiveIfApplicable( wct = wct, displayId = taskInfo.displayId, excludeTaskId = taskInfo.taskId, ) - val transition = - startLaunchTransition( - TRANSIT_TO_FRONT, - wct, - taskInfo.taskId, - remoteTransition, - taskInfo.displayId - ) - runOnTransit?.invoke(transition) + val exitResult = if (result is ExitResult.Exit) { result } else { null } + val transition = startLaunchTransition( + transitionType = TRANSIT_TO_FRONT, + wct = wct, + taskId = taskInfo.taskId, + exitingImmersiveTask = exitResult?.exitingTask, + remoteTransition = remoteTransition, + displayId = taskInfo.displayId, + ) + exitResult?.runOnTransitionStart?.invoke(transition) } private fun startLaunchTransition( transitionType: Int, wct: WindowContainerTransaction, taskId: Int, - remoteTransition: RemoteTransition?, + exitingImmersiveTask: Int? = null, + remoteTransition: RemoteTransition? = null, displayId: Int = DEFAULT_DISPLAY, ): IBinder { val taskIdToMinimize = addAndGetMinimizeChanges(displayId, wct, taskId) if (remoteTransition == null) { - val t = transitions.startTransition(transitionType, wct, null /* handler */) + val t = desktopMixedTransitionHandler.startLaunchTransition( + transitionType = transitionType, + wct = wct, + taskId = taskId, + minimizingTaskId = taskIdToMinimize, + exitingImmersiveTask = exitingImmersiveTask, + ) taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) } return t } @@ -698,7 +715,7 @@ class DesktopTasksController( mainExecutor, rootTaskDisplayAreaOrganizer, remoteTransition, taskIdToMinimize) val t = transitions.startTransition(transitionType, wct, remoteTransitionHandler) remoteTransitionHandler.setTransition(t) - taskIdToMinimize?.let { addPendingMinimizeTransition(t, it) } + taskIdToMinimize.let { addPendingMinimizeTransition(t, it) } return t } @@ -842,6 +859,38 @@ class DesktopTasksController( toggleResizeDesktopTaskTransitionHandler.startTransition(wct) } + private fun dragToMaximizeDesktopTask( + taskInfo: RunningTaskInfo, + taskSurface: SurfaceControl, + currentDragBounds: Rect, + motionEvent: MotionEvent + ) { + val displayLayout = displayController.getDisplayLayout(taskInfo.displayId) ?: return + val stableBounds = Rect() + displayLayout.getStableBounds(stableBounds) + if (isTaskMaximized(taskInfo, stableBounds)) { + // Handle the case where we attempt to drag-to-maximize when already maximized: the task + // position won't need to change but we want to animate the surface going back to the + // maximized position. + val containerBounds = taskInfo.configuration.windowConfiguration.bounds + if (containerBounds != currentDragBounds) { + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = containerBounds, + ) + } + return + } + + // TODO(b/375356605): Introduce a new ResizeTrigger for drag-to-top. + desktopModeEventLogger.logTaskResizingStarted( + ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent, taskInfo, displayController + ) + toggleDesktopTaskSize(taskInfo, ResizeTrigger.UNKNOWN_RESIZE_TRIGGER, motionEvent) + } + private fun getMaximizeBounds(taskInfo: RunningTaskInfo, stableBounds: Rect): Rect { if (taskInfo.isResizeable) { // if resizable then expand to entire stable bounds (full display minus insets) @@ -964,7 +1013,6 @@ class DesktopTasksController( taskSurface, startBounds = currentDragBounds, endBounds = destinationBounds, - isResizable = taskInfo.isResizeable, ) } return @@ -998,7 +1046,13 @@ class DesktopTasksController( taskSurface, startBounds = currentDragBounds, endBounds = dragStartBounds, - isResizable = taskInfo.isResizeable, + doOnEnd = { + Toast.makeText( + context, + com.android.wm.shell.R.string.desktop_mode_non_resizable_snap_text, + Toast.LENGTH_SHORT + ).show() + }, ) } else { val resizeTrigger = if (position == SnapPosition.LEFT) { @@ -1108,7 +1162,7 @@ class DesktopTasksController( if (runningTaskInfo != null) { // Task is already running, reorder it to the front wct.reorder(runningTaskInfo.token, /* onTop= */ true) - } else if (Flags.enableDesktopWindowingPersistence()) { + } else if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) { // Task is not running, start it wct.startTask( taskId, @@ -1250,10 +1304,7 @@ class DesktopTasksController( // Check if freeform task launch during recents should be handled shouldHandleMidRecentsFreeformLaunch -> handleMidRecentsFreeformTaskLaunch(task) // Check if the closing task needs to be handled - TransitionUtil.isClosingType(request.type) -> handleTaskClosing( - task, - request.type - ) + TransitionUtil.isClosingType(request.type) -> handleTaskClosing(task) // Check if the top task shouldn't be allowed to enter desktop mode isIncompatibleTask(task) -> handleIncompatibleTaskLaunch(task) // Check if fullscreen task should be updated @@ -1376,14 +1427,16 @@ class DesktopTasksController( wct.startTask(requestedTaskId, options.toBundle()) val taskIdToMinimize = bringDesktopAppsToFrontBeforeShowingNewTask( callingTask.displayId, wct, requestedTaskId) - val runOnTransit = desktopImmersiveController.exitImmersiveIfApplicable( - wct = wct, - displayId = callingTask.displayId, - excludeTaskId = requestedTaskId, - ) + val exitResult = desktopImmersiveController + .exitImmersiveIfApplicable( + wct = wct, + displayId = callingTask.displayId, + excludeTaskId = requestedTaskId, + ) val transition = transitions.startTransition(TRANSIT_OPEN, wct, null) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } - runOnTransit?.invoke(transition) + addPendingAppLaunchTransition(transition, requestedTaskId, taskIdToMinimize) + exitResult.asExit()?.runOnTransitionStart?.invoke(transition) } else { val splitPosition = splitScreenController.determineNewInstancePosition(callingTask) splitScreenController.startTask(requestedTaskId, splitPosition, @@ -1523,6 +1576,7 @@ class DesktopTasksController( desktopImmersiveController.exitImmersiveIfApplicable(transition, wct, task.displayId) // 2) minimize a Task if needed. val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) + addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) if (taskIdToMinimize != null) { addPendingMinimizeTransition(transition, taskIdToMinimize) return wct @@ -1554,6 +1608,7 @@ class DesktopTasksController( // minimize another Task. val taskIdToMinimize = addAndGetMinimizeChanges(task.displayId, wct, task.taskId) taskIdToMinimize?.let { addPendingMinimizeTransition(transition, it) } + addPendingAppLaunchTransition(transition, task.taskId, taskIdToMinimize) desktopImmersiveController.exitImmersiveIfApplicable( transition, wct, task.displayId ) @@ -1579,10 +1634,7 @@ class DesktopTasksController( } /** Handle task closing by removing wallpaper activity if it's the last active task */ - private fun handleTaskClosing( - task: RunningTaskInfo, - transitionType: Int - ): WindowContainerTransaction? { + private fun handleTaskClosing(task: RunningTaskInfo): WindowContainerTransaction? { logV("handleTaskClosing") if (!isDesktopModeShowing(task.displayId)) return null @@ -1603,7 +1655,7 @@ class DesktopTasksController( taskbarDesktopTaskListener?.onTaskbarCornerRoundingUpdate( doesAnyTaskRequireTaskbarRounding( task.displayId, - task.id + task.taskId ) ) return if (wct.isEmpty) null else wct @@ -1737,6 +1789,20 @@ class DesktopTasksController( } } + private fun addPendingAppLaunchTransition( + transition: IBinder, + launchTaskId: Int, + minimizeTaskId: Int?, + ) { + if (!DesktopModeFlags.ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS.isTrue) { + return + } + // TODO b/359523924: pass immersive task here? + desktopMixedTransitionHandler.addPendingMixedTransition( + DesktopMixedTransitionHandler.PendingMixedTransition.Launch( + transition, launchTaskId, minimizeTaskId, /* exitingImmersiveTask= */ null)) + } + fun removeDesktop(displayId: Int) { if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue()) return @@ -1908,11 +1974,15 @@ class DesktopTasksController( ) when (indicatorType) { IndicatorType.TO_FULLSCREEN_INDICATOR -> { - moveToFullscreenWithAnimation( - taskInfo, - position, - DesktopModeTransitionSource.TASK_DRAG - ) + if (DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)) { + dragToMaximizeDesktopTask(taskInfo, taskSurface, currentDragBounds, motionEvent) + } else { + moveToFullscreenWithAnimation( + taskInfo, + position, + DesktopModeTransitionSource.TASK_DRAG + ) + } } IndicatorType.TO_SPLIT_LEFT_INDICATOR -> { handleSnapResizingTask( @@ -1937,17 +2007,32 @@ class DesktopTasksController( ) } IndicatorType.NO_INDICATOR -> { + // 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) // If task bounds are outside valid drag area, snap them inward DragPositioningCallbackUtility.snapTaskBoundsIfNecessary( - currentDragBounds, + destinationBounds, validDragArea ) - if (currentDragBounds == dragStartBounds) return + if (destinationBounds == dragStartBounds) { + // There's no actual difference between the start and end bounds, so while a + // WCT change isn't needed, the dragged surface still needs to be snapped back + // to its original location. + releaseVisualIndicator() + returnToDragStartAnimator.start( + taskInfo.taskId, + taskSurface, + startBounds = currentDragBounds, + endBounds = dragStartBounds, + ) + return + } // Update task bounds so that the task position will match the position of its leash val wct = WindowContainerTransaction() - wct.setBounds(taskInfo.token, currentDragBounds) + wct.setBounds(taskInfo.token, destinationBounds) transitions.startTransition(TRANSIT_CHANGE, wct, null) releaseVisualIndicator() 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 dedd44f3950a..d537da802efd 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 @@ -60,7 +60,8 @@ import java.util.function.Supplier; * entering and exiting freeform. */ public class ExitDesktopTaskTransitionHandler implements Transitions.TransitionHandler { - private static final int FULLSCREEN_ANIMATION_DURATION = 336; + @VisibleForTesting + static final int FULLSCREEN_ANIMATION_DURATION = 336; private final Context mContext; private final Transitions mTransitions; diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt index f4df42cde10f..4e08d106052a 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/ReturnToDragStartAnimator.kt @@ -19,28 +19,24 @@ package com.android.wm.shell.desktopmode import android.animation.Animator import android.animation.RectEvaluator import android.animation.ValueAnimator -import android.content.Context import android.graphics.Rect import android.view.SurfaceControl -import android.widget.Toast import androidx.core.animation.addListener import com.android.internal.jank.Cuj import com.android.internal.jank.InteractionJankMonitor -import com.android.wm.shell.R import com.android.wm.shell.windowdecor.OnTaskRepositionAnimationListener import java.util.function.Supplier /** Animates the task surface moving from its current drag position to its pre-drag position. */ class ReturnToDragStartAnimator( - private val context: Context, private val transactionSupplier: Supplier<SurfaceControl.Transaction>, private val interactionJankMonitor: InteractionJankMonitor ) { private var boundsAnimator: Animator? = null private lateinit var taskRepositionAnimationListener: OnTaskRepositionAnimationListener - constructor(context: Context, interactionJankMonitor: InteractionJankMonitor) : - this(context, Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) + constructor(interactionJankMonitor: InteractionJankMonitor) : + this(Supplier { SurfaceControl.Transaction() }, interactionJankMonitor) /** Sets a listener for the start and end of the reposition animation. */ fun setTaskRepositionAnimationListener(listener: OnTaskRepositionAnimationListener) { @@ -53,7 +49,7 @@ class ReturnToDragStartAnimator( taskSurface: SurfaceControl, startBounds: Rect, endBounds: Rect, - isResizable: Boolean + doOnEnd: (() -> Unit)? = null, ) { val tx = transactionSupplier.get() @@ -87,13 +83,7 @@ class ReturnToDragStartAnimator( .apply() taskRepositionAnimationListener.onAnimationEnd(taskId) boundsAnimator = null - if (!isResizable) { - Toast.makeText( - context, - R.string.desktop_mode_non_resizable_snap_text, - Toast.LENGTH_SHORT - ).show() - } + doOnEnd?.invoke() interactionJankMonitor.end(Cuj.CUJ_DESKTOP_MODE_SNAP_RESIZE) } ) 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 2d11e02bd3c6..9e646f430c98 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 @@ -50,7 +50,9 @@ class DesktopPersistentRepository( DataStoreFactory.create( serializer = DesktopPersistentRepositoriesSerializer, produceFile = { context.dataStoreFile(DESKTOP_REPOSITORIES_DATASTORE_FILE) }, - scope = bgCoroutineScope)) + scope = bgCoroutineScope, + ), + ) /** Provides `dataStore.data` flow and handles exceptions thrown during collection */ private val dataStoreFlow: Flow<DesktopPersistentRepositories> = @@ -116,7 +118,11 @@ class DesktopPersistentRepository( val desktop = getDesktop(currentRepository, desktopId) .toBuilder() - .updateTaskStates(visibleTasks, minimizedTasks) + .updateTaskStates( + visibleTasks, + minimizedTasks, + freeformTasksInZOrder, + ) .updateZOrder(freeformTasksInZOrder) desktopPersistentRepositories @@ -169,9 +175,21 @@ class DesktopPersistentRepository( private fun Desktop.Builder.updateTaskStates( visibleTasks: ArraySet<Int>, - minimizedTasks: ArraySet<Int> + minimizedTasks: ArraySet<Int>, + freeformTasksInZOrder: ArrayList<Int>, ): Desktop.Builder { clearTasksByTaskId() + + // Handle the case where tasks are not marked as visible but are meant to be visible + // after reboot. E.g. User moves out of desktop when there are multiple tasks are + // visible, they will be marked as not visible afterwards. This ensures that they are + // still persisted as visible. + // TODO - b/350476823: Remove this logic once repository holds expanded tasks + if (freeformTasksInZOrder.size > visibleTasks.size + minimizedTasks.size && + visibleTasks.isEmpty() + ) { + visibleTasks.addAll(freeformTasksInZOrder.filterNot { it in minimizedTasks }) + } putAllTasksByTaskId( visibleTasks.associateWith { createDesktopTask(it, state = DesktopTaskState.VISIBLE) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt new file mode 100644 index 000000000000..771c3d1cb9a1 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializer.kt @@ -0,0 +1,24 @@ +/* + * 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.persistence + +import com.android.wm.shell.desktopmode.DesktopRepository + +/** Interface for initializing the [DesktopRepository]. */ +fun interface DesktopRepositoryInitializer { + fun initialize(repository: DesktopRepository) +} 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 new file mode 100644 index 000000000000..d8156561ff19 --- /dev/null +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerImpl.kt @@ -0,0 +1,66 @@ +/* + * 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.persistence + +import android.content.Context +import android.window.DesktopModeFlags +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.shared.annotations.ShellMainThread +import com.android.wm.shell.shared.desktopmode.DesktopModeStatus +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch + +/** + * Initializes the [DesktopRepository] from the [DesktopPersistentRepository]. + * + * This class is responsible for reading the [DesktopPersistentRepository] and initializing the + * [DesktopRepository] with the tasks that previously existed in desktop. + */ +class DesktopRepositoryInitializerImpl( + private val context: Context, + private val persistentRepository: DesktopPersistentRepository, + @ShellMainThread private val mainCoroutineScope: CoroutineScope, +) : DesktopRepositoryInitializer { + override fun initialize(repository: DesktopRepository) { + if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue()) return + // TODO: b/365962554 - Handle the case that user moves to desktop before it's initialized + mainCoroutineScope.launch { + val desktop = persistentRepository.readDesktop() ?: return@launch + + val maxTasks = + DesktopModeStatus.getMaxTaskLimit(context).takeIf { it > 0 } + ?: desktop.zOrderedTasksCount + + var visibleTasksCount = 0 + desktop.zOrderedTasksList + // Reverse it so we initialize the repo from bottom to top. + .reversed() + .mapNotNull { taskId -> desktop.tasksByTaskIdMap[taskId] } + .forEach { task -> + if (task.desktopTaskState == DesktopTaskState.VISIBLE + && visibleTasksCount < maxTasks + ) { + visibleTasksCount++ + repository.addTask(desktop.displayId, task.taskId, isVisible = false) + } else { + repository.addTask(desktop.displayId, task.taskId, isVisible = false) + repository.minimizeTask(desktop.displayId, task.taskId) + } + } + } + } +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java index a16446fffa15..cd20d97c7964 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskListener.java @@ -34,7 +34,6 @@ import com.android.wm.shell.desktopmode.DesktopTasksController; import com.android.wm.shell.protolog.ShellProtoLogGroup; 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.windowdecor.WindowDecorViewModel; import java.io.PrintWriter; @@ -54,14 +53,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, private final Optional<DesktopTasksController> mDesktopTasksController; private final WindowDecorViewModel mWindowDecorationViewModel; private final LaunchAdjacentController mLaunchAdjacentController; + private final Optional<TaskChangeListener> mTaskChangeListener; private final SparseArray<State> mTasks = new SparseArray<>(); - private static class State { - RunningTaskInfo mTaskInfo; - SurfaceControl mLeash; - } - public FreeformTaskListener( Context context, ShellInit shellInit, @@ -69,13 +64,15 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, Optional<DesktopRepository> desktopRepository, Optional<DesktopTasksController> desktopTasksController, LaunchAdjacentController launchAdjacentController, - WindowDecorViewModel windowDecorationViewModel) { + WindowDecorViewModel windowDecorationViewModel, + Optional<TaskChangeListener> taskChangeListener) { mContext = context; mShellTaskOrganizer = shellTaskOrganizer; mWindowDecorationViewModel = windowDecorationViewModel; mDesktopRepository = desktopRepository; mDesktopTasksController = desktopTasksController; mLaunchAdjacentController = launchAdjacentController; + mTaskChangeListener = taskChangeListener; if (shellInit != null) { shellInit.addInitCallback(this::onInit, this); } @@ -100,14 +97,10 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, state.mLeash = leash; mTasks.put(taskInfo.taskId, state); - if (DesktopModeStatus.canEnterDesktopMode(mContext)) { + if (!DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS.isTrue() && + DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopRepository.ifPresent(repository -> { - repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); - if (taskInfo.isVisible) { - repository.addActiveTask(taskInfo.displayId, taskInfo.taskId); - repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, - /* visible= */ true); - } + repository.addTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible); }); } updateLaunchAdjacentController(); @@ -119,7 +112,8 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId); mTasks.remove(taskInfo.taskId); - if (DesktopModeStatus.canEnterDesktopMode(mContext)) { + if (!DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS.isTrue() && + DesktopModeStatus.canEnterDesktopMode(mContext)) { mDesktopRepository.ifPresent(repository -> { // TODO: b/370038902 - Handle Activity#finishAndRemoveTask. if (!DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION.isTrue() @@ -129,7 +123,8 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, repository.removeClosingTask(taskInfo.taskId); repository.removeFreeformTask(taskInfo.displayId, taskInfo.taskId); } else { - repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, false); + repository.updateTask(taskInfo.displayId, taskInfo.taskId, /* isVisible= */ + false); repository.minimizeTask(taskInfo.displayId, taskInfo.taskId); } }); @@ -148,13 +143,17 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, mWindowDecorationViewModel.onTaskInfoChanged(taskInfo); state.mTaskInfo = taskInfo; if (DesktopModeStatus.canEnterDesktopMode(mContext)) { - mDesktopRepository.ifPresent(repository -> { - if (taskInfo.isVisible) { - repository.addActiveTask(taskInfo.displayId, taskInfo.taskId); - } - repository.updateTaskVisibility(taskInfo.displayId, taskInfo.taskId, - taskInfo.isVisible); - }); + if (DesktopModeFlags.ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS.isTrue()) { + // Pass task info changes to the [TaskChangeListener] since [TransitionsObserver] + // does not propagate all task info changes. + mTaskChangeListener.ifPresent(listener -> + listener.onNonTransitionTaskChanging(taskInfo)); + } else { + mDesktopRepository.ifPresent(repository -> { + repository.updateTask(taskInfo.displayId, taskInfo.taskId, + taskInfo.isVisible); + }); + } } updateLaunchAdjacentController(); } @@ -179,7 +178,7 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, taskInfo.taskId, taskInfo.isFocused); if (DesktopModeStatus.canEnterDesktopMode(mContext) && taskInfo.isFocused) { mDesktopRepository.ifPresent(repository -> { - repository.addOrMoveFreeformTaskToTop(taskInfo.displayId, taskInfo.taskId); + repository.addTask(taskInfo.displayId, taskInfo.taskId, taskInfo.isVisible); }); } } @@ -213,4 +212,9 @@ public class FreeformTaskListener implements ShellTaskOrganizer.TaskListener, public String toString() { return TAG; } + + private static class State { + RunningTaskInfo mTaskInfo; + SurfaceControl mLeash; + } } 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 58337ece0991..e848b889b314 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 @@ -27,6 +27,7 @@ import android.app.WindowConfiguration; import android.graphics.Rect; import android.os.IBinder; import android.util.ArrayMap; +import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.WindowManager; import android.window.TransitionInfo; @@ -38,6 +39,7 @@ import androidx.annotation.Nullable; import com.android.wm.shell.common.DisplayController; import com.android.wm.shell.common.ShellExecutor; +import com.android.wm.shell.shared.animation.MinimizeAnimator; import com.android.wm.shell.transition.Transitions; import java.util.ArrayList; @@ -137,7 +139,7 @@ public class FreeformTaskTransitionHandler break; case WindowManager.TRANSIT_TO_BACK: transitionHandled |= startMinimizeTransition( - transition, info.getType(), change); + transition, info.getType(), change, finishT, animations, onAnimFinish); break; case WindowManager.TRANSIT_CLOSE: if (change.getTaskInfo().getWindowingMode() == WINDOWING_MODE_FREEFORM) { @@ -206,7 +208,10 @@ public class FreeformTaskTransitionHandler private boolean startMinimizeTransition( IBinder transition, int type, - TransitionInfo.Change change) { + TransitionInfo.Change change, + SurfaceControl.Transaction finishT, + ArrayList<Animator> animations, + Runnable onAnimFinish) { if (!mPendingTransitionTokens.contains(transition)) { return false; } @@ -215,7 +220,23 @@ public class FreeformTaskTransitionHandler if (type != Transitions.TRANSIT_MINIMIZE) { return false; } - // TODO(b/361524575): Add minimize animations + + SurfaceControl.Transaction t = new SurfaceControl.Transaction(); + SurfaceControl sc = change.getLeash(); + finishT.hide(sc); + final DisplayMetrics displayMetrics = + mDisplayController + .getDisplayContext(taskInfo.displayId).getResources().getDisplayMetrics(); + final Animator animator = MinimizeAnimator.create( + displayMetrics, + change, + t, + (anim) -> { + animations.remove(anim); + onAnimFinish.run(); + return null; + }); + animations.add(animator); return true; } 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 7631ece761b5..18f9cc758e38 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 @@ -42,9 +42,9 @@ import java.util.Map; import java.util.Optional; /** - * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, - * maximizing and restoring transitions. It also reports transitions so that window decorations can - * be a part of transitions. + * The {@link Transitions.TransitionHandler} that handles freeform task launches, closes, maximizing + * and restoring transitions. It also reports transitions so that window decorations can be a part + * of transitions. */ public class FreeformTaskTransitionObserver implements Transitions.TransitionObserver { private final Transitions mTransitions; @@ -89,8 +89,8 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs // 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. - mDesktopImmersiveController.ifPresent(h -> - h.onTransitionReady(transition, info, startT, finishT)); + mDesktopImmersiveController.ifPresent( + h -> h.onTransitionReady(transition, info, startT, finishT)); } // Update focus state first to ensure the correct state can be queried from listeners. // TODO(371503964): Remove this once the unified task repository is ready. @@ -147,33 +147,28 @@ public class FreeformTaskTransitionObserver implements Transitions.TransitionObs TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mTaskChangeListener.ifPresent( - listener -> listener.onTaskOpening(change.getTaskInfo())); + mTaskChangeListener.ifPresent(listener -> listener.onTaskOpening(change.getTaskInfo())); mWindowDecorViewModel.onTaskOpening( - change.getTaskInfo(), change.getLeash(), startT, finishT); + change.getTaskInfo(), change.getLeash(), startT, finishT); } private void onCloseTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mTaskChangeListener.ifPresent( - listener -> listener.onTaskClosing(change.getTaskInfo())); + mTaskChangeListener.ifPresent(listener -> listener.onTaskClosing(change.getTaskInfo())); mWindowDecorViewModel.onTaskClosing(change.getTaskInfo(), startT, finishT); - } private void onChangeTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT) { - mTaskChangeListener.ifPresent(listener -> - listener.onTaskChanging(change.getTaskInfo())); + mTaskChangeListener.ifPresent(listener -> listener.onTaskChanging(change.getTaskInfo())); mWindowDecorViewModel.onTaskChanging( change.getTaskInfo(), change.getLeash(), startT, finishT); } - private void onToFrontTransitionReady( TransitionInfo.Change change, SurfaceControl.Transaction startT, diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt index 98bdf059e738..c0613e2dba48 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/FreeformTaskTransitionStarterInitializer.kt @@ -28,7 +28,7 @@ import com.android.wm.shell.windowdecor.WindowDecorViewModel class FreeformTaskTransitionStarterInitializer( shellInit: ShellInit, private val windowDecorViewModel: WindowDecorViewModel, - private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter + private val freeformTaskTransitionStarter: FreeformTaskTransitionStarter, ) { init { shellInit.addInitCallback(::onShellInit, this) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt index f07c069bb420..aee92eaaf3d5 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/freeform/TaskChangeListener.kt @@ -16,7 +16,7 @@ package com.android.wm.shell.freeform -import android.app.ActivityManager.RunningTaskInfo; +import android.app.ActivityManager.RunningTaskInfo /** * Interface used by [FreeformTaskTransitionObserver] to manage freeform tasks. @@ -27,9 +27,18 @@ interface TaskChangeListener { /** Notifies a task opening in freeform mode. */ fun onTaskOpening(taskInfo: RunningTaskInfo) - /** Notifies a task info update on the given task. */ + /** Notifies a task info update on the given task from Shell Transitions framework. */ fun onTaskChanging(taskInfo: RunningTaskInfo) + /** + * Notifies a task info update on the given task from [FreeformTaskListener]. + * + * This is used to propagate task info changes since not all task changes are propagated from + * [TransitionObserver] in [onTaskChanging]. It is recommended to use [onTaskChanging] instead + * of this method where possible. + */ + fun onNonTransitionTaskChanging(taskInfo: RunningTaskInfo) + /** Notifies a task moving to the front. */ fun onTaskMovingToFront(taskInfo: RunningTaskInfo) @@ -38,4 +47,4 @@ interface TaskChangeListener { /** Notifies a task is closing. */ fun onTaskClosing(taskInfo: RunningTaskInfo) -}
\ No newline at end of file +} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java index f060158766fe..4aeecbec7dfb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipAnimationController.java @@ -30,6 +30,7 @@ import android.annotation.NonNull; import android.app.TaskInfo; import android.content.Context; import android.content.pm.ActivityInfo; +import android.graphics.Point; import android.graphics.Rect; import android.os.SystemClock; import android.view.Surface; @@ -152,7 +153,6 @@ public class PipAnimationController { return mCurrentAnimator; } - @SuppressWarnings("unchecked") /** * Construct and return an animator that animates from the {@param startBounds} to the * {@param endBounds} with the given {@param direction}. If {@param direction} is type @@ -171,6 +171,7 @@ public class PipAnimationController { * leaving PiP to fullscreen, and the {@param endBounds} is the fullscreen bounds before the * rotation change. */ + @SuppressWarnings("unchecked") @VisibleForTesting public PipTransitionAnimator getAnimator(TaskInfo taskInfo, SurfaceControl leash, Rect baseBounds, Rect startBounds, Rect endBounds, Rect sourceHintRect, @@ -566,7 +567,7 @@ public class PipAnimationController { } getSurfaceTransactionHelper() .resetScale(tx, leash, getDestinationBounds()) - .crop(tx, leash, getDestinationBounds()) + .cropAndPosition(tx, leash, getDestinationBounds()) .round(tx, leash, true /* applyCornerRadius */) .shadow(tx, leash, shouldApplyShadowRadius()); tx.show(leash); @@ -590,18 +591,50 @@ public class PipAnimationController { // Just for simplicity we'll interpolate between the source rect hint insets and empty // insets to calculate the window crop final Rect initialSourceValue; + final Rect mainWindowFrame = taskInfo.topActivityMainWindowFrame; + final boolean hasNonMatchFrame = mainWindowFrame != null; + final boolean changeOrientation = + rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270; + final Rect baseBounds = new Rect(baseValue); + final Rect startBounds = new Rect(startValue); + final Rect endBounds = new Rect(endValue); if (isOutPipDirection) { - initialSourceValue = new Rect(endValue); + // TODO(b/356277166): handle rotation change with activity that provides main window + // frame. + if (hasNonMatchFrame && !changeOrientation) { + endBounds.set(mainWindowFrame); + } + initialSourceValue = new Rect(endBounds); + } else if (isInPipDirection) { + if (hasNonMatchFrame) { + baseBounds.set(mainWindowFrame); + if (startValue.equals(baseValue)) { + // If the start value is at initial state as in PIP animation, also override + // the start bounds with nonMatchParentBounds. + startBounds.set(mainWindowFrame); + } + } + initialSourceValue = new Rect(baseBounds); } else { - initialSourceValue = new Rect(baseValue); + // Note that we assume the window bounds always match task bounds in PIP mode. + initialSourceValue = new Rect(baseBounds); + } + + final Point leashOffset; + if (isInPipDirection) { + leashOffset = new Point(baseValue.left, baseValue.top); + } else if (isOutPipDirection) { + leashOffset = new Point(endValue.left, endValue.top); + } else { + leashOffset = new Point(baseValue.left, baseValue.top); } final Rect rotatedEndRect; final Rect lastEndRect; final Rect initialContainerRect; - if (rotationDelta == ROTATION_90 || rotationDelta == ROTATION_270) { - lastEndRect = new Rect(endValue); - rotatedEndRect = new Rect(endValue); + if (changeOrientation) { + lastEndRect = new Rect(endBounds); + rotatedEndRect = new Rect(endBounds); // Rotate the end bounds according to the rotation delta because the display will // be rotated to the same orientation. rotateBounds(rotatedEndRect, initialSourceValue, rotationDelta); @@ -617,9 +650,9 @@ public class PipAnimationController { // Crop a Rect matches the aspect ratio and pivots at the center point. // This is done for entering case only. if (isInPipDirection(direction)) { - final float aspectRatio = endValue.width() / (float) endValue.height(); + final float aspectRatio = endBounds.width() / (float) endBounds.height(); adjustedSourceRectHint.set(PipUtils.getEnterPipWithOverlaySrcRectHint( - startValue, aspectRatio)); + startBounds, aspectRatio)); } } else { adjustedSourceRectHint.set(sourceRectHint); @@ -644,7 +677,7 @@ public class PipAnimationController { // construct new Rect instances in case they are recycled return new PipTransitionAnimator<Rect>(taskInfo, leash, ANIM_TYPE_BOUNDS, - endValue, new Rect(baseValue), new Rect(startValue), new Rect(endValue)) { + endBounds, new Rect(baseBounds), new Rect(startBounds), new Rect(endBounds)) { private final RectEvaluator mRectEvaluator = new RectEvaluator(new Rect()); private final RectEvaluator mInsetsEvaluator = new RectEvaluator(new Rect()); @@ -668,11 +701,22 @@ public class PipAnimationController { setCurrentValue(bounds); if (inScaleTransition() || adjustedSourceRectHint.isEmpty()) { if (isOutPipDirection) { - getSurfaceTransactionHelper().crop(tx, leash, end) - .scale(tx, leash, end, bounds); + // Use the bounds relative to the task leash in case the leash does not + // start from (0, 0). + final Rect relativeEndBounds = new Rect(end); + relativeEndBounds.offset(-leashOffset.x, -leashOffset.y); + getSurfaceTransactionHelper() + .crop(tx, leash, relativeEndBounds) + .scale(tx, leash, relativeEndBounds, bounds, + false /* shouldOffset */); } else { - getSurfaceTransactionHelper().crop(tx, leash, base) - .scale(tx, leash, base, bounds, angle) + // TODO(b/356277166): add support to specify sourceRectHint with + // non-match parent activity. + // If there's a PIP resize animation, we should offset the bounds to + // (0, 0) since the window bounds should match the leash bounds in PIP + // mode. + getSurfaceTransactionHelper().cropAndPosition(tx, leash, base) + .scale(tx, leash, base, bounds, angle, inScaleTransition()) .round(tx, leash, base, bounds) .shadow(tx, leash, shouldApplyShadowRadius()); } @@ -680,7 +724,7 @@ public class PipAnimationController { final Rect insets = computeInsets(fraction); getSurfaceTransactionHelper().scaleAndCrop(tx, leash, adjustedSourceRectHint, initialSourceValue, bounds, insets, - isInPipDirection, fraction); + isInPipDirection, fraction, leashOffset); final Rect sourceBounds = new Rect(initialContainerRect); sourceBounds.inset(insets); getSurfaceTransactionHelper() @@ -733,8 +777,7 @@ public class PipAnimationController { getSurfaceTransactionHelper() .rotateAndScaleWithCrop(tx, leash, initialContainerRect, bounds, insets, degree, x, y, isOutPipDirection, - rotationDelta == ROTATION_270 /* clockwise */); - getSurfaceTransactionHelper() + rotationDelta == ROTATION_270 /* clockwise */) .round(tx, leash, sourceBounds, bounds) .shadow(tx, leash, shouldApplyShadowRadius()); if (!handlePipTransaction(leash, tx, bounds, 1f /* alpha */)) { @@ -772,7 +815,7 @@ public class PipAnimationController { tx.setPosition(leash, 0, 0); tx.setWindowCrop(leash, 0, 0); } else { - getSurfaceTransactionHelper().crop(tx, leash, destBounds); + getSurfaceTransactionHelper().cropAndPosition(tx, leash, destBounds); } if (mContentOverlay != null) { clearContentOverlay(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java index 3d1994cac534..b02bd0ffec6c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipSurfaceTransactionHelper.java @@ -16,8 +16,10 @@ package com.android.wm.shell.pip; +import android.annotation.NonNull; import android.content.Context; import android.graphics.Matrix; +import android.graphics.Point; import android.graphics.Rect; import android.graphics.RectF; import android.view.Choreographer; @@ -68,52 +70,102 @@ public class PipSurfaceTransactionHelper { * 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) { + public PipSurfaceTransactionHelper cropAndPosition(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect destinationBounds) { tx.setWindowCrop(leash, destinationBounds.width(), destinationBounds.height()) .setPosition(leash, destinationBounds.left, destinationBounds.top); return this; } /** + * Operates {@link SurfaceControl.Transaction#setCrop} on a given transaction and leash. + * + * @param tx the transaction to apply + * @param leash the leash to crop + * @param relativeDestinationBounds the bounds to crop, which is relative to the leash + * coordinate + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper crop(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect relativeDestinationBounds) { + tx.setCrop(leash, relativeDestinationBounds); + 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 */); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */, + true /* shouldOffset */); } /** * 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 */); + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull RectF destinationBounds) { + return scale(tx, leash, sourceBounds, destinationBounds, 0 /* degrees */, + true /* shouldOffset */); } /** - * Operates the scale (setMatrix) on a given transaction and leash + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, Rect destinationBounds, float degrees) { + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, boolean shouldOffset) { + mTmpDestinationRectF.set(destinationBounds); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, 0 /* degrees */, shouldOffset); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, float degrees) { + return scale(tx, leash, sourceBounds, destinationBounds, degrees, true /* shouldOffset */); + } + + /** + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) + * @return same {@link PipSurfaceTransactionHelper} instance for method chaining + */ + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, float degrees, boolean shouldOffset) { mTmpDestinationRectF.set(destinationBounds); - return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees); + return scale(tx, leash, sourceBounds, mTmpDestinationRectF, degrees, shouldOffset); } /** * Operates the scale (setMatrix) on a given transaction and leash, along with a rotation. + * + * @param shouldOffset {@code true} to offset the leash to (0, 0) * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scale(SurfaceControl.Transaction tx, SurfaceControl leash, - Rect sourceBounds, RectF destinationBounds, float degrees) { + public PipSurfaceTransactionHelper scale(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceBounds, + @NonNull RectF destinationBounds, float degrees, boolean shouldOffset) { 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); + // the source to (0, 0) if {@code shouldOffset} is true. + if (shouldOffset) { + mTmpSourceRectF.offsetTo(0, 0); + } mTmpDestinationRectF.set(destinationBounds); mTmpTransform.setRectToRect(mTmpSourceRectF, mTmpDestinationRectF, Matrix.ScaleToFit.FILL); mTmpTransform.postRotate(degrees, @@ -123,17 +175,19 @@ public class PipSurfaceTransactionHelper { } /** - * Operates the scale (setMatrix) on a given transaction and leash + * Operates the scale (setMatrix) on a given transaction and leash. + * + * @param leashOffset the offset of the leash bounds relative to the screen coordinate * @return same {@link PipSurfaceTransactionHelper} instance for method chaining */ - public PipSurfaceTransactionHelper scaleAndCrop(SurfaceControl.Transaction tx, - SurfaceControl leash, Rect sourceRectHint, - Rect sourceBounds, Rect destinationBounds, Rect insets, - boolean isInPipDirection, float fraction) { + public PipSurfaceTransactionHelper scaleAndCrop(@NonNull SurfaceControl.Transaction tx, + @NonNull SurfaceControl leash, @NonNull Rect sourceRectHint, @NonNull Rect sourceBounds, + @NonNull Rect destinationBounds, @NonNull Rect insets, boolean isInPipDirection, + float fraction, @NonNull Point leashOffset) { mTmpDestinationRect.set(sourceBounds); // Similar to {@link #scale}, we want to position the surface relative to the screen - // coordinates so offset the bounds to 0,0 - mTmpDestinationRect.offsetTo(0, 0); + // coordinates so offset the bounds relative to the leash. + mTmpDestinationRect.offset(-leashOffset.x, -leashOffset.y); mTmpDestinationRect.inset(insets); // Scale to the bounds no smaller than the destination and offset such that the top/left // of the scaled inset source rect aligns with the top/left of the destination bounds @@ -152,13 +206,13 @@ public class PipSurfaceTransactionHelper { scale = Math.max((float) destinationBounds.width() / sourceBounds.width(), (float) destinationBounds.height() / sourceBounds.height()); } - float left = destinationBounds.left - insets.left * scale; - float top = destinationBounds.top - insets.top * scale; + float left = destinationBounds.left - mTmpDestinationRect.left * scale; + float top = destinationBounds.top - mTmpDestinationRect.top * scale; if (scale == 1) { // Work around the 1 pixel off error by rounding the position down at very beginning. // We noticed such error from flicker tests, not visually. - left = sourceBounds.left; - top = sourceBounds.top; + left = leashOffset.x; + top = leashOffset.y; } mTmpTransform.setScale(scale, scale); tx.setMatrix(leash, mTmpTransform, mTmpFloat9) diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java index b4e03299f14c..c4e63dfdade9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java @@ -960,7 +960,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction boundsChangeTx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(boundsChangeTx, mLeash, destinationBounds) + .cropAndPosition(boundsChangeTx, mLeash, destinationBounds) .round(boundsChangeTx, mLeash, true /* applyCornerRadius */); mPipTransitionState.setTransitionState(PipTransitionState.ENTRY_SCHEDULED); @@ -988,7 +988,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, mLeash, destinationBounds) - .crop(tx, mLeash, destinationBounds) + .cropAndPosition(tx, mLeash, destinationBounds) .round(tx, mLeash, isInPip()); // The animation is finished in the Launcher and here we directly apply the final touch. applyEnterPipSyncTransaction(destinationBounds, () -> { @@ -1525,7 +1525,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, mPipBoundsState.setBounds(toBounds); final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(tx, mLeash, toBounds) + .cropAndPosition(tx, mLeash, toBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); if (shouldSyncPipTransactionWithMenu()) { mPipMenuController.resizePipMenu(mLeash, tx, toBounds); @@ -1628,7 +1628,7 @@ public class PipTaskOrganizer implements ShellTaskOrganizer.TaskListener, Rect destinationBounds) { final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); mSurfaceTransactionHelper - .crop(tx, mLeash, destinationBounds) + .cropAndPosition(tx, mLeash, destinationBounds) .resetScale(tx, mLeash, destinationBounds) .round(tx, mLeash, mPipTransitionState.isInPip()); return tx; 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 6da39951efbe..28b91c6cb812 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 @@ -30,7 +30,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; 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 android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static com.android.wm.shell.pip.PipAnimationController.ANIM_TYPE_ALPHA; @@ -45,6 +44,7 @@ import static com.android.wm.shell.transition.Transitions.TRANSIT_CLEANUP_PIP_EX 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.transitTypeToString; import android.annotation.IntDef; import android.app.ActivityManager; @@ -551,7 +551,7 @@ public class PipTransition extends PipTransitionController { } // Reset the scale with bounds change synchronously. if (hasValidLeash) { - mSurfaceTransactionHelper.crop(tx, leash, destinationBounds) + mSurfaceTransactionHelper.cropAndPosition(tx, leash, destinationBounds) .resetScale(tx, leash, destinationBounds) .round(tx, leash, true /* applyCornerRadius */); final Rect appBounds = mPipOrganizer.mAppBounds; @@ -588,7 +588,8 @@ public class PipTransition extends PipTransitionController { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, "%s: Destination bounds were changed during animation", TAG); rotateBounds(finishBounds, displayBounds, mEndFixedRotation, displayRotation); - mSurfaceTransactionHelper.crop(mFinishTransaction, leash, finishBounds); + mSurfaceTransactionHelper.cropAndPosition(mFinishTransaction, leash, + finishBounds); } } mFinishTransaction = null; @@ -1068,6 +1069,11 @@ public class PipTransition extends PipTransitionController { mPipBoundsState.mayUseCachedLauncherShelfHeight(); final Rect destinationBounds = mPipBoundsAlgorithm.getEntryDestinationBounds(); final Rect currentBounds = pipChange.getStartAbsBounds(); + // The app bounds should offset relative to the task leash to make the center calculation + // correctly. + final Rect relativeAppBounds = new Rect(taskInfo.topActivityMainWindowFrame != null + ? taskInfo.topActivityMainWindowFrame : currentBounds); + relativeAppBounds.offset(-currentBounds.left, -currentBounds.top); int rotationDelta = deltaRotation(startRotation, endRotation); Rect sourceHintRect = mPipOrganizer.takeSwipeSourceRectHint(); @@ -1089,6 +1095,8 @@ public class PipTransition extends PipTransitionController { if (taskInfo.pictureInPictureParams != null && taskInfo.pictureInPictureParams.isAutoEnterEnabled() && mPipTransitionState.getInSwipePipToHomeTransition()) { + // TODO(b/356277166): add support to swipe PIP to home with + // non-match parent activity. handleSwipePipToHomeTransition(startTransaction, finishTransaction, leash, sourceHintRect, destinationBounds, taskInfo); return; @@ -1119,8 +1127,8 @@ public class PipTransition extends PipTransitionController { // TODO(b/272819817): cleanup the null-check and extra logging. final boolean hasTopActivityInfo = taskInfo.topActivityInfo != null; if (hasTopActivityInfo && mFixedRotationState != FIXED_ROTATION_TRANSITION) { - animator.setAppIconContentOverlay( - mContext, currentBounds, destinationBounds, taskInfo.topActivityInfo, + animator.setAppIconContentOverlay(mContext, relativeAppBounds, + destinationBounds, taskInfo.topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } else { ProtoLog.w(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, @@ -1149,14 +1157,14 @@ public class PipTransition extends PipTransitionController { animationDuration = 0; } mSurfaceTransactionHelper - .crop(finishTransaction, leash, destinationBounds) + .cropAndPosition(finishTransaction, leash, destinationBounds) .round(finishTransaction, leash, true /* applyCornerRadius */); // Always reset to bounds animation type afterwards. setEnterAnimationType(ANIM_TYPE_BOUNDS); } else { throw new RuntimeException("Unrecognized animation type: " + enterAnimationType); } - mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), currentBounds); + mPipOrganizer.setContentOverlay(animator.getContentOverlayLeash(), relativeAppBounds); animator.setTransitionDirection(TRANSITION_DIRECTION_TO_PIP) .setPipAnimationCallback(mPipAnimationCallback) .setDuration(animationDuration); @@ -1340,11 +1348,11 @@ public class PipTransition extends PipTransitionController { "%s: Update pip for unhandled transition, change=%s, destBounds=%s, isInPip=%b", TAG, pipChange, destBounds, isInPip); mSurfaceTransactionHelper - .crop(startTransaction, leash, destBounds) + .cropAndPosition(startTransaction, leash, destBounds) .round(startTransaction, leash, isInPip) .shadow(startTransaction, leash, isInPip); mSurfaceTransactionHelper - .crop(finishTransaction, leash, destBounds) + .cropAndPosition(finishTransaction, leash, destBounds) .round(finishTransaction, leash, isInPip) .shadow(finishTransaction, leash, isInPip); // Make sure the PiP keeps invisible if it was faded out. If it needs to fade in, that will 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 94b344fb575a..5ffc64f412f1 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 @@ -368,10 +368,8 @@ public abstract class PipTransitionController implements Transitions.TransitionH /** * Finish the current transition if possible. - * - * @param tx transaction to be applied with a potentially new draw after finishing. */ - public void finishTransition(@Nullable SurfaceControl.Transaction tx) { + public void finishTransition() { } /** 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 7a0e6694cb51..d3ae411469cc 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 @@ -23,7 +23,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PIP; import static android.view.WindowManager.TRANSIT_TO_BACK; -import static android.view.WindowManager.transitTypeToString; import static com.android.wm.shell.common.pip.PipMenuController.ALPHA_NO_CHANGE; import static com.android.wm.shell.pip.PipAnimationController.TRANSITION_DIRECTION_LEAVE_PIP; @@ -35,6 +34,7 @@ import static com.android.wm.shell.pip.PipTransitionState.EXITING_PIP; import static com.android.wm.shell.pip.PipTransitionState.UNDEFINED; import static com.android.wm.shell.transition.Transitions.TRANSIT_EXIT_PIP; import static com.android.wm.shell.transition.Transitions.TRANSIT_REMOVE_PIP; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.animation.AnimationHandler; import android.animation.Animator; @@ -338,7 +338,7 @@ public class TvPipTransition extends PipTransitionController { final Rect pipBounds = mPipBoundsState.getBounds(); mSurfaceTransactionHelper .resetScale(startTransaction, pipLeash, pipBounds) - .crop(startTransaction, pipLeash, pipBounds) + .cropAndPosition(startTransaction, pipLeash, pipBounds) .shadow(startTransaction, pipLeash, false); final SurfaceControl.Transaction transaction = mTransactionFactory.getTransaction(); @@ -420,7 +420,7 @@ public class TvPipTransition extends PipTransitionController { mSurfaceTransactionHelper .resetScale(finishTransaction, leash, pipBounds) - .crop(finishTransaction, leash, pipBounds) + .cropAndPosition(finishTransaction, leash, pipBounds) .shadow(finishTransaction, leash, false); final Rect currentBounds = pipChange.getStartAbsBounds(); @@ -443,7 +443,7 @@ public class TvPipTransition extends PipTransitionController { SurfaceControl.Transaction tx = mTransactionFactory.getTransaction(); mSurfaceTransactionHelper .resetScale(tx, leash, pipBounds) - .crop(tx, leash, pipBounds) + .cropAndPosition(tx, leash, pipBounds) .shadow(tx, leash, false); mShellTaskOrganizer.applyTransaction(resizePipWct); tx.apply(); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java index 895c2aeba9ef..63c151268bdb 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipAlphaAnimator.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.animation; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.ValueAnimator; import android.annotation.IntDef; import android.content.Context; @@ -25,6 +26,7 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; @@ -34,8 +36,7 @@ import java.lang.annotation.RetentionPolicy; /** * Animator that handles the alpha animation for entering PIP */ -public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.AnimatorUpdateListener, - ValueAnimator.AnimatorListener { +public class PipAlphaAnimator extends ValueAnimator { @IntDef(prefix = {"FADE_"}, value = { FADE_IN, FADE_OUT @@ -47,15 +48,45 @@ public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.Ani public static final int FADE_IN = 0; public static final int FADE_OUT = 1; - private final int mEnterAnimationDuration; private final SurfaceControl mLeash; private final SurfaceControl.Transaction mStartTransaction; + private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + }; + + private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final float alpha = (Float) animation.getAnimatedValue(); + mSurfaceControlTransactionFactory.getTransaction() + .setAlpha(mLeash, alpha).apply(); + } + }; + // optional callbacks for tracking animation start and end @Nullable private Runnable mAnimationStartCallback; @Nullable private Runnable mAnimationEndCallback; - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + @NonNull private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; public PipAlphaAnimator(Context context, @@ -71,11 +102,11 @@ public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.Ani } mSurfaceControlTransactionFactory = new PipSurfaceTransactionHelper.VsyncSurfaceControlTransactionFactory(); - mEnterAnimationDuration = context.getResources() + final int enterAnimationDuration = context.getResources() .getInteger(R.integer.config_pipEnterAnimationDuration); - setDuration(mEnterAnimationDuration); - addListener(this); - addUpdateListener(this); + setDuration(enterAnimationDuration); + addListener(mAnimatorListener); + addUpdateListener(mAnimatorUpdateListener); } public void setAnimationStartCallback(@NonNull Runnable runnable) { @@ -86,32 +117,9 @@ public class PipAlphaAnimator extends ValueAnimator implements ValueAnimator.Ani mAnimationEndCallback = runnable; } - @Override - public void onAnimationStart(@NonNull Animator animation) { - if (mAnimationStartCallback != null) { - mAnimationStartCallback.run(); - } - if (mStartTransaction != null) { - mStartTransaction.apply(); - } + @VisibleForTesting + void setSurfaceControlTransactionFactory( + @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; } - - @Override - public void onAnimationUpdate(@NonNull ValueAnimator animation) { - final float alpha = (Float) animation.getAnimatedValue(); - mSurfaceControlTransactionFactory.getTransaction().setAlpha(mLeash, alpha).apply(); - } - - @Override - public void onAnimationEnd(@NonNull Animator animation) { - if (mAnimationEndCallback != null) { - mAnimationEndCallback.run(); - } - } - - @Override - public void onAnimationCancel(@NonNull Animator animation) {} - - @Override - public void onAnimationRepeat(@NonNull Animator animation) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java index 740b9af56144..e5137582822d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipEnterAnimator.java @@ -192,22 +192,7 @@ public class PipEnterAnimator extends ValueAnimator * calculated differently from generic transitions. * @param pipChange PiP change received as a transition target. */ - public void setEnterStartState(@NonNull TransitionInfo.Change pipChange, - @NonNull TransitionInfo.Change pipActivityChange) { - PipUtils.calcEndTransform(pipActivityChange, pipChange, mInitActivityScale, - mInitActivityPos); - if (mStartTransaction != null && pipActivityChange.getLeash() != null) { - mStartTransaction.setCrop(pipActivityChange.getLeash(), null); - mStartTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, - mInitActivityScale.y); - mStartTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, - mInitActivityPos.y); - mFinishTransaction.setCrop(pipActivityChange.getLeash(), null); - mFinishTransaction.setScale(pipActivityChange.getLeash(), mInitActivityScale.x, - mInitActivityScale.y); - mFinishTransaction.setPosition(pipActivityChange.getLeash(), mInitActivityPos.x, - mInitActivityPos.y); - } + public void setEnterStartState(@NonNull TransitionInfo.Change pipChange) { PipUtils.calcStartTransform(pipChange, mInitScale, mInitPos, mInitCrop); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java index a93ef12cb7fa..3f9b0c30e314 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipExpandAnimator.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.animation; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; @@ -27,6 +28,7 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.R; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; import com.android.wm.shell.shared.animation.Interpolators; @@ -34,8 +36,7 @@ import com.android.wm.shell.shared.animation.Interpolators; /** * Animator that handles bounds animations for exit-via-expanding PIP. */ -public class PipExpandAnimator extends ValueAnimator - implements ValueAnimator.AnimatorUpdateListener, ValueAnimator.AnimatorListener { +public class PipExpandAnimator extends ValueAnimator { @NonNull private final SurfaceControl mLeash; private final SurfaceControl.Transaction mStartTransaction; @@ -58,12 +59,61 @@ public class PipExpandAnimator extends ValueAnimator // Bounds updated by the evaluator as animator is running. private final Rect mAnimatedRect = new Rect(); - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; private final RectEvaluator mRectEvaluator; private final RectEvaluator mInsetEvaluator; private final PipSurfaceTransactionHelper mPipSurfaceTransactionHelper; + private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTransaction != null) { + mStartTransaction.apply(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mFinishTransaction != null) { + // finishTransaction might override some state (eg. corner radii) so we want to + // manually set the state to the end of the animation + mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, + mSourceRectHint, mBaseBounds, mAnimatedRect, getInsets(1f), + false /* isInPipDirection */, 1f) + .round(mFinishTransaction, mLeash, false /* applyCornerRadius */) + .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + }; + + private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + + // TODO (b/350801661): implement fixed rotation + Rect insets = getInsets(fraction); + mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, + mBaseBounds, mAnimatedRect, + insets, false /* isInPipDirection */, fraction) + .round(tx, mLeash, false /* applyCornerRadius */) + .shadow(tx, mLeash, false /* applyCornerRadius */); + tx.apply(); + } + }; + public PipExpandAnimator(Context context, @NonNull SurfaceControl leash, SurfaceControl.Transaction startTransaction, @@ -105,8 +155,8 @@ public class PipExpandAnimator extends ValueAnimator setObjectValues(startBounds, endBounds); setEvaluator(mRectEvaluator); setInterpolator(Interpolators.FAST_OUT_SLOW_IN); - addListener(this); - addUpdateListener(this); + addListener(mAnimatorListener); + addUpdateListener(mAnimatorUpdateListener); } public void setAnimationStartCallback(@NonNull Runnable runnable) { @@ -117,58 +167,15 @@ public class PipExpandAnimator extends ValueAnimator mAnimationEndCallback = runnable; } - @Override - public void onAnimationStart(@NonNull Animator animation) { - if (mAnimationStartCallback != null) { - mAnimationStartCallback.run(); - } - if (mStartTransaction != null) { - mStartTransaction.apply(); - } - } - - @Override - public void onAnimationEnd(@NonNull Animator animation) { - if (mFinishTransaction != null) { - // finishTransaction might override some state (eg. corner radii) so we want to - // manually set the state to the end of the animation - mPipSurfaceTransactionHelper.scaleAndCrop(mFinishTransaction, mLeash, mSourceRectHint, - mBaseBounds, mAnimatedRect, getInsets(1f), - false /* isInPipDirection */, 1f) - .round(mFinishTransaction, mLeash, false /* applyCornerRadius */) - .shadow(mFinishTransaction, mLeash, false /* applyCornerRadius */); - } - if (mAnimationEndCallback != null) { - mAnimationEndCallback.run(); - } - } - - @Override - public void onAnimationUpdate(@NonNull ValueAnimator animation) { - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - final float fraction = getAnimatedFraction(); - - // TODO (b/350801661): implement fixed rotation - - Rect insets = getInsets(fraction); - mPipSurfaceTransactionHelper.scaleAndCrop(tx, mLeash, mSourceRectHint, - mBaseBounds, mAnimatedRect, insets, false /* isInPipDirection */, fraction) - .round(tx, mLeash, false /* applyCornerRadius */) - .shadow(tx, mLeash, false /* applyCornerRadius */); - tx.apply(); - } - private Rect getInsets(float fraction) { final Rect startInsets = mSourceRectHintInsets; final Rect endInsets = mZeroInsets; return mInsetEvaluator.evaluate(fraction, startInsets, endInsets); } - // no-ops - - @Override - public void onAnimationCancel(@NonNull Animator animation) {} - - @Override - public void onAnimationRepeat(@NonNull Animator animation) {} + @VisibleForTesting + void setSurfaceControlTransactionFactory( + @NonNull PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java index d565776c9917..012dabbbb9f8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/animation/PipResizeAnimator.java @@ -17,6 +17,7 @@ package com.android.wm.shell.pip2.animation; import android.animation.Animator; +import android.animation.AnimatorListenerAdapter; import android.animation.RectEvaluator; import android.animation.ValueAnimator; import android.content.Context; @@ -27,13 +28,13 @@ import android.view.SurfaceControl; import androidx.annotation.NonNull; import androidx.annotation.Nullable; +import com.android.internal.annotations.VisibleForTesting; import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; /** * Animator that handles any resize related animation for PIP. */ -public class PipResizeAnimator extends ValueAnimator - implements ValueAnimator.AnimatorUpdateListener, Animator.AnimatorListener{ +public class PipResizeAnimator extends ValueAnimator { @NonNull private final Context mContext; @NonNull @@ -61,9 +62,47 @@ public class PipResizeAnimator extends ValueAnimator private final Rect mAnimatedRect = new Rect(); private final float mDelta; - private final PipSurfaceTransactionHelper.SurfaceControlTransactionFactory + private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mSurfaceControlTransactionFactory; + private final Animator.AnimatorListener mAnimatorListener = new AnimatorListenerAdapter() { + @Override + public void onAnimationStart(Animator animation) { + super.onAnimationStart(animation); + if (mAnimationStartCallback != null) { + mAnimationStartCallback.run(); + } + if (mStartTx != null) { + setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); + mStartTx.apply(); + } + } + + @Override + public void onAnimationEnd(Animator animation) { + super.onAnimationEnd(animation); + if (mFinishTx != null) { + setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); + } + if (mAnimationEndCallback != null) { + mAnimationEndCallback.run(); + } + } + }; + + private final ValueAnimator.AnimatorUpdateListener mAnimatorUpdateListener = + new AnimatorUpdateListener() { + @Override + public void onAnimationUpdate(@NonNull ValueAnimator animation) { + final SurfaceControl.Transaction tx = + mSurfaceControlTransactionFactory.getTransaction(); + final float fraction = getAnimatedFraction(); + final float degrees = (1.0f - fraction) * mDelta; + setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); + tx.apply(); + } + }; + public PipResizeAnimator(@NonNull Context context, @NonNull SurfaceControl leash, @Nullable SurfaceControl.Transaction startTransaction, @@ -89,8 +128,8 @@ public class PipResizeAnimator extends ValueAnimator mRectEvaluator = new RectEvaluator(mAnimatedRect); setObjectValues(startBounds, endBounds); - addListener(this); - addUpdateListener(this); + addListener(mAnimatorListener); + addUpdateListener(mAnimatorUpdateListener); setEvaluator(mRectEvaluator); setDuration(duration); } @@ -103,26 +142,6 @@ public class PipResizeAnimator extends ValueAnimator mAnimationEndCallback = runnable; } - @Override - public void onAnimationStart(@NonNull Animator animation) { - if (mAnimationStartCallback != null) { - mAnimationStartCallback.run(); - } - if (mStartTx != null) { - setBoundsAndRotation(mStartTx, mLeash, mBaseBounds, mStartBounds, mDelta); - mStartTx.apply(); - } - } - - @Override - public void onAnimationUpdate(@NonNull ValueAnimator animation) { - final SurfaceControl.Transaction tx = mSurfaceControlTransactionFactory.getTransaction(); - final float fraction = getAnimatedFraction(); - final float degrees = (1.0f - fraction) * mDelta; - setBoundsAndRotation(tx, mLeash, mBaseBounds, mAnimatedRect, degrees); - tx.apply(); - } - /** * Set a proper transform matrix for a leash to move it to given bounds with a certain rotation. * @@ -130,7 +149,7 @@ public class PipResizeAnimator extends ValueAnimator * @param targetBounds bounds to which we are scaling the leash. * @param degrees degrees of rotation - counter-clockwise is positive by convention. */ - public static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, + private static void setBoundsAndRotation(SurfaceControl.Transaction tx, SurfaceControl leash, Rect baseBounds, Rect targetBounds, float degrees) { Matrix transformTensor = new Matrix(); final float[] mMatrixTmp = new float[9]; @@ -144,19 +163,9 @@ public class PipResizeAnimator extends ValueAnimator tx.setMatrix(leash, transformTensor, mMatrixTmp); } - @Override - public void onAnimationEnd(@NonNull Animator animation) { - if (mFinishTx != null) { - setBoundsAndRotation(mFinishTx, mLeash, mBaseBounds, mEndBounds, 0f); - } - if (mAnimationEndCallback != null) { - mAnimationEndCallback.run(); - } + @VisibleForTesting + void setSurfaceControlTransactionFactory(@NonNull + PipSurfaceTransactionHelper.SurfaceControlTransactionFactory factory) { + mSurfaceControlTransactionFactory = factory; } - - @Override - public void onAnimationCancel(@NonNull Animator animation) {} - - @Override - public void onAnimationRepeat(@NonNull Animator animation) {} } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java index 8c1e5e6a3e84..58d2a8577d8c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PhonePipMenuController.java @@ -243,7 +243,6 @@ public class PhonePipMenuController implements PipMenuController, mSystemWindows.updateViewLayout(mPipMenuView, getPipMenuLayoutParams(mContext, MENU_WINDOW_TITLE, destinationBounds.width(), destinationBounds.height())); - updateMenuLayout(destinationBounds); } /** @@ -569,23 +568,6 @@ public class PhonePipMenuController implements PipMenuController, } } - /** - * Tell the PIP Menu to recalculate its layout given its current position on the display. - */ - public void updateMenuLayout(Rect bounds) { - final boolean isMenuVisible = isMenuVisible(); - if (DEBUG) { - ProtoLog.d(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE, - "%s: updateMenuLayout() state=%s" - + " isMenuVisible=%s" - + " callers=\n%s", TAG, mMenuState, isMenuVisible, - Debug.getCallers(5, " ")); - } - if (isMenuVisible) { - mPipMenuView.updateMenuLayout(bounds); - } - } - @Override public void onPipTransitionStateChanged(@PipTransitionState.TransitionState int oldState, @PipTransitionState.TransitionState int newState, Bundle extra) { @@ -597,7 +579,6 @@ public class PhonePipMenuController implements PipMenuController, detach(); break; case PipTransitionState.CHANGED_PIP_BOUNDS: - updateMenuLayout(mPipBoundsState.getBounds()); hideMenu(); break; case PipTransitionState.CHANGING_PIP_BOUNDS: diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java deleted file mode 100644 index ecb6ad690e26..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuIconsAlgorithm.java +++ /dev/null @@ -1,71 +0,0 @@ -/* - * Copyright (C) 2020 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.content.Context; -import android.graphics.Rect; -import android.view.View; -import android.view.ViewGroup; -import android.widget.FrameLayout; - -/** - * Helper class to calculate and place the menu icons on the PIP Menu. - */ -public class PipMenuIconsAlgorithm { - - private static final String TAG = "PipMenuIconsAlgorithm"; - - protected ViewGroup mViewRoot; - protected ViewGroup mTopEndContainer; - protected View mDragHandle; - protected View mSettingsButton; - protected View mDismissButton; - - protected PipMenuIconsAlgorithm(Context context) { - } - - /** - * Bind the necessary views. - */ - public void bindViews(ViewGroup viewRoot, ViewGroup topEndContainer, View dragHandle, - View settingsButton, View dismissButton) { - mViewRoot = viewRoot; - mTopEndContainer = topEndContainer; - mDragHandle = dragHandle; - mSettingsButton = settingsButton; - mDismissButton = dismissButton; - } - - /** - * Updates the position of the drag handle based on where the PIP window is on the screen. - */ - public void onBoundsChanged(Rect bounds) { - // On phones, the menu icons are always static and will never move based on the PIP window - // position. No need to do anything here. - } - - /** - * Set the gravity on the given view. - */ - protected static void setLayoutGravity(View v, int gravity) { - if (v.getLayoutParams() instanceof FrameLayout.LayoutParams) { - FrameLayout.LayoutParams params = (FrameLayout.LayoutParams) v.getLayoutParams(); - params.gravity = gravity; - v.setLayoutParams(params); - } - } -} diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java index 0910919b3064..7cfaadedeca3 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMenuView.java @@ -145,7 +145,6 @@ public class PipMenuView extends FrameLayout { protected View mSettingsButton; protected View mDismissButton; protected View mTopEndContainer; - protected PipMenuIconsAlgorithm mPipMenuIconsAlgorithm; // How long the shell will wait for the app to close the PiP if a custom action is set. private final int mPipForceCloseDelay; @@ -193,9 +192,6 @@ public class PipMenuView extends FrameLayout { mActionsGroup = findViewById(R.id.actions_group); mBetweenActionPaddingLand = getResources().getDimensionPixelSize( R.dimen.pip_between_action_padding_land); - mPipMenuIconsAlgorithm = new PipMenuIconsAlgorithm(mContext); - mPipMenuIconsAlgorithm.bindViews((ViewGroup) mViewRoot, (ViewGroup) mTopEndContainer, - findViewById(R.id.resize_handle), mSettingsButton, mDismissButton); mDismissFadeOutDurationMs = context.getResources() .getInteger(R.integer.config_pipExitAnimationDuration); @@ -339,10 +335,6 @@ public class PipMenuView extends FrameLayout { cancelDelayedHide(); } - void updateMenuLayout(Rect bounds) { - mPipMenuIconsAlgorithm.onBoundsChanged(bounds); - } - void hideMenu() { hideMenu(null); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java index 810eff8a055c..17392bc521d8 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipMotionHelper.java @@ -752,11 +752,11 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION)); break; case PipTransitionState.CHANGING_PIP_BOUNDS: - SurfaceControl.Transaction startTx = extra.getParcelable( + final SurfaceControl.Transaction startTx = extra.getParcelable( PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishTx = extra.getParcelable( + final SurfaceControl.Transaction finishTx = extra.getParcelable( PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); - Rect destinationBounds = extra.getParcelable( + final Rect destinationBounds = extra.getParcelable( PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); @@ -794,7 +794,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); // Signal that the transition is done - should update transition state by default. - mPipScheduler.scheduleFinishResizePip(destinationBounds, false /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds); } private void startResizeAnimation(SurfaceControl.Transaction startTx, @@ -803,11 +803,8 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, Preconditions.checkState(pipLeash != null, "No leash cached by mPipTransitionState=" + mPipTransitionState); - startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), - mPipBoundsState.getBounds().height()); - PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, - startTx, finishTx, mPipBoundsState.getBounds(), mPipBoundsState.getBounds(), + startTx, finishTx, destinationBounds, mPipBoundsState.getBounds(), destinationBounds, duration, 0f /* angle */); animator.setAnimationEndCallback(() -> { // In case an ongoing drag/fling was present before a deterministic resize transition @@ -818,7 +815,7 @@ public class PipMotionHelper implements PipAppOpsListener.Callback, cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition - mPipScheduler.scheduleFinishResizePip(destinationBounds, true /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds); }); animator.start(); } 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 f5ef64dff94b..751175f0f3e9 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 @@ -535,28 +535,26 @@ public class PipResizeGestureHandler implements Preconditions.checkState(pipLeash != null, "No leash cached by mPipTransitionState=" + mPipTransitionState); - SurfaceControl.Transaction startTx = extra.getParcelable( + final SurfaceControl.Transaction startTx = extra.getParcelable( PipTransition.PIP_START_TX, SurfaceControl.Transaction.class); - SurfaceControl.Transaction finishTx = extra.getParcelable( + final SurfaceControl.Transaction finishTx = extra.getParcelable( PipTransition.PIP_FINISH_TX, SurfaceControl.Transaction.class); + final Rect destinationBounds = extra.getParcelable( + PipTransition.PIP_DESTINATION_BOUNDS, Rect.class); final int duration = extra.getInt(ANIMATING_BOUNDS_CHANGE_DURATION, PipTransition.BOUNDS_CHANGE_JUMPCUT_DURATION); - startTx.setWindowCrop(pipLeash, mPipBoundsState.getBounds().width(), - mPipBoundsState.getBounds().height()); - PipResizeAnimator animator = new PipResizeAnimator(mContext, pipLeash, - startTx, finishTx, mPipBoundsState.getBounds(), mStartBoundsAfterRelease, - mLastResizeBounds, duration, mAngle); + startTx, finishTx, destinationBounds, mStartBoundsAfterRelease, + destinationBounds, duration, mAngle); animator.setAnimationEndCallback(() -> { // All motion operations have actually finished, so make bounds cache updates. - mUserResizeBounds.set(mLastResizeBounds); + mUserResizeBounds.set(destinationBounds); resetState(); cleanUpHighPerfSessionMaybe(); // Signal that we are done with resize transition - mPipScheduler.scheduleFinishResizePip( - mLastResizeBounds, true /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds); }); animator.start(); break; 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 fbbf6f3596d0..8b25b11e3a47 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 @@ -175,22 +175,12 @@ public class PipScheduler { * Note that we do not allow any actual WM Core changes at this point. * * @param toBounds destination bounds used only for internal state updates - not sent to Core. - * @param configAtEnd true if we are waiting for config updates at the end of the transition. */ - public void scheduleFinishResizePip(Rect toBounds, boolean configAtEnd) { - // Make updates to the internal state to reflect new bounds + public void scheduleFinishResizePip(Rect toBounds) { + // Make updates to the internal state to reflect new bounds before updating any transitions + // related state; transition state updates can trigger callbacks that use the cached bounds. onFinishingPipResize(toBounds); - - SurfaceControl.Transaction tx = null; - if (configAtEnd) { - tx = new SurfaceControl.Transaction(); - tx.addTransactionCommittedListener(mMainExecutor, () -> { - mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); - }); - } else { - mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); - } - mPipTransitionController.finishTransition(tx); + mPipTransitionController.finishTransition(); } /** 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 373ec806c40c..2c7584af1f07 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 @@ -177,8 +177,7 @@ public class PipTaskListener implements ShellTaskOrganizer.TaskListener, mPipBoundsState.getBounds(), destinationBounds, duration, 0f /* delta */); animator.setAnimationEndCallback(() -> { - mPipScheduler.scheduleFinishResizePip( - destinationBounds, false /* configAtEnd */); + mPipScheduler.scheduleFinishResizePip(destinationBounds); }); animator.start(); } 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 e90b32cd01e7..64d8887ae5cd 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 @@ -35,6 +35,7 @@ import android.app.ActivityManager; import android.app.PictureInPictureParams; import android.content.Context; import android.graphics.Point; +import android.graphics.PointF; import android.graphics.Rect; import android.os.Bundle; import android.os.IBinder; @@ -276,21 +277,25 @@ public class PipTransition extends PipTransitionController implements if (pipChange == null) { return false; } - SurfaceControl pipLeash = pipChange.getLeash(); + // We expect the PiP activity as a separate change in a config-at-end transition; + // only flings are not using config-at-end for resize bounds changes + TransitionInfo.Change pipActivityChange = getDeferConfigActivityChange(info, + pipChange.getTaskInfo().getToken()); + if (pipActivityChange != null) { + // Transform calculations use PiP params by default, so make sure they are null to + // default to using bounds for scaling calculations instead. + pipChange.getTaskInfo().pictureInPictureParams = null; + prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, + pipActivityChange); + } - // Even though the final bounds and crop are applied with finishTransaction since - // this is a visible change, we still need to handle the app draw coming in. Snapshot - // covering app draw during collection will be removed by startTransaction. So we make - // the crop equal to the final bounds and then let the current - // animator scale the leash back to starting bounds. - // Note: animator is responsible for applying the startTx but NOT finishTx. + SurfaceControl pipLeash = pipChange.getLeash(); startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(), pipChange.getEndAbsBounds().height()); - // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator. // Classes interested in continuing the animation would subscribe to this state update - // getting info such as endBounds, startTx, and finishTx as an extra Bundle once - // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS. + // getting info such as endBounds, startTx, and finishTx as an extra Bundle + // Once done state needs to be updated to CHANGED_PIP_BOUNDS via {@link PipScheduler#}. Bundle extra = new Bundle(); extra.putParcelable(PIP_START_TX, startTransaction); extra.putParcelable(PIP_FINISH_TX, finishTransaction); @@ -353,10 +358,12 @@ public class PipTransition extends PipTransitionController implements sourceRectHint = pipChange.getTaskInfo().pictureInPictureParams.getSourceRectHint(); } + prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, + pipActivityChange); startTransaction.merge(finishTransaction); PipEnterAnimator animator = new PipEnterAnimator(mContext, pipLeash, startTransaction, finishTransaction, destinationBounds, sourceRectHint, delta); - animator.setEnterStartState(pipChange, pipActivityChange); + animator.setEnterStartState(pipChange); animator.onEnterAnimationUpdate(1.0f /* fraction */, startTransaction); startTransaction.apply(); @@ -368,7 +375,7 @@ public class PipTransition extends PipTransitionController implements tx.apply(); }); } - finishInner(); + finishTransition(); return true; } @@ -393,14 +400,13 @@ public class PipTransition extends PipTransitionController implements final PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; final float aspectRatio = mPipBoundsAlgorithm.getAspectRatioOrDefault(params); - final Rect sourceRectHint = PipBoundsAlgorithm.getValidSourceHintRect(params, startBounds, endBounds); - - final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; final Rect adjustedSourceRectHint = sourceRectHint != null ? new Rect(sourceRectHint) : PipUtils.getEnterPipWithOverlaySrcRectHint(startBounds, aspectRatio); + final SurfaceControl pipLeash = mPipTransitionState.mPinnedTaskLeash; + // For opening type transitions, if there is a change of mode TO_FRONT/OPEN, // make sure that change has alpha of 1f, since it's init state might be set to alpha=0f // by the Transitions framework to simplify Task opening transitions. @@ -435,14 +441,16 @@ public class PipTransition extends PipTransitionController implements mContext, startBounds, endBounds, pipChange.getTaskInfo().topActivityInfo, mPipBoundsState.getLauncherState().getAppIconSizePx()); } - animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange, - pipActivityChange)); + + prepareConfigAtEndActivity(startTransaction, finishTransaction, pipChange, + pipActivityChange); + animator.setAnimationStartCallback(() -> animator.setEnterStartState(pipChange)); animator.setAnimationEndCallback(() -> { if (animator.getContentOverlayLeash() != null) { startOverlayFadeoutAnimation(animator.getContentOverlayLeash(), animator::clearAppIconOverlay); } - finishInner(); + finishTransition(); }); animator.start(); return true; @@ -512,8 +520,7 @@ public class PipTransition extends PipTransitionController implements PipAlphaAnimator animator = new PipAlphaAnimator(mContext, pipLeash, startTransaction, PipAlphaAnimator.FADE_IN); // This should update the pip transition state accordingly after we stop playing. - animator.setAnimationEndCallback(this::finishInner); - + animator.setAnimationEndCallback(this::finishTransition); animator.start(); return true; } @@ -569,12 +576,7 @@ public class PipTransition extends PipTransitionController implements PipExpandAnimator animator = new PipExpandAnimator(mContext, pipLeash, startTransaction, finishTransaction, endBounds, startBounds, endBounds, sourceRectHint, Surface.ROTATION_0); - - animator.setAnimationEndCallback(() -> { - mPipTransitionState.setState(PipTransitionState.EXITED_PIP); - finishCallback.onTransitionFinished(null); - }); - + animator.setAnimationEndCallback(this::finishTransition); animator.start(); return true; } @@ -698,33 +700,54 @@ public class PipTransition extends PipTransitionController implements return isPipMovedToBack || isPipClosed || isPipDismissed; } + private void prepareConfigAtEndActivity(@NonNull SurfaceControl.Transaction startTx, + @NonNull SurfaceControl.Transaction finishTx, + @NonNull TransitionInfo.Change pipChange, + @NonNull TransitionInfo.Change pipActivityChange) { + PointF initActivityScale = new PointF(); + PointF initActivityPos = new PointF(); + PipUtils.calcEndTransform(pipActivityChange, pipChange, initActivityScale, + initActivityPos); + if (pipActivityChange.getLeash() != null) { + startTx.setCrop(pipActivityChange.getLeash(), null); + startTx.setScale(pipActivityChange.getLeash(), initActivityScale.x, + initActivityScale.y); + startTx.setPosition(pipActivityChange.getLeash(), initActivityPos.x, + initActivityPos.y); + + finishTx.setCrop(pipActivityChange.getLeash(), null); + finishTx.setScale(pipActivityChange.getLeash(), initActivityScale.x, + initActivityScale.y); + finishTx.setPosition(pipActivityChange.getLeash(), initActivityPos.x, + initActivityPos.y); + } + } + // // Miscellaneous callbacks and listeners // - private void finishInner() { - finishTransition(null /* tx */); - if (mPipTransitionState.getState() == PipTransitionState.ENTERING_PIP) { - // If we were entering PiP (i.e. playing the animation) with a valid srcRectHint, - // and then we get a signal on client finishing its draw after the transition - // has ended, then we have fully entered PiP. - mPipTransitionState.setState(PipTransitionState.ENTERED_PIP); - } - } - @Override - public void finishTransition(@Nullable SurfaceControl.Transaction tx) { - WindowContainerTransaction wct = null; - if (tx != null && mPipTransitionState.mPipTaskToken != null) { - // Outside callers can only provide a transaction to be applied with the final draw. - // So no actual WM changes can be applied for this transition after this point. - wct = new WindowContainerTransaction(); - wct.setBoundsChangeTransaction(mPipTransitionState.mPipTaskToken, tx); - } + public void finishTransition() { if (mFinishCallback != null) { - mFinishCallback.onTransitionFinished(wct); + mFinishCallback.onTransitionFinished(null /* finishWct */); mFinishCallback = null; } + + final int currentState = mPipTransitionState.getState(); + int nextState = PipTransitionState.UNDEFINED; + switch (currentState) { + case PipTransitionState.ENTERING_PIP: + nextState = PipTransitionState.ENTERED_PIP; + break; + case PipTransitionState.CHANGING_PIP_BOUNDS: + nextState = PipTransitionState.CHANGED_PIP_BOUNDS; + break; + case PipTransitionState.EXITING_PIP: + nextState = PipTransitionState.EXITED_PIP; + break; + } + mPipTransitionState.setState(nextState); } @Override diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl index 245829ecafb3..371bdd5c6469 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/IRecentTasksListener.aidl @@ -45,4 +45,7 @@ oneway interface IRecentTasksListener { /** A task has moved to front. */ oneway void onTaskMovedToFront(in RunningTaskInfo taskInfo); + + /** A task info has changed. */ + oneway void onTaskInfoChanged(in RunningTaskInfo taskInfo); }
\ No newline at end of file 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 6086801491e2..faa20159f64a 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 @@ -47,7 +47,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; import com.android.internal.protolog.ProtoLog; -import com.android.window.flags.Flags; import com.android.wm.shell.common.ExternalInterfaceBinder; import com.android.wm.shell.common.RemoteCallable; import com.android.wm.shell.common.ShellExecutor; @@ -289,6 +288,11 @@ public class RecentTasksController implements TaskStackListenerCallback, } @Override + public void onTaskChangedThroughTransition(@NonNull ActivityManager.RunningTaskInfo taskInfo) { + notifyTaskInfoChanged(taskInfo); + } + + @Override public void onTaskMovedToFrontThroughTransition( ActivityManager.RunningTaskInfo runningTaskInfo) { notifyTaskMovedToFront(runningTaskInfo); @@ -355,6 +359,19 @@ public class RecentTasksController implements TaskStackListenerCallback, } } + private void notifyTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + if (mListener == null + || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() + || taskInfo.realActivity == null) { + return; + } + try { + mListener.onTaskInfoChanged(taskInfo); + } catch (RemoteException e) { + Slog.w(TAG, "Failed call onTaskInfoChanged", e); + } + } + private void notifyTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { if (mListener == null || !DesktopModeFlags.ENABLE_TASK_STACK_OBSERVER_IN_SHELL.isTrue() @@ -426,7 +443,7 @@ public class RecentTasksController implements TaskStackListenerCallback, // If task has their app bounds set to null which happens after reboot, set the // app bounds to persisted lastFullscreenBounds. Also set the position in parent // to the top left of the bounds. - if (Flags.enableDesktopWindowingPersistence() + if (DesktopModeFlags.ENABLE_DESKTOP_WINDOWING_PERSISTENCE.isTrue() && taskInfo.configuration.windowConfiguration.getAppBounds() == null) { taskInfo.configuration.windowConfiguration.setAppBounds( taskInfo.lastNonFullscreenBounds); @@ -636,6 +653,11 @@ public class RecentTasksController implements TaskStackListenerCallback, public void onTaskMovedToFront(ActivityManager.RunningTaskInfo taskInfo) { mListener.call(l -> l.onTaskMovedToFront(taskInfo)); } + + @Override + public void onTaskInfoChanged(ActivityManager.RunningTaskInfo taskInfo) { + mListener.call(l -> l.onTaskInfoChanged(taskInfo)); + } }; public IRecentTasksImpl(RecentTasksController controller) { diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt index 1af99f974a28..d28a462546f9 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/recents/TaskStackTransitionObserver.kt @@ -20,8 +20,9 @@ import android.app.ActivityManager.RunningTaskInfo import android.os.IBinder import android.util.ArrayMap import android.view.SurfaceControl -import android.window.TransitionInfo +import android.view.WindowManager.TRANSIT_CHANGE import android.window.DesktopModeFlags +import android.window.TransitionInfo import com.android.wm.shell.shared.TransitionUtil import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -69,8 +70,10 @@ class TaskStackTransitionObserver( // Find the first task that is opening, this should be the one at the front after // the transition if (TransitionUtil.isOpeningType(change.mode)) { - notifyTaskStackTransitionObserverListeners(taskInfo) + notifyOnTaskMovedToFront(taskInfo) break + } else if (change.mode == TRANSIT_CHANGE) { + notifyOnTaskChanged(taskInfo) } } } @@ -95,15 +98,23 @@ class TaskStackTransitionObserver( taskStackTransitionObserverListeners.remove(taskStackTransitionObserverListener) } - private fun notifyTaskStackTransitionObserverListeners(taskInfo: RunningTaskInfo) { + private fun notifyOnTaskMovedToFront(taskInfo: RunningTaskInfo) { taskStackTransitionObserverListeners.forEach { (listener, executor) -> executor.execute { listener.onTaskMovedToFrontThroughTransition(taskInfo) } } } + private fun notifyOnTaskChanged(taskInfo: RunningTaskInfo) { + taskStackTransitionObserverListeners.forEach { (listener, executor) -> + executor.execute { listener.onTaskChangedThroughTransition(taskInfo) } + } + } + /** Listener to use to get updates regarding task stack from this observer */ interface TaskStackTransitionObserverListener { /** Called when a task is moved to front. */ fun onTaskMovedToFrontThroughTransition(taskInfo: RunningTaskInfo) {} + /** Called when a task info has changed. */ + fun onTaskChangedThroughTransition(taskInfo: RunningTaskInfo) {} } } 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 7893267d014a..cc0e1df115c2 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 @@ -30,7 +30,6 @@ import static android.view.WindowManager.TRANSIT_CLOSE; import static android.view.WindowManager.TRANSIT_KEYGUARD_OCCLUDE; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.WindowContainerTransaction.HierarchyOp.HIERARCHY_OP_TYPE_REORDER; @@ -73,6 +72,7 @@ import static com.android.wm.shell.splitscreen.SplitScreenController.exitReasonT import static com.android.wm.shell.transition.MixedTransitionHelper.getPipReplacingChange; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE; import static com.android.wm.shell.transition.Transitions.TRANSIT_SPLIT_SCREEN_PAIR_OPEN; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.animation.Animator; import android.animation.AnimatorListenerAdapter; @@ -1625,6 +1625,21 @@ public class StageCoordinator implements SplitLayout.SplitLayoutHandler, leftTopTaskId = mainStageTopTaskId; rightBottomTaskId = sideStageTopTaskId; } + + if (Flags.enableFlexibleTwoAppSplit()) { + // Split screen can be laid out in such a way that some of the apps are offscreen. + // For the purposes of passing SplitBounds up to launcher (for use in thumbnails + // etc.), we crop the bounds down to the screen size. + topLeftBounds.left = + Math.max(topLeftBounds.left, 0); + topLeftBounds.top = + Math.max(topLeftBounds.top, 0); + bottomRightBounds.right = + Math.min(bottomRightBounds.right, mSplitLayout.getDisplayWidth()); + bottomRightBounds.top = + Math.min(bottomRightBounds.top, mSplitLayout.getDisplayHeight()); + } + SplitBounds splitBounds = new SplitBounds(topLeftBounds, bottomRightBounds, leftTopTaskId, rightBottomTaskId, mSplitLayout.calculateCurrentSnapPosition()); if (mainStageTopTaskId != INVALID_TASK_ID && sideStageTopTaskId != INVALID_TASK_ID) { 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 0d89f757903e..17483dd68632 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 @@ -40,6 +40,7 @@ import android.window.WindowContainerToken; import android.window.WindowContainerTransaction; import com.android.internal.protolog.ProtoLog; +import com.android.window.flags.Flags; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.activityembedding.ActivityEmbeddingController; import com.android.wm.shell.common.split.SplitScreenUtils; @@ -419,7 +420,9 @@ public class DefaultMixedHandler implements MixedTransitionHandler, for (int i = 0; i < info.getRootCount(); ++i) { out.addRoot(info.getRoot(i)); } - out.setAnimationOptions(info.getAnimationOptions()); + if (!Flags.moveAnimationOptionsToChange()) { + out.setAnimationOptions(info.getAnimationOptions()); + } return out; } 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 5437167f58d5..ec58292b352c 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 @@ -561,13 +561,12 @@ public class DefaultTransitionHandler implements Transitions.TransitionHandler { final TransitionInfo.AnimationOptions options; if (Flags.moveAnimationOptionsToChange()) { - options = info.getAnimationOptions(); - } else { options = change.getAnimationOptions(); + } else { + options = info.getAnimationOptions(); } if (options != null) { - attachThumbnail(animations, onAnimFinish, change, info.getAnimationOptions(), - cornerRadius); + attachThumbnail(animations, onAnimFinish, change, options, cornerRadius); } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java index a27c14bda15a..4feb4753096e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/transition/TransitionAnimationHelper.java @@ -24,7 +24,6 @@ import static android.view.WindowManager.TRANSIT_OPEN; import static android.view.WindowManager.TRANSIT_PREPARE_BACK_NAVIGATION; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_IS_DISPLAY; import static android.window.TransitionInfo.FLAG_STARTING_WINDOW_TRANSFER_RECIPIENT; @@ -34,6 +33,7 @@ import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITI import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_CLOSE; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_INTRA_OPEN; import static com.android.internal.policy.TransitionAnimation.WALLPAPER_TRANSITION_OPEN; +import static com.android.wm.shell.transition.Transitions.transitTypeToString; import android.annotation.ColorInt; import android.annotation.NonNull; 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 7c9cd0862b69..1d456aed5f4d 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 @@ -29,7 +29,6 @@ import static android.view.WindowManager.TRANSIT_SLEEP; import static android.view.WindowManager.TRANSIT_TO_BACK; import static android.view.WindowManager.TRANSIT_TO_FRONT; import static android.view.WindowManager.fixScale; -import static android.view.WindowManager.transitTypeToString; import static android.window.TransitionInfo.FLAGS_IS_NON_APP_WINDOW; import static android.window.TransitionInfo.FLAG_BACK_GESTURE_ANIMATED; import static android.window.TransitionInfo.FLAG_IN_TASK_WITH_EMBEDDED_ACTIVITY; @@ -725,7 +724,7 @@ public class Transitions implements RemoteCallable<Transitions>, @NonNull SurfaceControl.Transaction t, @NonNull SurfaceControl.Transaction finishT) { info.setUnreleasedWarningCallSiteForAllSurfaces("Transitions.onTransitionReady"); ProtoLog.v(ShellProtoLogGroup.WM_SHELL_TRANSITIONS, "onTransitionReady (#%d) %s: %s", - info.getDebugId(), transitionToken, info); + info.getDebugId(), transitionToken, info.toString(" " /* prefix */)); int activeIdx = findByToken(mPendingTransitions, transitionToken); if (activeIdx < 0) { final ActiveTransition existing = mKnownTransitions.get(transitionToken); @@ -1847,6 +1846,40 @@ public class Transitions implements RemoteCallable<Transitions>, } } + /** + * Like WindowManager#transitTypeToString(), but also covers known custom transition types as + * well. + */ + public static String transitTypeToString(int transitType) { + if (transitType < TRANSIT_FIRST_CUSTOM) { + return WindowManager.transitTypeToString(transitType); + } + + String typeStr = switch (transitType) { + case TRANSIT_EXIT_PIP -> "EXIT_PIP"; + case TRANSIT_EXIT_PIP_TO_SPLIT -> "EXIT_PIP_TO_SPLIT"; + case TRANSIT_REMOVE_PIP -> "REMOVE_PIP"; + case TRANSIT_SPLIT_SCREEN_PAIR_OPEN -> "SPLIT_SCREEN_PAIR_OPEN"; + case TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE -> "SPLIT_SCREEN_OPEN_TO_SIDE"; + case TRANSIT_SPLIT_DISMISS_SNAP -> "SPLIT_DISMISS_SNAP"; + case TRANSIT_SPLIT_DISMISS -> "SPLIT_DISMISS"; + case TRANSIT_MAXIMIZE -> "MAXIMIZE"; + case TRANSIT_RESTORE_FROM_MAXIMIZE -> "RESTORE_FROM_MAXIMIZE"; + case TRANSIT_DESKTOP_MODE_START_DRAG_TO_DESKTOP -> "DESKTOP_MODE_START_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_END_DRAG_TO_DESKTOP -> "DESKTOP_MODE_END_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP -> + "DESKTOP_MODE_CANCEL_DRAG_TO_DESKTOP"; + case TRANSIT_DESKTOP_MODE_TOGGLE_RESIZE -> "DESKTOP_MODE_TOGGLE_RESIZE"; + case TRANSIT_RESIZE_PIP -> "RESIZE_PIP"; + case TRANSIT_TASK_FRAGMENT_DRAG_RESIZE -> "TASK_FRAGMENT_DRAG_RESIZE"; + case TRANSIT_SPLIT_PASSTHROUGH -> "SPLIT_PASSTHROUGH"; + case TRANSIT_CLEANUP_PIP_EXIT -> "CLEANUP_PIP_EXIT"; + case TRANSIT_MINIMIZE -> "MINIMIZE"; + default -> ""; + }; + return typeStr + "(FIRST_CUSTOM+" + (transitType - TRANSIT_FIRST_CUSTOM) + ")"; + } + private static boolean getShellTransitEnabled() { try { if (AppGlobals.getPackageManager().hasSystemFeature( 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 3946b6173b0b..c9546731a193 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 @@ -180,12 +180,13 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL // all other cases, it is expected that the transition handler positions and crops the task // in order to allow the handler time to animate before the task before the final // position and crop are set. - final boolean shouldSetTaskPositionAndCrop = mTaskDragResizer.isResizingOrAnimating(); + final boolean shouldSetTaskVisibilityPositionAndCrop = + mTaskDragResizer.isResizingOrAnimating(); // Use |applyStartTransactionOnDraw| so that the transaction (that applies task crop) is // synced with the buffer transaction (that draws the View). Both will be shown on screen // at the same, whereas applying them independently causes flickering. See b/270202228. relayout(taskInfo, t, t, true /* applyStartTransactionOnDraw */, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } @VisibleForTesting @@ -193,7 +194,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL RelayoutParams relayoutParams, ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, - boolean setTaskCropAndPosition, + boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, InsetsState displayInsetsState, @@ -206,7 +207,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL ? R.dimen.freeform_decor_shadow_focused_thickness : R.dimen.freeform_decor_shadow_unfocused_thickness; relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - relayoutParams.mSetTaskPositionAndCrop = setTaskCropAndPosition; + relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; relayoutParams.mIsCaptionVisible = taskInfo.isFreeform() || (isStatusBarVisible && !isKeyguardVisibleAndOccluded); @@ -234,7 +235,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL @SuppressLint("MissingPermission") void relayout(RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean setTaskCropAndPosition, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { final boolean isFreeform = taskInfo.getWindowingMode() == WindowConfiguration.WINDOWING_MODE_FREEFORM; @@ -246,7 +247,8 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL final WindowContainerTransaction wct = new WindowContainerTransaction(); updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw, - setTaskCropAndPosition, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, + shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, + mIsKeyguardVisibleAndOccluded, mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); relayout(mRelayoutParams, startT, finishT, wct, oldRootView, mResult); diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt index e71b4f3abf14..7b71e41874c7 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopHandleManageWindowsMenu.kt @@ -53,11 +53,8 @@ class DesktopHandleManageWindowsMenu( private var menuViewContainer: AdditionalViewContainer? = null init { - show(snapshotList, onIconClickListener, onOutsideClickListener) - } - - override fun close() { - menuViewContainer?.releaseView() + createMenu(snapshotList, onIconClickListener, onOutsideClickListener) + animateOpen() } private fun calculateMenuPosition(): Point { @@ -106,4 +103,8 @@ class DesktopHandleManageWindowsMenu( view = menuView.rootView, ) } + + override fun removeFromContainer() { + menuViewContainer?.releaseView() + } } 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 173bc08970ca..dd68105d28c1 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 @@ -65,11 +65,10 @@ class DesktopHeaderManageWindowsMenu( var menuViewContainer: AdditionalViewContainer? = null init { - show(snapshotList, onIconClickListener, onOutsideClickListener) - } - - override fun close() { - menuViewContainer?.releaseView() + createMenu(snapshotList, onIconClickListener, onOutsideClickListener) + menuView.rootView.pivotX = 0f + menuView.rootView.pivotY = 0f + animateOpen() } override fun addToContainer(menuView: ManageWindowsView) { @@ -139,4 +138,8 @@ class DesktopHeaderManageWindowsMenu( surfaceControlTransactionSupplier ) } + + override fun removeFromContainer() { + menuViewContainer?.releaseView() + } } 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 a3324cc6f286..29b8ddd03970 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 @@ -130,7 +130,6 @@ import com.android.wm.shell.sysui.ShellInit; import com.android.wm.shell.transition.FocusTransitionObserver; import com.android.wm.shell.transition.Transitions; import com.android.wm.shell.windowdecor.DesktopModeWindowDecoration.ExclusionRegionListener; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.extension.InsetsStateKt; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -181,7 +180,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, private boolean mTransitionDragActive; private SparseArray<EventReceiver> mEventReceiversByDisplay = new SparseArray<>(); - private DesktopStatusBarInputLayerSupplier mStatusBarInputLayerSupplier; private final ExclusionRegionListener mExclusionRegionListener = new ExclusionRegionListenerImpl(); @@ -420,11 +418,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, return Unit.INSTANCE; }); } - if (Flags.enableHandleInputFix()) { - mStatusBarInputLayerSupplier = - new DesktopStatusBarInputLayerSupplier(mContext, mMainHandler); - mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor); - } + mFocusTransitionObserver.setLocalFocusTransitionListener(this, mMainExecutor); } @Override @@ -480,7 +474,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, removeTaskFromEventReceiver(oldTaskInfo.displayId); incrementEventReceiverTasks(taskInfo.displayId); } - decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); decoration.relayout(taskInfo, decoration.mHasGlobalFocus); mActivityOrientationChangeHandler.ifPresent(handler -> handler.handleActivityOrientationChange(oldTaskInfo, taskInfo)); @@ -519,7 +512,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, if (decoration == null) { createWindowDecoration(taskInfo, taskSurface, startT, finishT); } else { - decoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); decoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo)); @@ -673,7 +665,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, decoration.closeHandleMenu(); // When the app enters split-select, the handle will no longer be visible, meaning // we shouldn't receive input for it any longer. - decoration.detachStatusBarInputLayer(); + decoration.disposeStatusBarInputLayer(); mDesktopTasksController.requestSplit(decoration.mTaskInfo, false /* leftOrTop */); } @@ -1314,8 +1306,8 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, // should not be receiving any input. if (resultType == TO_SPLIT_LEFT_INDICATOR || resultType == TO_SPLIT_RIGHT_INDICATOR) { - relevantDecor.detachStatusBarInputLayer(); - // We should also detach the other split task's input layer if + relevantDecor.disposeStatusBarInputLayer(); + // We should also dispose the other split task's input layer if // applicable. final int splitPosition = mSplitScreenController .getSplitPosition(relevantDecor.mTaskInfo.taskId); @@ -1328,7 +1320,7 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, mSplitScreenController.getTaskInfo(oppositePosition); if (oppositeTaskInfo != null) { mWindowDecorByTaskId.get(oppositeTaskInfo.taskId) - .detachStatusBarInputLayer(); + .disposeStatusBarInputLayer(); } } } @@ -1578,7 +1570,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, touchEventListener, touchEventListener, touchEventListener, touchEventListener); windowDecoration.setExclusionRegionListener(mExclusionRegionListener); windowDecoration.setDragPositioningCallback(taskPositioner); - windowDecoration.setStatusBarInputLayer(getStatusBarInputLayer(taskInfo)); windowDecoration.relayout(taskInfo, startT, finishT, false /* applyStartTransactionOnDraw */, false /* shouldSetTaskPositionAndCrop */, mFocusTransitionObserver.hasGlobalFocus(taskInfo)); @@ -1587,18 +1578,6 @@ public class DesktopModeWindowDecorViewModel implements WindowDecorViewModel, } } - /** Decide which cached status bar input layer should be used for a decoration. */ - private AdditionalSystemViewContainer getStatusBarInputLayer( - RunningTaskInfo taskInfo - ) { - if (mStatusBarInputLayerSupplier == null) return null; - return mStatusBarInputLayerSupplier.getStatusBarInputLayer( - taskInfo, - mSplitScreenController.getSplitPosition(taskInfo.taskId), - mSplitScreenController.isLeftRightSplit() - ); - } - private RunningTaskInfo getOtherSplitTask(int taskId) { @SplitPosition int remainingTaskPosition = mSplitScreenController .getSplitPosition(taskId) == SPLIT_POSITION_BOTTOM_OR_RIGHT 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 dc27cfe9e35f..c88ac3d19635 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 @@ -106,7 +106,6 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus; import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; import com.android.wm.shell.shared.desktopmode.ManageWindowsViewContainer; import com.android.wm.shell.splitscreen.SplitScreenController; -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer; import com.android.wm.shell.windowdecor.extension.TaskInfoKt; import com.android.wm.shell.windowdecor.viewholder.AppHandleViewHolder; import com.android.wm.shell.windowdecor.viewholder.AppHeaderViewHolder; @@ -206,7 +205,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin private final MultiInstanceHelper mMultiInstanceHelper; private final WindowDecorCaptionHandleRepository mWindowDecorCaptionHandleRepository; private final DesktopRepository mDesktopRepository; - private AdditionalSystemViewContainer mStatusBarInputLayer; DesktopModeWindowDecoration( Context context, @@ -392,18 +390,25 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @Override void relayout(ActivityManager.RunningTaskInfo taskInfo, boolean hasGlobalFocus) { final SurfaceControl.Transaction t = mSurfaceControlTransactionSupplier.get(); - // The crop and position of the task should only be set when a task is fluid resizing. In - // all other cases, it is expected that the transition handler positions and crops the task - // in order to allow the handler time to animate before the task before the final - // position and crop are set. - final boolean shouldSetTaskPositionAndCrop = !DesktopModeStatus.isVeiledResizeEnabled() - && mTaskDragResizer.isResizingOrAnimating(); + // The visibility, crop and position of the task should only be set when a task is + // fluid resizing. In all other cases, it is expected that the transition handler sets + // those task properties to allow the handler time to animate with full control of the task + // leash. In general, allowing the window decoration to set any of these is likely to cause + // incorrect frames and flickering because relayouts from TaskListener#onTaskInfoChanged + // aren't synchronized with shell transition callbacks, so if they come too early it + // might show/hide or crop the task at a bad time. + // Fluid resizing is exempt from this because it intentionally doesn't use shell + // transitions to resize the task, so onTaskInfoChanged relayouts is the only way to make + // sure the crop is set correctly. + final boolean shouldSetTaskVisibilityPositionAndCrop = + !DesktopModeStatus.isVeiledResizeEnabled() + && mTaskDragResizer.isResizingOrAnimating(); // For headers only (i.e. in freeform): use |applyStartTransactionOnDraw| so that the // transaction (that applies task crop) is synced with the buffer transaction (that draws // the View). Both will be shown on screen at the same, whereas applying them independently // causes flickering. See b/270202228. final boolean applyTransactionOnDraw = taskInfo.isFreeform(); - relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskPositionAndCrop, + relayout(taskInfo, t, t, applyTransactionOnDraw, shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (!applyTransactionOnDraw) { t.apply(); @@ -430,19 +435,19 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void relayout(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { Trace.beginSection("DesktopModeWindowDecoration#relayout"); if (taskInfo.isFreeform()) { // The Task is in Freeform mode -> show its header in sync since it's an integral part // of the window itself - a delayed header might cause bad UX. relayoutInSync(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } else { // The Task is outside Freeform mode -> allow the handle view to be delayed since the // handle is just a small addition to the window. relayoutWithDelayedViewHost(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); } Trace.endSection(); } @@ -450,12 +455,12 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin /** Run the whole relayout phase immediately without delay. */ private void relayoutInSync(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (mResult.mRootView != null) { updateViewHost(mRelayoutParams, startT, mResult); } @@ -477,7 +482,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin */ private void relayoutWithDelayedViewHost(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { if (applyStartTransactionOnDraw) { throw new IllegalArgumentException( @@ -486,7 +491,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin // Clear the current ViewHost runnable as we will update the ViewHost here clearCurrentViewHostRunnable(); updateRelayoutParamsAndSurfaces(taskInfo, startT, finishT, - false /* applyStartTransactionOnDraw */, shouldSetTaskPositionAndCrop, + false /* applyStartTransactionOnDraw */, shouldSetTaskVisibilityPositionAndCrop, hasGlobalFocus); if (mResult.mRootView == null) { // This means something blocks the window decor from showing, e.g. the task is hidden. @@ -501,7 +506,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin @SuppressLint("MissingPermission") private void updateRelayoutParamsAndSurfaces(ActivityManager.RunningTaskInfo taskInfo, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, - boolean applyStartTransactionOnDraw, boolean shouldSetTaskPositionAndCrop, + boolean applyStartTransactionOnDraw, boolean shouldSetTaskVisibilityPositionAndCrop, boolean hasGlobalFocus) { Trace.beginSection("DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces"); @@ -526,9 +531,9 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin final boolean inFullImmersive = mDesktopRepository .isTaskInFullImmersiveState(taskInfo.taskId); updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw, - shouldSetTaskPositionAndCrop, mIsStatusBarVisible, mIsKeyguardVisibleAndOccluded, - inFullImmersive, mDisplayController.getInsetsState(taskInfo.displayId), - hasGlobalFocus); + shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible, + mIsKeyguardVisibleAndOccluded, inFullImmersive, + mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus); final WindowDecorLinearLayout oldRootView = mResult.mRootView; final SurfaceControl oldDecorationSurface = mDecorationContainerSurface; @@ -550,13 +555,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin notifyNoCaptionHandle(); } mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); Trace.endSection(); // DesktopModeWindowDecoration#updateRelayoutParamsAndSurfaces return; } if (oldRootView != mResult.mRootView) { - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); mWindowDecorViewHolder = createViewHolder(); } Trace.beginSection("DesktopModeWindowDecoration#relayout-binding"); @@ -574,9 +579,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin mTaskInfo, position, mResult.mCaptionWidth, mResult.mCaptionHeight, isCaptionVisible() )); - if (mStatusBarInputLayer != null) { - asAppHandle(mWindowDecorViewHolder).bindStatusBarInputLayer(mStatusBarInputLayer); - } } else { mWindowDecorViewHolder.bindData(new AppHeaderViewHolder.HeaderData( mTaskInfo, @@ -790,15 +792,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin } /** - * Detach the status bar input layer from this decoration. Intended to be + * Dispose of the view used to forward inputs in status bar region. Intended to be * used any time handle is no longer visible. */ - void detachStatusBarInputLayer() { + void disposeStatusBarInputLayer() { if (!isAppHandle(mWindowDecorViewHolder) || !Flags.enableHandleInputFix()) { return; } - asAppHandle(mWindowDecorViewHolder).detachStatusBarInputLayer(); + asAppHandle(mWindowDecorViewHolder).disposeStatusBarInputLayer(); } private WindowDecorationViewHolder createViewHolder() { @@ -857,7 +859,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin Context context, ActivityManager.RunningTaskInfo taskInfo, boolean applyStartTransactionOnDraw, - boolean shouldSetTaskPositionAndCrop, + boolean shouldSetTaskVisibilityPositionAndCrop, boolean isStatusBarVisible, boolean isKeyguardVisibleAndOccluded, boolean inFullImmersiveMode, @@ -953,7 +955,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin : R.dimen.freeform_decor_shadow_unfocused_thickness; } relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw; - relayoutParams.mSetTaskPositionAndCrop = shouldSetTaskPositionAndCrop; + relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop; // The configuration used to layout the window decoration. A copy is made instead of using // the original reference so that the configuration isn't mutated on config changes and @@ -1094,13 +1096,13 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin if (mAppIconBitmap != null && mAppName != null) { return; } - final ComponentName baseActivity = mTaskInfo.baseActivity; - if (baseActivity == null) { - Slog.e(TAG, "Base activity component not found in task"); + if (mTaskInfo.baseIntent == null) { + Slog.e(TAG, "Base intent not found in task"); return; } final PackageManager pm = mUserContext.getPackageManager(); - final ActivityInfo activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */); + final ActivityInfo activityInfo = + pm.getActivityInfo(mTaskInfo.baseIntent.getComponent(), 0 /* flags */); final IconProvider provider = new IconProvider(mContext); final Drawable appIconDrawable = provider.getIcon(activityInfo); final Drawable badgedAppIconDrawable = pm.getUserBadgedIcon(appIconDrawable, @@ -1452,7 +1454,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin void closeManageWindowsMenu() { if (mManageWindowsMenu != null) { - mManageWindowsMenu.close(); + mManageWindowsMenu.animateClose(); } mManageWindowsMenu = null; } @@ -1638,7 +1640,7 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin closeManageWindowsMenu(); mExclusionRegionListener.onExclusionRegionDismissed(mTaskInfo.taskId); disposeResizeVeil(); - detachStatusBarInputLayer(); + disposeStatusBarInputLayer(); clearCurrentViewHostRunnable(); if (canEnterDesktopMode(mContext) && Flags.enableDesktopWindowingAppHandleEducation()) { notifyNoCaptionHandle(); @@ -1755,16 +1757,6 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin + "}"; } - /** - * Set the view container to be used to forward input through status bar. Null in cases - * where input forwarding isn't needed. - */ - public void setStatusBarInputLayer( - @Nullable AdditionalSystemViewContainer additionalSystemViewContainer - ) { - mStatusBarInputLayer = additionalSystemViewContainer; - } - static class Factory { DesktopModeWindowDecoration create( diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt deleted file mode 100644 index 025bb403ec8b..000000000000 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopStatusBarInputLayerSupplier.kt +++ /dev/null @@ -1,117 +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.windowdecor - -import android.app.ActivityManager.RunningTaskInfo -import android.app.WindowConfiguration -import android.content.Context -import android.graphics.PixelFormat -import android.os.Handler -import android.view.Gravity -import android.view.View -import android.view.WindowManager -import com.android.wm.shell.shared.annotations.ShellMainThread -import com.android.wm.shell.shared.split.SplitScreenConstants -import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer - -/** - * Supplier for [AdditionalSystemViewContainer] objects to be used for forwarding input - * events through status bar to an app handle. Currently supports two simultaneous input layers. - * - * The supplier will pick one of two input layer view containers to use: one for tasks in - * fullscreen or top/left split stage, and one for tasks in right split stage. - */ -class DesktopStatusBarInputLayerSupplier( - private val context: Context, - @ShellMainThread handler: Handler -) { - private val inputLayers: MutableList<AdditionalSystemViewContainer> = mutableListOf() - - init { - // Post this as creation of the input layer views is a relatively expensive operation. - handler.post { - repeat(TOTAL_INPUT_LAYERS) { - inputLayers.add(createInputLayer()) - } - } - } - - private fun createInputLayer(): AdditionalSystemViewContainer { - val lp = WindowManager.LayoutParams( - WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, - WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE, - PixelFormat.TRANSPARENT - ) - lp.title = "Desktop status bar input layer" - lp.gravity = Gravity.LEFT or Gravity.TOP - lp.setTrustedOverlay() - - // Make this window a spy window to enable it to pilfer pointers from the system-wide - // gesture listener that receives events before window. This is to prevent notification - // shade gesture when we swipe down to enter desktop. - lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY - lp.flags = WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE - val view = View(context) - view.visibility = View.INVISIBLE - return AdditionalSystemViewContainer( - WindowManagerWrapper( - context.getSystemService<WindowManager>(WindowManager::class.java) - ), - view, - lp - ) - } - - /** - * Decide which cached status bar input layer should be used for a decoration, if any. - * - * [splitPosition] and [isLeftRightSplit] are used to determine which input layer we use. - * The first one is reserved for fullscreen tasks or tasks in top/left split, - * while the second one is exclusively used for tasks in right split stage. Note we care about - * left-right vs top-bottom split as the bottom stage should not use an input layer. - */ - fun getStatusBarInputLayer( - taskInfo: RunningTaskInfo, - @SplitScreenConstants.SplitPosition splitPosition: Int, - isLeftRightSplit: Boolean - ): AdditionalSystemViewContainer? { - if (!taskInfo.isVisibleRequested) return null - // Fullscreen and top/left split tasks will use the first input layer. - if (taskInfo.windowingMode == WindowConfiguration.WINDOWING_MODE_FULLSCREEN - || splitPosition == SplitScreenConstants.SPLIT_POSITION_TOP_OR_LEFT - ) { - return inputLayers[LEFT_TOP_INPUT_LAYER] - } - // Right split tasks will use the second one. - if (isLeftRightSplit && splitPosition == SplitScreenConstants.SPLIT_POSITION_BOTTOM_OR_RIGHT - ) { - return inputLayers[RIGHT_SPLIT_INPUT_LAYER] - } - // Which leaves bottom split and freeform tasks, which do not need an input layer - // as the status bar is not blocking them. - return null - } - - companion object { - private const val TOTAL_INPUT_LAYERS = 2 - // Input layer index for fullscreen tasks and tasks in top-left split - private const val LEFT_TOP_INPUT_LAYER = 0 - // Input layer index for tasks in right split stage. Does not include bottom split as that - // stage is not blocked by status bar. - private const val RIGHT_SPLIT_INPUT_LAYER = 1 - } -} 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 f97dfb89bc0d..b016c755e323 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 @@ -249,7 +249,9 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> if (!mTaskInfo.isVisible) { releaseViews(wct); - finishT.hide(mTaskSurface); + if (params.mSetTaskVisibilityPositionAndCrop) { + finishT.hide(mTaskSurface); + } return; } @@ -422,7 +424,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> private void updateTaskSurface(RelayoutParams params, SurfaceControl.Transaction startT, SurfaceControl.Transaction finishT, RelayoutResult<T> outResult) { - if (params.mSetTaskPositionAndCrop) { + if (params.mSetTaskVisibilityPositionAndCrop) { final Point taskPosition = mTaskInfo.positionInParent; startT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight); finishT.setWindowCrop(mTaskSurface, outResult.mWidth, outResult.mHeight) @@ -437,9 +439,13 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> shadowRadius = loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId); } - startT.setShadowRadius(mTaskSurface, shadowRadius).show(mTaskSurface); + startT.setShadowRadius(mTaskSurface, shadowRadius); finishT.setShadowRadius(mTaskSurface, shadowRadius); + if (params.mSetTaskVisibilityPositionAndCrop) { + startT.show(mTaskSurface); + } + if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FREEFORM) { if (!DesktopModeStatus.isVeiledResizeEnabled()) { // When fluid resize is enabled, add a background to freeform tasks @@ -758,7 +764,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> Configuration mWindowDecorConfig; boolean mApplyStartTransactionOnDraw; - boolean mSetTaskPositionAndCrop; + boolean mSetTaskVisibilityPositionAndCrop; boolean mHasGlobalFocus; void reset() { @@ -777,7 +783,7 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer> mIsCaptionVisible = false; mApplyStartTransactionOnDraw = false; - mSetTaskPositionAndCrop = false; + mSetTaskVisibilityPositionAndCrop = false; mWindowDecorConfig = null; mHasGlobalFocus = false; } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt index 1451f363ec73..8b6aaaf619e0 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/additionalviewcontainer/AdditionalSystemViewContainer.kt @@ -23,8 +23,8 @@ import android.view.Gravity import android.view.LayoutInflater import android.view.SurfaceControl import android.view.View +import android.view.WindowInsets import android.view.WindowManager -import android.view.WindowManager.LayoutParams import com.android.wm.shell.windowdecor.WindowManagerWrapper /** @@ -33,11 +33,27 @@ import com.android.wm.shell.windowdecor.WindowManagerWrapper */ class AdditionalSystemViewContainer( private val windowManagerWrapper: WindowManagerWrapper, - override val view: View, - val lp: LayoutParams + taskId: Int, + x: Int, + y: Int, + width: Int, + height: Int, + flags: Int, + @WindowInsets.Type.InsetsType forciblyShownTypes: Int = 0, + override val view: View ) : AdditionalViewContainer() { + val lp: WindowManager.LayoutParams = WindowManager.LayoutParams( + width, height, x, y, + WindowManager.LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, + flags, + PixelFormat.TRANSPARENT + ).apply { + title = "Additional view container of Task=$taskId" + gravity = Gravity.LEFT or Gravity.TOP + setTrustedOverlay() + this.forciblyShownTypes = forciblyShownTypes + } - /** Provide a layout id of a view to inflate for this view container. */ constructor( context: Context, windowManagerWrapper: WindowManagerWrapper, @@ -50,30 +66,15 @@ class AdditionalSystemViewContainer( @LayoutRes layoutId: Int ) : this( windowManagerWrapper = windowManagerWrapper, - view = LayoutInflater.from(context).inflate(layoutId, null /* parent */), - lp = createLayoutParams(x, y, width, height, flags, taskId) + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = LayoutInflater.from(context).inflate(layoutId, null /* parent */) ) - /** Provide a view directly for this view container */ - constructor( - windowManagerWrapper: WindowManagerWrapper, - taskId: Int, - x: Int, - y: Int, - width: Int, - height: Int, - flags: Int, - view: View, - forciblyShownTypes: Int = 0 - ) : this( - windowManagerWrapper = windowManagerWrapper, - view = view, - lp = createLayoutParams(x, y, width, height, flags, taskId).apply { - this.forciblyShownTypes = forciblyShownTypes - } - ) - - /** Do not supply a view at all, instead creating the view container with a basic view. */ constructor( context: Context, windowManagerWrapper: WindowManagerWrapper, @@ -85,7 +86,12 @@ class AdditionalSystemViewContainer( flags: Int ) : this( windowManagerWrapper = windowManagerWrapper, - lp = createLayoutParams(x, y, width, height, flags, taskId), + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, view = View(context) ) @@ -98,7 +104,7 @@ class AdditionalSystemViewContainer( } override fun setPosition(t: SurfaceControl.Transaction, x: Float, y: Float) { - lp.apply { + val lp = (view.layoutParams as WindowManager.LayoutParams).apply { this.x = x.toInt() this.y = y.toInt() } @@ -118,29 +124,13 @@ class AdditionalSystemViewContainer( ): AdditionalSystemViewContainer = AdditionalSystemViewContainer( windowManagerWrapper = windowManagerWrapper, - view = view, - lp = createLayoutParams(x, y, width, height, flags, taskId) + taskId = taskId, + x = x, + y = y, + width = width, + height = height, + flags = flags, + view = view ) } - companion object { - fun createLayoutParams( - x: Int, - y: Int, - width: Int, - height: Int, - flags: Int, - taskId: Int - ): LayoutParams { - return LayoutParams( - width, height, x, y, - LayoutParams.TYPE_STATUS_BAR_ADDITIONAL, - flags, - PixelFormat.TRANSPARENT - ).apply { - title = "Additional view container of Task=$taskId" - gravity = Gravity.LEFT or Gravity.TOP - setTrustedOverlay() - } - } - } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt index ff418c6daa02..e43c3a613157 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModel.kt @@ -21,10 +21,13 @@ import android.app.ActivityManager.RunningTaskInfo import android.content.Context import android.graphics.Rect import android.util.SparseArray +import android.window.DisplayAreaInfo +import android.window.WindowContainerTransaction import androidx.core.util.valueIterator import com.android.internal.annotations.VisibleForTesting import com.android.wm.shell.RootTaskDisplayAreaOrganizer import com.android.wm.shell.ShellTaskOrganizer +import com.android.wm.shell.common.DisplayChangeController import com.android.wm.shell.common.DisplayController import com.android.wm.shell.common.SyncTransactionQueue import com.android.wm.shell.desktopmode.DesktopRepository @@ -45,10 +48,16 @@ class DesktopTilingDecorViewModel( private val toggleResizeDesktopTaskTransitionHandler: ToggleResizeDesktopTaskTransitionHandler, private val returnToDragStartAnimator: ReturnToDragStartAnimator, private val taskRepository: DesktopRepository, -) { +) : DisplayChangeController.OnDisplayChangingListener { @VisibleForTesting var tilingTransitionHandlerByDisplayId = SparseArray<DesktopTilingWindowDecoration>() + init { + // TODO(b/374309287): Move this interface implementation to + // [DesktopModeWindowDecorViewModel] when the migration is done. + displayController.addDisplayChangingController(this) + } + fun snapToHalfScreen( taskInfo: ActivityManager.RunningTaskInfo, desktopModeWindowDecoration: DesktopModeWindowDecoration, @@ -102,7 +111,20 @@ class DesktopTilingDecorViewModel( fun onUserChange() { for (tilingHandler in tilingTransitionHandlerByDisplayId.valueIterator()) { - tilingHandler.onUserChange() + tilingHandler.resetTilingSession() } } + + override fun onDisplayChange( + displayId: Int, + fromRotation: Int, + toRotation: Int, + newDisplayAreaInfo: DisplayAreaInfo?, + t: WindowContainerTransaction?, + ) { + // Exit if the rotation hasn't changed or is changed by 180 degrees. [fromRotation] and + // [toRotation] can be one of the [@Surface.Rotation] values. + if ((fromRotation % 2 == toRotation % 2)) return + tilingTransitionHandlerByDisplayId.get(displayId)?.resetTilingSession() + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt index 9bf1304f2b39..209eb5e501b2 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManager.kt @@ -23,6 +23,7 @@ import android.graphics.Rect import android.graphics.Region import android.os.Binder import android.view.LayoutInflater +import android.view.RoundedCorner import android.view.SurfaceControl import android.view.SurfaceControlViewHost import android.view.View @@ -53,12 +54,14 @@ class DesktopTilingDividerWindowManager( private val transitionHandler: DesktopTilingWindowDecoration, private val transactionSupplier: Supplier<SurfaceControl.Transaction>, private var dividerBounds: Rect, + private val displayContext: Context, ) : WindowlessWindowManager(config, leash, null), DividerMoveCallback, View.OnLayoutChangeListener { private lateinit var viewHost: SurfaceControlViewHost private var tilingDividerView: TilingDividerView? = null private var dividerShown = false private var handleRegionWidth: Int = -1 private var setTouchRegion = true + private val maxRoundedCornerRadius = getMaxRoundedCornerRadius() /** * Gets bounds of divider window with screen based coordinate on the param Rect. @@ -93,7 +96,11 @@ class DesktopTilingDividerWindowManager( getDividerBounds(tmpDividerBounds) dividerView.setup(this, tmpDividerBounds) t.setRelativeLayer(leash, relativeLeash, 1) - .setPosition(leash, dividerBounds.left.toFloat(), dividerBounds.top.toFloat()) + .setPosition( + leash, + dividerBounds.left.toFloat() - maxRoundedCornerRadius, + dividerBounds.top.toFloat(), + ) .show(leash) syncQueue.runInSync { transaction -> transaction.merge(t) @@ -144,7 +151,7 @@ class DesktopTilingDividerWindowManager( */ override fun onDividerMove(pos: Int): Boolean { val t = transactionSupplier.get() - t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat()) + t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) val dividerWidth = dividerBounds.width() dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) return transitionHandler.onDividerHandleMoved(dividerBounds, t) @@ -157,7 +164,7 @@ class DesktopTilingDividerWindowManager( override fun onDividerMovedEnd(pos: Int) { setSlippery(true) val t = transactionSupplier.get() - t.setPosition(leash, pos.toFloat(), dividerBounds.top.toFloat()) + t.setPosition(leash, pos.toFloat() - maxRoundedCornerRadius, dividerBounds.top.toFloat()) val dividerWidth = dividerBounds.width() dividerBounds.set(pos, dividerBounds.top, pos + dividerWidth, dividerBounds.bottom) transitionHandler.onDividerHandleDragEnd(dividerBounds, t) @@ -166,7 +173,7 @@ class DesktopTilingDividerWindowManager( private fun getWindowManagerParams(): WindowManager.LayoutParams { val lp = WindowManager.LayoutParams( - dividerBounds.width(), + dividerBounds.width() + 2 * maxRoundedCornerRadius, dividerBounds.height(), TYPE_DOCK_DIVIDER, FLAG_NOT_FOCUSABLE or @@ -225,4 +232,15 @@ class DesktopTilingDividerWindowManager( } viewHost.relayout(lp) } + + private fun getMaxRoundedCornerRadius(): Int { + val display = displayContext.display + return listOf( + RoundedCorner.POSITION_TOP_LEFT, + RoundedCorner.POSITION_TOP_RIGHT, + RoundedCorner.POSITION_BOTTOM_RIGHT, + RoundedCorner.POSITION_BOTTOM_LEFT, + ) + .maxOf { position -> display.getRoundedCorner(position)?.getRadius() ?: 0 } + } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt index 6ea1d14cc734..c46767c3a51d 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecoration.kt @@ -23,10 +23,12 @@ import android.content.res.Resources import android.graphics.Bitmap import android.graphics.Rect import android.os.IBinder +import android.os.UserHandle import android.util.Slog import android.view.SurfaceControl import android.view.SurfaceControl.Transaction import android.view.WindowManager.TRANSIT_CHANGE +import android.view.WindowManager.TRANSIT_OPEN import android.view.WindowManager.TRANSIT_TO_BACK import android.view.WindowManager.TRANSIT_TO_FRONT import android.window.TransitionInfo @@ -125,7 +127,6 @@ class DesktopTilingWindowDecoration( resizeMetadata.getLeash(), startBounds = currentBounds, endBounds = destinationBounds, - isResizable = taskInfo.isResizeable, ) } } @@ -194,6 +195,7 @@ class DesktopTilingWindowDecoration( val builder = SurfaceControl.Builder() rootTdaOrganizer.attachToDisplayArea(displayId, builder) val leash = builder.setName(TILING_DIVIDER_TAG).setContainerLayer().build() + val displayContext = displayController.getDisplayContext(displayId) ?: return null val tilingManager = displayLayout?.let { dividerBounds = inflateDividerBounds(it) @@ -206,6 +208,7 @@ class DesktopTilingWindowDecoration( this, transactionSupplier, dividerBounds, + displayContext, ) } // a leash to present the divider on top of, without re-parenting. @@ -342,7 +345,9 @@ class DesktopTilingWindowDecoration( private fun isMinimized(changeMode: Int, infoType: Int): Boolean { return (changeMode == TRANSIT_TO_BACK && - (infoType == TRANSIT_MINIMIZE || infoType == TRANSIT_TO_BACK)) + (infoType == TRANSIT_MINIMIZE || + infoType == TRANSIT_TO_BACK || + infoType == TRANSIT_OPEN)) } class AppResizingHelper( @@ -358,6 +363,8 @@ class DesktopTilingWindowDecoration( private lateinit var resizeVeilBitmap: Bitmap private lateinit var resizeVeil: ResizeVeil private val displayContext = displayController.getDisplayContext(taskInfo.displayId) + private val userContext = + context.createContextAsUser(UserHandle.of(taskInfo.userId), /* flags= */ 0) fun initIfNeeded() { if (!isInitialised) { @@ -376,7 +383,7 @@ class DesktopTilingWindowDecoration( displayContext?.let { createIconFactory(displayContext, R.dimen.desktop_mode_resize_veil_icon_size) } ?: return - val pm = context.getApplicationContext().getPackageManager() + val pm = userContext.getPackageManager() val activityInfo = pm.getActivityInfo(baseActivity, 0 /* flags */) val provider = IconProvider(displayContext) val appIconDrawable = provider.getIcon(activityInfo) @@ -478,7 +485,7 @@ class DesktopTilingWindowDecoration( } } - fun onUserChange() { + fun resetTilingSession() { if (leftTaskResizingHelper != null) { removeTask(leftTaskResizingHelper, taskVanished = false, shouldDelayUpdate = true) leftTaskResizingHelper = null diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt index 065a5d77ccd3..89229051941c 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/tiling/TilingDividerView.kt @@ -49,11 +49,10 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion var handleRegionWidth: Int = 0 private var handleRegionHeight = 0 private var lastAcceptedPos = 0 - @VisibleForTesting - var handleStartY = 0 - @VisibleForTesting - var handleEndY = 0 + @VisibleForTesting var handleStartY = 0 + @VisibleForTesting var handleEndY = 0 private var canResize = false + private var resized = false /** * Tracks divider bar visible bounds in screen-based coordination. Used to calculate with * insets. @@ -119,7 +118,7 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion val dividerSize = resources.getDimensionPixelSize(R.dimen.split_divider_bar_width) val backgroundLeft = (width - dividerSize) / 2 val backgroundTop = 0 - val backgroundRight = left + dividerSize + val backgroundRight = backgroundLeft + dividerSize val backgroundBottom = height backgroundRect.set(backgroundLeft, backgroundTop, backgroundRight, backgroundBottom) } @@ -209,7 +208,6 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion if (!isWithinHandleRegion(yTouchPosInDivider)) return true callback.onDividerMoveStart(touchPos) setTouching() - startPos = touchPos canResize = true } @@ -223,19 +221,20 @@ class TilingDividerView : FrameLayout, View.OnTouchListener, DragDetector.Motion val pos = dividerBounds.left + touchPos - startPos if (callback.onDividerMove(pos)) { lastAcceptedPos = touchPos + resized = true } } MotionEvent.ACTION_CANCEL, MotionEvent.ACTION_UP -> { if (!canResize) return true - dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos - if (moving) { + if (moving && resized) { + dividerBounds.left = dividerBounds.left + lastAcceptedPos - startPos callback.onDividerMovedEnd(dividerBounds.left) - moving = false - canResize = false } - + moving = false + canResize = false + resized = false releaseTouching() } } 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 b43a9839f042..b5700ffb046b 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 @@ -36,10 +36,13 @@ import android.view.accessibility.AccessibilityNodeInfo.AccessibilityAction import android.widget.ImageButton import androidx.core.view.ViewCompat import androidx.core.view.accessibility.AccessibilityNodeInfoCompat.AccessibilityActionCompat +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.windowdecor.WindowManagerWrapper import com.android.wm.shell.windowdecor.additionalviewcontainer.AdditionalSystemViewContainer +import com.android.wm.shell.windowdecor.viewholder.WindowDecorationViewHolder.Data /** * A desktop mode window decoration used when the window is in full "focus" (i.e. fullscreen/split). @@ -66,12 +69,10 @@ internal class AppHandleViewHolder( ) : Data() private lateinit var taskInfo: RunningTaskInfo - private val position: Point = Point() - private var width: Int = 0 - private var height: Int = 0 private val captionView: View = rootView.requireViewById(R.id.desktop_mode_caption) private val captionHandle: ImageButton = rootView.requireViewById(R.id.caption_handle) private val inputManager = context.getSystemService(InputManager::class.java) + private var statusBarInputLayerExists = false // An invisible View that takes up the same coordinates as captionHandle but is layered // above the status bar. The purpose of this View is to receive input intended for @@ -111,54 +112,21 @@ internal class AppHandleViewHolder( ) { captionHandle.imageTintList = ColorStateList.valueOf(getCaptionHandleBarColor(taskInfo)) this.taskInfo = taskInfo - this.position.set(position) - this.width = width - this.height = height - if (!isCaptionVisible && statusBarInputLayer != null) { - detachStatusBarInputLayer() + // If handle is not in status bar region(i.e., bottom stage in vertical split), + // do not create an input layer + if (position.y >= SystemBarUtils.getStatusBarHeight(context)) return + if (!isCaptionVisible && statusBarInputLayerExists) { + disposeStatusBarInputLayer() return } - } - - fun bindStatusBarInputLayer( - statusBarLayer: AdditionalSystemViewContainer - ) { - // Input layer view modification takes a significant amount of time; + // Input layer view creation / modification takes a significant amount of time; // post them so we don't hold up DesktopModeWindowDecoration#relayout. - if (statusBarLayer == statusBarInputLayer) { + if (statusBarInputLayerExists) { handler.post { updateStatusBarInputLayer(position) } - return - } - // Remove the old input layer when changing to a new one. - if (statusBarInputLayer != null) detachStatusBarInputLayer() - if (statusBarLayer.view.visibility == View.INVISIBLE) { - statusBarLayer.view.visibility = View.VISIBLE - } - statusBarInputLayer = statusBarLayer - statusBarInputLayer?.let { - inputLayer -> setupAppHandleA11y(inputLayer.view) - } - handler.post { - val view = statusBarInputLayer?.view - ?: error("Unable to find statusBarInputLayer View") - // Caption handle is located within the status bar region, meaning the - // DisplayPolicy will attempt to transfer this input to status bar if it's - // a swipe down. Pilfer here to keep the gesture in handle alone. - view.setOnTouchListener { v, event -> - if (event.actionMasked == ACTION_DOWN) { - inputManager.pilferPointers(v.viewRootImpl.inputToken) - } - captionHandle.dispatchTouchEvent(event) - return@setOnTouchListener true - } - view.setOnHoverListener { _, event -> - captionHandle.onHoverEvent(event) - } - val lp = statusBarInputLayer?.view?.layoutParams as WindowManager.LayoutParams - lp.x = position.x - lp.y = position.y - lp.width = width - lp.height = height + } else { + // Input layer is created on a delay; prevent multiple from being created. + statusBarInputLayerExists = true + handler.post { createStatusBarInputLayer(position, width, height) } } } @@ -170,6 +138,40 @@ internal class AppHandleViewHolder( animateCaptionHandleAlpha(startValue = 0f, endValue = 1f) } + private fun createStatusBarInputLayer(handlePosition: Point, + handleWidth: Int, + handleHeight: Int) { + if (!Flags.enableHandleInputFix()) return + statusBarInputLayer = AdditionalSystemViewContainer(context, windowManagerWrapper, + taskInfo.taskId, handlePosition.x, handlePosition.y, handleWidth, handleHeight, + WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE + ) + val view = statusBarInputLayer?.view ?: error("Unable to find statusBarInputLayer View") + val lp = statusBarInputLayer?.lp ?: error("Unable to find statusBarInputLayer " + + "LayoutParams") + lp.title = "Handle Input Layer of task " + taskInfo.taskId + lp.setTrustedOverlay() + // Make this window a spy window to enable it to pilfer pointers from the system-wide + // gesture listener that receives events before window. This is to prevent notification + // shade gesture when we swipe down to enter desktop. + lp.inputFeatures = WindowManager.LayoutParams.INPUT_FEATURE_SPY + view.setOnHoverListener { _, event -> + captionHandle.onHoverEvent(event) + } + // Caption handle is located within the status bar region, meaning the + // DisplayPolicy will attempt to transfer this input to status bar if it's + // a swipe down. Pilfer here to keep the gesture in handle alone. + view.setOnTouchListener { v, event -> + if (event.actionMasked == ACTION_DOWN) { + inputManager.pilferPointers(v.viewRootImpl.inputToken) + } + captionHandle.dispatchTouchEvent(event) + return@setOnTouchListener true + } + setupAppHandleA11y(view) + windowManagerWrapper.updateViewLayout(view, lp) + } + private fun setupAppHandleA11y(view: View) { view.accessibilityDelegate = object : View.AccessibilityDelegate() { override fun onInitializeAccessibilityNodeInfo( @@ -222,12 +224,15 @@ internal class AppHandleViewHolder( } /** - * Remove the input listeners from the input layer and remove it from this view holder. + * Remove the input layer from [WindowManager]. Should be used when caption handle + * is not visible. */ - fun detachStatusBarInputLayer() { - statusBarInputLayer?.view?.setOnTouchListener(null) - statusBarInputLayer?.view?.setOnHoverListener(null) - statusBarInputLayer = null + fun disposeStatusBarInputLayer() { + statusBarInputLayerExists = false + handler.post { + statusBarInputLayer?.releaseView() + statusBarInputLayer = null + } } private fun getCaptionHandleBarColor(taskInfo: RunningTaskInfo): Int { 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 72d4dc6ffac9..13a8518ae8ed 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 @@ -700,7 +700,7 @@ public class BackAnimationControllerTest extends ShellTestCase { eq(tInfo), eq(st), eq(ft), eq(callback)); mBackTransitionHandler.onAnimationFinished(); - final TransitionInfo.Change openToClose = createAppChange(openTaskId, TRANSIT_CLOSE, + final TransitionInfo.Change openToClose = createAppChangeFromChange(open, TRANSIT_CLOSE, FLAG_BACK_GESTURE_ANIMATED); tInfo2 = createTransitionInfo(TRANSIT_CLOSE_PREPARE_BACK_NAVIGATION, openToClose); mBackTransitionHandler.mClosePrepareTransition = mock(IBinder.class); @@ -830,6 +830,16 @@ public class BackAnimationControllerTest extends ShellTestCase { return change; } + private TransitionInfo.Change createAppChangeFromChange( + TransitionInfo.Change originalChange, @TransitionInfo.TransitionMode int mode, + @TransitionInfo.ChangeFlags int flags) { + final TransitionInfo.Change change = new TransitionInfo.Change( + originalChange.getTaskInfo().token, originalChange.getLeash()); + change.setMode(mode); + change.setFlags(flags); + return change; + } + private static TransitionInfo createTransitionInfo( @WindowManager.TransitionType int type, TransitionInfo.Change ... changes) { final TransitionInfo info = new TransitionInfo(type, 0); 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 4ac066e4ffb0..1f2eaa6757e8 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 @@ -99,10 +99,11 @@ class BubbleViewInfoTest : ShellTestCase() { val shellController = ShellController(context, shellInit, shellCommandHandler, mock<DisplayInsetsController>(), mainExecutor) bubblePositioner = BubblePositioner(context, windowManager) + val bubbleLogger = mock<BubbleLogger>() val bubbleData = BubbleData( context, - mock<BubbleLogger>(), + bubbleLogger, bubblePositioner, BubbleEducationController(context), mainExecutor, @@ -125,7 +126,7 @@ class BubbleViewInfoTest : ShellTestCase() { WindowManagerShellWrapper(mainExecutor), mock<UserManager>(), mock<LauncherApps>(), - mock<BubbleLogger>(), + bubbleLogger, mock<TaskStackListenerImpl>(), ShellTaskOrganizer(mainExecutor), bubblePositioner, @@ -154,7 +155,7 @@ class BubbleViewInfoTest : ShellTestCase() { bubbleController, mainExecutor ) - bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData) + bubbleBarLayerView = BubbleBarLayerView(context, bubbleController, bubbleData, bubbleLogger) } @Test diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java index c52d9dd24165..dc0f213338be 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/split/SplitLayoutTests.java @@ -190,7 +190,7 @@ public class SplitLayoutTests extends ShellTestCase { } private void waitDividerFlingFinished() { - verify(mSplitLayout).flingDividerPosition(anyInt(), anyInt(), anyInt(), + verify(mSplitLayout).flingDividerPosition(anyInt(), anyInt(), anyInt(), any(), mRunnableCaptor.capture()); mRunnableCaptor.getValue().run(); } 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 3e9c732b9c3b..aabd973fce90 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 @@ -43,6 +43,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreef import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions @@ -50,10 +51,10 @@ import junit.framework.Assert.assertEquals import junit.framework.Assert.assertTrue import kotlin.test.assertNotNull import kotlinx.coroutines.CoroutineScope -import kotlinx.coroutines.cancel import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel import kotlinx.coroutines.runBlocking import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.setMain @@ -94,6 +95,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Mock lateinit var resizeTransitionHandler: ToggleResizeDesktopTaskTransitionHandler @Mock lateinit var taskStackListener: TaskStackListenerImpl @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession private lateinit var handler: DesktopActivityOrientationChangeHandler @@ -116,7 +118,13 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) taskRepository = - DesktopRepository(context, shellInit, persistentRepository, testScope) + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + testScope + ) whenever(shellTaskOrganizer.getRunningTasks(anyInt())).thenAnswer { runningTasks } whenever(transitions.startTransition(anyInt(), any(), isNull())).thenAnswer { Binder() } whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( @@ -172,8 +180,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { activityInfo.screenOrientation = SCREEN_ORIENTATION_PORTRAIT task.topActivityInfo = activityInfo whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - taskRepository.addActiveTask(DEFAULT_DISPLAY, task.taskId) - taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task.taskId, visible = true) + taskRepository.addTask(DEFAULT_DISPLAY, task.taskId, isVisible = true) runningTasks.add(task) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, @@ -196,7 +203,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { @Test fun handleActivityOrientationChange_notInDesktopMode_doNothing() { val task = setUpFreeformTask(isResizeable = false) - taskRepository.updateTaskVisibility(task.displayId, task.taskId, visible = false) + taskRepository.updateTask(task.displayId, task.taskId, isVisible = false) taskStackListener.onActivityRequestedOrientationChanged(task.taskId, SCREEN_ORIENTATION_LANDSCAPE) @@ -261,9 +268,7 @@ class DesktopActivityOrientationChangeHandlerTest : ShellTestCase() { task.topActivityInfo = activityInfo task.isResizeable = isResizeable whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - taskRepository.addActiveTask(displayId, task.taskId) - taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) - taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + taskRepository.addTask(displayId, task.taskId, isVisible = true) runningTasks.add(task) return task } 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 e83f5c7a79a1..e05a0b54fcf4 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 @@ -81,7 +81,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Before fun setUp() { desktopRepository = DesktopRepository( - context, ShellInit(TestShellExecutor()), mock(), mock() + context, ShellInit(TestShellExecutor()), mock(), mock(), mock() ) whenever(mockDisplayController.getDisplayLayout(DEFAULT_DISPLAY)) .thenReturn(mockDisplayLayout) @@ -347,7 +347,7 @@ class DesktopImmersiveControllerTest : ShellTestCase() { wct = wct, displayId = DEFAULT_DISPLAY, excludeTaskId = task.taskId - )?.invoke(transition) + ).asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY @@ -402,7 +402,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { immersive = true ) - controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition) + controller.exitImmersiveIfApplicable(wct, task) + .asExit()?.runOnTransitionStart?.invoke(transition) assertThat(controller.pendingExternalExitTransitions.any { exit -> exit.transition == transition && exit.displayId == DEFAULT_DISPLAY @@ -412,23 +413,36 @@ class DesktopImmersiveControllerTest : ShellTestCase() { @Test @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) - fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotAddPendingExitOnRun() { + fun exitImmersiveIfApplicable_byTask_notInImmersive_doesNotExit() { val task = createFreeformTask() whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) val wct = WindowContainerTransaction() - val transition = Binder() desktopRepository.setTaskInFullImmersiveState( - displayId = DEFAULT_DISPLAY, + displayId = task.displayId, taskId = task.taskId, immersive = false ) - controller.exitImmersiveIfApplicable(wct, task)?.invoke(transition) + val result = controller.exitImmersiveIfApplicable(wct, task) - assertThat(controller.pendingExternalExitTransitions.any { exit -> - exit.transition == transition && exit.displayId == DEFAULT_DISPLAY - && exit.taskId == task.taskId - }).isFalse() + assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun exitImmersiveIfApplicable_byDisplay_notInImmersive_doesNotExit() { + val task = createFreeformTask() + whenever(mockShellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) + val wct = WindowContainerTransaction() + desktopRepository.setTaskInFullImmersiveState( + displayId = task.displayId, + taskId = task.taskId, + immersive = false + ) + + val result = controller.exitImmersiveIfApplicable(wct, task.displayId) + + assertThat(result).isEqualTo(DesktopImmersiveController.ExitResult.NoExit) } @Test @@ -631,7 +645,8 @@ class DesktopImmersiveControllerTest : ShellTestCase() { taskId = task.taskId, immersive = true ) - controller.exitImmersiveIfApplicable(wct, task)?.invoke(Binder()) + controller.exitImmersiveIfApplicable(wct, task) + .asExit()?.runOnTransitionStart?.invoke(Binder()) controller.moveTaskToNonImmersive(task) 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 be0663cbd70d..df061e368071 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 @@ -31,16 +31,22 @@ import android.testing.AndroidTestingRunner import android.testing.TestableLooper.RunWithLooper import android.view.SurfaceControl import android.view.WindowManager +import android.view.WindowManager.TRANSIT_OPEN +import android.view.WindowManager.TransitionType import android.window.TransitionInfo 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 import com.android.wm.shell.ShellTestCase import com.android.wm.shell.TestRunningTaskInfoBuilder +import com.android.wm.shell.desktopmode.DesktopMixedTransitionHandler.PendingMixedTransition import com.android.wm.shell.freeform.FreeformTaskTransitionHandler +import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.Transitions +import com.google.common.truth.Truth.assertThat import org.junit.Assert.assertFalse import org.junit.Assert.assertNull import org.junit.Assert.assertTrue @@ -48,9 +54,13 @@ 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.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.eq import org.mockito.kotlin.mock import org.mockito.kotlin.whenever @@ -71,9 +81,12 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { @Mock lateinit var desktopRepository: DesktopRepository @Mock lateinit var freeformTaskTransitionHandler: FreeformTaskTransitionHandler @Mock lateinit var closeDesktopTaskTransitionHandler: CloseDesktopTaskTransitionHandler + @Mock lateinit var desktopImmersiveController: DesktopImmersiveController @Mock lateinit var interactionJankMonitor: InteractionJankMonitor @Mock lateinit var mockHandler: Handler @Mock lateinit var closingTaskLeash: SurfaceControl + @Mock lateinit var shellInit: ShellInit + @Mock lateinit var rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer private lateinit var mixedHandler: DesktopMixedTransitionHandler @@ -86,8 +99,11 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { desktopRepository, freeformTaskTransitionHandler, closeDesktopTaskTransitionHandler, + desktopImmersiveController, interactionJankMonitor, - mockHandler + mockHandler, + shellInit, + rootTaskDisplayAreaOrganizer, ) } @@ -146,7 +162,7 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { val transition = mock<IBinder>() val transitionInfo = createTransitionInfo( - changeMode = WindowManager.TRANSIT_OPEN, + changeMode = TRANSIT_OPEN, task = createTask(WINDOWING_MODE_FREEFORM) ) whenever(freeformTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any())) @@ -164,7 +180,9 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS) fun startAnimation_withClosingDesktopTask_callsCloseTaskHandler() { + val wct = WindowContainerTransaction() val transition = mock<IBinder>() val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(2) @@ -172,6 +190,9 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { closeDesktopTaskTransitionHandler.startAnimation(any(), any(), any(), any(), any()) ) .thenReturn(true) + whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)) + .thenReturn(transition) + mixedHandler.startRemoveTransition(wct) val started = mixedHandler.startAnimation( transition = transition, @@ -187,12 +208,17 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { } @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS) fun startAnimation_withClosingLastDesktopTask_dispatchesTransition() { + val wct = WindowContainerTransaction() val transition = mock<IBinder>() val transitionInfo = createTransitionInfo(task = createTask(WINDOWING_MODE_FREEFORM)) whenever(desktopRepository.getExpandedTaskCount(any())).thenReturn(1) whenever(transitions.dispatchTransition(any(), any(), any(), any(), any(), any())) .thenReturn(mock()) + whenever(transitions.startTransition(WindowManager.TRANSIT_CLOSE, wct, mixedHandler)) + .thenReturn(transition) + mixedHandler.startRemoveTransition(wct) mixedHandler.startAnimation( transition = transition, @@ -220,6 +246,324 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) } + @Test + @DisableFlags( + Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP, + Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startLaunchTransition_immersiveAndAppLaunchFlagsDisabled_doesNotUseMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(Binder()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = task.taskId, + exitingImmersiveTask = null + ) + + verify(transitions).startTransition(TRANSIT_OPEN, wct, /* handler= */ null) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startLaunchTransition_immersiveMixEnabled_usesMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(Binder()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = task.taskId, + exitingImmersiveTask = null + ) + + verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startLaunchTransition_desktopAppLaunchEnabled_usesMixedHandler() { + val wct = WindowContainerTransaction() + val task = createTask(WINDOWING_MODE_FREEFORM) + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(Binder()) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = task.taskId, + exitingImmersiveTask = null + ) + + verify(transitions).startTransition(TRANSIT_OPEN, wct, mixedHandler) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startAndAnimateLaunchTransition_withoutImmersiveChange_dispatchesAllChangesToLeftOver() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + exitingImmersiveTask = null, + ) + val launchTaskChange = createChange(launchingTask) + val otherChange = createChange(createTask(WINDOWING_MODE_FREEFORM)) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, otherChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(transitions).dispatchTransition( + eq(transition), + argThat { info -> + info.changes.contains(launchTaskChange) && info.changes.contains(otherChange) + }, + any(), + any(), + any(), + eq(mixedHandler), + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startAndAnimateLaunchTransition_withImmersiveChange_mixesAnimations() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val immersiveTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + exitingImmersiveTask = immersiveTask.taskId, + ) + val launchTaskChange = createChange(launchingTask) + val immersiveChange = createChange(immersiveTask) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, immersiveChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(desktopImmersiveController) + .animateResizeChange(eq(immersiveChange), any(), any(), any()) + verify(transitions).dispatchTransition( + eq(transition), + argThat { info -> + info.changes.contains(launchTaskChange) && !info.changes.contains(immersiveChange) + }, + any(), + any(), + any(), + eq(mixedHandler), + ) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + minimizingTaskId = null, + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer, times(0)) + .reparentToDisplayArea(anyInt(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun startAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + minimizingTaskId = minimizingTask.taskId, + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, minimizeChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( + anyInt(), eq(minimizeChange.leash), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun addPendingAndAnimateLaunchTransition_noMinimizeChange_doesNotReparentMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Launch( + transition = transition, + launchingTask = launchingTask.taskId, + minimizingTask = null, + exitingImmersiveTask = null, + ) + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer, times(0)) + .reparentToDisplayArea(anyInt(), any(), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_DESKTOP_APP_LAUNCH_TRANSITIONS) + fun addPendingAndAnimateLaunchTransition_withMinimizeChange_reparentsMinimizeChange() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val minimizingTask = createTask(WINDOWING_MODE_FREEFORM) + val launchTaskChange = createChange(launchingTask) + val minimizeChange = createChange(minimizingTask) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.addPendingMixedTransition( + PendingMixedTransition.Launch( + transition = transition, + launchingTask = launchingTask.taskId, + minimizingTask = minimizingTask.taskId, + exitingImmersiveTask = null, + ) + ) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange, minimizeChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + verify(rootTaskDisplayAreaOrganizer).reparentToDisplayArea( + anyInt(), eq(minimizeChange.leash), any()) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startAndAnimateLaunchTransition_removesPendingMixedTransition() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + exitingImmersiveTask = null, + ) + val launchTaskChange = createChange(launchingTask) + mixedHandler.startAnimation( + transition, + createTransitionInfo( + TRANSIT_OPEN, + listOf(launchTaskChange) + ), + SurfaceControl.Transaction(), + SurfaceControl.Transaction(), + ) { } + + assertThat(mixedHandler.pendingMixedTransitions).isEmpty() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_FULLY_IMMERSIVE_IN_DESKTOP) + fun startAndAnimateLaunchTransition_aborted_removesPendingMixedTransition() { + val wct = WindowContainerTransaction() + val launchingTask = createTask(WINDOWING_MODE_FREEFORM) + val transition = Binder() + whenever(transitions.startTransition(eq(TRANSIT_OPEN), eq(wct), anyOrNull())) + .thenReturn(transition) + + mixedHandler.startLaunchTransition( + transitionType = TRANSIT_OPEN, + wct = wct, + taskId = launchingTask.taskId, + exitingImmersiveTask = null, + ) + mixedHandler.onTransitionConsumed( + transition = transition, + aborted = true, + finishTransaction = SurfaceControl.Transaction() + ) + + assertThat(mixedHandler.pendingMixedTransitions).isEmpty() + } + private fun createTransitionInfo( type: Int = WindowManager.TRANSIT_CLOSE, changeMode: Int = WindowManager.TRANSIT_CLOSE, @@ -235,6 +579,18 @@ class DesktopMixedTransitionHandlerTest : ShellTestCase() { ) } + private fun createTransitionInfo( + @TransitionType type: Int, + changes: List<TransitionInfo.Change> = emptyList() + ): TransitionInfo = TransitionInfo(type, /* flags= */ 0).apply { + changes.forEach { change -> addChange(change) } + } + + private fun createChange(task: RunningTaskInfo): TransitionInfo.Change = + TransitionInfo.Change(task.token, SurfaceControl()).apply { + taskInfo = task + } + private fun createTask(@WindowingMode windowingMode: Int): RunningTaskInfo = TestRunningTaskInfoBuilder() .setActivityType(ACTIVITY_TYPE_STANDARD) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt index f25faa53f356..7c4ce4acfc9c 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt @@ -59,6 +59,7 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeStatus 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.transition.Transitions.TRANSIT_MINIMIZE import kotlin.test.assertFalse import kotlin.test.assertTrue import org.junit.Before @@ -522,6 +523,23 @@ class DesktopModeLoggerTransitionObserverTest : ShellTestCase() { } @Test + fun transitMinimize_logExitReasongMinimized() { + // add a freeform task + transitionObserver.addTaskInfosToCachedMap(createTaskInfo(WINDOWING_MODE_FREEFORM)) + transitionObserver.isSessionActive = true + + // minimize the task + val change = createChange(TRANSIT_MINIMIZE, createTaskInfo(WINDOWING_MODE_FULLSCREEN)) + val transitionInfo = TransitionInfoBuilder(TRANSIT_MINIMIZE).addChange(change).build() + callOnTransitionReady(transitionInfo) + + assertFalse(transitionObserver.isSessionActive) + verify(desktopModeEventLogger, times(1)).logSessionExit(eq(ExitReason.TASK_MINIMIZED)) + verify(desktopModeEventLogger, times(1)).logTaskRemoved(eq(DEFAULT_TASK_UPDATE)) + verifyZeroInteractions(desktopModeEventLogger) + } + + @Test fun sessionExitByRecents_cancelledAnimation_sessionRestored() { // add a freeform task to an existing session val taskInfo = createTaskInfo(WINDOWING_MODE_FREEFORM) 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 d90443c99d37..414c1a658b95 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 @@ -18,6 +18,7 @@ package com.android.wm.shell.desktopmode import android.graphics.Rect import android.platform.test.annotations.EnableFlags +import android.platform.test.flag.junit.SetFlagsRule import android.testing.AndroidTestingRunner import android.util.ArraySet import android.view.Display.DEFAULT_DISPLAY @@ -29,6 +30,7 @@ import com.android.wm.shell.TestShellExecutor import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.sysui.ShellInit import com.google.common.truth.Truth.assertThat import junit.framework.Assert.fail @@ -43,6 +45,7 @@ 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.Mock @@ -50,6 +53,7 @@ import org.mockito.Mockito.inOrder import org.mockito.Mockito.spy import org.mockito.kotlin.any import org.mockito.kotlin.never +import org.mockito.kotlin.times import org.mockito.kotlin.verify import org.mockito.kotlin.whenever @@ -58,12 +62,15 @@ import org.mockito.kotlin.whenever @ExperimentalCoroutinesApi class DesktopRepositoryTest : ShellTestCase() { + @JvmField @Rule val setFlagsRule = SetFlagsRule() + private lateinit var repo: DesktopRepository private lateinit var shellInit: ShellInit private lateinit var datastoreScope: CoroutineScope @Mock private lateinit var testExecutor: ShellExecutor @Mock private lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer @Before fun setUp() { @@ -71,7 +78,14 @@ class DesktopRepositoryTest : ShellTestCase() { datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - repo = DesktopRepository(context, shellInit, persistentRepository, datastoreScope) + repo = + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + datastoreScope + ) whenever(runBlocking { persistentRepository.readDesktop(any(), any()) }).thenReturn( Desktop.getDefaultInstance() ) @@ -84,65 +98,65 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun addActiveTask_notifiesListener() { + fun addTask_notifiesActiveTaskListener() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1) } @Test - fun addActiveTask_taskIsActive() { + fun addTask_marksTaskActive() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.isActiveTask(1)).isTrue() } @Test - fun addSameActiveTaskTwice_notifiesOnce() { + fun addSameTaskTwice_notifiesOnce() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(1) } @Test - fun addActiveTask_multipleTasksAdded_notifiesForAllTasks() { + fun addTask_multipleTasksAdded_notifiesForAllTasks() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 2) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) } @Test - fun addActiveTask_multipleDisplays_notifiesCorrectListener() { + fun addTask_multipleDisplays_notifiesCorrectListener() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 2) - repo.addActiveTask(SECOND_DISPLAY, taskId = 3) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(SECOND_DISPLAY, taskId = 3, isVisible = true) assertThat(listener.activeChangesOnDefaultDisplay).isEqualTo(2) assertThat(listener.activeChangesOnSecondaryDisplay).isEqualTo(1) } @Test - fun removeActiveTask_notifiesListener() { + fun removeActiveTask_notifiesActiveTaskListener() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeActiveTask(1) @@ -151,10 +165,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun removeActiveTask_taskNotActive() { + fun removeActiveTask_marksTaskNotActive() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeActiveTask(1) @@ -175,7 +189,7 @@ class DesktopRepositoryTest : ShellTestCase() { fun remoteActiveTask_listenerForOtherDisplayNotNotified() { val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeActiveTask(1) @@ -201,8 +215,8 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun updateTaskVisibility_singleVisibleNonClosingTask_updatesTasksCorrectly() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + fun updateTask_singleVisibleNonClosingTask_updatesTasksCorrectly() { + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.isVisibleTask(1)).isTrue() assertThat(repo.isClosingTask(1)).isFalse() @@ -214,8 +228,35 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun updateTaskVisibility_multipleTasks_persistsVisibleTasks() = + runTest(StandardTestDispatcher()) { + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + + inOrder(persistentRepository).run { + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(arrayOf(1)), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf() + ) + verify(persistentRepository) + .addOrUpdateDesktop( + DEFAULT_USER_ID, + DEFAULT_DESKTOP_ID, + visibleTasks = ArraySet(arrayOf(1, 2)), + minimizedTasks = ArraySet(), + freeformTasksInZOrder = arrayListOf() + ) + } + } + + @Test fun isOnlyVisibleNonClosingTask_singleVisibleClosingTask() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.addClosingTask(DEFAULT_DISPLAY, 1) // A visible task that's closing @@ -229,13 +270,14 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_singleVisibleMinimizedTask() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.minimizeTask(DEFAULT_DISPLAY, 1) + val taskId = 1 + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) + repo.minimizeTask(DEFAULT_DISPLAY, taskId) // The visible task that's closing - assertThat(repo.isVisibleTask(1)).isTrue() - assertThat(repo.isMinimizedTask(1)).isTrue() - assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + assertThat(repo.isVisibleTask(taskId)).isFalse() + assertThat(repo.isMinimizedTask(taskId)).isTrue() + assertThat(repo.isOnlyVisibleNonClosingTask(taskId)).isFalse() // Not a visible task assertThat(repo.isVisibleTask(99)).isFalse() assertThat(repo.isOnlyVisibleNonClosingTask(99)).isFalse() @@ -243,17 +285,19 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_multipleVisibleNonClosingTasks() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) // Not the only task assertThat(repo.isVisibleTask(1)).isTrue() assertThat(repo.isClosingTask(1)).isFalse() assertThat(repo.isOnlyVisibleNonClosingTask(1)).isFalse() + // Not the only task assertThat(repo.isVisibleTask(2)).isTrue() assertThat(repo.isClosingTask(2)).isFalse() assertThat(repo.isOnlyVisibleNonClosingTask(2)).isFalse() + // Not a visible task assertThat(repo.isVisibleTask(99)).isFalse() assertThat(repo.isClosingTask(99)).isFalse() @@ -262,9 +306,9 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun isOnlyVisibleNonClosingTask_multipleDisplays() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) - repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 3, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.updateTask(SECOND_DISPLAY, taskId = 3, isVisible = true) // Not the only task on DEFAULT_DISPLAY assertThat(repo.isVisibleTask(1)).isTrue() @@ -282,7 +326,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addVisibleTasksListener_notifiesVisibleFreeformTask() { - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() @@ -295,7 +339,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun addListener_tasksOnDifferentDisplay_doesNotNotify() { - repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true) + repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = true) val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) @@ -307,13 +351,13 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun updateTaskVisibility_addVisibleTasksNotifiesListener() { + fun updateTask_visible_addVisibleTasksNotifiesListener() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) @@ -321,12 +365,12 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun updateTaskVisibility_addVisibleTaskNotifiesListenerForThatDisplay() { + fun updateTask_visibleTask_addVisibleTaskNotifiesListenerForThatDisplay() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) @@ -334,7 +378,7 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(listener.visibleTasksCountOnSecondaryDisplay).isEqualTo(0) assertThat(listener.visibleChangesOnSecondaryDisplay).isEqualTo(0) - repo.updateTaskVisibility(displayId = 1, taskId = 2, visible = true) + repo.updateTask(displayId = 1, taskId = 2, isVisible = true) executor.flushAll() // Listener for secondary display is notified @@ -345,17 +389,17 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun updateTaskVisibility_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { + fun updateTask_taskOnDefaultBecomesVisibleOnSecondDisplay_listenersNotified() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(1) // Mark task 1 visible on secondary display - repo.updateTaskVisibility(displayId = 1, taskId = 1, visible = true) + repo.updateTask(displayId = 1, taskId = 1, isVisible = true) executor.flushAll() // Default display should have 2 calls @@ -370,22 +414,22 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun updateTaskVisibility_removeVisibleTasksNotifiesListener() { + fun updateTask_removeVisibleTasksNotifiesListener() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = false) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(0) @@ -397,17 +441,17 @@ class DesktopRepositoryTest : ShellTestCase() { * This tests that task is removed from the last parent display when it vanishes. */ @Test - fun updateTaskVisibility_removeVisibleTasksRemovesTaskWithInvalidDisplay() { + fun updateTask_removeVisibleTasksRemovesTaskWithInvalidDisplay() { val listener = TestVisibilityListener() val executor = TestShellExecutor() repo.addVisibleTasksListener(listener, executor) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) executor.flushAll() assertThat(listener.visibleTasksCountOnDefaultDisplay).isEqualTo(2) - repo.updateTaskVisibility(INVALID_DISPLAY, taskId = 1, visible = false) + repo.updateTask(INVALID_DISPLAY, taskId = 1, isVisible = false) executor.flushAll() assertThat(listener.visibleChangesOnDefaultDisplay).isEqualTo(3) @@ -420,30 +464,30 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) // New task increments count to 1 - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Visibility update to same task does not increase count - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Second task visible increments count - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(2) // Hiding a task decrements count - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) // Hiding all tasks leaves count at 0 - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 2, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 2, isVisible = false) assertThat(repo.getVisibleTaskCount(displayId = 9)).isEqualTo(0) // Hiding a not existing task, count remains at 0 - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 999, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 999, isVisible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) } @@ -453,42 +497,42 @@ class DesktopRepositoryTest : ShellTestCase() { assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on default display increments count for that display only - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = true) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(0) // New task on secondary display, increments count for that display only - repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 2, visible = true) + repo.updateTask(SECOND_DISPLAY, taskId = 2, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(1) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) // Marking task visible on another display, updates counts for both displays - repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = true) + repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = true) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Marking task that is on secondary display, hidden on default display, does not affect // secondary display - repo.updateTaskVisibility(DEFAULT_DISPLAY, taskId = 1, visible = false) + repo.updateTask(DEFAULT_DISPLAY, taskId = 1, isVisible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(2) // Hiding a task on that display, decrements count - repo.updateTaskVisibility(SECOND_DISPLAY, taskId = 1, visible = false) + repo.updateTask(SECOND_DISPLAY, taskId = 1, isVisible = false) assertThat(repo.getVisibleTaskCount(DEFAULT_DISPLAY)).isEqualTo(0) assertThat(repo.getVisibleTaskCount(SECOND_DISPLAY)).isEqualTo(1) } @Test - fun addOrMoveFreeformTaskToTop_didNotExist_addsToTop() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + fun addTask_didNotExist_addsToTop() { + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks.size).isEqualTo(3) @@ -499,11 +543,11 @@ class DesktopRepositoryTest : ShellTestCase() { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) - fun addOrMoveFreeformTaskToTop_noTaskExists_persistenceEnabled_addsToTop() = + fun addTask_noTaskExists_persistenceEnabled_addsToTop() = runTest(StandardTestDispatcher()) { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks).containsExactly(7, 6, 5).inOrder() @@ -520,7 +564,7 @@ class DesktopRepositoryTest : ShellTestCase() { .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, - visibleTasks = ArraySet(), + visibleTasks = ArraySet(arrayOf(5)), minimizedTasks = ArraySet(), freeformTasksInZOrder = arrayListOf(6, 5) ) @@ -528,7 +572,7 @@ class DesktopRepositoryTest : ShellTestCase() { .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, - visibleTasks = ArraySet(), + visibleTasks = ArraySet(arrayOf(5, 6)), minimizedTasks = ArraySet(), freeformTasksInZOrder = arrayListOf(7, 6, 5) ) @@ -536,12 +580,12 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun addOrMoveFreeformTaskToTop_alreadyExists_movesToTop() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + fun addTask_alreadyExists_movesToTop() { + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks.size).isEqualTo(3) @@ -549,10 +593,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun addOrMoveFreeformTaskToTop_taskIsMinimized_unminimizesTask() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + fun addTask_taskIsMinimized_unminimizesTask() { + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) repo.minimizeTask(displayId = 0, taskId = 6) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) @@ -564,9 +608,9 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun minimizeTask_persistenceEnabled_taskIsPersistedAsMinimized() = runTest(StandardTestDispatcher()) { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) repo.minimizeTask(displayId = 0, taskId = 6) @@ -586,7 +630,7 @@ class DesktopRepositoryTest : ShellTestCase() { .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, - visibleTasks = ArraySet(), + visibleTasks = ArraySet(arrayOf(5)), minimizedTasks = ArraySet(), freeformTasksInZOrder = arrayListOf(6, 5) ) @@ -594,15 +638,15 @@ class DesktopRepositoryTest : ShellTestCase() { .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, - visibleTasks = ArraySet(), + visibleTasks = ArraySet(arrayOf(5, 6)), minimizedTasks = ArraySet(), freeformTasksInZOrder = arrayListOf(7, 6, 5) ) - verify(persistentRepository) + verify(persistentRepository, times(2)) .addOrUpdateDesktop( DEFAULT_USER_ID, DEFAULT_DESKTOP_ID, - visibleTasks = ArraySet(), + visibleTasks = ArraySet(arrayOf(5, 7)), minimizedTasks = ArraySet(arrayOf(6)), freeformTasksInZOrder = arrayListOf(7, 6, 5) ) @@ -610,10 +654,10 @@ class DesktopRepositoryTest : ShellTestCase() { } @Test - fun addOrMoveFreeformTaskToTop_taskIsUnminimized_noop() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 5) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 6) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, 7) + fun addTask_taskIsUnminimized_noop() { + repo.addTask(DEFAULT_DISPLAY, 5, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 6, isVisible = true) + repo.addTask(DEFAULT_DISPLAY, 7, isVisible = true) val tasks = repo.getFreeformTasksInZOrder(DEFAULT_DISPLAY) assertThat(tasks).containsExactly(7, 6, 5).inOrder() @@ -622,7 +666,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_invalidDisplay_removesTaskFromFreeformTasks() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) @@ -636,7 +680,7 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeFreeformTask_invalidDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { runTest(StandardTestDispatcher()) { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(INVALID_DISPLAY, taskId = 1) @@ -661,7 +705,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_validDisplay_removesTaskFromFreeformTasks() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) @@ -673,7 +717,7 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeFreeformTask_validDisplay_persistenceEnabled_removesTaskFromFreeformTasks() { runTest(StandardTestDispatcher()) { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(DEFAULT_DISPLAY, taskId = 1) @@ -698,7 +742,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_validDisplay_differentDisplay_doesNotRemovesTask() { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) @@ -710,7 +754,7 @@ class DesktopRepositoryTest : ShellTestCase() { @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) fun removeFreeformTask_validDisplayButDifferentDisplay_persistenceEnabled_doesNotRemoveTask() { runTest(StandardTestDispatcher()) { - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId = 1) + repo.addTask(DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.removeFreeformTask(SECOND_DISPLAY, taskId = 1) @@ -736,8 +780,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_removesTaskBoundsBeforeMaximize() { val taskId = 1 - repo.addActiveTask(THIRD_DISPLAY, taskId) - repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeMaximize(taskId, Rect(0, 0, 200, 200)) repo.removeFreeformTask(THIRD_DISPLAY, taskId) @@ -748,8 +791,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_removesTaskBoundsBeforeImmersive() { val taskId = 1 - repo.addActiveTask(THIRD_DISPLAY, taskId) - repo.addOrMoveFreeformTaskToTop(THIRD_DISPLAY, taskId) + repo.addTask(THIRD_DISPLAY, taskId, isVisible = true) repo.saveBoundsBeforeFullImmersive(taskId, Rect(0, 0, 200, 200)) repo.removeFreeformTask(THIRD_DISPLAY, taskId) @@ -762,8 +804,7 @@ class DesktopRepositoryTest : ShellTestCase() { val taskId = 1 val listener = TestListener() repo.addActiveTaskListener(listener) - repo.addActiveTask(DEFAULT_DISPLAY, taskId) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.removeFreeformTask(THIRD_DISPLAY, taskId) @@ -774,8 +815,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_unminimizesTask() { val taskId = 1 - repo.addActiveTask(DEFAULT_DISPLAY, taskId) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.minimizeTask(DEFAULT_DISPLAY, taskId) repo.removeFreeformTask(DEFAULT_DISPLAY, taskId) @@ -786,8 +826,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeFreeformTask_updatesTaskVisibility() { val taskId = 1 - repo.addActiveTask(DEFAULT_DISPLAY, taskId) - repo.addOrMoveFreeformTaskToTop(DEFAULT_DISPLAY, taskId) + repo.addTask(DEFAULT_DISPLAY, taskId, isVisible = true) repo.removeFreeformTask(THIRD_DISPLAY, taskId) @@ -855,8 +894,7 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun minimizeTask_withInvalidDisplay_minimizesCorrectTask() { - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 0) - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 0) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 0, isVisible = true) repo.minimizeTask(displayId = INVALID_DISPLAY, taskId = 0) @@ -888,9 +926,9 @@ class DesktopRepositoryTest : ShellTestCase() { @Test - fun updateTaskVisibility_minimizedTaskBecomesVisible_unminimizesTask() { + fun updateTask_minimizedTaskBecomesVisible_unminimizesTask() { repo.minimizeTask(displayId = 10, taskId = 2) - repo.updateTaskVisibility(displayId = 10, taskId = 2, visible = true) + repo.updateTask(displayId = 10, taskId = 2, isVisible = true) val isMinimizedTask = repo.isMinimizedTask(taskId = 2) @@ -899,13 +937,9 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getExpandedTasksOrdered_returnsFreeformTasksInCorrectOrder() { - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) - // The front-most task will be the one added last through `addOrMoveFreeformTaskToTop` - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) - repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 2) - repo.addOrMoveFreeformTaskToTop(displayId = 0, taskId = 1) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) val tasks = repo.getExpandedTasksOrdered(displayId = 0) @@ -914,13 +948,9 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun getExpandedTasksOrdered_excludesMinimizedTasks() { - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) - // The front-most task will be the one added last through `addOrMoveFreeformTaskToTop` - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2) - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) val tasks = repo.getExpandedTasksOrdered(displayId = DEFAULT_DISPLAY) @@ -976,13 +1006,10 @@ class DesktopRepositoryTest : ShellTestCase() { @Test fun removeDesktop_multipleTasks_removesAll() { - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) - repo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 3) - // The front-most task will be the one added last through `addOrMoveFreeformTaskToTop` - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 3) - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 2) - repo.addOrMoveFreeformTaskToTop(displayId = DEFAULT_DISPLAY, taskId = 1) + // The front-most task will be the one added last through `addTask`. + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 3, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) + repo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) repo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) val tasksBeforeRemoval = repo.removeDesktop(displayId = DEFAULT_DISPLAY) 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 new file mode 100644 index 000000000000..e977966a45fe --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTaskChangeListenerTest.kt @@ -0,0 +1,177 @@ +/* + * 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 + +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 +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask +import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFullscreenTask +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.never +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +/** + * Tests for {@link DesktopTaskChangeListener} + * + * Build/Install/Run: atest WMShellUnitTests:DesktopTaskChangeListenerTest + */ +@SmallTest +@RunWith(AndroidTestingRunner::class) +class DesktopTaskChangeListenerTest : ShellTestCase() { + + @JvmField @Rule val setFlagsRule = SetFlagsRule() + + private lateinit var desktopTaskChangeListener: DesktopTaskChangeListener + + private val desktopRepository = mock<DesktopRepository>() + + @Before + fun setUp() { + desktopTaskChangeListener = DesktopTaskChangeListener(desktopRepository) + } + + @Test + fun onTaskOpening_fullscreenTask_notActiveDesktopTask_noop() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopRepository, never()).addTask(task.displayId, task.taskId, task.isVisible) + verify(desktopRepository, never()).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + fun onTaskOpening_freeformTask_activeDesktopTask_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopRepository).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + fun onTaskOpening_freeformTask_visibleDesktopTask_addsTaskToRepository() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopRepository).addTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskOpening_freeformTask_nonVisibleDesktopTask_addsTaskToRepository() { + val task = createFreeformTask().apply { isVisible = false } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskOpening(task) + + verify(desktopRepository).addTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskChanging_freeformTaskOutsideDesktop_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopRepository).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + fun onTaskChanging_visibleTaskInDesktop_updatesTaskVisibility() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopRepository).updateTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskChanging_nonVisibleTask_updatesTaskVisibility() { + val task = createFreeformTask().apply { isVisible = false } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskChanging(task) + + verify(desktopRepository).updateTask(task.displayId, task.taskId, task.isVisible) + } + + @Test + fun onTaskMovingToFront_freeformTaskOutsideDesktop_removesTaskFromRepo() { + val task = createFullscreenTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskMovingToFront(task) + + verify(desktopRepository).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavEnabled_nonClosingTask_minimizesTaskInRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopRepository.isClosingTask(task.taskId)).thenReturn(false) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopRepository).updateTask(task.displayId, task.taskId, isVisible = false) + verify(desktopRepository).minimizeTask(task.displayId, task.taskId) + } + + @Test + @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavDisabled_closingTask_removesTaskInRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopRepository.isClosingTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopRepository, never()).minimizeTask(task.displayId, task.taskId) + verify(desktopRepository).removeClosingTask(task.taskId) + verify(desktopRepository).removeFreeformTask(task.displayId, task.taskId) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) + fun onTaskClosing_backNavEnabled_closingTask_removesTaskFromRepo() { + val task = createFreeformTask().apply { isVisible = true } + whenever(desktopRepository.isActiveTask(task.taskId)).thenReturn(true) + whenever(desktopRepository.isClosingTask(task.taskId)).thenReturn(true) + + desktopTaskChangeListener.onTaskClosing(task) + + verify(desktopRepository).removeClosingTask(task.taskId) + verify(desktopRepository).removeFreeformTask(task.displayId, task.taskId) + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopTasksControllerTest.kt index 0a6dfbf4c828..b157d557c1d8 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 @@ -110,6 +110,7 @@ import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createSplit import com.android.wm.shell.desktopmode.minimize.DesktopWindowLimitRemoteHandler import com.android.wm.shell.desktopmode.persistence.Desktop import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.draganddrop.DragAndDropController import com.android.wm.shell.freeform.FreeformTaskTransitionStarter import com.android.wm.shell.recents.RecentTasksController @@ -196,6 +197,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock lateinit var transitions: Transitions @Mock lateinit var keyguardManager: KeyguardManager @Mock lateinit var mReturnToDragStartAnimator: ReturnToDragStartAnimator + @Mock lateinit var desktopMixedTransitionHandler: DesktopMixedTransitionHandler @Mock lateinit var exitDesktopTransitionHandler: ExitDesktopTaskTransitionHandler @Mock lateinit var enterDesktopTransitionHandler: EnterDesktopTaskTransitionHandler @Mock lateinit var dragAndDropTransitionHandler: DesktopModeDragAndDropTransitionHandler @@ -223,6 +225,7 @@ class DesktopTasksControllerTest : ShellTestCase() { @Mock private lateinit var mockInputManager: InputManager @Mock private lateinit var mockFocusTransitionObserver: FocusTransitionObserver @Mock lateinit var motionEvent: MotionEvent + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession @Mock @@ -263,7 +266,8 @@ class DesktopTasksControllerTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) shellInit = spy(ShellInit(testExecutor)) - taskRepository = DesktopRepository(context, shellInit, persistentRepository, testScope) + taskRepository = + DesktopRepository(context, shellInit, persistentRepository, repositoryInitializer, testScope) desktopTasksLimiter = DesktopTasksLimiter( transitions, @@ -288,6 +292,12 @@ class DesktopTasksControllerTest : ShellTestCase() { val tda = DisplayAreaInfo(MockToken().token(), DEFAULT_DISPLAY, 0) tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN whenever(rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)).thenReturn(tda) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), any<RunningTaskInfo>())) + .thenReturn(DesktopImmersiveController.ExitResult.NoExit) + whenever(mMockDesktopImmersiveController + .exitImmersiveIfApplicable(any(), anyInt(), anyOrNull())) + .thenReturn(DesktopImmersiveController.ExitResult.NoExit) controller = createController() controller.setSplitScreenController(splitScreenController) @@ -323,6 +333,7 @@ class DesktopTasksControllerTest : ShellTestCase() { transitions, keyguardManager, mReturnToDragStartAnimator, + desktopMixedTransitionHandler, enterDesktopTransitionHandler, exitDesktopTransitionHandler, dragAndDropTransitionHandler, @@ -1389,7 +1400,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.moveTaskToFront(task1, remoteTransition = null) - val wct = getLatestWct(type = TRANSIT_TO_FRONT) + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps).hasSize(1) wct.assertReorderAt(index = 0, task1) } @@ -1398,10 +1409,17 @@ class DesktopTasksControllerTest : ShellTestCase() { fun moveTaskToFront_bringsTasksOverLimit_minimizesBackTask() { setUpHomeTask() val freeformTasks = (1..MAX_TASK_LIMIT + 1).map { _ -> setUpFreeformTask() } + whenever(desktopMixedTransitionHandler.startLaunchTransition( + eq(TRANSIT_TO_FRONT), + any(), + eq(freeformTasks[0].taskId), + anyOrNull(), + anyOrNull(), + )).thenReturn(Binder()) controller.moveTaskToFront(freeformTasks[0], remoteTransition = null) - val wct = getLatestWct(type = TRANSIT_TO_FRONT) + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_TO_FRONT) assertThat(wct.hierarchyOps.size).isEqualTo(2) // move-to-front + minimize wct.assertReorderAt(0, freeformTasks[0], toTop = true) wct.assertReorderAt(1, freeformTasks[1], toTop = false) @@ -1443,7 +1461,7 @@ class DesktopTasksControllerTest : ShellTestCase() { controller.moveTaskToFront(task.taskId, remoteTransition = null) - val wct = getLatestWct(type = TRANSIT_OPEN) + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps).hasSize(1) wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) } @@ -1453,10 +1471,13 @@ class DesktopTasksControllerTest : ShellTestCase() { val freeformTasks = (1..MAX_TASK_LIMIT).map { _ -> setUpFreeformTask() } val task = createTaskInfo(1001) whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(null) + whenever(desktopMixedTransitionHandler + .startLaunchTransition(eq(TRANSIT_OPEN), any(), eq(task.taskId), anyOrNull(), anyOrNull())) + .thenReturn(Binder()) controller.moveTaskToFront(task.taskId, remoteTransition = null) - val wct = getLatestWct(type = TRANSIT_OPEN) + val wct = getLatestDesktopMixedTaskWct(type = TRANSIT_OPEN) assertThat(wct.hierarchyOps.size).isEqualTo(2) // launch + minimize wct.assertLaunchTaskAt(0, task.taskId, WINDOWING_MODE_FREEFORM) wct.assertReorderAt(1, freeformTasks[0], toTop = false) @@ -1695,7 +1716,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test - fun onDesktopWindowMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { + fun onTaskMinimize_singleActiveTask_hasWallpaperActivityToken_removesWallpaper() { val task = setUpFreeformTask() val transition = Binder() whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) @@ -1791,7 +1812,10 @@ class DesktopTasksControllerTest : ShellTestCase() { whenever(freeformTaskTransitionStarter.startMinimizedModeTransition(any())) .thenReturn(transition) whenever(mMockDesktopImmersiveController.exitImmersiveIfApplicable(any(), eq(task))) - .thenReturn(runOnTransit) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = task.taskId, + runOnTransitionStart = runOnTransit, + )) controller.minimizeTask(task) @@ -2382,8 +2406,7 @@ class DesktopTasksControllerTest : ShellTestCase() { taskRepository.wallpaperActivityToken = wallpaperToken taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) // Task is being minimized so mark it as not visible. - taskRepository - .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false) + taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) assertNull(result, "Should not handle request") @@ -2497,8 +2520,7 @@ class DesktopTasksControllerTest : ShellTestCase() { taskRepository.wallpaperActivityToken = wallpaperToken taskRepository.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = task2.taskId) // Task is being minimized so mark it as not visible. - taskRepository - .updateTaskVisibility(displayId = DEFAULT_DISPLAY, task2.taskId, false) + taskRepository.updateTask(displayId = DEFAULT_DISPLAY, task2.taskId, isVisible = false) val result = controller.handleRequest(Binder(), createTransition(task2, type = TRANSIT_TO_BACK)) assertNull(result, "Should not handle request") @@ -2573,8 +2595,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task3.isFocused = false taskRepository.wallpaperActivityToken = wallpaperToken taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) - taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId, - visible = false) + taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) controller.enterFullscreen(DEFAULT_DISPLAY, transitionSource = UNKNOWN) @@ -2897,6 +2918,123 @@ class DesktopTasksControllerTest : ShellTestCase() { } @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToExitDesktop() { + val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(false) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + Rect(100, 50, 500, 1000), /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration) + + // Assert the task exits desktop mode + val wct = getLatestExitDesktopWct() + assertThat(wct.changes[task.token.asBinder()]?.windowingMode) + .isEqualTo(WINDOWING_MODE_UNDEFINED) + } + + @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize() { + val task = setUpFreeformTask(bounds = Rect(0, 0, 100, 100)) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + Rect(100, 50, 500, 1000), /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration) + + // Assert bounds set to stable bounds + val wct = getLatestToggleResizeDesktopTaskWct() + assertThat(findBoundsChange(wct, task)).isEqualTo(STABLE_BOUNDS) + } + + @Test + fun onDesktopDragEnd_fullscreenIndicator_dragToMaximize_noBoundsChange() { + val task = setUpFreeformTask(bounds = STABLE_BOUNDS) + val spyController = spy(controller) + val mockSurface = mock(SurfaceControl::class.java) + val mockDisplayLayout = mock(DisplayLayout::class.java) + val tda = rootTaskDisplayAreaOrganizer.getDisplayAreaInfo(DEFAULT_DISPLAY)!! + tda.configuration.windowConfiguration.windowingMode = WINDOWING_MODE_FULLSCREEN + whenever(displayController.getDisplayLayout(task.displayId)).thenReturn(mockDisplayLayout) + whenever(mockDisplayLayout.stableInsets()).thenReturn(Rect(0, 100, 2000, 2000)) + whenever(mockDisplayLayout.getStableBounds(any())).thenAnswer { i -> + (i.arguments.first() as Rect).set(STABLE_BOUNDS) + } + whenever(DesktopModeStatus.shouldMaximizeWhenDragToTopEdge(context)).thenReturn(true) + whenever(spyController.getVisualIndicator()).thenReturn(desktopModeVisualIndicator) + whenever(desktopModeVisualIndicator.updateIndicatorType(anyOrNull())) + .thenReturn(DesktopModeVisualIndicator.IndicatorType.TO_FULLSCREEN_INDICATOR) + + // Drag move the task to the top edge + val currentDragBounds = Rect(100, 50, 500, 1000) + spyController.onDragPositioningMove(task, mockSurface, 200f, Rect(100, 200, 500, 1000)) + spyController.onDragPositioningEnd( + task, + mockSurface, + Point(100, 50), /* position */ + PointF(200f, 300f), /* inputCoordinate */ + currentDragBounds, /* currentDragBounds */ + Rect(0, 50, 2000, 2000) /* validDragArea */, + Rect() /* dragStartBounds */, + motionEvent, + desktopWindowDecoration) + + // Assert that task is NOT updated via WCT + verify(toggleResizeDesktopTaskTransitionHandler, never()).startTransition(any(), any()) + // Assert that task leash is updated via Surface Animations + verify(mReturnToDragStartAnimator).start( + eq(task.taskId), + eq(mockSurface), + eq(currentDragBounds), + eq(STABLE_BOUNDS), + anyOrNull(), + ) + } + + @Test fun enterSplit_freeformTaskIsMovedToSplit() { val task1 = setUpFreeformTask() val task2 = setUpFreeformTask() @@ -2928,8 +3066,7 @@ class DesktopTasksControllerTest : ShellTestCase() { task3.isFocused = false taskRepository.wallpaperActivityToken = wallpaperToken taskRepository.minimizeTask(DEFAULT_DISPLAY, task1.taskId) - taskRepository.updateTaskVisibility(DEFAULT_DISPLAY, task3.taskId, - visible = false) + taskRepository.updateTask(DEFAULT_DISPLAY, task3.taskId, isVisible = false) controller.enterSplit(DEFAULT_DISPLAY, leftOrTop = false) @@ -3105,7 +3242,10 @@ class DesktopTasksControllerTest : ShellTestCase() { .thenReturn(transition) whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(immersiveTask.displayId), eq(freeformTask.taskId))) - .thenReturn(runOnStartTransit) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = immersiveTask.taskId, + runOnTransitionStart = runOnStartTransit, + )) runOpenInstance(immersiveTask, freeformTask.taskId) @@ -3205,7 +3345,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(mockSurface), eq(currentDragBounds), eq(bounds), - eq(true) + anyOrNull(), ) verify(desktopModeEventLogger, times(1)).logTaskResizingEnded( ResizeTrigger.SNAP_LEFT_MENU, @@ -3262,7 +3402,7 @@ class DesktopTasksControllerTest : ShellTestCase() { eq(mockSurface), eq(currentDragBounds), eq(preDragBounds), - eq(false) + any(), ) verify(desktopModeEventLogger, never()).logTaskResizingStarted( any(), @@ -3422,7 +3562,6 @@ class DesktopTasksControllerTest : ShellTestCase() { ) } - @Test fun onUnhandledDrag_newFreeformIntent() { testOnUnhandledDrag(DesktopModeVisualIndicator.IndicatorType.TO_DESKTOP_INDICATOR, @@ -3507,7 +3646,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit) + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = 5, + runOnTransitionStart = runOnStartTransit, + )) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) @@ -3524,7 +3667,11 @@ class DesktopTasksControllerTest : ShellTestCase() { val runOnStartTransit = RunOnStartTransitionCallback() val transition = Binder() whenever(mMockDesktopImmersiveController - .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)).thenReturn(runOnStartTransit) + .exitImmersiveIfApplicable(wct, task.displayId, task.taskId)) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = 5, + runOnTransitionStart = runOnStartTransit, + )) whenever(enterDesktopTransitionHandler.moveToDesktop(wct, UNKNOWN)).thenReturn(transition) controller.moveTaskToDesktop(taskId = task.taskId, wct = wct, transitionSource = UNKNOWN) @@ -3541,8 +3688,13 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) - .thenReturn(runOnStartTransit) - whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = 5, + runOnTransitionStart = runOnStartTransit, + )) + whenever(desktopMixedTransitionHandler + .startLaunchTransition(any(), any(), anyInt(), anyOrNull(), anyOrNull())) + .thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) @@ -3558,8 +3710,13 @@ class DesktopTasksControllerTest : ShellTestCase() { val transition = Binder() whenever(mMockDesktopImmersiveController .exitImmersiveIfApplicable(any(), eq(task.displayId), eq(task.taskId))) - .thenReturn(runOnStartTransit) - whenever(transitions.startTransition(any(), any(), anyOrNull())).thenReturn(transition) + .thenReturn(DesktopImmersiveController.ExitResult.Exit( + exitingTask = 5, + runOnTransitionStart = runOnStartTransit, + )) + whenever(desktopMixedTransitionHandler + .startLaunchTransition(any(), any(), eq(task.taskId), anyOrNull(), anyOrNull())) + .thenReturn(transition) controller.moveTaskToFront(task.taskId, remoteTransition = null) @@ -3808,11 +3965,7 @@ class DesktopTasksControllerTest : ShellTestCase() { } else { whenever(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) } - if (active) { - taskRepository.addActiveTask(displayId, task.taskId) - taskRepository.updateTaskVisibility(displayId, task.taskId, visible = true) - } - taskRepository.addOrMoveFreeformTaskToTop(displayId, task.taskId) + taskRepository.addTask(displayId, task.taskId, isVisible = active) if (!background) { runningTasks.add(task) } @@ -3915,13 +4068,11 @@ class DesktopTasksControllerTest : ShellTestCase() { } private fun markTaskVisible(task: RunningTaskInfo) { - taskRepository.updateTaskVisibility( - task.displayId, task.taskId, visible = true) + taskRepository.updateTask(task.displayId, task.taskId, isVisible = true) } private fun markTaskHidden(task: RunningTaskInfo) { - taskRepository.updateTaskVisibility( - task.displayId, task.taskId, visible = false) + taskRepository.updateTask(task.displayId, task.taskId, isVisible = false) } private fun getLatestWct( @@ -3929,7 +4080,6 @@ class DesktopTasksControllerTest : ShellTestCase() { handlerClass: Class<out TransitionHandler>? = null ): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) - if (handlerClass == null) { verify(transitions).startTransition(eq(type), arg.capture(), isNull()) } else { @@ -3948,6 +4098,16 @@ class DesktopTasksControllerTest : ShellTestCase() { return arg.value } + private fun getLatestDesktopMixedTaskWct( + @WindowManager.TransitionType type: Int = TRANSIT_OPEN, + ): WindowContainerTransaction { + val arg: ArgumentCaptor<WindowContainerTransaction> = + ArgumentCaptor.forClass(WindowContainerTransaction::class.java) + verify(desktopMixedTransitionHandler) + .startLaunchTransition(eq(type), capture(arg), anyInt(), anyOrNull(), anyOrNull()) + return arg.value + } + private fun getLatestEnterDesktopWct(): WindowContainerTransaction { val arg = ArgumentCaptor.forClass(WindowContainerTransaction::class.java) verify(enterDesktopTransitionHandler).moveToDesktop(arg.capture(), any()) @@ -4046,7 +4206,6 @@ private fun WindowContainerTransaction.assertNoRemoveAt(index: Int, token: Windo } private fun WindowContainerTransaction.hasRemoveAt(index: Int, token: WindowContainerToken) { - assertIndexInBounds(index) val op = hierarchyOps[index] assertThat(op.type).isEqualTo(HIERARCHY_OP_TYPE_REMOVE_TASK) 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 4d7e47fa51bd..01b69aed8465 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 @@ -41,6 +41,7 @@ import com.android.wm.shell.ShellTestCase import com.android.wm.shell.common.ShellExecutor import com.android.wm.shell.desktopmode.DesktopTestHelpers.Companion.createFreeformTask import com.android.wm.shell.desktopmode.persistence.DesktopPersistentRepository +import com.android.wm.shell.desktopmode.persistence.DesktopRepositoryInitializer import com.android.wm.shell.shared.desktopmode.DesktopModeStatus import com.android.wm.shell.sysui.ShellInit import com.android.wm.shell.transition.TransitionInfoBuilder @@ -89,6 +90,7 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Mock lateinit var handler: Handler @Mock lateinit var testExecutor: ShellExecutor @Mock lateinit var persistentRepository: DesktopPersistentRepository + @Mock lateinit var repositoryInitializer: DesktopRepositoryInitializer private lateinit var mockitoSession: StaticMockitoSession private lateinit var desktopTasksLimiter: DesktopTasksLimiter @@ -106,7 +108,13 @@ class DesktopTasksLimiterTest : ShellTestCase() { testScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) desktopTaskRepo = - DesktopRepository(context, shellInit, persistentRepository, testScope) + DesktopRepository( + context, + shellInit, + persistentRepository, + repositoryInitializer, + testScope + ) desktopTasksLimiter = DesktopTasksLimiter(transitions, desktopTaskRepo, shellTaskOrganizer, MAX_TASK_LIMIT, interactionJankMonitor, mContext, handler) @@ -248,8 +256,8 @@ class DesktopTasksLimiterTest : ShellTestCase() { @Test @DisableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) fun removeLeftoverMinimizedTasks_activeNonMinimizedTasksStillAround_doesNothing() { - desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 1) - desktopTaskRepo.addActiveTask(displayId = DEFAULT_DISPLAY, taskId = 2) + desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 1, isVisible = true) + desktopTaskRepo.addTask(displayId = DEFAULT_DISPLAY, taskId = 2, isVisible = true) desktopTaskRepo.minimizeTask(displayId = DEFAULT_DISPLAY, taskId = 2) val wct = WindowContainerTransaction() @@ -487,29 +495,26 @@ class DesktopTasksLimiterTest : ShellTestCase() { verify(interactionJankMonitor).end(eq(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)) } - private fun setUpFreeformTask( - displayId: Int = DEFAULT_DISPLAY, - ): RunningTaskInfo { + private fun setUpFreeformTask(displayId: Int = DEFAULT_DISPLAY): RunningTaskInfo { val task = createFreeformTask(displayId) `when`(shellTaskOrganizer.getRunningTaskInfo(task.taskId)).thenReturn(task) - desktopTaskRepo.addActiveTask(displayId, task.taskId) - desktopTaskRepo.addOrMoveFreeformTaskToTop(displayId, task.taskId) + desktopTaskRepo.addTask(displayId, task.taskId, task.isVisible) return task } private fun markTaskVisible(task: RunningTaskInfo) { - desktopTaskRepo.updateTaskVisibility( + desktopTaskRepo.updateTask( task.displayId, task.taskId, - visible = true + isVisible = true ) } private fun markTaskHidden(task: RunningTaskInfo) { - desktopTaskRepo.updateTaskVisibility( + desktopTaskRepo.updateTask( task.displayId, task.taskId, - visible = false + isVisible = false ) } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java index fefa933c5208..a82e5e8434b2 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/ExitDesktopTaskTransitionHandlerTest.java @@ -19,8 +19,6 @@ package com.android.wm.shell.desktopmode; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; -import static androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread; - import static com.android.wm.shell.desktopmode.DesktopModeTransitionTypes.TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; import static org.junit.Assert.assertTrue; @@ -28,6 +26,7 @@ import static org.mockito.Mockito.doReturn; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.when; +import android.animation.AnimatorTestRule; import android.annotation.NonNull; import android.app.ActivityManager; import android.app.WindowConfiguration; @@ -36,6 +35,8 @@ import android.content.res.Resources; import android.graphics.Point; import android.os.Handler; import android.os.IBinder; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; import android.util.DisplayMetrics; import android.view.SurfaceControl; import android.view.WindowManager; @@ -53,17 +54,23 @@ import com.android.wm.shell.shared.desktopmode.DesktopModeTransitionSource; 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; import org.mockito.MockitoAnnotations; -import java.util.ArrayList; import java.util.function.Supplier; /** Tests of {@link com.android.wm.shell.desktopmode.ExitDesktopTaskTransitionHandler} */ @SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { + @Rule + public final AnimatorTestRule mAnimatorTestRule = new AnimatorTestRule(this); + @Mock private Transitions mTransitions; @Mock @@ -105,7 +112,7 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { } @Test - public void testTransitExitDesktopModeAnimation() throws Throwable { + public void testTransitExitDesktopModeAnimation() { final int transitionType = TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN; final int taskId = 1; WindowContainerTransaction wct = new WindowContainerTransaction(); @@ -118,21 +125,16 @@ public class ExitDesktopTaskTransitionHandlerTest extends ShellTestCase { TransitionInfo.Change change = createChange(WindowManager.TRANSIT_CHANGE, taskId, WINDOWING_MODE_FULLSCREEN); TransitionInfo info = createTransitionInfo(TRANSIT_EXIT_DESKTOP_MODE_UNKNOWN, change); - ArrayList<Exception> exceptions = new ArrayList<>(); - runOnUiThread(() -> { - try { - assertTrue(mExitDesktopTaskTransitionHandler - .startAnimation(mToken, info, - new SurfaceControl.Transaction(), - new SurfaceControl.Transaction(), - mTransitionFinishCallback)); - } catch (Exception e) { - exceptions.add(e); - } - }); - if (!exceptions.isEmpty()) { - throw exceptions.get(0); - } + + final boolean animated = mExitDesktopTaskTransitionHandler + .startAnimation(mToken, info, + new SurfaceControl.Transaction(), + new SurfaceControl.Transaction(), + mTransitionFinishCallback); + mAnimatorTestRule.advanceTimeBy( + ExitDesktopTaskTransitionHandler.FULLSCREEN_ANIMATION_DURATION); + + assertTrue(animated); } private TransitionInfo.Change createChange(@WindowManager.TransitionType int type, int taskId, 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 new file mode 100644 index 000000000000..975342902814 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/persistence/DesktopRepositoryInitializerTest.kt @@ -0,0 +1,119 @@ +/* + * 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.persistence + +import android.platform.test.annotations.EnableFlags +import android.testing.AndroidTestingRunner +import android.view.Display.DEFAULT_DISPLAY +import androidx.test.filters.SmallTest +import com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE +import com.android.wm.shell.ShellTestCase +import com.android.wm.shell.common.ShellExecutor +import com.android.wm.shell.desktopmode.DesktopRepository +import com.android.wm.shell.sysui.ShellInit +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.SupervisorJob +import kotlinx.coroutines.cancel +import kotlinx.coroutines.test.StandardTestDispatcher +import kotlinx.coroutines.test.runTest +import kotlinx.coroutines.test.setMain +import org.junit.After +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.Mockito.inOrder +import org.mockito.Mockito.spy +import org.mockito.kotlin.any +import org.mockito.kotlin.mock +import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidTestingRunner::class) +@ExperimentalCoroutinesApi +class DesktopRepositoryInitializerTest : ShellTestCase() { + + private lateinit var repositoryInitializer: DesktopRepositoryInitializer + private lateinit var shellInit: ShellInit + private lateinit var datastoreScope: CoroutineScope + + private lateinit var desktopRepository: DesktopRepository + private val persistentRepository = mock<DesktopPersistentRepository>() + private val testExecutor = mock<ShellExecutor>() + + @Before + fun setUp() { + Dispatchers.setMain(StandardTestDispatcher()) + shellInit = spy(ShellInit(testExecutor)) + datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob()) + repositoryInitializer = + DesktopRepositoryInitializerImpl(context, persistentRepository, datastoreScope) + desktopRepository = + DesktopRepository( + context, shellInit, persistentRepository, repositoryInitializer, datastoreScope) + } + + @Test + @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_PERSISTENCE) + fun initWithPersistence_multipleTasks_addedCorrectly() = + runTest(StandardTestDispatcher()) { + val freeformTasksInZOrder = listOf(1, 2, 3) + whenever(persistentRepository.readDesktop(any(), any())) + .thenReturn( + Desktop.newBuilder() + .setDesktopId(1) + .addAllZOrderedTasks(freeformTasksInZOrder) + .putTasksByTaskId( + 1, + DesktopTask.newBuilder() + .setTaskId(1) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build()) + .putTasksByTaskId( + 2, + DesktopTask.newBuilder() + .setTaskId(2) + .setDesktopTaskState(DesktopTaskState.VISIBLE) + .build()) + .putTasksByTaskId( + 3, + DesktopTask.newBuilder() + .setTaskId(3) + .setDesktopTaskState(DesktopTaskState.MINIMIZED) + .build()) + .build()) + + repositoryInitializer.initialize(desktopRepository) + + verify(persistentRepository).readDesktop(any(), any()) + assertThat(desktopRepository.getActiveTasks(DEFAULT_DISPLAY)) + .containsExactly(1, 2, 3) + .inOrder() + assertThat(desktopRepository.getExpandedTasksOrdered(DEFAULT_DISPLAY)) + .containsExactly(1, 2) + .inOrder() + assertThat(desktopRepository.getMinimizedTasks(DEFAULT_DISPLAY)).containsExactly(3) + } + + @After + fun tearDown() { + datastoreScope.cancel() + } +} 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 8dd154566361..b504a88e71fc 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 @@ -23,6 +23,8 @@ import static android.view.Display.INVALID_DISPLAY; import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn; import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; import static com.android.window.flags.Flags.FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION; +import static com.android.window.flags.Flags.FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS; +import static com.android.window.flags.Flags.FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.never; @@ -30,6 +32,7 @@ import static org.mockito.Mockito.verify; 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; @@ -59,9 +62,8 @@ import org.mockito.quality.Strictness; import java.util.Optional; /** - * Tests for {@link FreeformTaskListener} - * Build/Install/Run: - * atest WMShellUnitTests:FreeformTaskListenerTests + * Tests for {@link FreeformTaskListener} Build/Install/Run: atest + * WMShellUnitTests:FreeformTaskListenerTests */ @SmallTest @RunWith(AndroidJUnit4.class) @@ -84,53 +86,99 @@ public final class FreeformTaskListenerTests extends ShellTestCase { private DesktopTasksController mDesktopTasksController; @Mock private LaunchAdjacentController mLaunchAdjacentController; + @Mock + private TaskChangeListener mTaskChangeListener; + private FreeformTaskListener mFreeformTaskListener; private StaticMockitoSession mMockitoSession; @Before public void setup() { - mMockitoSession = mockitoSession().initMocks(this) - .strictness(Strictness.LENIENT).mockStatic(DesktopModeStatus.class).startMocking(); + mMockitoSession = + mockitoSession() + .initMocks(this) + .strictness(Strictness.LENIENT) + .mockStatic(DesktopModeStatus.class) + .startMocking(); doReturn(true).when(() -> DesktopModeStatus.canEnterDesktopMode(any())); - mFreeformTaskListener = new FreeformTaskListener( - mContext, - mShellInit, - mTaskOrganizer, - Optional.of(mDesktopRepository), - Optional.of(mDesktopTasksController), - mLaunchAdjacentController, - mWindowDecorViewModel); + mFreeformTaskListener = + new FreeformTaskListener( + mContext, + mShellInit, + mTaskOrganizer, + Optional.of(mDesktopRepository), + Optional.of(mDesktopTasksController), + mLaunchAdjacentController, + mWindowDecorViewModel, + Optional.of(mTaskChangeListener)); + } + + @Test + @DisableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskAppeared_noTransitionObservers_visibleTask_addsTaskToRepo() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + verify(mDesktopRepository).addTask(task.displayId, task.taskId, task.isVisible = true); + } + + @Test + @DisableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskAppeared_noTransitionObservers_nonVisibleTask_addsTaskToRepo() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = false; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + verify(mDesktopRepository).addTask(task.displayId, task.taskId, task.isVisible); + } + + @Test + @EnableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskAppeared_useTransitionObserver_noopInRepository() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + verify(mDesktopRepository, never()).addTask(task.displayId, task.taskId, task.isVisible); } @Test - public void testFocusTaskChanged_freeformTaskIsAddedToRepo() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + public void focusTaskChanged_addsFreeformTaskToRepo() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isFocused = true; mFreeformTaskListener.onFocusTaskChanged(task); - verify(mDesktopRepository) - .addOrMoveFreeformTaskToTop(task.displayId, task.taskId); + verify(mDesktopRepository).addTask(task.displayId, task.taskId, task.isVisible); } @Test - public void testFocusTaskChanged_fullscreenTaskIsNotAddedToRepo() { - ActivityManager.RunningTaskInfo fullscreenTask = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FULLSCREEN).build(); + public void focusTaskChanged_fullscreenTaskNotAddedToRepo() { + ActivityManager.RunningTaskInfo fullscreenTask = + new TestRunningTaskInfoBuilder() + .setWindowingMode(WINDOWING_MODE_FULLSCREEN) + .build(); fullscreenTask.isFocused = true; mFreeformTaskListener.onFocusTaskChanged(fullscreenTask); verify(mDesktopRepository, never()) - .addOrMoveFreeformTaskToTop(fullscreenTask.displayId, fullscreenTask.taskId); + .addTask(fullscreenTask.displayId, fullscreenTask.taskId, fullscreenTask.isVisible); } @Test - public void testVisibilityTaskChanged_visible_setLaunchAdjacentDisabled() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + public void visibilityTaskChanged_visible_setLaunchAdjacentDisabled() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isVisible = true; mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); @@ -139,9 +187,9 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test - public void testVisibilityTaskChanged_NotVisible_setLaunchAdjacentEnabled() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + public void visibilityTaskChanged_notVisible_setLaunchAdjacentEnabled() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isVisible = true; mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); @@ -154,9 +202,10 @@ public final class FreeformTaskListenerTests extends ShellTestCase { @Test @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - public void onTaskVanished_nonClosingTask_isMinimized() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + @DisableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskVanished_nonClosingTask_noTransitionObservers_isMinimized() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isVisible = true; mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); @@ -169,10 +218,11 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @DisableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) @EnableFlags(FLAG_ENABLE_DESKTOP_WINDOWING_BACK_NAVIGATION) - public void onTaskVanished_closingTask_isNotMinimized() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + public void onTaskVanished_closingTask_noTransitionObservers_isNotMinimized() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isVisible = true; mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); @@ -188,9 +238,23 @@ public final class FreeformTaskListenerTests extends ShellTestCase { } @Test + @EnableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskVanished_usesTransitionObservers_noopInRepo() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + mFreeformTaskListener.onTaskVanished(task); + + verify(mDesktopRepository, never()).minimizeTask(task.displayId, task.taskId); + verify(mDesktopRepository, never()).removeClosingTask(task.taskId); + verify(mDesktopRepository, never()).removeFreeformTask(task.displayId, task.taskId); + } + + @Test public void onTaskInfoChanged_withDesktopController_forwards() { - ActivityManager.RunningTaskInfo task = new TestRunningTaskInfoBuilder() - .setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); task.isVisible = true; mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); @@ -199,6 +263,36 @@ public final class FreeformTaskListenerTests extends ShellTestCase { verify(mDesktopTasksController).onTaskInfoChanged(task); } + @Test + @DisableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + public void onTaskInfoChanged_noTransitionObservers_updatesTask() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + mFreeformTaskListener.onTaskInfoChanged(task); + + verify(mTaskChangeListener, never()).onTaskChanging(any()); + verify(mDesktopRepository).updateTask(task.displayId, task.taskId, task.isVisible); + } + + @Test + @EnableFlags(FLAG_ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS) + @DisableFlags(FLAG_SHOW_DESKTOP_WINDOWING_DEV_OPTION) + public void onTaskInfoChanged_useTransitionObserver_noopInRepository() { + ActivityManager.RunningTaskInfo task = + new TestRunningTaskInfoBuilder().setWindowingMode(WINDOWING_MODE_FREEFORM).build(); + task.isVisible = true; + mFreeformTaskListener.onTaskAppeared(task, mMockSurfaceControl); + + mFreeformTaskListener.onTaskInfoChanged(task); + + verify(mTaskChangeListener).onNonTransitionTaskChanging(any()); + verify(mDesktopRepository, never()) + .updateTask(task.displayId, task.taskId, task.isVisible); + } + @After public void tearDown() { mMockitoSession.finishMocking(); 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 90ab2b8285cd..5aed4611cdc8 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 @@ -58,25 +58,17 @@ import org.mockito.MockitoAnnotations; import java.util.Optional; -/** - * Tests for {@link FreeformTaskTransitionObserver}. - */ +/** Tests for {@link FreeformTaskTransitionObserver}. */ @SmallTest public class FreeformTaskTransitionObserverTest { public final SetFlagsRule mSetFlagsRule = new SetFlagsRule(); - @Mock - private ShellInit mShellInit; - @Mock - private Transitions mTransitions; - @Mock - private DesktopImmersiveController mDesktopImmersiveController; - @Mock - private WindowDecorViewModel mWindowDecorViewModel; - @Mock - private TaskChangeListener mTaskChangeListener; - @Mock - private FocusTransitionObserver mFocusTransitionObserver; + @Mock private ShellInit mShellInit; + @Mock private Transitions mTransitions; + @Mock private DesktopImmersiveController mDesktopImmersiveController; + @Mock private WindowDecorViewModel mWindowDecorViewModel; + @Mock private TaskChangeListener mTaskChangeListener; + @Mock private FocusTransitionObserver mFocusTransitionObserver; private FreeformTaskTransitionObserver mTransitionObserver; @@ -85,20 +77,22 @@ public class FreeformTaskTransitionObserverTest { MockitoAnnotations.initMocks(this); PackageManager pm = mock(PackageManager.class); - doReturn(true).when(pm).hasSystemFeature( - PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); + doReturn(true).when(pm).hasSystemFeature(PackageManager.FEATURE_FREEFORM_WINDOW_MANAGEMENT); final Context context = mock(Context.class); doReturn(pm).when(context).getPackageManager(); - mTransitionObserver = new FreeformTaskTransitionObserver( - context, mShellInit, mTransitions, - Optional.of(mDesktopImmersiveController), - mWindowDecorViewModel, Optional.of(mTaskChangeListener), mFocusTransitionObserver); - - final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass( - Runnable.class); - verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), - same(mTransitionObserver)); + mTransitionObserver = + new FreeformTaskTransitionObserver( + context, + mShellInit, + mTransitions, + Optional.of(mDesktopImmersiveController), + mWindowDecorViewModel, + Optional.of(mTaskChangeListener), + mFocusTransitionObserver); + + final ArgumentCaptor<Runnable> initRunnableCaptor = ArgumentCaptor.forClass(Runnable.class); + verify(mShellInit).addInitCallback(initRunnableCaptor.capture(), same(mTransitionObserver)); initRunnableCaptor.getValue().run(); } @@ -109,10 +103,9 @@ public class FreeformTaskTransitionObserverTest { @Test public void openTransition_createsWindowDecor() { - final TransitionInfo.Change change = - createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(change).build(); + final TransitionInfo.Change change = createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -120,16 +113,15 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mWindowDecorViewModel).onTaskOpening( - change.getTaskInfo(), change.getLeash(), startT, finishT); + verify(mWindowDecorViewModel) + .onTaskOpening(change.getTaskInfo(), change.getLeash(), startT, finishT); } @Test public void openTransition_notifiesOnTaskOpening() { - final TransitionInfo.Change change = - createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(change).build(); + final TransitionInfo.Change change = createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -144,8 +136,10 @@ public class FreeformTaskTransitionObserverTest { public void toFrontTransition_notifiesOnTaskMovingToFront() { final TransitionInfo.Change change = createChange(TRANSIT_TO_FRONT, /* taskId= */ 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_FRONT, /* flags= */ 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_TO_FRONT, /* flags= */ 0) + .addChange(change) + .build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -160,8 +154,10 @@ public class FreeformTaskTransitionObserverTest { public void toBackTransition_notifiesOnTaskMovingToBack() { final TransitionInfo.Change change = createChange(TRANSIT_TO_BACK, /* taskId= */ 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_TO_BACK, /* flags= */ 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_TO_BACK, /* flags= */ 0) + .addChange(change) + .build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -173,11 +169,11 @@ public class FreeformTaskTransitionObserverTest { } @Test - public void changeTransition_notifiesOnTaskChanging() { + public void changeTransition_notifiesOnTaskChange() { final TransitionInfo.Change change = createChange(TRANSIT_CHANGE, /* taskId= */ 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CHANGE, /* flags= */ 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_CHANGE, /* flags= */ 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -192,8 +188,8 @@ public class FreeformTaskTransitionObserverTest { public void closeTransition_preparesWindowDecor() { final TransitionInfo.Change change = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -201,16 +197,15 @@ public class FreeformTaskTransitionObserverTest { mTransitionObserver.onTransitionReady(transition, info, startT, finishT); mTransitionObserver.onTransitionStarting(transition); - verify(mWindowDecorViewModel).onTaskClosing( - change.getTaskInfo(), startT, finishT); + verify(mWindowDecorViewModel).onTaskClosing(change.getTaskInfo(), startT, finishT); } @Test public void closeTransition_notifiesOnTaskClosing() { final TransitionInfo.Change change = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -225,8 +220,8 @@ public class FreeformTaskTransitionObserverTest { public void closeTransition_doesntCloseWindowDecorDuringTransition() throws Exception { final TransitionInfo.Change change = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build(); final IBinder transition = mock(IBinder.class); final SurfaceControl.Transaction startT = mock(SurfaceControl.Transaction.class); @@ -241,8 +236,8 @@ public class FreeformTaskTransitionObserverTest { public void closeTransition_closesWindowDecorAfterTransition() throws Exception { final TransitionInfo.Change change = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change).build(); + final TransitionInfo info = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change).build(); final AutoCloseable windowDecor = mock(AutoCloseable.class); @@ -261,8 +256,8 @@ public class FreeformTaskTransitionObserverTest { // The playing transition final TransitionInfo.Change change1 = createChange(TRANSIT_OPEN, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_OPEN, 0) - .addChange(change1).build(); + final TransitionInfo info1 = + new TransitionInfoBuilder(TRANSIT_OPEN, 0).addChange(change1).build(); final IBinder transition1 = mock(IBinder.class); final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); @@ -273,8 +268,8 @@ public class FreeformTaskTransitionObserverTest { // The merged transition final TransitionInfo.Change change2 = createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM); - final TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change2).build(); + final TransitionInfo info2 = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change2).build(); final IBinder transition2 = mock(IBinder.class); final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); @@ -292,8 +287,8 @@ public class FreeformTaskTransitionObserverTest { // The playing transition final TransitionInfo.Change change1 = createChange(TRANSIT_CLOSE, 1, WINDOWING_MODE_FREEFORM); - final TransitionInfo info1 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change1).build(); + final TransitionInfo info1 = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change1).build(); final IBinder transition1 = mock(IBinder.class); final SurfaceControl.Transaction startT1 = mock(SurfaceControl.Transaction.class); @@ -304,8 +299,8 @@ public class FreeformTaskTransitionObserverTest { // The merged transition final TransitionInfo.Change change2 = createChange(TRANSIT_CLOSE, 2, WINDOWING_MODE_FREEFORM); - final TransitionInfo info2 = new TransitionInfoBuilder(TRANSIT_CLOSE, 0) - .addChange(change2).build(); + final TransitionInfo info2 = + new TransitionInfoBuilder(TRANSIT_CLOSE, 0).addChange(change2).build(); final IBinder transition2 = mock(IBinder.class); final SurfaceControl.Transaction startT2 = mock(SurfaceControl.Transaction.class); @@ -368,9 +363,10 @@ public class FreeformTaskTransitionObserverTest { taskInfo.taskId = taskId; taskInfo.configuration.windowConfiguration.setWindowingMode(windowingMode); - final TransitionInfo.Change change = new TransitionInfo.Change( - new WindowContainerToken(mock(IWindowContainerToken.class)), - mock(SurfaceControl.class)); + final TransitionInfo.Change change = + new TransitionInfo.Change( + new WindowContainerToken(mock(IWindowContainerToken.class)), + mock(SurfaceControl.class)); change.setMode(mode); change.setTaskInfo(taskInfo); return change; 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 e74c804d4f40..bcb7461bfae7 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 @@ -280,7 +280,7 @@ public class PipTaskOrganizerTest extends ShellTestCase { private void preparePipSurfaceTransactionHelper() { doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) - .crop(any(), any(), any()); + .cropAndPosition(any(), any(), any()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) .resetScale(any(), any(), any()); doReturn(mMockPipSurfaceTransactionHelper).when(mMockPipSurfaceTransactionHelper) diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipAlphaAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipAlphaAnimatorTest.java new file mode 100644 index 000000000000..9cc18ffdaed7 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipAlphaAnimatorTest.java @@ -0,0 +1,144 @@ +/* + * 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.pip2.animation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +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 androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +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 PipAlphaAnimator}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipAlphaAnimatorTest { + + @Mock private Context mMockContext; + + @Mock private Resources mMockResources; + + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + + @Mock private SurfaceControl.Transaction mMockTransaction; + + @Mock private Runnable mMockStartCallback; + + @Mock private Runnable mMockEndCallback; + + private PipAlphaAnimator mPipAlphaAnimator; + private SurfaceControl mTestLeash; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getInteger(anyInt())).thenReturn(0); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + when(mMockTransaction.setAlpha(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipAlphaAnimatorTest") + .setCallsite("PipAlphaAnimatorTest") + .build(); + } + + @Test + public void setAnimationStartCallback_fadeInAnimator_callbackStartCallback() { + mPipAlphaAnimator = new PipAlphaAnimator(mMockContext, mTestLeash, mMockTransaction, + PipAlphaAnimator.FADE_IN); + + mPipAlphaAnimator.setAnimationStartCallback(mMockStartCallback); + mPipAlphaAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipAlphaAnimator.start(); + mPipAlphaAnimator.pause(); + }); + + verify(mMockStartCallback).run(); + verifyZeroInteractions(mMockEndCallback); + } + + @Test + public void setAnimationEndCallback_fadeInAnimator_callbackStartAndEndCallback() { + mPipAlphaAnimator = new PipAlphaAnimator(mMockContext, mTestLeash, mMockTransaction, + PipAlphaAnimator.FADE_IN); + + mPipAlphaAnimator.setAnimationStartCallback(mMockStartCallback); + mPipAlphaAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipAlphaAnimator.start(); + mPipAlphaAnimator.end(); + }); + + verify(mMockStartCallback).run(); + verify(mMockStartCallback).run(); + } + + @Test + public void onAnimationEnd_fadeInAnimator_leashVisibleAtEnd() { + mPipAlphaAnimator = new PipAlphaAnimator(mMockContext, mTestLeash, mMockTransaction, + PipAlphaAnimator.FADE_IN); + mPipAlphaAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipAlphaAnimator.start(); + clearInvocations(mMockTransaction); + mPipAlphaAnimator.end(); + }); + + verify(mMockTransaction).setAlpha(mTestLeash, 1.0f); + } + + @Test + public void onAnimationEnd_fadeOutAnimator_leashInvisibleAtEnd() { + mPipAlphaAnimator = new PipAlphaAnimator(mMockContext, mTestLeash, mMockTransaction, + PipAlphaAnimator.FADE_OUT); + mPipAlphaAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipAlphaAnimator.start(); + clearInvocations(mMockTransaction); + mPipAlphaAnimator.end(); + }); + + verify(mMockTransaction).setAlpha(mTestLeash, 0f); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java new file mode 100644 index 000000000000..e19a10a78417 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipExpandAnimatorTest.java @@ -0,0 +1,185 @@ +/* + * 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.pip2.animation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; + +import android.content.Context; +import android.content.res.Resources; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.Surface; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +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 PipExpandAnimator}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipExpandAnimatorTest { + + @Mock private Context mMockContext; + + @Mock private Resources mMockResources; + + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + + @Mock private SurfaceControl.Transaction mMockTransaction; + + @Mock private SurfaceControl.Transaction mMockStartTransaction; + + @Mock private SurfaceControl.Transaction mMockFinishTransaction; + + @Mock private Runnable mMockStartCallback; + + @Mock private Runnable mMockEndCallback; + + private PipExpandAnimator mPipExpandAnimator; + private Rect mBaseBounds; + private Rect mStartBounds; + private Rect mEndBounds; + private Rect mSourceRectHint; + @Surface.Rotation private int mRotation; + private SurfaceControl mTestLeash; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockContext.getResources()).thenReturn(mMockResources); + when(mMockResources.getInteger(anyInt())).thenReturn(0); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + // No-op on the mMockTransaction + when(mMockTransaction.setAlpha(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setCrop(any(SurfaceControl.class), any(Rect.class))) + .thenReturn(mMockTransaction); + when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + when(mMockTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockTransaction); + // Do the same for mMockFinishTransaction + when(mMockFinishTransaction.setAlpha(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setCrop(any(SurfaceControl.class), any(Rect.class))) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setCornerRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + when(mMockFinishTransaction.setShadowRadius(any(SurfaceControl.class), anyFloat())) + .thenReturn(mMockFinishTransaction); + + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipExpandAnimatorTest") + .setCallsite("PipExpandAnimatorTest") + .build(); + } + + @Test + public void setAnimationStartCallback_expand_callbackStartCallback() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + mPipExpandAnimator.pause(); + }); + + verify(mMockStartCallback).run(); + verifyZeroInteractions(mMockEndCallback); + } + + @Test + public void setAnimationEndCallback_expand_callbackStartAndEndCallback() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + mPipExpandAnimator.end(); + }); + + verify(mMockStartCallback).run(); + verify(mMockEndCallback).run(); + } + + @Test + public void onAnimationEnd_expand_leashIsFullscreen() { + mRotation = Surface.ROTATION_0; + mBaseBounds = new Rect(0, 0, 1_000, 2_000); + mStartBounds = new Rect(500, 1_000, 1_000, 2_000); + mEndBounds = new Rect(mBaseBounds); + mPipExpandAnimator = new PipExpandAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, mSourceRectHint, + mRotation); + mPipExpandAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipExpandAnimator.setAnimationStartCallback(mMockStartCallback); + mPipExpandAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipExpandAnimator.start(); + clearInvocations(mMockTransaction); + mPipExpandAnimator.end(); + }); + + verify(mMockTransaction).setCrop(mTestLeash, mEndBounds); + verify(mMockTransaction).setCornerRadius(mTestLeash, 0f); + verify(mMockTransaction).setShadowRadius(mTestLeash, 0f); + } +} diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java new file mode 100644 index 000000000000..0adb50b81896 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/pip2/animation/PipResizeAnimatorTest.java @@ -0,0 +1,276 @@ +/* + * 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.pip2.animation; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.clearInvocations; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyZeroInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.kotlin.MatchersKt.eq; +import static org.junit.Assert.assertEquals; + +import android.content.Context; +import android.graphics.Matrix; +import android.graphics.Rect; +import android.testing.AndroidTestingRunner; +import android.testing.TestableLooper; +import android.view.SurfaceControl; + +import androidx.test.filters.SmallTest; +import androidx.test.platform.app.InstrumentationRegistry; + +import com.android.wm.shell.pip2.PipSurfaceTransactionHelper; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +/** + * Unit test against {@link PipResizeAnimator}. + */ +@SmallTest +@TestableLooper.RunWithLooper +@RunWith(AndroidTestingRunner.class) +public class PipResizeAnimatorTest { + + private static final float FLOAT_COMPARISON_DELTA = 0.001f; + + @Mock private Context mMockContext; + + @Mock private PipSurfaceTransactionHelper.SurfaceControlTransactionFactory mMockFactory; + + @Mock private SurfaceControl.Transaction mMockTransaction; + + @Mock private SurfaceControl.Transaction mMockStartTransaction; + + @Mock private SurfaceControl.Transaction mMockFinishTransaction; + + @Mock private Runnable mMockStartCallback; + + @Mock private Runnable mMockEndCallback; + + private PipResizeAnimator mPipResizeAnimator; + private Rect mBaseBounds; + private Rect mStartBounds; + private Rect mEndBounds; + private SurfaceControl mTestLeash; + private ArgumentCaptor<Matrix> mArgumentCaptor; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mMockFactory.getTransaction()).thenReturn(mMockTransaction); + when(mMockTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockTransaction); + when(mMockStartTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockStartTransaction); + when(mMockFinishTransaction.setMatrix(any(SurfaceControl.class), any(Matrix.class), any())) + .thenReturn(mMockFinishTransaction); + + mArgumentCaptor = ArgumentCaptor.forClass(Matrix.class); + mTestLeash = new SurfaceControl.Builder() + .setContainerLayer() + .setName("PipResizeAnimatorTest") + .setCallsite("PipResizeAnimatorTest") + .build(); + } + + @Test + public void setAnimationStartCallback_resize_callbackStartCallback() { + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + mPipResizeAnimator.pause(); + }); + + verify(mMockStartCallback).run(); + verifyZeroInteractions(mMockEndCallback); + } + + @Test + public void setAnimationEndCallback_resize_callbackStartAndEndCallback() { + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + mPipResizeAnimator.end(); + }); + + verify(mMockStartCallback).run(); + verify(mMockEndCallback).run(); + } + + @Test + public void onAnimationEnd_resizeDown_sizeChanged() { + // Resize from 800x800 to 400x400, eg. resize down + mBaseBounds = new Rect(100, 100, 500, 500); + mStartBounds = new Rect(200, 200, 1_000, 1_000); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Start transaction scales down from its final state + verify(mMockStartTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], + mStartBounds.width() / (float) mEndBounds.width(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], + mStartBounds.height() / (float) mEndBounds.height(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mStartBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mStartBounds.top, FLOAT_COMPARISON_DELTA); + + // Final animation transaction scales to 1 and puts the leash at final position + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + + // Finish transaction resets scale and puts the leash at final position + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + } + + @Test + public void onAnimationEnd_resizeUp_sizeChanged() { + // Resize from 400x400 to 800x800, eg. resize up + mBaseBounds = new Rect(200, 200, 1_000, 1_000); + mStartBounds = new Rect(100, 100, 500, 500); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 0; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Start transaction scales up from its final state + verify(mMockStartTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], + mStartBounds.width() / (float) mEndBounds.width(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], + mStartBounds.height() / (float) mEndBounds.height(), FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mStartBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mStartBounds.top, FLOAT_COMPARISON_DELTA); + + // Final animation transaction scales to 1 and puts the leash at final position + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + + // Finish transaction resets scale and puts the leash at final position + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSCALE_X], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSCALE_Y], 1f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_X], mEndBounds.left, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MTRANS_Y], mEndBounds.top, FLOAT_COMPARISON_DELTA); + } + + @Test + public void onAnimationEnd_withInitialDelta_rotateToZeroDegree() { + mBaseBounds = new Rect(200, 200, 1_000, 1_000); + mStartBounds = new Rect(100, 100, 500, 500); + mEndBounds = new Rect(mBaseBounds); + final int duration = 10; + final float delta = 45; + final float[] matrix = new float[9]; + mPipResizeAnimator = new PipResizeAnimator(mMockContext, mTestLeash, + mMockStartTransaction, mMockFinishTransaction, + mBaseBounds, mStartBounds, mEndBounds, + duration, delta); + mPipResizeAnimator.setSurfaceControlTransactionFactory(mMockFactory); + + mPipResizeAnimator.setAnimationStartCallback(mMockStartCallback); + mPipResizeAnimator.setAnimationEndCallback(mMockEndCallback); + InstrumentationRegistry.getInstrumentation().runOnMainSync(() -> { + mPipResizeAnimator.start(); + clearInvocations(mMockTransaction); + mPipResizeAnimator.end(); + }); + + // Final animation transaction sets skew to zero + verify(mMockTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSKEW_X], 0f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSKEW_Y], 0f, FLOAT_COMPARISON_DELTA); + + // Finish transaction sets skew to zero + verify(mMockFinishTransaction).setMatrix(eq(mTestLeash), mArgumentCaptor.capture(), any()); + mArgumentCaptor.getValue().getValues(matrix); + assertEquals(matrix[Matrix.MSKEW_X], 0f, FLOAT_COMPARISON_DELTA); + assertEquals(matrix[Matrix.MSKEW_Y], 0f, FLOAT_COMPARISON_DELTA); + } +} 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 afdb68776d04..efe4fb18f273 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 @@ -34,6 +34,7 @@ import com.android.wm.shell.common.ShellExecutor 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.extension.isFullscreen import com.google.common.truth.Truth.assertThat import dagger.Lazy import org.junit.Before @@ -107,8 +108,8 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(change.taskInfo?.windowingMode) } @@ -130,8 +131,8 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(1) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(1) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(WindowConfiguration.WINDOWING_MODE_FULLSCREEN) } @@ -161,9 +162,9 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) .isEqualTo(freeformOpenChange.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(freeformOpenChange.taskInfo?.windowingMode) } @@ -199,9 +200,15 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(change.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId).isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) .isEqualTo(change.taskInfo?.windowingMode) + + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1) + with(listener.taskInfoOnTaskChanged.last()) { + assertThat(taskId).isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(windowingMode).isEqualTo(mergedChange.taskInfo?.windowingMode) + } } @Test @@ -236,18 +243,151 @@ class TaskStackTransitionObserverTest { callOnTransitionFinished() executor.flushAll() - assertThat(listener.taskInfoToBeNotified.taskId).isEqualTo(mergedChange.taskInfo?.taskId) - assertThat(listener.taskInfoToBeNotified.windowingMode) - .isEqualTo(mergedChange.taskInfo?.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(mergedChange.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) + .isEqualTo(mergedChange.taskInfo?.windowingMode) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun taskChange_freeformWindowToFullscreenWindow_listenerNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + val freeformState = + createChange( + WindowManager.TRANSIT_OPEN, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val transitionInfoOpen = + TransitionInfoBuilder(WindowManager.TRANSIT_OPEN, 0).addChange(freeformState).build() + callOnTransitionReady(transitionInfoOpen) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.windowingMode) + .isEqualTo(freeformState.taskInfo?.windowingMode) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + + // create change transition to update the windowing mode to full screen. + val fullscreenState = + createChange( + WindowManager.TRANSIT_CHANGE, + createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + val transitionInfoChange = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .addChange(fullscreenState) + .build() + + callOnTransitionReady(transitionInfoChange) + callOnTransitionFinished() + executor.flushAll() + + // Asserting whether freeformState remains the same as before the change + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + + // Asserting changes + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(1) + with(listener.taskInfoOnTaskChanged.last()) { + assertThat(taskId).isEqualTo(fullscreenState.taskInfo?.taskId) + assertThat(isFullscreen).isEqualTo(true) + } + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun singleTransition_withOpenAndChange_onlyOpenIsNotified() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + // Creating multiple changes to be fired in a single transition + val freeformState = + createChange( + mode = WindowManager.TRANSIT_OPEN, + taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FREEFORM) + ) + val fullscreenState = + createChange( + mode = WindowManager.TRANSIT_CHANGE, + taskInfo = createTaskInfo(1, WindowConfiguration.WINDOWING_MODE_FULLSCREEN) + ) + + val transitionInfoWithChanges = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .addChange(freeformState) + .addChange(fullscreenState) + .build() + + callOnTransitionReady(transitionInfoWithChanges) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskMovedToFront.taskId) + .isEqualTo(freeformState.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskMovedToFront.isFullscreen).isEqualTo(false) + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(0) + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_TASK_STACK_OBSERVER_IN_SHELL) + fun singleTransition_withMultipleChanges_listenerNotified_forEachChange() { + val listener = TestListener() + val executor = TestShellExecutor() + transitionObserver.addTaskStackTransitionObserverListener(listener, executor) + + val taskId = 1 + + // Creating multiple changes to be fired in a single transition + val changes = + listOf( + WindowConfiguration.WINDOWING_MODE_FREEFORM, + WindowConfiguration.WINDOW_CONFIG_DISPLAY_ROTATION, + WindowConfiguration.WINDOWING_MODE_FULLSCREEN + ) + .map { change -> + createChange( + mode = WindowManager.TRANSIT_CHANGE, + taskInfo = createTaskInfo(taskId, change) + ) + } + + val transitionInfoWithChanges = + TransitionInfoBuilder(WindowManager.TRANSIT_CHANGE, 0) + .apply { changes.forEach { c -> this@apply.addChange(c) } } + .build() + + callOnTransitionReady(transitionInfoWithChanges) + callOnTransitionFinished() + executor.flushAll() + + assertThat(listener.taskInfoOnTaskChanged.size).isEqualTo(changes.size) + changes.forEachIndexed { index, change -> + assertThat(listener.taskInfoOnTaskChanged[index].taskId) + .isEqualTo(change.taskInfo?.taskId) + assertThat(listener.taskInfoOnTaskChanged[index].windowingMode) + .isEqualTo(change.taskInfo?.windowingMode) + } } class TestListener : TaskStackTransitionObserver.TaskStackTransitionObserverListener { - var taskInfoToBeNotified = ActivityManager.RunningTaskInfo() + var taskInfoOnTaskMovedToFront = ActivityManager.RunningTaskInfo() + var taskInfoOnTaskChanged = mutableListOf<ActivityManager.RunningTaskInfo>() override fun onTaskMovedToFrontThroughTransition( taskInfo: ActivityManager.RunningTaskInfo ) { - taskInfoToBeNotified = taskInfo + taskInfoOnTaskMovedToFront = taskInfo + } + + override fun onTaskChangedThroughTransition(taskInfo: ActivityManager.RunningTaskInfo) { + taskInfoOnTaskChanged += taskInfo } } 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 new file mode 100644 index 000000000000..c19232b6f787 --- /dev/null +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/animation/WindowAnimatorTest.kt @@ -0,0 +1,198 @@ +/* + * 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.shared.animation + +import android.graphics.PointF +import android.graphics.Rect +import android.util.DisplayMetrics +import android.view.SurfaceControl +import android.window.TransitionInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import androidx.test.internal.runner.junit4.statement.UiThreadStatement.runOnUiThread +import com.android.app.animation.Interpolators +import com.google.common.truth.Truth.assertThat +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith +import org.mockito.ArgumentMatchers.any +import org.mockito.ArgumentMatchers.anyFloat +import org.mockito.kotlin.argumentCaptor +import org.mockito.kotlin.mock +import org.mockito.kotlin.whenever + +@SmallTest +@RunWith(AndroidJUnit4::class) +class WindowAnimatorTest { + + private val transaction = mock<SurfaceControl.Transaction>() + private val change = mock<TransitionInfo.Change>() + private val leash = mock<SurfaceControl>() + + private val displayMetrics = DisplayMetrics().apply { density = 1f } + + private val positionXArgumentCaptor = argumentCaptor<Float>() + private val positionYArgumentCaptor = argumentCaptor<Float>() + private val scaleXArgumentCaptor = argumentCaptor<Float>() + private val scaleYArgumentCaptor = argumentCaptor<Float>() + + @Before + fun setup() { + whenever(change.leash).thenReturn(leash) + whenever(change.endAbsBounds).thenReturn(END_BOUNDS) + whenever(transaction.setPosition(any(), anyFloat(), anyFloat())).thenReturn(transaction) + whenever(transaction.setScale(any(), anyFloat(), anyFloat())).thenReturn(transaction) + whenever( + transaction.setPosition( + any(), + positionXArgumentCaptor.capture(), + positionYArgumentCaptor.capture(), + ) + ) + .thenReturn(transaction) + whenever( + transaction.setScale( + any(), + scaleXArgumentCaptor.capture(), + scaleYArgumentCaptor.capture(), + ) + ) + .thenReturn(transaction) + } + + @Test + fun createBoundsAnimator_returnsCorrectDefaultAnimatorParams() = runOnUiThread { + val boundsAnimParams = + WindowAnimator.BoundsAnimationParams( + durationMs = 100L, + interpolator = Interpolators.STANDARD_ACCELERATE, + ) + + val valueAnimator = + WindowAnimator.createBoundsAnimator( + displayMetrics, + boundsAnimParams, + change, + transaction + ) + valueAnimator.start() + + assertThat(valueAnimator.duration).isEqualTo(100L) + assertThat(valueAnimator.interpolator).isEqualTo(Interpolators.STANDARD_ACCELERATE) + val expectedPosition = PointF(END_BOUNDS.left.toFloat(), END_BOUNDS.top.toFloat()) + assertTransactionParams(expectedPosition, expectedScale = PointF(1f, 1f)) + } + + @Test + fun createBoundsAnimator_startScaleAndOffset_correctPosAndScale() = runOnUiThread { + val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) + whenever(change.endAbsBounds).thenReturn(bounds) + val boundsAnimParams = + WindowAnimator.BoundsAnimationParams( + durationMs = 100L, + startOffsetYDp = 10f, + startScale = 0.5f, + interpolator = Interpolators.STANDARD_ACCELERATE, + ) + + val valueAnimator = + WindowAnimator.createBoundsAnimator( + displayMetrics, + boundsAnimParams, + change, + transaction + ) + valueAnimator.start() + + assertTransactionParams( + expectedPosition = PointF(150f, 260f), + expectedScale = PointF(0.5f, 0.5f), + ) + } + + @Test + fun createBoundsAnimator_endScaleAndOffset_correctPosAndScale() = runOnUiThread { + val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) + whenever(change.endAbsBounds).thenReturn(bounds) + val boundsAnimParams = + WindowAnimator.BoundsAnimationParams( + durationMs = 100L, + endOffsetYDp = 10f, + endScale = 0.5f, + interpolator = Interpolators.STANDARD_ACCELERATE, + ) + + val valueAnimator = + WindowAnimator.createBoundsAnimator( + displayMetrics, + boundsAnimParams, + change, + transaction + ) + valueAnimator.start() + valueAnimator.end() + + assertTransactionParams( + expectedPosition = PointF(150f, 260f), + expectedScale = PointF(0.5f, 0.5f), + ) + } + + @Test + fun createBoundsAnimator_middleOfAnimation_correctPosAndScale() = runOnUiThread { + val bounds = Rect(/* left= */ 100, /* top= */ 200, /* right= */ 300, /* bottom= */ 400) + whenever(change.endAbsBounds).thenReturn(bounds) + val boundsAnimParams = + WindowAnimator.BoundsAnimationParams( + durationMs = 100L, + endOffsetYDp = 10f, + startScale = 0.5f, + endScale = 0.9f, + interpolator = Interpolators.LINEAR, + ) + + val valueAnimator = + WindowAnimator.createBoundsAnimator( + displayMetrics, + boundsAnimParams, + change, + transaction + ) + valueAnimator.currentPlayTime = 50 + + assertTransactionParams( + // We should have a window of size 140x140, which we centre by placing at pos 130, 230. + // Then add 10*0.5 as y-offset + expectedPosition = PointF(130f, 235f), + expectedScale = PointF(0.7f, 0.7f), + ) + } + + private fun assertTransactionParams(expectedPosition: PointF, expectedScale: PointF) { + assertThat(positionXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.x) + assertThat(positionYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedPosition.y) + assertThat(scaleXArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.x) + assertThat(scaleYArgumentCaptor.lastValue).isWithin(TOLERANCE).of(expectedScale.y) + } + + companion object { + private val END_BOUNDS = + Rect(/* left= */ 10, /* top= */ 20, /* right= */ 30, /* bottom= */ 40) + + private const val TOLERANCE = 1e-3f + } +} 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 f9f760e3f482..1215c52209a5 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 @@ -64,13 +64,14 @@ class DesktopHeaderManageWindowsMenuTest : ShellTestCase() { context = context, shellInit = ShellInit(TestShellExecutor()), persistentRepository = mock(), + repositoryInitializer = mock(), mainCoroutineScope = mock() ) } @After fun tearDown() { - menu.close() + menu.animateClose() } @Test 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 c42be7fef2a6..36c5be1d7191 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 @@ -946,7 +946,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { } @Test - fun testDecor_onClickToSplitScreen_detachesStatusBarInputLayer() { + fun testDecor_onClickToSplitScreen_disposesStatusBarInputLayer() { val toSplitScreenListenerCaptor = forClass(Function0::class.java) as ArgumentCaptor<Function0<Unit>> val decor = createOpenTaskDecoration( @@ -956,7 +956,7 @@ class DesktopModeWindowDecorViewModelTests : ShellTestCase() { toSplitScreenListenerCaptor.value.invoke() - verify(decor).detachStatusBarInputLayer() + verify(decor).disposeStatusBarInputLayer() } @Test 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 8a2c7782906d..f6fed29bbbe8 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 @@ -49,6 +49,7 @@ import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; +import static org.mockito.kotlin.VerificationKt.times; import android.app.ActivityManager; import android.app.assist.AssistContent; @@ -849,7 +850,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); runnableArgument.getValue().run(); verify(mMockSurfaceControlViewHostFactory).create(any(), any(), any()); } @@ -876,7 +878,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); @@ -890,7 +893,8 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase { taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN); ArgumentCaptor<Runnable> runnableArgument = ArgumentCaptor.forClass(Runnable.class); spyWindowDecor.relayout(taskInfo, true /* hasGlobalFocus */); - verify(mMockHandler).post(runnableArgument.capture()); + // Once for view host, the other for the AppHandle input layer. + verify(mMockHandler, times(2)).post(runnableArgument.capture()); spyWindowDecor.close(); 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 cb7fadee9822..8e0434cb28f7 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 @@ -218,8 +218,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(captionContainerSurfaceBuilder, never()).build(); verify(mMockSurfaceControlViewHostFactory, never()).create(any(), any(), any()); - verify(mMockSurfaceControlFinishT).hide(mMockTaskSurface); - assertNull(mRelayoutResult.mRootView); } @@ -281,8 +279,6 @@ public class WindowDecorationTests extends ShellTestCase { verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS); - verify(mMockSurfaceControlStartT) - .show(mMockTaskSurface); verify(mMockSurfaceControlStartT).setShadowRadius(mMockTaskSurface, 10); assertEquals(300, mRelayoutResult.mWidth); @@ -863,7 +859,7 @@ public class WindowDecorationTests extends ShellTestCase { final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - mRelayoutParams.mSetTaskPositionAndCrop = false; + mRelayoutParams.mSetTaskVisibilityPositionAndCrop = false; windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); verify(mMockSurfaceControlStartT, never()).setWindowCrop( @@ -891,7 +887,7 @@ public class WindowDecorationTests extends ShellTestCase { taskInfo.configuration.densityDpi = DisplayMetrics.DENSITY_DEFAULT * 2; final TestWindowDecoration windowDecor = createWindowDecoration(taskInfo); - mRelayoutParams.mSetTaskPositionAndCrop = true; + mRelayoutParams.mSetTaskVisibilityPositionAndCrop = true; windowDecor.relayout(taskInfo, true /* hasGlobalFocus */); verify(mMockSurfaceControlStartT).setWindowCrop( diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt index 0ccd424c2b30..80ad1df44a1b 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDecorViewModelTest.kt @@ -36,9 +36,11 @@ import org.junit.Before import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) @@ -73,6 +75,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { returnToDragStartAnimatorMock, desktopRepository, ) + whenever(contextMock.createContextAsUser(any(), any())).thenReturn(contextMock) } @Test @@ -143,7 +146,7 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { } @Test - fun userChange_starting_allTilingSessionsShouldBeDestroyed() { + fun onUserChange_allTilingSessionsShouldBeDestroyed() { desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( 1, desktopTilingDecoration, @@ -155,7 +158,29 @@ class DesktopTilingDecorViewModelTest : ShellTestCase() { desktopTilingDecorViewModel.onUserChange() - verify(desktopTilingDecoration, times(2)).onUserChange() + verify(desktopTilingDecoration, times(2)).resetTilingSession() + } + + @Test + fun displayOrientationChange_tilingForDisplayShouldBeDestroyed() { + desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( + 1, + desktopTilingDecoration, + ) + desktopTilingDecorViewModel.tilingTransitionHandlerByDisplayId.put( + 2, + desktopTilingDecoration, + ) + + desktopTilingDecorViewModel.onDisplayChange(1, 1, 2, null, null) + + verify(desktopTilingDecoration, times(1)).resetTilingSession() + verify(displayControllerMock, times(1)) + .addDisplayChangingController(eq(desktopTilingDecorViewModel)) + + desktopTilingDecorViewModel.onDisplayChange(1, 1, 3, null, null) + // No extra calls after 180 degree change. + verify(desktopTilingDecoration, times(1)).resetTilingSession() } companion object { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt index 0ee3f4695e85..3143946fa828 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingDividerWindowManagerTest.kt @@ -16,9 +16,12 @@ package com.android.wm.shell.windowdecor.tiling +import android.content.Context import android.content.res.Configuration import android.graphics.Rect import android.testing.AndroidTestingRunner +import android.view.Display +import android.view.RoundedCorner import android.view.SurfaceControl import androidx.test.annotation.UiThreadTest import androidx.test.filters.SmallTest @@ -29,6 +32,7 @@ import kotlin.test.Test import org.junit.Before import org.junit.runner.RunWith import org.mockito.kotlin.any +import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.times import org.mockito.kotlin.verify @@ -55,10 +59,17 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { private lateinit var desktopTilingWindowManager: DesktopTilingDividerWindowManager + private val context = mock<Context>() + private val display = mock<Display>() + private val roundedCorner = mock<RoundedCorner>() + @Before fun setup() { config = Configuration() config.setToDefaults() + whenever(context.display).thenReturn(display) + whenever(display.getRoundedCorner(any())).thenReturn(roundedCorner) + whenever(roundedCorner.radius).thenReturn(CORNER_RADIUS) desktopTilingWindowManager = DesktopTilingDividerWindowManager( config, @@ -69,6 +80,7 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { transitionHandlerMock, transactionSupplierMock, BOUNDS, + context, ) } @@ -85,7 +97,6 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { // Ensure a surfaceControl transaction runs to show the divider. verify(transactionSupplierMock, times(1)).get() - verify(syncQueueMock, times(1)).runInSync(any()) desktopTilingWindowManager.release() verify(transaction, times(1)).hide(any()) @@ -93,7 +104,24 @@ class DesktopTilingDividerWindowManagerTest : ShellTestCase() { verify(transaction, times(1)).apply() } + @Test + @UiThreadTest + fun testWindowManager_accountsForRoundedCornerDimensions() { + whenever(transactionSupplierMock.get()).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setRelativeLayer(any(), any(), any())).thenReturn(transaction) + whenever(transaction.setPosition(any(), any(), any())).thenReturn(transaction) + whenever(transaction.show(any())).thenReturn(transaction) + + desktopTilingWindowManager.generateViewHost(surfaceControl) + + // Ensure a surfaceControl transaction runs to show the divider. + verify(transaction, times(1)) + .setPosition(any(), eq(BOUNDS.left.toFloat() - CORNER_RADIUS), any()) + } + companion object { private val BOUNDS = Rect(1, 2, 3, 4) + private val CORNER_RADIUS = 28 } } diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt index 0b04a211f717..f371f5223419 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/DesktopTilingWindowDecorationTest.kt @@ -49,6 +49,7 @@ import org.mockito.ArgumentCaptor import org.mockito.ArgumentMatchers.eq import org.mockito.Captor import org.mockito.kotlin.any +import org.mockito.kotlin.anyOrNull import org.mockito.kotlin.capture import org.mockito.kotlin.mock import org.mockito.kotlin.never @@ -112,6 +113,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { returnToDragStartAnimator, desktopRepository, ) + whenever(context.createContextAsUser(any(), any())).thenReturn(context) } @Test @@ -195,7 +197,7 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { verify(toggleResizeDesktopTaskTransitionHandler, times(1)) .startTransition(capture(wctCaptor), any()) - verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), any()) + verify(returnToDragStartAnimator, times(1)).start(any(), any(), any(), any(), anyOrNull()) for (change in wctCaptor.value.changes) { val bounds = change.value.configuration.windowConfiguration.bounds val leftBounds = getLeftTaskBounds() @@ -473,12 +475,13 @@ class DesktopTilingWindowDecorationTest : ShellTestCase() { tilingDecoration.rightTaskResizingHelper = tiledTaskHelper tilingDecoration.desktopTilingDividerWindowManager = desktopTilingDividerWindowManager - tilingDecoration.onUserChange() + tilingDecoration.resetTilingSession() assertThat(tilingDecoration.leftTaskResizingHelper).isNull() assertThat(tilingDecoration.rightTaskResizingHelper).isNull() verify(desktopWindowDecoration, times(2)).removeDragResizeListener(any()) verify(tiledTaskHelper, times(2)).dispose() + verify(context, never()).getApplicationContext() } private fun initTiledTaskHelperMock(taskInfo: ActivityManager.RunningTaskInfo) { diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt index fd5eb8855d32..c96ce955f217 100644 --- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt +++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/tiling/TilingDividerViewTest.kt @@ -32,8 +32,10 @@ import org.junit.Test import org.junit.runner.RunWith import org.mockito.kotlin.any import org.mockito.kotlin.mock +import org.mockito.kotlin.never import org.mockito.kotlin.times import org.mockito.kotlin.verify +import org.mockito.kotlin.whenever @SmallTest @RunWith(AndroidTestingRunner::class) @@ -68,6 +70,7 @@ class TilingDividerViewTest : ShellTestCase() { tilingDividerView.handleMotionEvent(viewMock, downMotionEvent) verify(dividerMoveCallbackMock, times(1)).onDividerMoveStart(any()) + whenever(dividerMoveCallbackMock.onDividerMove(any())).thenReturn(true) val motionEvent = getMotionEvent(downTime, MotionEvent.ACTION_MOVE, x.toFloat(), y.toFloat()) tilingDividerView.handleMotionEvent(viewMock, motionEvent) @@ -79,6 +82,24 @@ class TilingDividerViewTest : ShellTestCase() { verify(dividerMoveCallbackMock, times(1)).onDividerMovedEnd(any()) } + @Test + @UiThreadTest + fun testCallbackOnTouch_doesNotHappen_whenNoTouchMove() { + val x = 5 + val y = 5 + val downTime: Long = SystemClock.uptimeMillis() + + val downMotionEvent = + getMotionEvent(downTime, MotionEvent.ACTION_DOWN, x.toFloat(), y.toFloat()) + tilingDividerView.handleMotionEvent(viewMock, downMotionEvent) + verify(dividerMoveCallbackMock, times(1)).onDividerMoveStart(any()) + + val upMotionEvent = + getMotionEvent(downTime, MotionEvent.ACTION_UP, x.toFloat(), y.toFloat()) + tilingDividerView.handleMotionEvent(viewMock, upMotionEvent) + verify(dividerMoveCallbackMock, never()).onDividerMovedEnd(any()) + } + private fun getMotionEvent(eventTime: Long, action: Int, x: Float, y: Float): MotionEvent { val properties = MotionEvent.PointerProperties() properties.id = 0 diff --git a/libs/androidfw/PngCrunch.cpp b/libs/androidfw/PngCrunch.cpp index cf3c0eeff402..e94540544cf5 100644 --- a/libs/androidfw/PngCrunch.cpp +++ b/libs/androidfw/PngCrunch.cpp @@ -506,8 +506,7 @@ bool WritePng(const Image* image, const NinePatch* nine_patch, OutputStream* out // Set up the write functions which write to our custom data sources. png_set_write_fn(write_ptr, (png_voidp)out, WriteDataToStream, nullptr); - // We want small files and can take the performance hit to achieve this goal. - png_set_compression_level(write_ptr, Z_BEST_COMPRESSION); + png_set_compression_level(write_ptr, options.compression_level); // Begin analysis of the image data. // Scan the entire image and determine if: diff --git a/libs/androidfw/include/androidfw/Png.h b/libs/androidfw/include/androidfw/Png.h index 2ece43e08110..72be59b8f313 100644 --- a/libs/androidfw/include/androidfw/Png.h +++ b/libs/androidfw/include/androidfw/Png.h @@ -31,6 +31,8 @@ constexpr size_t kPngSignatureSize = 8u; struct PngOptions { int grayscale_tolerance = 0; + // By default we want small files and can take the performance hit to achieve this goal. + int compression_level = 9; }; /** diff --git a/libs/appfunctions/api/current.txt b/libs/appfunctions/api/current.txt index 27817e9eb984..faf84a8ab5ac 100644 --- a/libs/appfunctions/api/current.txt +++ b/libs/appfunctions/api/current.txt @@ -3,8 +3,9 @@ package com.google.android.appfunctions.sidecar { public final class AppFunctionManager { ctor public AppFunctionManager(android.content.Context); - method public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); - method public void isAppFunctionEnabled(@NonNull String, @NonNull String, @NonNull java.util.concurrent.Executor, @NonNull android.os.OutcomeReceiver<java.lang.Boolean,java.lang.Exception>); + method @RequiresPermission(anyOf={android.Manifest.permission.EXECUTE_APP_FUNCTIONS_TRUSTED, android.Manifest.permission.EXECUTE_APP_FUNCTIONS}, conditional=true) public void executeAppFunction(@NonNull com.google.android.appfunctions.sidecar.ExecuteAppFunctionRequest, @NonNull java.util.concurrent.Executor, @NonNull android.os.CancellationSignal, @NonNull java.util.function.Consumer<com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse>); + 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 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 field public static final int APP_FUNCTION_STATE_DISABLED = 2; // 0x2 @@ -34,6 +35,7 @@ package com.google.android.appfunctions.sidecar { } public final class ExecuteAppFunctionResponse { + method public int getErrorCategory(); method @Nullable public String getErrorMessage(); method @NonNull public android.os.Bundle getExtras(); method public int getResultCode(); @@ -41,14 +43,19 @@ package com.google.android.appfunctions.sidecar { method public boolean isSuccess(); method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newFailure(int, @Nullable String, @Nullable android.os.Bundle); method @NonNull public static com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse newSuccess(@NonNull android.app.appsearch.GenericDocument, @Nullable android.os.Bundle); + field public static final int ERROR_CATEGORY_APP = 3; // 0x3 + field public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; // 0x1 + field public static final int ERROR_CATEGORY_SYSTEM = 2; // 0x2 + field public static final int ERROR_CATEGORY_UNKNOWN = 0; // 0x0 field public static final String PROPERTY_RETURN_VALUE = "returnValue"; - field public static final int RESULT_APP_UNKNOWN_ERROR = 2; // 0x2 - field public static final int RESULT_CANCELLED = 6; // 0x6 - field public static final int RESULT_DENIED = 1; // 0x1 - field public static final int RESULT_DISABLED = 5; // 0x5 - field public static final int RESULT_INTERNAL_ERROR = 3; // 0x3 - field public static final int RESULT_INVALID_ARGUMENT = 4; // 0x4 + field public static final int RESULT_APP_UNKNOWN_ERROR = 3000; // 0xbb8 + field public static final int RESULT_CANCELLED = 2001; // 0x7d1 + field public static final int RESULT_DENIED = 1000; // 0x3e8 + field public static final int RESULT_DISABLED = 1002; // 0x3ea + field public static final int RESULT_FUNCTION_NOT_FOUND = 1003; // 0x3eb + field public static final int RESULT_INVALID_ARGUMENT = 1001; // 0x3e9 field public static final int RESULT_OK = 0; // 0x0 + field public static final int RESULT_SYSTEM_ERROR = 2000; // 0x7d0 } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java index 43377d8eb91c..2075104ff868 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/AppFunctionManager.java @@ -20,6 +20,7 @@ import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.IntDef; import android.annotation.NonNull; +import android.annotation.RequiresPermission; import android.annotation.SuppressLint; import android.annotation.UserHandleAware; import android.content.Context; @@ -41,8 +42,6 @@ import java.util.function.Consumer; * <p>This class wraps {@link android.app.appfunctions.AppFunctionManager} functionalities and * exposes it here as a sidecar library (avoiding direct dependency on the platform API). */ -// TODO(b/357551503): Implement get and set enabled app function APIs. -// TODO(b/367329899): Add sidecar library to Android B builds. public final class AppFunctionManager { /** * The default state of the app function. Call {@link #setAppFunctionEnabled} with this to reset @@ -70,9 +69,9 @@ public final class AppFunctionManager { @IntDef( prefix = {"APP_FUNCTION_STATE_"}, value = { - APP_FUNCTION_STATE_DEFAULT, - APP_FUNCTION_STATE_ENABLED, - APP_FUNCTION_STATE_DISABLED + APP_FUNCTION_STATE_DEFAULT, + APP_FUNCTION_STATE_ENABLED, + APP_FUNCTION_STATE_DISABLED }) @Retention(RetentionPolicy.SOURCE) public @interface EnabledState {} @@ -102,7 +101,16 @@ public final class AppFunctionManager { * <p>Proxies request and response to the underlying {@link * android.app.appfunctions.AppFunctionManager#executeAppFunction}, converting the request and * response in the appropriate type required by the function. + * + * <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) public void executeAppFunction( @NonNull ExecuteAppFunctionRequest sidecarRequest, @NonNull @CallbackExecutor Executor executor, @@ -128,25 +136,15 @@ public final class AppFunctionManager { /** * Returns a boolean through a callback, indicating whether the app function is enabled. * - * <p>* This method can only check app functions owned by the caller, or those where the caller - * has visibility to the owner package and holds either the {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS} or {@link - * Manifest.permission#EXECUTE_APP_FUNCTIONS_TRUSTED} permission. - * - * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: - * - * <ul> - * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not - * have access to it. - * </ul> - * - * @param functionIdentifier the identifier of the app function to check (unique within the - * target package) and in most cases, these are automatically generated by the AppFunctions - * SDK - * @param targetPackage the package name of the app function's owner - * @param executor the executor to run the request - * @param callback the callback to receive the function enabled check result + * <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) public void isAppFunctionEnabled( @NonNull String functionIdentifier, @NonNull String targetPackage, @@ -156,22 +154,23 @@ public final class AppFunctionManager { } /** - * Sets the enabled state of the app function owned by the calling package. - * - * <p>If operation fails, the callback's {@link OutcomeReceiver#onError} is called with errors: + * Returns a boolean through a callback, indicating whether the app function is enabled. * - * <ul> - * <li>{@link IllegalArgumentException}, if the function is not found or the caller does not - * have access to it. - * </ul> + * <p>See {@link android.app.appfunctions.AppFunctionManager#isAppFunctionEnabled} for the + * documented behaviour of this method. + */ + public void isAppFunctionEnabled( + @NonNull String functionIdentifier, + @NonNull Executor executor, + @NonNull OutcomeReceiver<Boolean, Exception> callback) { + mManager.isAppFunctionEnabled(functionIdentifier, executor, callback); + } + + /** + * Sets the enabled state of the app function owned by the calling package. * - * @param functionIdentifier the identifier of the app function to enable (unique within the - * calling package). In most cases, identifiers are automatically generated by the - * AppFunctions SDK - * @param newEnabledState the new state of the app function - * @param executor the executor to run the callback - * @param callback the callback to receive the result of the function enablement. The call was - * successful if no exception was thrown. + * <p>See {@link android.app.appfunctions.AppFunctionManager#setAppFunctionEnabled} for the + * documented behavoir of this method. */ // Constants in @EnabledState should always mirror those in // android.app.appfunctions.AppFunctionManager. diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java index fa6d2ff12313..593c5213dd52 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionRequest.java @@ -34,8 +34,8 @@ public final class ExecuteAppFunctionRequest { @NonNull private final String mTargetPackageName; /** - * Returns the unique string identifier of the app function to be executed. TODO(b/357551503): - * Document how callers can get the available function identifiers. + * The unique string identifier of the app function to be executed. This identifier is used to + * execute a specific app function. */ @NonNull private final String mFunctionIdentifier; @@ -49,8 +49,6 @@ public final class ExecuteAppFunctionRequest { * * <p>The document may have missing parameters. Developers are advised to implement defensive * handling measures. - * - * <p>TODO(b/357551503): Document how function parameters can be obtained for function execution */ @NonNull private final GenericDocument mParameters; @@ -71,7 +69,19 @@ public final class ExecuteAppFunctionRequest { return mTargetPackageName; } - /** Returns the unique string identifier of the app function to be executed. */ + /** + * Returns the unique string identifier of the app function to be executed. + * + * <p>When there is a package change or the device starts up, the metadata of available + * functions is indexed by AppSearch. AppSearch stores the indexed information as {@code + * AppFunctionStaticMetadata} document. + * + * <p>The ID can be obtained by querying the {@code AppFunctionStaticMetadata} documents from + * AppSearch. + * + * <p>If the {@code functionId} provided is invalid, the caller will get an invalid argument + * response. + */ @NonNull public String getFunctionIdentifier() { return mFunctionIdentifier; @@ -83,6 +93,12 @@ public final class ExecuteAppFunctionRequest { * * <p>The bundle may have missing parameters. Developers are advised to implement defensive * handling measures. + * + * <p>Similar to {@link #getFunctionIdentifier()} the parameters required by a function can be + * obtained by querying AppSearch for the corresponding {@code AppFunctionStaticMetadata}. This + * metadata will contain enough information for the caller to resolve the required parameters + * either using information from the metadata itself or using the AppFunction SDK for function + * callers. */ @NonNull public GenericDocument getParameters() { @@ -128,10 +144,7 @@ public final class ExecuteAppFunctionRequest { @NonNull public ExecuteAppFunctionRequest build() { return new ExecuteAppFunctionRequest( - mTargetPackageName, - mFunctionIdentifier, - mExtras, - mParameters); + mTargetPackageName, mFunctionIdentifier, mExtras, mParameters); } } } diff --git a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java index 969e5d58b909..4e88fb025a9d 100644 --- a/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java +++ b/libs/appfunctions/java/com/google/android/appfunctions/sidecar/ExecuteAppFunctionResponse.java @@ -50,37 +50,102 @@ public final class ExecuteAppFunctionResponse { */ public static final String PROPERTY_RETURN_VALUE = "returnValue"; - /** The call was successful. */ + /** + * The call was successful. + * + * <p>This result code does not belong in an error category. + */ public static final int RESULT_OK = 0; - /** The caller does not have the permission to execute an app function. */ - public static final int RESULT_DENIED = 1; + /** + * The caller does not have the permission to execute an app function. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_DENIED = 1000; - /** An unknown error occurred while processing the call in the AppFunctionService. */ - public static final int RESULT_APP_UNKNOWN_ERROR = 2; + /** + * The caller supplied invalid arguments to the execution request. + * + * <p>This error may be considered similar to {@link IllegalArgumentException}. + * + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. + */ + public static final int RESULT_INVALID_ARGUMENT = 1001; /** - * An internal error occurred within AppFunctionManagerService. + * The caller tried to execute a disabled app function. * - * <p>This error may be considered similar to {@link IllegalStateException} + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. */ - public static final int RESULT_INTERNAL_ERROR = 3; + public static final int RESULT_DISABLED = 1002; /** - * The caller supplied invalid arguments to the call. + * The caller tried to execute a function that does not exist. * - * <p>This error may be considered similar to {@link IllegalArgumentException}. + * <p>This error is in the {@link #ERROR_CATEGORY_REQUEST_ERROR} category. */ - public static final int RESULT_INVALID_ARGUMENT = 4; + public static final int RESULT_FUNCTION_NOT_FOUND = 1003; - /** The caller tried to execute a disabled app function. */ - public static final int RESULT_DISABLED = 5; + /** + * An internal unexpected error coming from the system. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_SYSTEM_ERROR = 2000; /** * The operation was cancelled. Use this error code to report that a cancellation is done after * receiving a cancellation signal. + * + * <p>This error is in the {@link #ERROR_CATEGORY_SYSTEM} category. + */ + public static final int RESULT_CANCELLED = 2001; + + /** + * An unknown error occurred while processing the call in the AppFunctionService. + * + * <p>This error is thrown when the service is connected in the remote application but an + * unexpected error is thrown from the bound application. + * + * <p>This error is in the {@link #ERROR_CATEGORY_APP} category. + */ + public static final int RESULT_APP_UNKNOWN_ERROR = 3000; + + /** + * The error category is unknown. + * + * <p>This is the default value for {@link #getErrorCategory}. + */ + public static final int ERROR_CATEGORY_UNKNOWN = 0; + + /** + * The error is caused by the app requesting a function execution. + * + * <p>For example, the caller provided invalid parameters in the execution request e.g. an + * invalid function ID. + * + * <p>Errors in the category fall in the range 1000-1999 inclusive. */ - public static final int RESULT_CANCELLED = 6; + public static final int ERROR_CATEGORY_REQUEST_ERROR = 1; + + /** + * The error is caused by an issue in the system. + * + * <p>For example, the AppFunctionService implementation is not found by the system. + * + * <p>Errors in the category fall in the range 2000-2999 inclusive. + */ + public static final int ERROR_CATEGORY_SYSTEM = 2; + + /** + * The error is caused by the app providing the function. + * + * <p>For example, the app crashed when the system is executing the request. + * + * <p>Errors in the category fall in the range 3000-3999 inclusive. + */ + public static final int ERROR_CATEGORY_APP = 3; /** The result code of the app function execution. */ @ResultCode private final int mResultCode; @@ -170,6 +235,36 @@ public final class ExecuteAppFunctionResponse { } /** + * Returns the error category of the {@link ExecuteAppFunctionResponse}. + * + * <p>This method categorizes errors based on their underlying cause, allowing developers to + * implement targeted error handling and provide more informative error messages to users. It + * maps ranges of result codes to specific error categories. + * + * <p>When constructing a {@link #newFailure} response, use the appropriate result code value to + * ensure correct categorization of the failed response. + * + * <p>This method returns {@code ERROR_CATEGORY_UNKNOWN} if the result code does not belong to + * any error category, for example, in the case of a successful result with {@link #RESULT_OK}. + * + * <p>See {@link ErrorCategory} for a complete list of error categories and their corresponding + * result code ranges. + */ + @ErrorCategory + public int getErrorCategory() { + if (mResultCode >= 1000 && mResultCode < 2000) { + return ERROR_CATEGORY_REQUEST_ERROR; + } + if (mResultCode >= 2000 && mResultCode < 3000) { + return ERROR_CATEGORY_SYSTEM; + } + if (mResultCode >= 3000 && mResultCode < 4000) { + return ERROR_CATEGORY_APP; + } + return ERROR_CATEGORY_UNKNOWN; + } + + /** * Returns a generic document containing the return value of the executed function. * * <p>The {@link #PROPERTY_RETURN_VALUE} key can be used to obtain the return value. @@ -237,11 +332,28 @@ public final class ExecuteAppFunctionResponse { RESULT_OK, RESULT_DENIED, RESULT_APP_UNKNOWN_ERROR, - RESULT_INTERNAL_ERROR, + RESULT_SYSTEM_ERROR, + RESULT_FUNCTION_NOT_FOUND, RESULT_INVALID_ARGUMENT, RESULT_DISABLED, RESULT_CANCELLED }) @Retention(RetentionPolicy.SOURCE) public @interface ResultCode {} + + /** + * Error categories. + * + * @hide + */ + @IntDef( + prefix = {"ERROR_CATEGORY_"}, + value = { + ERROR_CATEGORY_UNKNOWN, + ERROR_CATEGORY_REQUEST_ERROR, + ERROR_CATEGORY_APP, + ERROR_CATEGORY_SYSTEM + }) + @Retention(RetentionPolicy.SOURCE) + public @interface ErrorCategory {} } diff --git a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt index 1f9fddd3c1ec..264f84209caf 100644 --- a/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt +++ b/libs/appfunctions/tests/src/com/google/android/appfunctions/sidecar/tests/SidecarConverterTest.kt @@ -105,7 +105,7 @@ class SidecarConverterTest { val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() val platformResponse = ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, null, null ) @@ -119,7 +119,7 @@ class SidecarConverterTest { assertThat(sidecarResponse.resultDocument.id).isEqualTo(emptyGd.id) assertThat(sidecarResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) assertThat(sidecarResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR) + .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) assertThat(sidecarResponse.errorMessage).isNull() } @@ -152,7 +152,7 @@ class SidecarConverterTest { val emptyGd = GenericDocument.Builder<GenericDocument.Builder<*>>("", "", "").build() val sidecarResponse = com.google.android.appfunctions.sidecar.ExecuteAppFunctionResponse.newFailure( - ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR, + ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR, null, null ) @@ -166,7 +166,7 @@ class SidecarConverterTest { assertThat(platformResponse.resultDocument.id).isEqualTo(emptyGd.id) assertThat(platformResponse.resultDocument.schemaType).isEqualTo(emptyGd.schemaType) assertThat(platformResponse.resultCode) - .isEqualTo(ExecuteAppFunctionResponse.RESULT_INTERNAL_ERROR) + .isEqualTo(ExecuteAppFunctionResponse.RESULT_SYSTEM_ERROR) assertThat(platformResponse.errorMessage).isNull() } } diff --git a/libs/hwui/Android.bp b/libs/hwui/Android.bp index 266c23631800..fcb7efc35c94 100644 --- a/libs/hwui/Android.bp +++ b/libs/hwui/Android.bp @@ -384,6 +384,7 @@ cc_defaults { "jni/ScopedParcel.cpp", "jni/Shader.cpp", "jni/RenderEffect.cpp", + "jni/RuntimeEffectUtils.cpp", "jni/Typeface.cpp", "jni/Utils.cpp", "jni/YuvToJpegEncoder.cpp", @@ -579,6 +580,7 @@ cc_defaults { "utils/Color.cpp", "utils/LinearAllocator.cpp", "utils/StringUtils.cpp", + "utils/StatsUtils.cpp", "utils/TypefaceUtils.cpp", "utils/VectorDrawableUtils.cpp", "AnimationContext.cpp", diff --git a/libs/hwui/ColorFilter.h b/libs/hwui/ColorFilter.h index 3a3bfb47dfdd..f6b6be08997a 100644 --- a/libs/hwui/ColorFilter.h +++ b/libs/hwui/ColorFilter.h @@ -22,6 +22,7 @@ #include <memory> #include "GraphicsJNI.h" +#include "RuntimeEffectUtils.h" #include "SkColorFilter.h" namespace android { @@ -113,6 +114,36 @@ private: std::vector<float> mMatrix; }; +class RuntimeColorFilter : public ColorFilter { +public: + RuntimeColorFilter(SkRuntimeEffectBuilder* builder) : mBuilder(builder) {} + + void updateUniforms(JNIEnv* env, const char* name, const float vals[], int count, + bool isColor) { + UpdateFloatUniforms(env, mBuilder, name, vals, count, isColor); + discardInstance(); + } + + void updateUniforms(JNIEnv* env, const char* name, const int vals[], int count) { + UpdateIntUniforms(env, mBuilder, name, vals, count); + discardInstance(); + } + + void updateChild(JNIEnv* env, const char* name, SkFlattenable* childEffect) { + UpdateChild(env, mBuilder, name, childEffect); + discardInstance(); + } + +private: + sk_sp<SkColorFilter> createInstance() override { + // TODO: throw error if null + return mBuilder->makeColorFilter(); + } + +private: + SkRuntimeEffectBuilder* mBuilder; +}; + } // namespace uirenderer } // namespace android diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig index 93df47853769..5ad788c67816 100644 --- a/libs/hwui/aconfig/hwui_flags.aconfig +++ b/libs/hwui/aconfig/hwui_flags.aconfig @@ -139,3 +139,18 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "bitmap_ashmem_long_name" + namespace: "core_graphics" + description: "Whether to have more information in ashmem filenames for bitmaps" + bug: "369619160" +} + +flag { + name: "animated_image_drawable_filter_bitmap" + is_exported: true + namespace: "core_graphics" + description: "API's that enable animated image drawables to use nearest sampling when scaling." + bug: "370523334" +} diff --git a/libs/hwui/hwui/AnimatedImageDrawable.cpp b/libs/hwui/hwui/AnimatedImageDrawable.cpp index 69613c7d17cb..5e379aad9326 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.cpp +++ b/libs/hwui/hwui/AnimatedImageDrawable.cpp @@ -347,4 +347,26 @@ int AnimatedImageDrawable::currentFrameDuration() { return adjustFrameDuration(mSkAnimatedImage->currentFrameDuration()); } +bool AnimatedImageDrawable::getFilterBitmap() const { + const SkFilterMode kFilterBitmap = mSkAnimatedImage->getFilterMode(); + if (kFilterBitmap == SkFilterMode::kLinear) { + return true; + } + return false; +} + +bool AnimatedImageDrawable::setFilterBitmap(bool filterBitmap) { + if (filterBitmap) { + if (mSkAnimatedImage->getFilterMode() == SkFilterMode::kLinear) { + return false; + } + mSkAnimatedImage->setFilterMode(SkFilterMode::kLinear); + } else { + if (mSkAnimatedImage->getFilterMode() == SkFilterMode::kNearest) { + return false; + } + mSkAnimatedImage->setFilterMode(SkFilterMode::kNearest); + } + return true; +} } // namespace android diff --git a/libs/hwui/hwui/AnimatedImageDrawable.h b/libs/hwui/hwui/AnimatedImageDrawable.h index 1e965abc82b5..22123249b7d6 100644 --- a/libs/hwui/hwui/AnimatedImageDrawable.h +++ b/libs/hwui/hwui/AnimatedImageDrawable.h @@ -87,6 +87,11 @@ public: bool isRunning(); int getRepetitionCount() const { return mSkAnimatedImage->getRepetitionCount(); } void setRepetitionCount(int count) { mSkAnimatedImage->setRepetitionCount(count); } + // Returns true if the filter mode is set to linear sampling; false if it is + // set to nearest neighbor sampling. + bool getFilterBitmap() const; + // Returns true if the filter mode was changed; false otherwise. + bool setFilterBitmap(bool filterBitmap); void setOnAnimationEndListener(std::unique_ptr<OnAnimationEndListener> listener) { mEndListener = std::move(listener); diff --git a/libs/hwui/hwui/Bitmap.cpp b/libs/hwui/hwui/Bitmap.cpp index b73380e38fd1..cc292d9de7b2 100644 --- a/libs/hwui/hwui/Bitmap.cpp +++ b/libs/hwui/hwui/Bitmap.cpp @@ -15,6 +15,7 @@ */ #include "Bitmap.h" +#include <android-base/file.h> #include "HardwareBitmapUploader.h" #include "Properties.h" #ifdef __ANDROID__ // Layoutlib does not support render thread @@ -46,16 +47,27 @@ #include <SkImage.h> #include <SkImageAndroid.h> #include <SkImagePriv.h> +#include <SkJpegEncoder.h> #include <SkJpegGainmapEncoder.h> #include <SkPixmap.h> +#include <SkPngEncoder.h> #include <SkRect.h> #include <SkStream.h> -#include <SkJpegEncoder.h> -#include <SkPngEncoder.h> #include <SkWebpEncoder.h> +#include <atomic> +#include <format> #include <limits> +#ifdef __ANDROID__ +#include <com_android_graphics_hwui_flags.h> +namespace hwui_flags = com::android::graphics::hwui::flags; +#else +namespace hwui_flags { +constexpr bool bitmap_ashmem_long_name() { return false; } +} +#endif + namespace android { #ifdef __ANDROID__ @@ -86,6 +98,28 @@ static uint64_t AHardwareBuffer_getAllocationSize(AHardwareBuffer* aHardwareBuff } #endif +// generate an ID for this Bitmap, id is a 64-bit integer of 3 parts: +// 0000xxxxxx - the lower 6 decimal digits is a monotonically increasing number +// 000x000000 - the 7th decimal digit is the storage type (see PixelStorageType) +// xxx0000000 - the 8th decimal digit and above is the current pid +// +// e.g. 43231000076 - means this bitmap is the 76th bitmap created, has the +// storage type of 'Heap', and is created in a process with pid 4323. +// +// NOTE: +// 1) the monotonic number could increase beyond 1000,000 and wrap around, which +// only happens when more than 1,000,000 bitmaps have been created over time. +// This could result in two IDs being the same despite being really rare. +// 2) the IDs are intentionally represented in decimal to make it easier to +// reason and associate with numbers shown in heap dump (mostly in decimal) +// and PIDs shown in different tools (mostly in decimal as well). +uint64_t Bitmap::getId(PixelStorageType type) { + static std::atomic<uint64_t> idCounter{0}; + return (idCounter.fetch_add(1) % 1000000) + + static_cast<uint64_t>(type) * 1000000 + + static_cast<uint64_t>(getpid()) * 10000000; +} + bool Bitmap::computeAllocationSize(size_t rowBytes, int height, size_t* size) { return 0 <= height && height <= std::numeric_limits<size_t>::max() && !__builtin_mul_overflow(rowBytes, (size_t)height, size) && @@ -117,6 +151,20 @@ static sk_sp<Bitmap> allocateBitmap(SkBitmap* bitmap, AllocPixelRef alloc) { return wrapper; } +std::string Bitmap::getAshmemId(const char* tag, uint64_t bitmapId, + int width, int height, size_t size) { + if (!hwui_flags::bitmap_ashmem_long_name()) { + return "bitmap"; + } + static std::string sCmdline = [] { + std::string temp; + android::base::ReadFileToString("/proc/self/cmdline", &temp); + return temp; + }(); + return std::format("bitmap/{}-id_{}-{}x{}-size_{}-{}", + tag, bitmapId, width, height, size, sCmdline); +} + sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap) { return allocateBitmap(bitmap, &Bitmap::allocateAshmemBitmap); } @@ -124,7 +172,9 @@ sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(SkBitmap* bitmap) { sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info, size_t rowBytes) { #ifdef __ANDROID__ // Create new ashmem region with read/write priv - int fd = ashmem_create_region("bitmap", size); + uint64_t id = getId(PixelStorageType::Ashmem); + auto ashmemId = getAshmemId("allocate", id, info.width(), info.height(), size); + int fd = ashmem_create_region(ashmemId.c_str(), size); if (fd < 0) { return nullptr; } @@ -140,7 +190,7 @@ sk_sp<Bitmap> Bitmap::allocateAshmemBitmap(size_t size, const SkImageInfo& info, close(fd); return nullptr; } - return sk_sp<Bitmap>(new Bitmap(addr, fd, size, info, rowBytes)); + return sk_sp<Bitmap>(new Bitmap(addr, fd, size, info, rowBytes, id)); #else return Bitmap::allocateHeapBitmap(size, info, rowBytes); #endif @@ -261,7 +311,8 @@ void Bitmap::reconfigure(const SkImageInfo& newInfo, size_t rowBytes) { Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBytes) : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::Heap) { + , mPixelStorageType(PixelStorageType::Heap) + , mId(getId(mPixelStorageType)) { mPixelStorage.heap.address = address; mPixelStorage.heap.size = size; traceBitmapCreate(); @@ -270,16 +321,19 @@ Bitmap::Bitmap(void* address, size_t size, const SkImageInfo& info, size_t rowBy Bitmap::Bitmap(SkPixelRef& pixelRef, const SkImageInfo& info) : SkPixelRef(info.width(), info.height(), pixelRef.pixels(), pixelRef.rowBytes()) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::WrappedPixelRef) { + , mPixelStorageType(PixelStorageType::WrappedPixelRef) + , mId(getId(mPixelStorageType)) { pixelRef.ref(); mPixelStorage.wrapped.pixelRef = &pixelRef; traceBitmapCreate(); } -Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes) +Bitmap::Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, + size_t rowBytes, uint64_t id) : SkPixelRef(info.width(), info.height(), address, rowBytes) , mInfo(validateAlpha(info)) - , mPixelStorageType(PixelStorageType::Ashmem) { + , mPixelStorageType(PixelStorageType::Ashmem) + , mId(id != INVALID_BITMAP_ID ? id : getId(mPixelStorageType)) { mPixelStorage.ashmem.address = address; mPixelStorage.ashmem.fd = fd; mPixelStorage.ashmem.size = mappedSize; @@ -293,7 +347,8 @@ Bitmap::Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes , mInfo(validateAlpha(info)) , mPixelStorageType(PixelStorageType::Hardware) , mPalette(palette) - , mPaletteGenerationId(getGenerationID()) { + , mPaletteGenerationId(getGenerationID()) + , mId(getId(mPixelStorageType)) { mPixelStorage.hardware.buffer = buffer; mPixelStorage.hardware.size = AHardwareBuffer_getAllocationSize(buffer); AHardwareBuffer_acquire(buffer); @@ -578,6 +633,7 @@ void Bitmap::setGainmap(sp<uirenderer::Gainmap>&& gainmap) { } std::mutex Bitmap::mLock{}; + size_t Bitmap::mTotalBitmapBytes = 0; size_t Bitmap::mTotalBitmapCount = 0; diff --git a/libs/hwui/hwui/Bitmap.h b/libs/hwui/hwui/Bitmap.h index 3d55d859ed5f..8abe6a8c445a 100644 --- a/libs/hwui/hwui/Bitmap.h +++ b/libs/hwui/hwui/Bitmap.h @@ -37,10 +37,10 @@ class SkWStream; namespace android { enum class PixelStorageType { - WrappedPixelRef, - Heap, - Ashmem, - Hardware, + WrappedPixelRef = 0, + Heap = 1, + Ashmem = 2, + Hardware = 3, }; // TODO: Find a better home for this. It's here because hwui/Bitmap is exported and CanvasTransform @@ -79,6 +79,9 @@ public: static sk_sp<Bitmap> allocateHeapBitmap(const SkImageInfo& info); static sk_sp<Bitmap> allocateHeapBitmap(size_t size, const SkImageInfo& i, size_t rowBytes); + static std::string getAshmemId(const char* tag, uint64_t bitmapId, + int width, int height, size_t size); + /* The createFrom factories construct a new Bitmap object by wrapping the already allocated * memory that is provided as an input param. */ @@ -104,6 +107,10 @@ public: void setColorSpace(sk_sp<SkColorSpace> colorSpace); void setAlphaType(SkAlphaType alphaType); + uint64_t getId() const { + return mId; + } + void getSkBitmap(SkBitmap* outBitmap); SkBitmap getSkBitmap() { @@ -177,11 +184,14 @@ public: static bool compress(const SkBitmap& bitmap, JavaCompressFormat format, int32_t quality, SkWStream* stream); private: + static constexpr uint64_t INVALID_BITMAP_ID = 0u; + 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); + Bitmap(void* address, int fd, size_t mappedSize, const SkImageInfo& info, size_t rowBytes, + uint64_t id = INVALID_BITMAP_ID); #ifdef __ANDROID__ // Layoutlib does not support hardware acceleration Bitmap(AHardwareBuffer* buffer, const SkImageInfo& info, size_t rowBytes, BitmapPalette palette); @@ -229,6 +239,9 @@ private: sk_sp<SkImage> mImage; // Cache is used only for HW Bitmaps with Skia pipeline. + uint64_t mId; // unique ID for this bitmap + static uint64_t getId(PixelStorageType type); + // for tracing total number and memory usage of bitmaps static std::mutex mLock; static size_t mTotalBitmapBytes; diff --git a/libs/hwui/hwui/ImageDecoder.cpp b/libs/hwui/hwui/ImageDecoder.cpp index e074a27db38f..a9a5db8181ba 100644 --- a/libs/hwui/hwui/ImageDecoder.cpp +++ b/libs/hwui/hwui/ImageDecoder.cpp @@ -27,8 +27,8 @@ #include <SkColorSpace.h> #include <SkColorType.h> #include <SkEncodedOrigin.h> -#include <SkImageInfo.h> #include <SkGainmapInfo.h> +#include <SkImageInfo.h> #include <SkMatrix.h> #include <SkPaint.h> #include <SkPngChunkReader.h> @@ -43,6 +43,8 @@ #include <memory> +#include "modules/skcms/src/skcms_public.h" + using namespace android; sk_sp<SkColorSpace> ImageDecoder::getDefaultColorSpace() const { diff --git a/libs/hwui/jni/AnimatedImageDrawable.cpp b/libs/hwui/jni/AnimatedImageDrawable.cpp index b01e38d014a9..2c8530d4daeb 100644 --- a/libs/hwui/jni/AnimatedImageDrawable.cpp +++ b/libs/hwui/jni/AnimatedImageDrawable.cpp @@ -276,6 +276,18 @@ static void AnimatedImageDrawable_nSetBounds(JNIEnv* env, jobject /*clazz*/, jlo drawable->setStagingBounds(rect); } +static jboolean AnimatedImageDrawable_nSetFilterBitmap(JNIEnv* env, jobject /*clazz*/, + jlong nativePtr, jboolean filterBitmap) { + auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); + return drawable->setFilterBitmap(filterBitmap); +} + +static jboolean AnimatedImageDrawable_nGetFilterBitmap(JNIEnv* env, jobject /*clazz*/, + jlong nativePtr) { + auto* drawable = reinterpret_cast<AnimatedImageDrawable*>(nativePtr); + return drawable->getFilterBitmap(); +} + static const JNINativeMethod gAnimatedImageDrawableMethods[] = { {"nCreate", "(JLandroid/graphics/ImageDecoder;IIJZLandroid/graphics/Rect;)J", (void*)AnimatedImageDrawable_nCreate}, @@ -294,6 +306,8 @@ static const JNINativeMethod gAnimatedImageDrawableMethods[] = { {"nNativeByteSize", "(J)J", (void*)AnimatedImageDrawable_nNativeByteSize}, {"nSetMirrored", "(JZ)V", (void*)AnimatedImageDrawable_nSetMirrored}, {"nSetBounds", "(JLandroid/graphics/Rect;)V", (void*)AnimatedImageDrawable_nSetBounds}, + {"nSetFilterBitmap", "(JZ)Z", (void*)AnimatedImageDrawable_nSetFilterBitmap}, + {"nGetFilterBitmap", "(J)Z", (void*)AnimatedImageDrawable_nGetFilterBitmap}, }; int register_android_graphics_drawable_AnimatedImageDrawable(JNIEnv* env) { diff --git a/libs/hwui/jni/Bitmap.cpp b/libs/hwui/jni/Bitmap.cpp index 010c4e8dfb3a..29efd98b41d0 100644 --- a/libs/hwui/jni/Bitmap.cpp +++ b/libs/hwui/jni/Bitmap.cpp @@ -196,7 +196,7 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, int density) { static jmethodID gBitmap_constructorMethodID = GetMethodIDOrDie(env, gBitmap_class, - "<init>", "(JIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); + "<init>", "(JJIIIZ[BLandroid/graphics/NinePatch$InsetStruct;Z)V"); bool isMutable = bitmapCreateFlags & kBitmapCreateFlag_Mutable; bool isPremultiplied = bitmapCreateFlags & kBitmapCreateFlag_Premultiplied; @@ -209,7 +209,8 @@ jobject createBitmap(JNIEnv* env, Bitmap* bitmap, bitmapWrapper->bitmap().setImmutable(); } jobject obj = env->NewObject(gBitmap_class, gBitmap_constructorMethodID, - reinterpret_cast<jlong>(bitmapWrapper), bitmap->width(), bitmap->height(), density, + static_cast<jlong>(bitmap->getId()), reinterpret_cast<jlong>(bitmapWrapper), + bitmap->width(), bitmap->height(), density, isPremultiplied, ninePatchChunk, ninePatchInsets, fromMalloc); if (env->ExceptionCheck() != 0) { @@ -668,14 +669,20 @@ static binder_status_t writeBlobFromFd(AParcel* parcel, int32_t size, int fd) { return STATUS_OK; } -static binder_status_t writeBlob(AParcel* parcel, const int32_t size, const void* data, bool immutable) { +static binder_status_t writeBlob(AParcel* parcel, uint64_t bitmapId, const SkBitmap& bitmap) { + const size_t size = bitmap.computeByteSize(); + const void* data = bitmap.getPixels(); + const bool immutable = bitmap.isImmutable(); + if (size <= 0 || data == nullptr) { return STATUS_NOT_ENOUGH_DATA; } binder_status_t error = STATUS_OK; if (shouldUseAshmem(parcel, size)) { // Create new ashmem region with read/write priv - base::unique_fd fd(ashmem_create_region("bitmap", size)); + auto ashmemId = Bitmap::getAshmemId("writeblob", bitmapId, + bitmap.width(), bitmap.height(), size); + base::unique_fd fd(ashmem_create_region(ashmemId.c_str(), size)); if (fd.get() < 0) { return STATUS_NO_MEMORY; } @@ -883,8 +890,7 @@ static jboolean Bitmap_writeToParcel(JNIEnv* env, jobject, p.allowFds() ? "allowed" : "forbidden"); #endif - size_t size = bitmap.computeByteSize(); - status = writeBlob(p.get(), size, bitmap.getPixels(), bitmap.isImmutable()); + status = writeBlob(p.get(), bitmapWrapper->bitmap().getId(), bitmap); if (status) { doThrowRE(env, "Could not copy bitmap to parcel blob."); return JNI_FALSE; diff --git a/libs/hwui/jni/BitmapFactory.cpp b/libs/hwui/jni/BitmapFactory.cpp index 49a7f73fb3a3..8b43f1db84af 100644 --- a/libs/hwui/jni/BitmapFactory.cpp +++ b/libs/hwui/jni/BitmapFactory.cpp @@ -10,6 +10,7 @@ #include <stdint.h> #include <stdio.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include <memory> @@ -630,6 +631,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, } bitmap::reinitBitmap(env, javaBitmap, outputBitmap.info(), isPremultiplied); outputBitmap.notifyPixelsChanged(); + uirenderer::logBitmapDecode(*reuseBitmap); // If a java bitmap was passed in for reuse, pass it back return javaBitmap; } @@ -650,6 +652,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, } } + uirenderer::logBitmapDecode(*hardwareBitmap); return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); } @@ -659,6 +662,7 @@ static jobject doDecode(JNIEnv* env, std::unique_ptr<SkStreamRewindable> stream, heapBitmap->setGainmap(std::move(gainmap)); } + uirenderer::logBitmapDecode(*heapBitmap); // now create the java bitmap return bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags, ninePatchChunk, ninePatchInsets, -1); diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp index f7e8e073a272..5ffd5b9016d8 100644 --- a/libs/hwui/jni/BitmapRegionDecoder.cpp +++ b/libs/hwui/jni/BitmapRegionDecoder.cpp @@ -19,6 +19,7 @@ #include <HardwareBitmapUploader.h> #include <androidfw/Asset.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include <memory> @@ -376,6 +377,7 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in recycledBitmap->setGainmap(std::move(gainmap)); } bitmap::reinitBitmap(env, javaBitmap, recycledBitmap->info(), !requireUnpremul); + uirenderer::logBitmapDecode(*recycledBitmap); return javaBitmap; } @@ -392,12 +394,14 @@ static jobject nativeDecodeRegion(JNIEnv* env, jobject, jlong brdHandle, jint in hardwareBitmap->setGainmap(std::move(gm)); } } + uirenderer::logBitmapDecode(*hardwareBitmap); return bitmap::createBitmap(env, hardwareBitmap.release(), bitmapCreateFlags); } Bitmap* heapBitmap = heapAlloc.getStorageObjAndReset(); if (hasGainmap && heapBitmap != nullptr) { heapBitmap->setGainmap(std::move(gainmap)); } + uirenderer::logBitmapDecode(*heapBitmap); return android::bitmap::createBitmap(env, heapBitmap, bitmapCreateFlags); } diff --git a/libs/hwui/jni/ColorFilter.cpp b/libs/hwui/jni/ColorFilter.cpp index 0b95148d3c82..20301d2c76ec 100644 --- a/libs/hwui/jni/ColorFilter.cpp +++ b/libs/hwui/jni/ColorFilter.cpp @@ -18,7 +18,9 @@ #include "ColorFilter.h" #include "GraphicsJNI.h" +#include "RuntimeEffectUtils.h" #include "SkBlendMode.h" +#include "include/effects/SkRuntimeEffect.h" namespace android { @@ -89,6 +91,78 @@ public: filter->setMatrix(getMatrixFromJFloatArray(env, jarray)); } } + + static jlong RuntimeColorFilter_createColorFilter(JNIEnv* env, jobject, jstring agsl) { + ScopedUtfChars strSksl(env, agsl); + auto result = SkRuntimeEffect::MakeForColorFilter(SkString(strSksl.c_str()), + SkRuntimeEffect::Options{}); + if (result.effect.get() == nullptr) { + doThrowIAE(env, result.errorText.c_str()); + return 0; + } + auto builder = new SkRuntimeEffectBuilder(std::move(result.effect)); + auto* runtimeColorFilter = new RuntimeColorFilter(builder); + runtimeColorFilter->incStrong(nullptr); + return static_cast<jlong>(reinterpret_cast<uintptr_t>(runtimeColorFilter)); + } + + static void RuntimeColorFilter_updateUniformsFloatArray(JNIEnv* env, jobject, + jlong colorFilterPtr, + jstring uniformName, + jfloatArray uniforms, + jboolean isColor) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaFloatArray autoValues(env, uniforms, 0, kRO_JNIAccess); + if (filter) { + filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length(), + isColor); + } + } + + static void RuntimeColorFilter_updateUniformsFloats(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring uniformName, jfloat value1, + jfloat value2, jfloat value3, jfloat value4, + jint count) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + const float values[4] = {value1, value2, value3, value4}; + if (filter) { + filter->updateUniforms(env, name.c_str(), values, count, false); + } + } + + static void RuntimeColorFilter_updateUniformsIntArray(JNIEnv* env, jobject, + jlong colorFilterPtr, jstring uniformName, + jintArray uniforms) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + AutoJavaIntArray autoValues(env, uniforms, 0); + if (filter) { + filter->updateUniforms(env, name.c_str(), autoValues.ptr(), autoValues.length()); + } + } + + static void RuntimeColorFilter_updateUniformsInts(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring uniformName, jint value1, jint value2, + jint value3, jint value4, jint count) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, uniformName); + const int values[4] = {value1, value2, value3, value4}; + if (filter) { + filter->updateUniforms(env, name.c_str(), values, count); + } + } + + static void RuntimeColorFilter_updateChild(JNIEnv* env, jobject, jlong colorFilterPtr, + jstring childName, jlong childPtr) { + auto* filter = reinterpret_cast<RuntimeColorFilter*>(colorFilterPtr); + ScopedUtfChars name(env, childName); + auto* child = reinterpret_cast<SkFlattenable*>(childPtr); + if (filter && child) { + filter->updateChild(env, name.c_str(), child); + } + } }; static const JNINativeMethod colorfilter_methods[] = { @@ -107,6 +181,20 @@ static const JNINativeMethod colormatrix_methods[] = { {"nativeColorMatrixFilter", "([F)J", (void*)ColorFilterGlue::CreateColorMatrixFilter}, {"nativeSetColorMatrix", "(J[F)V", (void*)ColorFilterGlue::SetColorMatrix}}; +static const JNINativeMethod runtime_color_filter_methods[] = { + {"nativeCreateRuntimeColorFilter", "(Ljava/lang/String;)J", + (void*)ColorFilterGlue::RuntimeColorFilter_createColorFilter}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[FZ)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloatArray}, + {"nativeUpdateUniforms", "(JLjava/lang/String;FFFFI)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsFloats}, + {"nativeUpdateUniforms", "(JLjava/lang/String;[I)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsIntArray}, + {"nativeUpdateUniforms", "(JLjava/lang/String;IIIII)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateUniformsInts}, + {"nativeUpdateChild", "(JLjava/lang/String;J)V", + (void*)ColorFilterGlue::RuntimeColorFilter_updateChild}}; + int register_android_graphics_ColorFilter(JNIEnv* env) { android::RegisterMethodsOrDie(env, "android/graphics/ColorFilter", colorfilter_methods, NELEM(colorfilter_methods)); @@ -118,7 +206,10 @@ int register_android_graphics_ColorFilter(JNIEnv* env) { NELEM(lighting_methods)); android::RegisterMethodsOrDie(env, "android/graphics/ColorMatrixColorFilter", colormatrix_methods, NELEM(colormatrix_methods)); - + android::RegisterMethodsOrDie(env, "android/graphics/RuntimeColorFilter", + runtime_color_filter_methods, + NELEM(runtime_color_filter_methods)); + return 0; } diff --git a/libs/hwui/jni/ImageDecoder.cpp b/libs/hwui/jni/ImageDecoder.cpp index aebc4db37898..90fd3d87cba7 100644 --- a/libs/hwui/jni/ImageDecoder.cpp +++ b/libs/hwui/jni/ImageDecoder.cpp @@ -37,6 +37,7 @@ #include <hwui/Bitmap.h> #include <hwui/ImageDecoder.h> #include <sys/stat.h> +#include <utils/StatsUtils.h> #include "Bitmap.h" #include "BitmapFactory.h" @@ -485,6 +486,7 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong hwBitmap->setGainmap(std::move(gm)); } } + uirenderer::logBitmapDecode(*hwBitmap); return bitmap::createBitmap(env, hwBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); } @@ -498,6 +500,8 @@ static jobject ImageDecoder_nDecodeBitmap(JNIEnv* env, jobject /*clazz*/, jlong nativeBitmap->setImmutable(); } + + uirenderer::logBitmapDecode(*nativeBitmap); return bitmap::createBitmap(env, nativeBitmap.release(), bitmapCreateFlags, ninePatchChunk, ninePatchInsets); } diff --git a/libs/hwui/jni/RuntimeEffectUtils.cpp b/libs/hwui/jni/RuntimeEffectUtils.cpp new file mode 100644 index 000000000000..46db8633c66e --- /dev/null +++ b/libs/hwui/jni/RuntimeEffectUtils.cpp @@ -0,0 +1,101 @@ +/* + * 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. + */ + +#include "RuntimeEffectUtils.h" + +#include "include/effects/SkRuntimeEffect.h" + +namespace android { +namespace uirenderer { + +static inline int ThrowIAEFmt(JNIEnv* env, const char* fmt, ...) { + va_list args; + va_start(args, fmt); + int ret = jniThrowExceptionFmt(env, "java/lang/IllegalArgumentException", fmt, args); + va_end(args); + return ret; +} + +bool isIntUniformType(const SkRuntimeEffect::Uniform::Type& type) { + switch (type) { + case SkRuntimeEffect::Uniform::Type::kFloat: + case SkRuntimeEffect::Uniform::Type::kFloat2: + case SkRuntimeEffect::Uniform::Type::kFloat3: + case SkRuntimeEffect::Uniform::Type::kFloat4: + case SkRuntimeEffect::Uniform::Type::kFloat2x2: + case SkRuntimeEffect::Uniform::Type::kFloat3x3: + case SkRuntimeEffect::Uniform::Type::kFloat4x4: + return false; + case SkRuntimeEffect::Uniform::Type::kInt: + case SkRuntimeEffect::Uniform::Type::kInt2: + case SkRuntimeEffect::Uniform::Type::kInt3: + case SkRuntimeEffect::Uniform::Type::kInt4: + return true; + } +} + +void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const float values[], int count, bool isColor) { + SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (isColor != ((uniform.fVar->flags & SkRuntimeEffect::Uniform::kColor_Flag) != 0)) { + if (isColor) { + jniThrowExceptionFmt( + env, "java/lang/IllegalArgumentException", + "attempting to set a color uniform using the non-color specific APIs: %s %x", + uniformName, uniform.fVar->flags); + } else { + ThrowIAEFmt(env, + "attempting to set a non-color uniform using the setColorUniform APIs: %s", + uniformName); + } + } else if (isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a int uniform using the setUniform APIs: %s", + uniformName); + } else if (!uniform.set<float>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const int values[], int count) { + SkRuntimeEffectBuilder::BuilderUniform uniform = builder->uniform(uniformName); + if (uniform.fVar == nullptr) { + ThrowIAEFmt(env, "unable to find uniform named %s", uniformName); + } else if (!isIntUniformType(uniform.fVar->type)) { + ThrowIAEFmt(env, "attempting to set a non-int uniform using the setIntUniform APIs: %s", + uniformName); + } else if (!uniform.set<int>(values, count)) { + ThrowIAEFmt(env, "mismatch in byte size for uniform [expected: %zu actual: %zu]", + uniform.fVar->sizeInBytes(), sizeof(float) * count); + } +} + +void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName, + SkFlattenable* childEffect) { + SkRuntimeShaderBuilder::BuilderChild builderChild = builder->child(childName); + if (builderChild.fChild == nullptr) { + ThrowIAEFmt(env, "unable to find shader named %s", childName); + return; + } + + builderChild = sk_ref_sp(childEffect); +} + +} // namespace uirenderer +} // namespace android
\ No newline at end of file diff --git a/libs/hwui/jni/RuntimeEffectUtils.h b/libs/hwui/jni/RuntimeEffectUtils.h new file mode 100644 index 000000000000..75623c0f7ac1 --- /dev/null +++ b/libs/hwui/jni/RuntimeEffectUtils.h @@ -0,0 +1,37 @@ +/* + * 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. + */ + +#ifndef RUNTIMEEFFECTUTILS_H +#define RUNTIMEEFFECTUTILS_H + +#include "GraphicsJNI.h" +#include "include/effects/SkRuntimeEffect.h" + +namespace android { +namespace uirenderer { + +void UpdateFloatUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const float values[], int count, bool isColor); + +void UpdateIntUniforms(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* uniformName, + const int values[], int count); + +void UpdateChild(JNIEnv* env, SkRuntimeEffectBuilder* builder, const char* childName, + SkFlattenable* childEffect); +} // namespace uirenderer +} // namespace android + +#endif // MAIN_RUNTIMEEFFECTUTILS_H diff --git a/libs/hwui/jni/android_graphics_RenderNode.cpp b/libs/hwui/jni/android_graphics_RenderNode.cpp index 6e03bbd0fa16..d9dc8eb8c1b1 100644 --- a/libs/hwui/jni/android_graphics_RenderNode.cpp +++ b/libs/hwui/jni/android_graphics_RenderNode.cpp @@ -593,9 +593,9 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, Matrix4 transform; SkIRect clipBounds; + uirenderer::Rect initialClipBounds; + const auto clipFlags = props.getClippingFlags(); if (enableClip) { - uirenderer::Rect initialClipBounds; - const auto clipFlags = props.getClippingFlags(); if (clipFlags) { props.getClippingRectForFlags(clipFlags, &initialClipBounds); } else { @@ -659,8 +659,8 @@ static void android_view_RenderNode_requestPositionUpdates(JNIEnv* env, jobject, static_cast<jint>(bounds.left), static_cast<jint>(bounds.top), static_cast<jint>(bounds.right), static_cast<jint>(bounds.bottom), static_cast<jint>(clipBounds.fLeft), static_cast<jint>(clipBounds.fTop), - static_cast<jint>(clipBounds.fRight), - static_cast<jint>(clipBounds.fBottom)); + static_cast<jint>(clipBounds.fRight), static_cast<jint>(clipBounds.fBottom), + static_cast<jint>(props.getWidth()), static_cast<jint>(props.getHeight())); } if (!keepListening) { env->DeleteGlobalRef(mListener); @@ -891,7 +891,7 @@ int register_android_view_RenderNode(JNIEnv* env) { gPositionListener.callPositionChanged = GetStaticMethodIDOrDie( env, clazz, "callPositionChanged", "(Ljava/lang/ref/WeakReference;JIIII)Z"); gPositionListener.callPositionChanged2 = GetStaticMethodIDOrDie( - env, clazz, "callPositionChanged2", "(Ljava/lang/ref/WeakReference;JIIIIIIII)Z"); + env, clazz, "callPositionChanged2", "(Ljava/lang/ref/WeakReference;JIIIIIIIIII)Z"); gPositionListener.callApplyStretch = GetStaticMethodIDOrDie( env, clazz, "callApplyStretch", "(Ljava/lang/ref/WeakReference;JFFFFFFFFFF)Z"); gPositionListener.callPositionLost = GetStaticMethodIDOrDie( diff --git a/libs/hwui/libhwui.map.txt b/libs/hwui/libhwui.map.txt index 2414299321a9..b5591941453d 100644 --- a/libs/hwui/libhwui.map.txt +++ b/libs/hwui/libhwui.map.txt @@ -67,6 +67,7 @@ LIBHWUI_PLATFORM { SkFILEStream::SkFILEStream*; SkImageInfo::*; SkMemoryStream::SkMemoryStream*; + android::uirenderer::logBitmapDecode*; }; local: *; diff --git a/libs/hwui/platform/host/android/api-level.h b/libs/hwui/platform/host/android/api-level.h deleted file mode 120000 index 4fb4784f9f60..000000000000 --- a/libs/hwui/platform/host/android/api-level.h +++ /dev/null @@ -1 +0,0 @@ -../../../../../../../bionic/libc/include/android/api-level.h
\ No newline at end of file diff --git a/libs/hwui/utils/StatsUtils.cpp b/libs/hwui/utils/StatsUtils.cpp new file mode 100644 index 000000000000..5c4027e1a846 --- /dev/null +++ b/libs/hwui/utils/StatsUtils.cpp @@ -0,0 +1,102 @@ +/* + * Copyright 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. + */ + +#ifdef __ANDROID__ +#include <dlfcn.h> +#include <log/log.h> +#include <statslog_hwui.h> +#include <statssocket_lazy.h> +#include <utils/Errors.h> + +#include <mutex> +#endif + +#include <unistd.h> + +#include "StatsUtils.h" + +namespace android { +namespace uirenderer { + +#ifdef __ANDROID__ + +namespace { + +int32_t toStatsColorSpaceTransfer(skcms_TFType transferType) { + switch (transferType) { + case skcms_TFType_sRGBish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_SRGBISH; + case skcms_TFType_PQish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_PQISH; + case skcms_TFType_HLGish: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_HLGISH; + default: + return stats::IMAGE_DECODED__COLOR_SPACE_TRANSFER__COLOR_SPACE_TRANSFER_UNKNOWN; + } +} + +int32_t toStatsBitmapFormat(SkColorType type) { + switch (type) { + case kAlpha_8_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_A_8; + case kRGB_565_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGB_565; + case kN32_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_ARGB_8888; + case kRGBA_F16_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_F16; + case kRGBA_1010102_SkColorType: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_RGBA_1010102; + default: + return stats::IMAGE_DECODED__FORMAT__BITMAP_FORMAT_UNKNOWN; + } +} + +} // namespace + +#endif + +void logBitmapDecode(const SkImageInfo& info, bool hasGainmap) { +#ifdef __ANDROID__ + + if (!statssocket::lazy::IsAvailable()) { + std::once_flag once; + std::call_once(once, []() { ALOGD("libstatssocket not available, dropping stats"); }); + return; + } + + skcms_TFType tfnType = skcms_TFType_Invalid; + + if (info.colorSpace()) { + skcms_TransferFunction tfn; + info.colorSpace()->transferFn(&tfn); + tfnType = skcms_TransferFunction_getType(&tfn); + } + + auto status = + stats::stats_write(uirenderer::stats::IMAGE_DECODED, static_cast<int32_t>(getuid()), + uirenderer::toStatsColorSpaceTransfer(tfnType), hasGainmap, + uirenderer::toStatsBitmapFormat(info.colorType())); + ALOGW_IF(status != OK, "Image decoding logging dropped!"); +#endif +} + +void logBitmapDecode(const Bitmap& bitmap) { + logBitmapDecode(bitmap.info(), bitmap.hasGainmap()); +} + +} // namespace uirenderer +} // namespace android diff --git a/libs/hwui/utils/StatsUtils.h b/libs/hwui/utils/StatsUtils.h new file mode 100644 index 000000000000..0c247014a8eb --- /dev/null +++ b/libs/hwui/utils/StatsUtils.h @@ -0,0 +1,33 @@ +/* + * Copyright 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. + */ + +#pragma once + +#include <SkBitmap.h> +#include <SkColorSpace.h> +#include <SkColorType.h> +#include <cutils/compiler.h> +#include <hwui/Bitmap.h> + +namespace android { +namespace uirenderer { + +ANDROID_API void logBitmapDecode(const SkImageInfo& info, bool hasGainmap); + +ANDROID_API void logBitmapDecode(const Bitmap& bitmap); + +} // namespace uirenderer +} // namespace android |