summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/app/Notification.java31
-rw-r--r--core/java/android/app/admin/DevicePolicyManager.java11
-rw-r--r--core/java/android/os/UserManager.java6
-rw-r--r--core/java/com/android/internal/widget/NotificationExpandButton.java14
-rw-r--r--core/res/res/drawable/ic_notification_2025_collapse.xml25
-rw-r--r--core/res/res/drawable/ic_notification_2025_expand.xml25
-rw-r--r--core/res/res/drawable/notification_2025_expand_button_pill_bg.xml29
-rw-r--r--core/res/res/layout/notification_2025_expand_button.xml61
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_base.xml5
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_call.xml5
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_media.xml5
-rw-r--r--core/res/res/layout/notification_2025_template_collapsed_messaging.xml5
-rw-r--r--core/res/res/layout/notification_2025_template_conversation.xml5
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_base.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_big_picture.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_big_text.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_call.xml2
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_inbox.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_media.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_messaging.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_expanded_progress.xml3
-rw-r--r--core/res/res/layout/notification_2025_template_header.xml9
-rw-r--r--core/res/res/values/dimens.xml23
-rw-r--r--core/res/res/values/symbols.xml4
-rw-r--r--libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml2
-rw-r--r--libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml5
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt5
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt5
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt110
-rw-r--r--libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt5
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java79
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java53
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerView.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java15
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt19
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java7
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopViaMenuOfStaticOverviewTaskTest.kt27
-rw-r--r--libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopViaMenuOfStaticOverviewTask.kt70
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/Android.bp33
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt27
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt1
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipUtils.kt39
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfAutoEnterPipOnGoToHomeTest.kt170
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipOnUserLeaveHintTest.kt168
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipTransition.kt46
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipViaAppUiButtonTest.kt101
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/bubbles/BubbleViewInfoTest.kt5
-rw-r--r--packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt13
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt6
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt2
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt3
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt238
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt14
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt8
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt21
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt21
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt5
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt1
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt410
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt10
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt36
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt47
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java10
-rw-r--r--packages/SystemUI/res/values/dimens.xml7
-rw-r--r--packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java16
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java14
-rw-r--r--packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java6
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java21
-rw-r--r--services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java5
72 files changed, 2001 insertions, 238 deletions
diff --git a/core/java/android/app/Notification.java b/core/java/android/app/Notification.java
index eeb1ebb69b03..8ffea237c71d 100644
--- a/core/java/android/app/Notification.java
+++ b/core/java/android/app/Notification.java
@@ -6453,6 +6453,13 @@ public class Notification implements Parcelable
big.setColorStateList(R.id.snooze_button, "setImageTintList", actionColor);
big.setColorStateList(R.id.bubble_button, "setImageTintList", actionColor);
+ if (Flags.notificationsRedesignTemplates()) {
+ int margin = getContentMarginTop(mContext,
+ R.dimen.notification_2025_content_margin_top);
+ big.setViewLayoutMargin(R.id.notification_main_column, RemoteViews.MARGIN_TOP,
+ margin, TypedValue.COMPLEX_UNIT_PX);
+ }
+
boolean validRemoteInput = false;
// In the UI, contextual actions appear separately from the standard actions, so we
@@ -6549,6 +6556,30 @@ public class Notification implements Parcelable
return big;
}
+ /**
+ * Calculate the top margin for the content in px, to allow enough space for the top line
+ * above, using the given resource ID for the desired spacing.
+ *
+ * @hide
+ */
+ public static int getContentMarginTop(Context context, @DimenRes int spacingRes) {
+ final Resources resources = context.getResources();
+ // The margin above the text, at the top of the notification (originally in dp)
+ int notifMargin = resources.getDimensionPixelSize(R.dimen.notification_2025_margin);
+ // Spacing between the text lines, scaling with the font size (originally in sp)
+ int spacing = resources.getDimensionPixelSize(spacingRes);
+
+ // Size of the text in the notification top line (originally in sp)
+ int[] textSizeAttr = new int[] { android.R.attr.textSize };
+ TypedArray typedArray = context.obtainStyledAttributes(
+ R.style.TextAppearance_DeviceDefault_Notification_Info, textSizeAttr);
+ int textSize = typedArray.getDimensionPixelSize(0 /* index */, -1 /* default */);
+ typedArray.recycle();
+
+ // Adding up all the values as pixels
+ return notifMargin + spacing + textSize;
+ }
+
private boolean hasValidRemoteInput(Action action) {
if (TextUtils.isEmpty(action.title) || action.actionIntent == null) {
// Weird actions
diff --git a/core/java/android/app/admin/DevicePolicyManager.java b/core/java/android/app/admin/DevicePolicyManager.java
index 39c27a165588..84d67415a4b4 100644
--- a/core/java/android/app/admin/DevicePolicyManager.java
+++ b/core/java/android/app/admin/DevicePolicyManager.java
@@ -17430,12 +17430,17 @@ public class DevicePolicyManager {
}
/**
- * Removes a manged profile from the device only when called from a managed profile's context
+ * Removes a managed profile from the device.
+ *
+ * <p>
+ * Removes the managed profile which is specified by the context user
+ * ({@code Context.createContextAsUser()}).
+ * <p>
*
- * @param user UserHandle of the profile to be removed
* @return {@code true} when removal of managed profile was successful, {@code false} when
- * removal was unsuccessful or throws IllegalArgumentException when provided user was not a
+ * removal was unsuccessful or throws IllegalArgumentException when specified user was not a
* managed profile
+ *
* @hide
*/
@SystemApi
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 507bcb8c2717..08f68f1874e7 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -4206,12 +4206,16 @@ public class UserManager {
private boolean getUserRestrictionFromQuery(@NonNull Pair<String, Integer> restrictionPerUser) {
return UserManagerCache.getUserRestrictionFromQuery(
(Pair<String, Integer> q) -> mService.hasUserRestriction(q.first, q.second),
+ // bypass cache if the flag is disabled
+ (Pair<String, Integer> q) -> !android.multiuser.Flags.cacheUserRestrictionsReadOnly(),
restrictionPerUser);
}
/** @hide */
public static final void invalidateUserRestriction() {
- UserManagerCache.invalidateUserRestrictionFromQuery();
+ if (android.multiuser.Flags.cacheUserRestrictionsReadOnly()) {
+ UserManagerCache.invalidateUserRestrictionFromQuery();
+ }
}
/**
diff --git a/core/java/com/android/internal/widget/NotificationExpandButton.java b/core/java/com/android/internal/widget/NotificationExpandButton.java
index d4dd1e705653..751cfde70164 100644
--- a/core/java/com/android/internal/widget/NotificationExpandButton.java
+++ b/core/java/com/android/internal/widget/NotificationExpandButton.java
@@ -16,6 +16,8 @@
package com.android.internal.widget;
+import static android.app.Flags.notificationsRedesignTemplates;
+
import android.annotation.ColorInt;
import android.annotation.Nullable;
import android.content.Context;
@@ -130,10 +132,18 @@ public class NotificationExpandButton extends FrameLayout {
int drawableId;
int contentDescriptionId;
if (mExpanded) {
- drawableId = R.drawable.ic_collapse_notification;
+ if (notificationsRedesignTemplates()) {
+ drawableId = R.drawable.ic_notification_2025_collapse;
+ } else {
+ drawableId = R.drawable.ic_collapse_notification;
+ }
contentDescriptionId = R.string.expand_button_content_description_expanded;
} else {
- drawableId = R.drawable.ic_expand_notification;
+ if (notificationsRedesignTemplates()) {
+ drawableId = R.drawable.ic_notification_2025_expand;
+ } else {
+ drawableId = R.drawable.ic_expand_notification;
+ }
contentDescriptionId = R.string.expand_button_content_description_collapsed;
}
setContentDescription(mContext.getText(contentDescriptionId));
diff --git a/core/res/res/drawable/ic_notification_2025_collapse.xml b/core/res/res/drawable/ic_notification_2025_collapse.xml
new file mode 100644
index 000000000000..1b40c555f84d
--- /dev/null
+++ b/core/res/res/drawable/ic_notification_2025_collapse.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,432L296,616L240,560L480,320L720,560L664,616L480,432Z"/>
+</vector>
diff --git a/core/res/res/drawable/ic_notification_2025_expand.xml b/core/res/res/drawable/ic_notification_2025_expand.xml
new file mode 100644
index 000000000000..ea5e0f09d2ef
--- /dev/null
+++ b/core/res/res/drawable/ic_notification_2025_expand.xml
@@ -0,0 +1,25 @@
+<!--
+ ~ 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.
+ -->
+<vector xmlns:android="http://schemas.android.com/apk/res/android"
+ android:width="24dp"
+ android:height="24dp"
+ android:viewportWidth="960"
+ android:viewportHeight="960"
+ android:tint="?attr/colorControlNormal">
+ <path
+ android:fillColor="@android:color/white"
+ android:pathData="M480,616L240,376L296,320L480,504L664,320L720,376L480,616Z"/>
+</vector>
diff --git a/core/res/res/drawable/notification_2025_expand_button_pill_bg.xml b/core/res/res/drawable/notification_2025_expand_button_pill_bg.xml
new file mode 100644
index 000000000000..74f697a176f3
--- /dev/null
+++ b/core/res/res/drawable/notification_2025_expand_button_pill_bg.xml
@@ -0,0 +1,29 @@
+<!--
+ ~ 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.
+ -->
+<layer-list xmlns:android="http://schemas.android.com/apk/res/android">
+ <item android:id="@+id/expand_button_pill_colorized_layer">
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="@dimen/notification_2025_expand_button_pill_height" />
+ <solid android:color="@android:color/white" />
+ </shape>
+ </item>
+ <item>
+ <shape xmlns:android="http://schemas.android.com/apk/res/android">
+ <corners android:radius="@dimen/notification_2025_expand_button_pill_height" />
+ <solid android:color="@color/notification_expand_button_state_tint" />
+ </shape>
+ </item>
+</layer-list>
diff --git a/core/res/res/layout/notification_2025_expand_button.xml b/core/res/res/layout/notification_2025_expand_button.xml
new file mode 100644
index 000000000000..c8263c26f38f
--- /dev/null
+++ b/core/res/res/layout/notification_2025_expand_button.xml
@@ -0,0 +1,61 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ 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
+ -->
+
+<!-- extends FrameLayout -->
+<com.android.internal.widget.NotificationExpandButton
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ android:id="@+id/expand_button"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="top|end"
+ android:contentDescription="@string/expand_button_content_description_collapsed"
+ >
+
+ <LinearLayout
+ android:id="@+id/expand_button_pill"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:minHeight="@dimen/notification_2025_expand_button_pill_height"
+ android:minWidth="@dimen/notification_2025_expand_button_pill_width"
+ android:paddingVertical="@dimen/notification_2025_expand_button_vertical_icon_padding"
+ android:paddingHorizontal="@dimen/notification_2025_expand_button_horizontal_icon_padding"
+ android:orientation="horizontal"
+ android:background="@drawable/notification_2025_expand_button_pill_bg"
+ android:gravity="center"
+ android:layout_gravity="center_vertical"
+ android:duplicateParentState="true"
+ >
+
+ <TextView
+ android:id="@+id/expand_button_number"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:textAppearance="@style/TextAppearance.DeviceDefault.Notification.Info"
+ android:gravity="center_vertical"
+ android:visibility="gone"
+ />
+
+ <ImageView
+ android:id="@+id/expand_button_icon"
+ android:layout_width="@dimen/notification_2025_expand_button_icon_size"
+ android:layout_height="@dimen/notification_2025_expand_button_icon_size"
+ android:scaleType="fitCenter"
+ android:importantForAccessibility="no"
+ />
+
+ </LinearLayout>
+
+</com.android.internal.widget.NotificationExpandButton>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_base.xml b/core/res/res/layout/notification_2025_template_collapsed_base.xml
index e91e1115ac1c..c827dcb16584 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_base.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_base.xml
@@ -164,10 +164,11 @@
android:minWidth="@dimen/notification_content_margin_end"
>
- <include layout="@layout/notification_expand_button"
+ <include layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical|end"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_call.xml b/core/res/res/layout/notification_2025_template_collapsed_call.xml
index c4bca1142ece..ce38c1645fb1 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_call.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_call.xml
@@ -66,10 +66,11 @@
>
<include
- layout="@layout/notification_expand_button"
+ layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_media.xml b/core/res/res/layout/notification_2025_template_collapsed_media.xml
index 2d367337bb6f..0021b8384847 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_media.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_media.xml
@@ -185,10 +185,11 @@
android:minWidth="@dimen/notification_content_margin_end"
>
- <include layout="@layout/notification_expand_button"
+ <include layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical|end"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
index fbecb8c30b9c..f3e4ce13ff4b 100644
--- a/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_collapsed_messaging.xml
@@ -189,10 +189,11 @@
android:minWidth="@dimen/notification_content_margin_end"
>
- <include layout="@layout/notification_expand_button"
+ <include layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center_vertical|end"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
/>
</FrameLayout>
diff --git a/core/res/res/layout/notification_2025_template_conversation.xml b/core/res/res/layout/notification_2025_template_conversation.xml
index f31f65e90950..6be5a1cee7f9 100644
--- a/core/res/res/layout/notification_2025_template_conversation.xml
+++ b/core/res/res/layout/notification_2025_template_conversation.xml
@@ -148,10 +148,11 @@
android:clipToPadding="false"
android:clipChildren="false"
/>
- <include layout="@layout/notification_expand_button"
+ <include layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_gravity="center"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
/>
</LinearLayout>
</LinearLayout>
diff --git a/core/res/res/layout/notification_2025_template_expanded_base.xml b/core/res/res/layout/notification_2025_template_expanded_base.xml
index e480fe5eef1e..d364c659d0db 100644
--- a/core/res/res/layout/notification_2025_template_expanded_base.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_base.xml
@@ -40,13 +40,14 @@
<include layout="@layout/notification_2025_template_header" />
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/notification_2025_content_margin_start"
android:layout_marginEnd="@dimen/notification_content_margin_end"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:orientation="vertical"
>
diff --git a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
index 18bafe068fcb..12e11728f608 100644
--- a/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_big_picture.xml
@@ -32,11 +32,12 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:clipToPadding="false"
android:orientation="vertical"
>
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_2025_template_expanded_big_text.xml b/core/res/res/layout/notification_2025_template_expanded_big_text.xml
index 9ff141b7c946..c9dd868795de 100644
--- a/core/res/res/layout/notification_2025_template_expanded_big_text.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_big_text.xml
@@ -30,12 +30,13 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:layout_marginBottom="@dimen/notification_content_margin"
android:clipToPadding="false"
android:orientation="vertical"
>
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<com.android.internal.widget.RemeasuringLinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_2025_template_expanded_call.xml b/core/res/res/layout/notification_2025_template_expanded_call.xml
index 2af0ec2972df..0be61253c917 100644
--- a/core/res/res/layout/notification_2025_template_expanded_call.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_call.xml
@@ -48,8 +48,8 @@
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_weight="1"
android:layout_marginStart="@dimen/notification_2025_content_margin_start"
+ android:layout_weight="1"
android:orientation="vertical"
android:minHeight="68dp"
>
diff --git a/core/res/res/layout/notification_2025_template_expanded_inbox.xml b/core/res/res/layout/notification_2025_template_expanded_inbox.xml
index 9fb44ccccfa0..8434b3644f81 100644
--- a/core/res/res/layout/notification_2025_template_expanded_inbox.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_inbox.xml
@@ -28,10 +28,11 @@
android:layout_width="match_parent"
android:layout_height="match_parent"
android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:clipToPadding="false"
android:orientation="vertical">
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_2025_template_expanded_media.xml b/core/res/res/layout/notification_2025_template_expanded_media.xml
index 578a0b2b6d0b..e90ab792581f 100644
--- a/core/res/res/layout/notification_2025_template_expanded_media.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_media.xml
@@ -34,11 +34,12 @@
android:id="@+id/notification_media_content"
>
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:layout_marginStart="@dimen/notification_2025_content_margin_start"
android:layout_marginEnd="@dimen/notification_content_margin_end"
android:orientation="vertical"
diff --git a/core/res/res/layout/notification_2025_template_expanded_messaging.xml b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
index e3c201465eb0..7f5a36b5f865 100644
--- a/core/res/res/layout/notification_2025_template_expanded_messaging.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_messaging.xml
@@ -33,10 +33,11 @@
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_gravity="top"
- android:layout_marginTop="@dimen/notification_2025_header_height"
android:clipChildren="false"
android:orientation="vertical">
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<com.android.internal.widget.RemeasuringLinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
diff --git a/core/res/res/layout/notification_2025_template_expanded_progress.xml b/core/res/res/layout/notification_2025_template_expanded_progress.xml
index afa4bc6dd7f8..5d4fc4c87fac 100644
--- a/core/res/res/layout/notification_2025_template_expanded_progress.xml
+++ b/core/res/res/layout/notification_2025_template_expanded_progress.xml
@@ -41,13 +41,14 @@
<include layout="@layout/notification_2025_template_header" />
+ <!-- Note: the top margin is being set in code based on the estimated space needed for
+ the header text. -->
<LinearLayout
android:id="@+id/notification_main_column"
android:layout_width="match_parent"
android:layout_height="wrap_content"
android:layout_marginStart="@dimen/notification_2025_content_margin_start"
android:layout_marginEnd="@dimen/notification_content_margin_end"
- android:layout_marginTop="@dimen/notification_content_margin_top"
android:orientation="vertical"
>
diff --git a/core/res/res/layout/notification_2025_template_header.xml b/core/res/res/layout/notification_2025_template_header.xml
index 2d30d8a8bbb6..3f34eb3cbf0c 100644
--- a/core/res/res/layout/notification_2025_template_header.xml
+++ b/core/res/res/layout/notification_2025_template_header.xml
@@ -57,11 +57,11 @@
xmlns:android="http://schemas.android.com/apk/res/android"
android:id="@+id/notification_top_line"
android:layout_width="wrap_content"
- android:layout_height="match_parent"
+ android:layout_height="wrap_content"
android:layout_alignParentStart="true"
- android:layout_centerVertical="true"
android:layout_toStartOf="@id/expand_button"
android:layout_alignWithParentIfMissing="true"
+ android:layout_marginVertical="@dimen/notification_2025_margin"
android:clipChildren="false"
android:gravity="center_vertical"
android:paddingStart="@dimen/notification_2025_content_margin_start"
@@ -81,10 +81,11 @@
android:focusable="false"
/>
- <include layout="@layout/notification_expand_button"
+ <include layout="@layout/notification_2025_expand_button"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_centerVertical="true"
+ android:layout_gravity="top|end"
+ android:layout_margin="@dimen/notification_2025_margin"
android:layout_alignParentEnd="true" />
<include layout="@layout/notification_close_button"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index fb21c7532ea3..a4735fe6c7af 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -310,9 +310,15 @@
<!-- Size of the stroke with for the emphasized notification button style -->
<dimen name="emphasized_button_stroke_width">1dp</dimen>
- <!-- height of the content margin to accomodate for the header -->
+ <!-- height of the content margin to accommodate for the header -->
<dimen name="notification_content_margin_top">50dp</dimen>
+ <!-- The spacing between the content and the header text above it, scaling with text size.
+ This value is chosen so that, taking into account the text spacing for both the text in the
+ top line and the text in the content, the distance between them is 4dp with the default
+ screen configuration (and will grow accordingly for larger font sizes) -->
+ <dimen name="notification_2025_content_margin_top">10sp</dimen>
+
<!-- height of the content margin that is applied at the end of the notification content -->
<dimen name="notification_content_margin">20dp</dimen>
@@ -384,9 +390,24 @@
<!-- the height of the expand button pill -->
<dimen name="notification_expand_button_pill_height">24dp</dimen>
+ <!-- the height of the expand button pill (2025 redesign version) -->
+ <dimen name="notification_2025_expand_button_pill_height">20dp</dimen>
+
+ <!-- the width of the expand button pill (2025 redesign version) -->
+ <dimen name="notification_2025_expand_button_pill_width">28dp</dimen>
+
+ <!-- the size of the expand arrow (2025 redesign version) -->
+ <dimen name="notification_2025_expand_button_icon_size">16sp</dimen>
+
<!-- the padding of the expand icon in the notification header -->
<dimen name="notification_expand_button_icon_padding">2dp</dimen>
+ <!-- the padding of the expand icon in the notification header -->
+ <dimen name="notification_2025_expand_button_vertical_icon_padding">2dp</dimen>
+
+ <!-- the padding of the expand icon in the notification header -->
+ <dimen name="notification_2025_expand_button_horizontal_icon_padding">6dp</dimen>
+
<!-- the size of the notification close button -->
<dimen name="notification_close_button_size">16dp</dimen>
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index e0c92d9231b0..6ee2839788af 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -3230,6 +3230,8 @@
<java-symbol type="id" name="header_text_divider" />
<java-symbol type="id" name="header_text_secondary_divider" />
<java-symbol type="drawable" name="ic_expand_notification" />
+ <java-symbol type="drawable" name="ic_notification_2025_collapse" />
+ <java-symbol type="drawable" name="ic_notification_2025_expand" />
<java-symbol type="drawable" name="ic_collapse_notification" />
<java-symbol type="drawable" name="notification_close_button_icon" />
<java-symbol type="drawable" name="ic_expand_bundle" />
@@ -3242,6 +3244,8 @@
<java-symbol type="dimen" name="notification_heading_margin_end" />
<java-symbol type="dimen" name="notification_content_margin_top" />
<java-symbol type="dimen" name="notification_content_margin" />
+ <java-symbol type="dimen" name="notification_2025_margin" />
+ <java-symbol type="dimen" name="notification_2025_content_margin_top" />
<java-symbol type="dimen" name="notification_progress_margin_horizontal" />
<java-symbol type="dimen" name="notification_header_background_height" />
<java-symbol type="dimen" name="notification_header_touchable_height" />
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
index f8f8338e5f04..fd578a959e3b 100644
--- a/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifest.xml
@@ -3,6 +3,8 @@
<application android:debuggable="true" android:supportsRtl="true" >
<uses-library android:name="android.test.runner" />
+ <activity android:name="com.android.wm.shell.bubbles.bar.BubbleBarAnimationHelperTest$TestActivity"
+ android:exported="true"/>
</application>
<instrumentation
diff --git a/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
index ffcd7d46fbae..bb111dbeffff 100644
--- a/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
+++ b/libs/WindowManager/Shell/multivalentTests/AndroidManifestRobolectric.xml
@@ -1,3 +1,8 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.wm.shell.multivalenttests">
+ <application>
+ <activity android:name="com.android.wm.shell.bubbles.bar.BubbleBarAnimationHelperTest$TestActivity"
+ android:exported="true"/>
+ </application>
</manifest>
+
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
index 2b4e5417f188..c62d2a06bad5 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleControllerBubbleBarTest.kt
@@ -35,11 +35,11 @@ import com.android.internal.statusbar.IStatusBarService
import com.android.wm.shell.Flags
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.WindowManagerShellWrapper
import com.android.wm.shell.bubbles.Bubbles.SysuiProxy
import com.android.wm.shell.bubbles.properties.ProdBubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.SyncTransactionQueue
@@ -268,7 +268,8 @@ class BubbleControllerBubbleBarTest {
bubbleDataRepository,
mock<IStatusBarService>(),
mock<WindowManager>(),
- WindowManagerShellWrapper(mainExecutor),
+ mock<DisplayInsetsController>(),
+ mock<DisplayImeController>(),
mock<UserManager>(),
mock<LauncherApps>(),
bubbleLogger,
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
index 680d015dfd2f..3043e2bcb0be 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleViewInfoTaskTest.kt
@@ -36,10 +36,10 @@ import com.android.internal.statusbar.IStatusBarService
import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.WindowManagerShellWrapper
import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.SyncTransactionQueue
@@ -141,7 +141,8 @@ class BubbleViewInfoTaskTest {
bubbleDataRepository,
mock<IStatusBarService>(),
windowManager,
- WindowManagerShellWrapper(mainExecutor),
+ mock<DisplayInsetsController>(),
+ mock<DisplayImeController>(),
mock<UserManager>(),
mock<LauncherApps>(),
bubbleLogger,
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
index 3e01256fd67c..957f0ca502a1 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelperTest.kt
@@ -17,14 +17,19 @@
package com.android.wm.shell.bubbles.bar
import android.animation.AnimatorTestRule
+import android.app.Activity
import android.app.ActivityManager
import android.content.Context
import android.graphics.Insets
+import android.graphics.Outline
import android.graphics.Rect
+import android.os.Bundle
import android.view.View
import android.view.ViewGroup
import android.view.WindowManager
import android.widget.FrameLayout
+import android.widget.FrameLayout.LayoutParams
+import androidx.test.core.app.ActivityScenario
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -63,6 +68,7 @@ import org.mockito.kotlin.whenever
class BubbleBarAnimationHelperTest {
@get:Rule val animatorTestRule: AnimatorTestRule = AnimatorTestRule(this)
+ private lateinit var activityScenario: ActivityScenario<TestActivity>
companion object {
const val SCREEN_WIDTH = 2000
@@ -83,6 +89,8 @@ class BubbleBarAnimationHelperTest {
fun setUp() {
ProtoLog.REQUIRE_PROTOLOGTOOL = false
ProtoLog.init()
+ activityScenario = ActivityScenario.launch(TestActivity::class.java)
+ activityScenario.onActivity { activity -> container = activity.container }
val windowManager = context.getSystemService(WindowManager::class.java)
bubblePositioner = BubblePositioner(context, windowManager)
bubblePositioner.setShowingInBubbleBar(true)
@@ -102,8 +110,6 @@ class BubbleBarAnimationHelperTest {
mainExecutor = TestShellExecutor()
bgExecutor = TestShellExecutor()
- container = FrameLayout(context)
-
animationHelper = BubbleBarAnimationHelper(context, bubblePositioner)
}
@@ -121,7 +127,7 @@ class BubbleBarAnimationHelperTest {
val semaphore = Semaphore(0)
val after = Runnable { semaphore.release() }
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateSwitch(fromBubble, toBubble, after)
animatorTestRule.advanceTimeBy(1000)
}
@@ -145,7 +151,7 @@ class BubbleBarAnimationHelperTest {
.updateHandleColor(/* isRegionDark= */ true, /* animated= */ false)
val toBubble = createBubble(key = "to").initialize(container)
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateSwitch(fromBubble, toBubble, /* afterAnimation= */ null)
animatorTestRule.advanceTimeBy(1000)
}
@@ -161,7 +167,7 @@ class BubbleBarAnimationHelperTest {
val toBubbleTaskController = mock<TaskViewTaskController>()
val toBubble = createBubble("to", toBubbleTaskController).initialize(container)
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateSwitch(fromBubble, toBubble) {}
// Start the animation, but don't finish
animatorTestRule.advanceTimeBy(100)
@@ -183,7 +189,7 @@ class BubbleBarAnimationHelperTest {
val semaphore = Semaphore(0)
val after = Runnable { semaphore.release() }
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateSwitch(fromBubble, overflow, after)
animatorTestRule.advanceTimeBy(1000)
}
@@ -206,7 +212,7 @@ class BubbleBarAnimationHelperTest {
val semaphore = Semaphore(0)
val after = Runnable { semaphore.release() }
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateSwitch(overflow, toBubble, after)
animatorTestRule.advanceTimeBy(1000)
}
@@ -226,7 +232,7 @@ class BubbleBarAnimationHelperTest {
val taskController = mock<TaskViewTaskController>()
val bubble = createBubble("key", taskController).initialize(container)
- getInstrumentation().runOnMainSync {
+ activityScenario.onActivity {
animationHelper.animateExpansion(bubble) {}
animatorTestRule.advanceTimeBy(1000)
}
@@ -243,6 +249,80 @@ class BubbleBarAnimationHelperTest {
verify(taskController).setWindowBounds(any())
}
+ @Test
+ fun animateExpansion() {
+ val bubble = createBubble(key = "b1").initialize(container)
+ val bbev = bubble.bubbleBarExpandedView!!
+
+ val semaphore = Semaphore(0)
+ val after = Runnable { semaphore.release() }
+
+ activityScenario.onActivity {
+ bbev.onTaskCreated()
+ animationHelper.animateExpansion(bubble, after)
+ animatorTestRule.advanceTimeBy(1000)
+ }
+ getInstrumentation().waitForIdleSync()
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+ assertThat(bbev.alpha).isEqualTo(1)
+ }
+
+ @Test
+ fun onImeTopChanged_noOverlap() {
+ val bubble = createBubble(key = "b1").initialize(container)
+ val bbev = bubble.bubbleBarExpandedView!!
+
+ val semaphore = Semaphore(0)
+ val after = Runnable { semaphore.release() }
+
+ activityScenario.onActivity {
+ bbev.onTaskCreated()
+ animationHelper.animateExpansion(bubble, after)
+ animatorTestRule.advanceTimeBy(1000)
+ }
+ getInstrumentation().waitForIdleSync()
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+
+ val bbevBottom = bbev.contentBottomOnScreen + bubblePositioner.insets.top
+ activityScenario.onActivity {
+ // notify that the IME top coordinate is greater than the bottom of the expanded view.
+ // there's no overlap so it should not be clipped.
+ animationHelper.onImeTopChanged(bbevBottom * 2)
+ }
+ val outline = Outline()
+ bbev.outlineProvider.getOutline(bbev, outline)
+ assertThat(outline.mRect.bottom).isEqualTo(bbev.height)
+ }
+
+ @Test
+ fun onImeTopChanged_overlapsWithExpandedView() {
+ val bubble = createBubble(key = "b1").initialize(container)
+ val bbev = bubble.bubbleBarExpandedView!!
+
+ val semaphore = Semaphore(0)
+ val after = Runnable { semaphore.release() }
+
+ activityScenario.onActivity {
+ bbev.onTaskCreated()
+ animationHelper.animateExpansion(bubble, after)
+ animatorTestRule.advanceTimeBy(1000)
+ }
+ getInstrumentation().waitForIdleSync()
+
+ assertThat(semaphore.tryAcquire(5, TimeUnit.SECONDS)).isTrue()
+
+ activityScenario.onActivity {
+ // notify that the IME top coordinate is less than the bottom of the expanded view,
+ // meaning it overlaps with it so we should be clipping the expanded view.
+ animationHelper.onImeTopChanged(bbev.contentBottomOnScreen - 10)
+ }
+ val outline = Outline()
+ bbev.outlineProvider.getOutline(bbev, outline)
+ assertThat(outline.mRect.bottom).isEqualTo(bbev.height - 10)
+ }
+
private fun createBubble(
key: String,
taskViewTaskController: TaskViewTaskController = mock<TaskViewTaskController>(),
@@ -273,14 +353,24 @@ class BubbleBarAnimationHelperTest {
}
private fun Bubble.initialize(container: ViewGroup): Bubble {
- getInstrumentation().runOnMainSync { container.addView(bubbleBarExpandedView) }
+ activityScenario.onActivity { container.addView(bubbleBarExpandedView) }
// Mark taskView's visible
bubbleBarExpandedView!!.onContentVisibilityChanged(true)
return this
}
private fun BubbleOverflow.initialize(container: ViewGroup): BubbleOverflow {
- getInstrumentation().runOnMainSync { container.addView(bubbleBarExpandedView) }
+ activityScenario.onActivity { container.addView(bubbleBarExpandedView) }
return this
}
+
+ class TestActivity : Activity() {
+ lateinit var container: FrameLayout
+ override fun onCreate(savedInstanceState: Bundle?) {
+ super.onCreate(savedInstanceState)
+ container = FrameLayout(applicationContext)
+ container.layoutParams = LayoutParams(50, 50)
+ setContentView(container)
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
index 5c86b321b60f..9b1645e9534c 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/bar/BubbleBarLayerViewTest.kt
@@ -37,7 +37,6 @@ import com.android.internal.statusbar.IStatusBarService
import com.android.wm.shell.R
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.TestShellExecutor
-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
@@ -54,6 +53,7 @@ import com.android.wm.shell.bubbles.animation.AnimatableScaleMatrix
import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.bubbles.storage.BubblePersistentRepository
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.SyncTransactionQueue
@@ -180,7 +180,8 @@ class BubbleBarLayerViewTest {
bubbleDataRepository,
mock<IStatusBarService>(),
windowManager,
- WindowManagerShellWrapper(mainExecutor),
+ mock<DisplayInsetsController>(),
+ mock<DisplayImeController>(),
mock<UserManager>(),
mock<LauncherApps>(),
bubbleLogger,
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 9aba3aaa3268..348f13a493b1 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
@@ -90,13 +90,15 @@ import com.android.launcher3.icons.BubbleIconFactory;
import com.android.wm.shell.Flags;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.bubbles.properties.BubbleProperties;
import com.android.wm.shell.bubbles.shortcut.BubbleShortcutHelper;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.ExternalInterfaceBinder;
import com.android.wm.shell.common.FloatingContentCoordinator;
+import com.android.wm.shell.common.ImeListener;
import com.android.wm.shell.common.RemoteCallable;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SingleInstanceRemoteListener;
@@ -106,7 +108,6 @@ import com.android.wm.shell.common.TaskStackListenerImpl;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.onehanded.OneHandedController;
import com.android.wm.shell.onehanded.OneHandedTransitionCallback;
-import com.android.wm.shell.pip.PinnedStackListenerForwarder;
import com.android.wm.shell.shared.annotations.ShellBackgroundThread;
import com.android.wm.shell.shared.annotations.ShellMainThread;
import com.android.wm.shell.shared.bubbles.BubbleBarLocation;
@@ -182,7 +183,8 @@ public class BubbleController implements ConfigurationChangeListener,
@Nullable private final BubbleStackView.SurfaceSynchronizer mSurfaceSynchronizer;
private final FloatingContentCoordinator mFloatingContentCoordinator;
private final BubbleDataRepository mDataRepository;
- private final WindowManagerShellWrapper mWindowManagerShellWrapper;
+ private final DisplayInsetsController mDisplayInsetsController;
+ private final DisplayImeController mDisplayImeController;
private final UserManager mUserManager;
private final LauncherApps mLauncherApps;
private final IStatusBarService mBarService;
@@ -291,7 +293,8 @@ public class BubbleController implements ConfigurationChangeListener,
BubbleDataRepository dataRepository,
@Nullable IStatusBarService statusBarService,
WindowManager windowManager,
- WindowManagerShellWrapper windowManagerShellWrapper,
+ DisplayInsetsController displayInsetsController,
+ DisplayImeController displayImeController,
UserManager userManager,
LauncherApps launcherApps,
BubbleLogger bubbleLogger,
@@ -318,7 +321,8 @@ public class BubbleController implements ConfigurationChangeListener,
ServiceManager.getService(Context.STATUS_BAR_SERVICE))
: statusBarService;
mWindowManager = windowManager;
- mWindowManagerShellWrapper = windowManagerShellWrapper;
+ mDisplayInsetsController = displayInsetsController;
+ mDisplayImeController = displayImeController;
mUserManager = userManager;
mFloatingContentCoordinator = floatingContentCoordinator;
mDataRepository = dataRepository;
@@ -403,11 +407,15 @@ public class BubbleController implements ConfigurationChangeListener,
mMainExecutor.execute(() -> removeBubble(bubble.getKey(), DISMISS_INVALID_INTENT));
});
- try {
- mWindowManagerShellWrapper.addPinnedStackListener(new BubblesImeListener());
- } catch (RemoteException e) {
- e.printStackTrace();
- }
+ BubblesImeListener bubblesImeListener =
+ new BubblesImeListener(mDisplayController, mContext.getDisplayId());
+ // the insets controller is notified whenever the IME visibility changes whether the IME is
+ // requested by a bubbled task or non-bubbled task. in the latter case, we need to update
+ // the position of the stack to avoid overlapping with the IME.
+ mDisplayInsetsController.addInsetsChangedListener(mContext.getDisplayId(),
+ bubblesImeListener);
+ // the ime controller is notified when the IME is requested only by a bubbled task.
+ mDisplayImeController.addPositionProcessor(bubblesImeListener);
mBubbleData.setCurrentUserId(mCurrentUserId);
@@ -2515,16 +2523,57 @@ public class BubbleController implements ConfigurationChangeListener,
return contextForUser.getPackageManager();
}
- /** PinnedStackListener that dispatches IME visibility updates to the stack. */
- //TODO(b/170442945): Better way to do this / insets listener?
- private class BubblesImeListener extends PinnedStackListenerForwarder.PinnedTaskListener {
+ /** {@link ImeListener} that dispatches IME visibility updates to the stack. */
+ private class BubblesImeListener extends ImeListener implements
+ DisplayImeController.ImePositionProcessor {
+
+ BubblesImeListener(DisplayController displayController, int displayId) {
+ super(displayController, displayId);
+ }
+
@Override
- public void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
- mBubblePositioner.setImeVisible(imeVisible, imeHeight);
+ protected void onImeVisibilityChanged(boolean imeVisible, int imeHeight) {
+ if (getDisplayId() != mContext.getDisplayId()) {
+ return;
+ }
+ // the imeHeight here is actually the ime inset; it only includes the part of the ime
+ // that overlaps with the Bubbles window. adjust it to include the bottom screen inset,
+ // so we have the total height of the ime.
+ int totalImeHeight = imeHeight + mBubblePositioner.getInsets().bottom;
+ mBubblePositioner.setImeVisible(imeVisible, totalImeHeight);
if (mStackView != null) {
mStackView.setImeVisible(imeVisible);
}
}
+
+ @Override
+ public int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
+ boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
+ if (mContext.getDisplayId() != displayId) {
+ return IME_ANIMATION_DEFAULT;
+ }
+
+ if (showing) {
+ mBubblePositioner.setImeVisible(true, hiddenTop - shownTop);
+ } else {
+ mBubblePositioner.setImeVisible(false, 0);
+ }
+ if (mStackView != null) {
+ mStackView.setImeVisible(showing);
+ }
+
+ return IME_ANIMATION_DEFAULT;
+ }
+
+ @Override
+ public void onImePositionChanged(int displayId, int imeTop, SurfaceControl.Transaction t) {
+ if (mContext.getDisplayId() != displayId) {
+ return;
+ }
+ if (mLayerView != null) {
+ mLayerView.onImeTopChanged(imeTop);
+ }
+ }
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index de85d9af127d..1a61793eab87 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -67,6 +67,11 @@ public class BubblePositioner {
private @Surface.Rotation int mRotation = Surface.ROTATION_0;
private Insets mInsets;
private boolean mImeVisible;
+ /**
+ * The height of the IME excluding the bottom inset. If the IME is 100 pixels tall and we have
+ * 20 pixels bottom inset, the IME height is adjusted to 80 to represent the overlap with the
+ * Bubbles window.
+ */
private int mImeHeight;
private Rect mPositionRect;
private int mDefaultMaxBubbles;
@@ -336,10 +341,16 @@ public class BubblePositioner {
return mImeVisible;
}
- /** Sets whether the IME is visible. **/
+ /**
+ * Sets whether the IME is visible and its height.
+ *
+ * @param visible whether the IME is visible
+ * @param height the total height of the IME from the bottom of the physical screen
+ **/
public void setImeVisible(boolean visible, int height) {
mImeVisible = visible;
- mImeHeight = height;
+ // adjust the IME to account for the height as seen by the Bubbles window
+ mImeHeight = visible ? Math.max(height - getInsets().bottom, 0) : 0;
}
private int getExpandedViewLargeScreenInsetFurthestEdge(boolean isOverflow) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
index 3188e5b9c6d2..de6d1f6c8852 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarAnimationHelper.java
@@ -30,6 +30,8 @@ import static com.android.wm.shell.bubbles.bar.BubbleBarExpandedView.TASK_VIEW_A
import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED;
import static com.android.wm.shell.shared.animation.Interpolators.EMPHASIZED_DECELERATE;
+import static java.lang.Math.max;
+
import android.animation.Animator;
import android.animation.AnimatorListenerAdapter;
import android.animation.AnimatorSet;
@@ -375,7 +377,6 @@ public class BubbleBarAnimationHelper {
return animator;
}
-
/**
* Animate the expanded bubble when it is being dragged
*/
@@ -586,6 +587,18 @@ public class BubbleBarAnimationHelper {
}
}
+ /** Handles IME position changes. */
+ public void onImeTopChanged(int imeTop) {
+ BubbleBarExpandedView bbev = getExpandedView();
+ if (bbev == null) {
+ Log.w(TAG, "Bubble bar expanded view was null when IME top changed");
+ return;
+ }
+ int bbevBottom = bbev.getContentBottomOnScreen();
+ int clip = max(bbevBottom - imeTop, 0);
+ bbev.updateBottomClip(clip);
+ }
+
private @Nullable BubbleBarExpandedView getExpandedView() {
BubbleViewProvider bubble = mExpandedBubble;
if (bubble != null) {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 65c929ab6fb4..e073b02dc630 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -137,6 +137,7 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
private Executor mBackgroundExecutor;
private final Rect mSampleRect = new Rect();
private final int[] mLoc = new int[2];
+ private final Rect mTempBounds = new Rect();
/** Height of the caption inset at the top of the TaskView */
private int mCaptionHeight;
@@ -161,6 +162,9 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
private boolean mIsAnimating;
private boolean mIsDragging;
+ private boolean mIsClipping = false;
+ private int mBottomClip = 0;
+
/** An enum value that tracks the visibility state of the task view */
private enum TaskViewVisibilityState {
/** The task view is going away, and we're waiting for the surface to be destroyed. */
@@ -203,7 +207,8 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
setOutlineProvider(new ViewOutlineProvider() {
@Override
public void getOutline(View view, Outline outline) {
- outline.setRoundRect(0, 0, view.getWidth(), view.getHeight(), mCurrentCornerRadius);
+ outline.setRoundRect(0, 0, view.getWidth(), view.getHeight() - mBottomClip,
+ mCurrentCornerRadius);
}
});
// Set a touch sink to ensure that clicks on the caption area do not propagate to the parent
@@ -661,6 +666,52 @@ public class BubbleBarExpandedView extends FrameLayout implements BubbleTaskView
}
}
+ /** The y coordinate of the bottom of the expanded view. */
+ public int getContentBottomOnScreen() {
+ if (mOverflowView != null) {
+ mOverflowView.getBoundsOnScreen(mTempBounds);
+ }
+ if (mTaskView != null) {
+ mTaskView.getBoundsOnScreen(mTempBounds);
+ }
+ // return the bottom of the content rect, adjusted for insets so the result is in screen
+ // coordinate
+ return mTempBounds.bottom + mPositioner.getInsets().top;
+ }
+
+ /** Update the amount by which to clip the expanded view at the bottom. */
+ public void updateBottomClip(int bottomClip) {
+ mBottomClip = bottomClip;
+ onClipUpdate();
+ }
+
+ private void onClipUpdate() {
+ if (mBottomClip == 0) {
+ if (mIsClipping) {
+ mIsClipping = false;
+ if (mTaskView != null) {
+ mTaskView.setClipBounds(null);
+ mTaskView.setEnableSurfaceClipping(false);
+ }
+ invalidateOutline();
+ }
+ } else {
+ if (!mIsClipping) {
+ mIsClipping = true;
+ if (mTaskView != null) {
+ mTaskView.setEnableSurfaceClipping(true);
+ }
+ }
+ invalidateOutline();
+ if (mTaskView != null) {
+ Rect clipBounds = new Rect(0, 0,
+ mTaskView.getWidth(),
+ mTaskView.getHeight() - mBottomClip);
+ mTaskView.setClipBounds(clipBounds);
+ }
+ }
+ }
+
private void recreateRegionSamplingHelper() {
if (mRegionSamplingHelper != null) {
mRegionSamplingHelper.stopAndDestroy();
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 88f34f3043e1..eaa0bd250fc4 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
@@ -424,6 +424,13 @@ public class BubbleBarLayerView extends FrameLayout
}
}
+ /** Handles IME position changes. */
+ public void onImeTopChanged(int imeTop) {
+ if (mIsExpanded) {
+ mAnimationHelper.onImeTopChanged(imeTop);
+ }
+ }
+
/**
* Log the event only if {@link #mExpandedBubble} is a {@link Bubble}.
* <p>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
index 9ebb7f5aa270..eb1e72790a25 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/DisplayImeController.java
@@ -419,8 +419,12 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
// already (e.g., when focussing an editText in activity B, while and editText in
// activity A is focussed), we will not get a call of #insetsControlChanged, and
// therefore have to start the show animation from here
- startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */,
- statsToken);
+ if (visible || mImeShowing) {
+ // only start the animation if we're either already showing or becoming visible.
+ // otherwise starting another hide animation causes flickers.
+ startAnimation(mImeRequestedVisible /* show */, false /* forceRestart */,
+ statsToken);
+ }
// In case of a hide, the statsToken should not been send yet (as the animation
// is still ongoing). It will be sent at the end of the animation
@@ -723,6 +727,10 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
* Allows other things to synchronize with the ime position
*/
public interface ImePositionProcessor {
+
+ /** Default animation flags. */
+ int IME_ANIMATION_DEFAULT = 0;
+
/**
* Indicates that ime shouldn't animate alpha. It will always be opaque. Used when stuff
* behind the IME shouldn't be visible (for example during split-screen adjustment where
@@ -732,6 +740,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
/** @hide */
@IntDef(prefix = {"IME_ANIMATION_"}, value = {
+ IME_ANIMATION_DEFAULT,
IME_ANIMATION_NO_ALPHA,
})
@interface ImeAnimationFlags {
@@ -758,7 +767,7 @@ public class DisplayImeController implements DisplayController.OnDisplaysChanged
@ImeAnimationFlags
default int onImeStartPositioning(int displayId, int hiddenTop, int shownTop,
boolean showing, boolean isFloating, SurfaceControl.Transaction t) {
- return 0;
+ return IME_ANIMATION_DEFAULT;
}
/**
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt
index a34d7bed497b..8851b6a2107d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/common/ImeListener.kt
@@ -22,8 +22,8 @@ import android.view.InsetsState
import com.android.wm.shell.common.DisplayInsetsController.OnInsetsChangedListener
abstract class ImeListener(
- private val mDisplayController: DisplayController,
- private val mDisplayId: Int
+ private val displayController: DisplayController,
+ val displayId: Int
) : OnInsetsChangedListener {
// The last insets state
private val mInsetsState = InsetsState()
@@ -36,17 +36,11 @@ abstract class ImeListener(
// Get the stable bounds that account for display cutout and system bars to calculate the
// relative IME height
- val layout = mDisplayController.getDisplayLayout(mDisplayId)
- if (layout == null) {
- return
- }
+ val layout = displayController.getDisplayLayout(displayId) ?: return
layout.getStableBounds(mTmpBounds)
- val wasVisible = getImeVisibilityAndHeight(mInsetsState).first
- val oldHeight = getImeVisibilityAndHeight(mInsetsState).second
-
- val isVisible = getImeVisibilityAndHeight(insetsState).first
- val newHeight = getImeVisibilityAndHeight(insetsState).second
+ val (wasVisible, oldHeight) = getImeVisibilityAndHeight(mInsetsState)
+ val (isVisible, newHeight) = getImeVisibilityAndHeight(insetsState)
mInsetsState.set(insetsState, true)
if (wasVisible != isVisible || oldHeight != newHeight) {
@@ -54,8 +48,7 @@ abstract class ImeListener(
}
}
- private fun getImeVisibilityAndHeight(
- insetsState: InsetsState): Pair<Boolean, Int> {
+ private fun getImeVisibilityAndHeight(insetsState: InsetsState): Pair<Boolean, Int> {
val source = insetsState.peekSource(InsetsSource.ID_IME)
val frame = if (source != null && source.isVisible) source.frame else null
val height = if (frame != null) mTmpBounds.bottom - frame.top else 0
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 d02c6b05e5b6..d8c7f4cbb698 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
@@ -47,7 +47,6 @@ import com.android.launcher3.icons.IconProvider;
import com.android.window.flags.Flags;
import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.activityembedding.ActivityEmbeddingController;
import com.android.wm.shell.apptoweb.AppToWebGenericLinksParser;
import com.android.wm.shell.apptoweb.AssistContentRequester;
@@ -233,7 +232,8 @@ public abstract class WMShellModule {
FloatingContentCoordinator floatingContentCoordinator,
IStatusBarService statusBarService,
WindowManager windowManager,
- WindowManagerShellWrapper windowManagerShellWrapper,
+ DisplayInsetsController displayInsetsController,
+ DisplayImeController displayImeController,
UserManager userManager,
LauncherApps launcherApps,
TaskStackListenerImpl taskStackListener,
@@ -265,7 +265,8 @@ public abstract class WMShellModule {
new BubblePersistentRepository(context)),
statusBarService,
windowManager,
- windowManagerShellWrapper,
+ displayInsetsController,
+ displayImeController,
userManager,
launcherApps,
logger,
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopViaMenuOfStaticOverviewTaskTest.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopViaMenuOfStaticOverviewTaskTest.kt
new file mode 100644
index 000000000000..fac749be40f1
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/functional/EnterDesktopViaMenuOfStaticOverviewTaskTest.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.functional
+
+import android.platform.test.annotations.Postsubmit
+import com.android.wm.shell.scenarios.EnterDesktopViaMenuOfStaticOverviewTask
+import org.junit.runner.RunWith
+import org.junit.runners.BlockJUnit4ClassRunner
+
+/* Functional test for [EnterDesktopViaMenuOfStaticOverviewTask]. */
+@RunWith(BlockJUnit4ClassRunner::class)
+@Postsubmit
+class EnterDesktopViaMenuOfStaticOverviewTaskTest : EnterDesktopViaMenuOfStaticOverviewTask()
diff --git a/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopViaMenuOfStaticOverviewTask.kt b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopViaMenuOfStaticOverviewTask.kt
new file mode 100644
index 000000000000..57647d6aa5df
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/e2e/desktopmode/scenarios/src/com/android/wm/shell/scenarios/EnterDesktopViaMenuOfStaticOverviewTask.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.scenarios
+
+import android.app.Instrumentation
+import android.tools.NavBar
+import android.tools.Rotation
+import android.tools.traces.parsers.WindowManagerStateHelper
+import androidx.test.platform.app.InstrumentationRegistry
+import androidx.test.uiautomator.UiDevice
+import com.android.launcher3.tapl.LauncherInstrumentation
+import com.android.server.wm.flicker.helpers.MailAppHelper
+import com.android.window.flags.Flags
+import com.android.wm.shell.Utils
+import org.junit.After
+import org.junit.Assume
+import org.junit.Before
+import org.junit.Ignore
+import org.junit.Rule
+import org.junit.Test
+
+@Ignore("Base Test Class")
+abstract class EnterDesktopViaMenuOfStaticOverviewTask constructor() {
+
+ private val instrumentation: Instrumentation = InstrumentationRegistry.getInstrumentation()
+ private val tapl = LauncherInstrumentation()
+ private val wmHelper = WindowManagerStateHelper(instrumentation)
+ private val device = UiDevice.getInstance(instrumentation)
+ private val mailApp = MailAppHelper(instrumentation)
+
+ @Rule
+ @JvmField
+ val testSetupRule = Utils.testSetupRule(NavBar.MODE_GESTURAL, Rotation.ROTATION_0)
+
+ @Before
+ fun setup() {
+ Assume.assumeTrue(Flags.enableDesktopWindowingMode() && tapl.isTablet)
+ // Clear all tasks
+ val overview = tapl.goHome().switchToOverview()
+ if (overview.hasTasks()) {
+ overview.dismissAllTasks()
+ }
+ mailApp.open()
+ tapl.goHome().switchToOverview()
+ }
+
+ @Test
+ open fun desktopViaMenuOfStaticOverviewTask() {
+ tapl.overview.getCurrentTask().tapMenu().tapDesktopMenuItem()
+ }
+
+ @After
+ fun teardown() {
+ mailApp.exit()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
index f40edaebec5e..6e0dcdb21fa8 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
+++ b/libs/WindowManager/Shell/tests/flicker/pip/Android.bp
@@ -115,6 +115,11 @@ test_module_config {
"com.android.wm.shell.flicker.pip.PipPinchInTest",
"com.android.wm.shell.flicker.pip.SetRequestedOrientationWhilePinned",
"com.android.wm.shell.flicker.pip.ShowPipAndRotateDisplay",
+ "com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfAutoEnterPipOnGoToHomeTest",
+ "com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfEnterPipOnUserLeaveHintTest",
+ "com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfEnterPipViaAppUiButtonTest",
+ "com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfExitPipToAppViaExpandButtonTest",
+ "com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfExitPipToAppViaIntentTest",
],
test_suites: ["device-tests"],
}
@@ -266,12 +271,7 @@ test_module_config {
test_suites: ["device-tests"],
}
-test_module_config {
- name: "WMShellFlickerTestsPip-nonMatchParent",
- base: "WMShellFlickerTestsPip",
- include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.*"],
- test_suites: ["device-tests"],
-}
+// Not-match Parent test cases
test_module_config {
name: "WMShellFlickerTestsPip-BottomHalfExitPipToAppViaExpandButtonTest",
@@ -287,5 +287,26 @@ test_module_config {
test_suites: ["device-tests"],
}
+test_module_config {
+ name: "WMShellFlickerTestsPip-BottomHalfAutoEnterPipOnGoToHomeTest",
+ base: "WMShellFlickerTestsPip",
+ include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfAutoEnterPipOnGoToHomeTest"],
+ test_suites: ["device-tests"],
+}
+
+test_module_config {
+ name: "WMShellFlickerTestsPip-BottomHalfEnterPipOnUserLeaveHintTest",
+ base: "WMShellFlickerTestsPip",
+ include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfEnterPipOnUserLeaveHintTest"],
+ test_suites: ["device-tests"],
+}
+
+test_module_config {
+ name: "WMShellFlickerTestsPip-BottomHalfEnterPipViaAppUiButtonTest",
+ base: "WMShellFlickerTestsPip",
+ include_filters: ["com.android.wm.shell.flicker.pip.nonmatchparent.BottomHalfEnterPipViaAppUiButtonTest"],
+ test_suites: ["device-tests"],
+}
+
// End breakdowns for WMShellFlickerTestsPip module
////////////////////////////////////////////////////////////////////////////////
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
index 609a2849f915..84d53d59e7df 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/AutoEnterPipOnGoToHomeTest.kt
@@ -24,20 +24,16 @@ import android.platform.test.annotations.RequiresFlagsDisabled
import android.tools.flicker.junit.FlickerParametersRunnerFactory
import android.tools.flicker.legacy.FlickerBuilder
import android.tools.flicker.legacy.LegacyFlickerTest
-import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder
-import android.tools.flicker.subject.exceptions.IncorrectRegionException
-import android.tools.flicker.subject.layers.LayerSubject
import com.android.server.wm.flicker.helpers.PipAppHelper
import com.android.wm.shell.Flags
import com.android.wm.shell.flicker.pip.common.EnterPipTransition
+import com.android.wm.shell.flicker.pip.common.widthNotSmallerThan
import org.junit.Assume
import org.junit.FixMethodOrder
import org.junit.Test
import org.junit.runner.RunWith
import org.junit.runners.MethodSorters
import org.junit.runners.Parameterized
-import kotlin.math.abs
-
/**
* Test entering pip from an app via auto-enter property when navigating to home.
@@ -77,22 +73,6 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran
}
}
- override val defaultTeardown: FlickerBuilder.() -> Unit = { teardown { pipApp.exit(wmHelper) } }
-
- private val widthNotSmallerThan: LayerSubject.(LayerSubject) -> Unit = {
- val width = visibleRegion.region.bounds.width()
- val otherWidth = it.visibleRegion.region.bounds.width()
- if (width < otherWidth && abs(width - otherWidth) > EPSILON) {
- val errorMsgBuilder =
- ExceptionMessageBuilder()
- .forSubject(this)
- .forIncorrectRegion("width. $width smaller than $otherWidth")
- .setExpected(width)
- .setActual(otherWidth)
- throw IncorrectRegionException(errorMsgBuilder)
- }
- }
-
@Postsubmit
@Test
override fun pipLayerReduces() {
@@ -161,9 +141,4 @@ open class AutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) : EnterPipTran
override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
super.visibleLayersShownMoreThanOneConsecutiveEntry()
}
-
- companion object {
- // TODO(b/363080056): A margin of error allowed on certain layer size calculations.
- const val EPSILON = 1
- }
}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
index 4399a237bcbb..38f37b4bc1d2 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/EnterPipOnUserLeaveHintTest.kt
@@ -88,6 +88,7 @@ class EnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) : EnterPipTransiti
super.pipOverlayLayerAppearThenDisappear()
}
+ // TODO(b/385086051): check if we can remove optional = true in the test.
@Presubmit
@Test
fun pipAppWindowVisibleChanges() {
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipUtils.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipUtils.kt
new file mode 100644
index 000000000000..3b98d9b99130
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/common/PipUtils.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.wm.shell.flicker.pip.common
+
+import android.tools.flicker.subject.exceptions.ExceptionMessageBuilder
+import android.tools.flicker.subject.exceptions.IncorrectRegionException
+import android.tools.flicker.subject.layers.LayerSubject
+import kotlin.math.abs
+
+// TODO(b/363080056): A margin of error allowed on certain layer size calculations.
+const val EPSILON = 1
+
+internal val widthNotSmallerThan: LayerSubject.(LayerSubject) -> Unit = {
+ val width = visibleRegion.region.bounds.width()
+ val otherWidth = it.visibleRegion.region.bounds.width()
+ if (width < otherWidth && abs(width - otherWidth) > EPSILON) {
+ val errorMsgBuilder =
+ ExceptionMessageBuilder()
+ .forSubject(this)
+ .forIncorrectRegion("width. $width smaller than $otherWidth")
+ .setExpected(width)
+ .setActual(otherWidth)
+ throw IncorrectRegionException(errorMsgBuilder)
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfAutoEnterPipOnGoToHomeTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfAutoEnterPipOnGoToHomeTest.kt
new file mode 100644
index 000000000000..89f02266ce35
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfAutoEnterPipOnGoToHomeTest.kt
@@ -0,0 +1,170 @@
+/*
+ * 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.flicker.pip.nonmatchparent
+
+import android.platform.test.annotations.Postsubmit
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresDevice
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import androidx.test.filters.FlakyTest
+import com.android.window.flags.Flags
+import com.android.wm.shell.flicker.pip.common.widthNotSmallerThan
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from an app with non-match parent layout via auto-enter property when
+ * navigating to home.
+ *
+ * To run this test: `atest WMShellFlickerTestsPip:BottomHalfAutoEnterPipOnGoToHomeTest`
+ *
+ * Actions:
+ * ```
+ * Launch [BottomHalfPipLaunchingActivity] in full screen
+ * Launch [BottomHalfPipActivity] with bottom half layout
+ * Select "Auto-enter PiP" radio button
+ * Press Home button or swipe up to go Home and put [BottomHalfPipActivity] in pip mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. All assertions are inherited from [EnterPipTest]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+// TODO(b/380796448): re-enable tests after the support of non-match parent PIP animation for PIP2.
+@RequiresFlagsDisabled(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2)
+@RequiresFlagsEnabled(Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY)
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class BottomHalfAutoEnterPipOnGoToHomeTest(flicker: LegacyFlickerTest) :
+ BottomHalfEnterPipTransition(flicker) {
+
+ override val thisTransition: FlickerBuilder.() -> Unit = { transitions {
+ tapl.goHome()
+ pipApp.waitForPip(wmHelper)
+ } }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableAutoEnterForPipActivity()
+ }
+ }
+
+ @FlakyTest(bugId = 289943985)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() {
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+ }
+
+ /* Gestural Navigation */
+
+ /**
+ * Checks that [pipApp] window's width is first decreasing then increasing.
+ *
+ * In gestural navigation mode, auto entering PiP can initially make the layer smaller before it
+ * gets larger.
+ * This tests verifies the width of the PiP layer first decreases and then increases due to
+ * size and scale animations going to different directions.
+ *
+ * Note that we still allow a margin of error of 1px, since around the time
+ * of handoff between gesture nav task view simulator and
+ * SwipePipToHomeAnimator, crop can get a bit smaller and scale can get a
+ * bit larger if swiped aggressively - this can produce off-by-1 errors for
+ * width too.
+ */
+ @Postsubmit
+ @Test
+ fun pipLayerWidthDecreasesThenIncreases() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ var previousLayer = pipLayerList[0]
+ var currentLayer = previousLayer
+ var i = 0
+ invoke("layer area is decreasing") {
+ if (i < pipLayerList.size - 1) {
+ previousLayer = currentLayer
+ currentLayer = pipLayerList[++i]
+ previousLayer.widthNotSmallerThan(currentLayer)
+ }
+ }.then().invoke("layer are is increasing", true /* isOptional */) {
+ if (i < pipLayerList.size - 1) {
+ previousLayer = currentLayer
+ currentLayer = pipLayerList[++i]
+ currentLayer.widthNotSmallerThan(previousLayer)
+ }
+ }
+ }
+ }
+
+ /* 3-button Navigation */
+
+ /**
+ * The PIP layer reduces continuously in 3-Button navigation mode.
+ */
+ @Postsubmit
+ @Test
+ override fun pipLayerReduces() {
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.notBiggerThan(previous.visibleRegion.region)
+ }
+ }
+ }
+
+ /** Checks that [pipApp] window is animated towards default position in right bottom corner */
+ @FlakyTest(bugId = 255578530)
+ @Test
+ fun pipLayerMovesTowardsRightBottomCorner() {
+ // in gestural nav the swipe makes PiP first go upwards
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ flicker.assertLayers {
+ val pipLayerList = this.layers { pipApp.layerMatchesAnyOf(it) && it.isVisible }
+ // Pip animates towards the right bottom corner, but because it is being resized at the
+ // same time, it is possible it shrinks first quickly below the default position and get
+ // moved up after that in just few last frames
+ pipLayerList.zipWithNext { previous, current ->
+ current.visibleRegion.isToTheRightBottom(previous.visibleRegion.region, 3)
+ }
+ }
+ }
+
+ @Presubmit
+ @Test
+ override fun focusChanges() {
+ // in gestural nav the focus goes to different activity on swipe up
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.focusChanges()
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipOnUserLeaveHintTest.kt
new file mode 100644
index 000000000000..8642b6c779e4
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipOnUserLeaveHintTest.kt
@@ -0,0 +1,168 @@
+/*
+ * 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.flicker.pip.nonmatchparent
+
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresDevice
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import com.android.window.flags.Flags
+import org.junit.Assume
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from an app with bottom half layout via [onUserLeaveHint] and by navigating to
+ * home.
+ *
+ * To run this test: `atest WMShellFlickerTestsPip:BottomHalfEnterPipOnUserLeaveHintTest`
+ *
+ * Actions:
+ * ```
+ * Launch [BottomHalfPipLaunchingActivity] in full screen
+ * Launch [BottomHalfPipActivity] with bottom half layout
+ * Select "Via code behind" radio button
+ * Press Home button or swipe up to go Home and put [pipApp] in pip mode
+ * ```
+ * Notes:
+ * ```
+ * 1. All assertions are inherited from [EnterPipTest]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+// TODO(b/380796448): re-enable tests after the support of non-match parent PIP animation for PIP2.
+@RequiresFlagsDisabled(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2)
+@RequiresFlagsEnabled(Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY)
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class BottomHalfEnterPipOnUserLeaveHintTest(flicker: LegacyFlickerTest) :
+ BottomHalfEnterPipTransition(flicker)
+{
+ override val thisTransition: FlickerBuilder.() -> Unit = { transitions {
+ tapl.goHome()
+ pipApp.waitForPip(wmHelper)
+ } }
+
+ override val defaultEnterPip: FlickerBuilder.() -> Unit = {
+ setup {
+ pipApp.launchViaIntent(wmHelper)
+ pipApp.enableEnterPipOnUserLeaveHint()
+ }
+ }
+
+ // gestural navigation
+
+ // TODO(b/385086051): check if we can remove optional = true in the test.
+ @Presubmit
+ @Test
+ fun pipAppWindowVisibleChanges() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ flicker.assertWm {
+ this.isAppWindowVisible(pipApp)
+ .then()
+ .isAppWindowInvisible(pipApp, isOptional = true)
+ .then()
+ .isAppWindowVisible(pipApp, isOptional = true)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun pipAppLayerVisibleChanges() {
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayers {
+ this.isVisible(pipApp).then().isInvisible(pipApp).then().isVisible(pipApp)
+ }
+ }
+
+ @Presubmit
+ @Test
+ fun pipLayerRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeTrue(flicker.scenario.isGesturalNavigation)
+ // pip layer in gesture nav will disappear during transition
+ flicker.assertLayersStart { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+ flicker.assertLayersEnd { this.visibleRegion(pipApp).coversAtMost(displayBounds) }
+ }
+
+ // 3-button navigation
+
+ @Presubmit
+ @Test
+ override fun pipAppWindowAlwaysVisible() {
+ // In gestural nav the pip will first move behind home and then above home. The visual
+ // appearance visible->invisible->visible is asserted by pipAppLayerAlwaysVisible().
+ // But the internal states of activity don't need to follow that, such as a temporary
+ // visibility state can be changed quickly outside a transaction so the test doesn't
+ // detect that. Hence, skip the case to avoid restricting the internal implementation.
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppWindowAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipAppLayerAlwaysVisible() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipAppLayerAlwaysVisible()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipOverlayLayerAppearThenDisappear() {
+ // no overlay in gesture nav for non-auto enter PiP transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipOverlayLayerAppearThenDisappear()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipLayerReduces() {
+ // in gestural nav the pip enters through alpha animation
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipLayerReduces()
+ }
+
+ @Presubmit
+ @Test
+ override fun focusChanges() {
+ // in gestural nav the focus goes to different activity on swipe up
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.focusChanges()
+ }
+
+ @Presubmit
+ @Test
+ override fun pipLayerOrOverlayRemainInsideVisibleBounds() {
+ // pip layer in gesture nav will disappear during transition
+ Assume.assumeFalse(flicker.scenario.isGesturalNavigation)
+ super.pipLayerOrOverlayRemainInsideVisibleBounds()
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipTransition.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipTransition.kt
new file mode 100644
index 000000000000..a455de96b25d
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipTransition.kt
@@ -0,0 +1,46 @@
+/*
+ * 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.flicker.pip.nonmatchparent
+
+import android.platform.test.annotations.Presubmit
+import android.tools.flicker.legacy.LegacyFlickerTest
+import com.android.server.wm.flicker.helpers.BottomHalfPipAppHelper
+import com.android.server.wm.flicker.helpers.PipAppHelper
+import com.android.wm.shell.flicker.pip.common.EnterPipTransition
+import org.junit.Test
+
+/**
+ * The base class to test enter PIP animation on bottom half activity.
+ */
+abstract class BottomHalfEnterPipTransition(flicker: LegacyFlickerTest) :
+ EnterPipTransition(flicker)
+{
+ override val pipApp: PipAppHelper = BottomHalfPipAppHelper(
+ instrumentation,
+ useLaunchingActivity = true
+ )
+
+ @Presubmit
+ @Test
+ override fun pipWindowRemainInsideVisibleBounds() {
+ // We only verify the start and end because we update the layout when
+ // the BottomHalfPipActivity goes to PIP mode, which may lead to intermediate state that
+ // the window frame may be out of the display frame.
+ flicker.assertWmStart { visibleRegion(pipApp).coversAtMost(displayBounds) }
+ flicker.assertLayersEnd { visibleRegion(pipApp).coversAtMost(displayBounds) }
+ }
+} \ No newline at end of file
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipViaAppUiButtonTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipViaAppUiButtonTest.kt
new file mode 100644
index 000000000000..cfdd4e550504
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/nonmatchparent/BottomHalfEnterPipViaAppUiButtonTest.kt
@@ -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.
+ */
+
+package com.android.wm.shell.flicker.pip.nonmatchparent
+
+import android.platform.test.annotations.Presubmit
+import android.platform.test.annotations.RequiresDevice
+import android.platform.test.annotations.RequiresFlagsDisabled
+import android.platform.test.annotations.RequiresFlagsEnabled
+import android.tools.flicker.junit.FlickerParametersRunnerFactory
+import android.tools.flicker.legacy.FlickerBuilder
+import android.tools.flicker.legacy.LegacyFlickerTest
+import android.tools.traces.parsers.toFlickerComponent
+import com.android.server.wm.flicker.testapp.ActivityOptions.BottomHalfPip.LAUNCHING_APP_COMPONENT
+import com.android.window.flags.Flags
+import org.junit.FixMethodOrder
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.junit.runners.MethodSorters
+import org.junit.runners.Parameterized
+
+/**
+ * Test entering pip from an app by interacting with the app UI
+ *
+ * To run this test: `atest WMShellFlickerTestsPip:BottomHalfEnterPipViaAppUiButtonTest`
+ *
+ * Actions:
+ * ```
+ * Launch [BottomHalfPipLaunchingActivity] in full screen
+ * Launch [BottomHalfPipActivity] with bottom half layout
+ * Press an "enter pip" button to put [pipApp] in pip mode
+ * ```
+ *
+ * Notes:
+ * ```
+ * 1. Some default assertions (e.g., nav bar, status bar and screen covered)
+ * are inherited from [PipTransition]
+ * 2. Part of the test setup occurs automatically via
+ * [android.tools.flicker.legacy.runner.TransitionRunner],
+ * including configuring navigation mode, initial orientation and ensuring no
+ * apps are running before setup
+ * ```
+ */
+// TODO(b/380796448): re-enable tests after the support of non-match parent PIP animation for PIP2.
+@RequiresFlagsDisabled(com.android.wm.shell.Flags.FLAG_ENABLE_PIP2)
+@RequiresFlagsEnabled(Flags.FLAG_BETTER_SUPPORT_NON_MATCH_PARENT_ACTIVITY)
+@RequiresDevice
+@RunWith(Parameterized::class)
+@Parameterized.UseParametersRunnerFactory(FlickerParametersRunnerFactory::class)
+@FixMethodOrder(MethodSorters.NAME_ASCENDING)
+class BottomHalfEnterPipViaAppUiButtonTest(flicker: LegacyFlickerTest) :
+ BottomHalfEnterPipTransition(flicker)
+{
+ override val thisTransition: FlickerBuilder.() -> Unit = {
+ transitions { pipApp.clickEnterPipButton(wmHelper) }
+ }
+
+ /**
+ * Checks if the focus changes to the launching activity behind when the bottom half [pipApp]
+ * goes to PIP mode.
+ */
+ @Presubmit
+ @Test
+ override fun focusChanges() {
+ flicker.assertEventLog {
+ this.focusChanges(
+ pipApp.packageName,
+ LAUNCHING_APP_COMPONENT.packageName
+ )
+ }
+ }
+
+ @Presubmit
+ @Test
+ override fun launcherLayerBecomesVisible() {
+ // Disable the test since the background activity is BottomHalfPipLaunchingActivity.
+ }
+
+ /**
+ * Checks if the launching activity behind the bottom half [pipApp] is always visible during
+ * the transition.
+ */
+ @Presubmit
+ @Test
+ fun launchingAppLayerAlwaysVisible() {
+ flicker.assertLayers { isVisible(LAUNCHING_APP_COMPONENT.toFlickerComponent()) }
+ }
+} \ No newline at end of file
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 1f2eaa6757e8..eeb83df48ab5 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
@@ -33,10 +33,10 @@ import com.android.launcher3.icons.BubbleIconFactory
import com.android.wm.shell.ShellTaskOrganizer
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.TestShellExecutor
-import com.android.wm.shell.WindowManagerShellWrapper
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView
import com.android.wm.shell.bubbles.properties.BubbleProperties
import com.android.wm.shell.common.DisplayController
+import com.android.wm.shell.common.DisplayImeController
import com.android.wm.shell.common.DisplayInsetsController
import com.android.wm.shell.common.FloatingContentCoordinator
import com.android.wm.shell.common.ShellExecutor
@@ -123,7 +123,8 @@ class BubbleViewInfoTest : ShellTestCase() {
mock<BubbleDataRepository>(),
mock<IStatusBarService>(),
windowManager,
- WindowManagerShellWrapper(mainExecutor),
+ mock<DisplayInsetsController>(),
+ mock<DisplayImeController>(),
mock<UserManager>(),
mock<LauncherApps>(),
bubbleLogger,
diff --git a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt
index 00bad5203f07..220614bf064f 100644
--- a/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt
+++ b/packages/SettingsLib/Preference/testutils/com/android/settingslib/preference/PreferenceBindingTestUtils.kt
@@ -19,18 +19,27 @@ package com.android.settingslib.preference
import android.content.Context
import androidx.annotation.VisibleForTesting
import androidx.preference.Preference
+import androidx.preference.PreferenceScreen
import com.android.settingslib.metadata.PersistentPreference
import com.android.settingslib.metadata.PreferenceMetadata
/** Creates [Preference] widget and binds with metadata. */
+@Suppress("UNCHECKED_CAST")
@VisibleForTesting
-fun <P : Preference> PreferenceMetadata.createAndBindWidget(context: Context): P {
+fun <P : Preference> PreferenceMetadata.createAndBindWidget(
+ context: Context,
+ preferenceScreen: PreferenceScreen? = null,
+): P {
val binding = PreferenceBindingFactory.defaultFactory.getPreferenceBinding(this)!!
return (binding.createWidget(context) as P).also {
if (this is PersistentPreference<*>) {
- storage(context)?.let { keyValueStore ->
+ storage(context).let { keyValueStore ->
it.preferenceDataStore = PreferenceDataStoreAdapter(keyValueStore)
}
+ // Attach preference to preference screen, otherwise `Preference.performClick` does not
+ // interact with underlying datastore
+ (preferenceScreen ?: PreferenceScreenFactory(context).getOrCreatePreferenceScreen())
+ .addPreference(it)
}
binding.bind(it, this)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 957fdf7bcd8f..58bfd08dfdbc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -340,11 +340,12 @@ fun ContentScope.NotificationScrollingStack(
// set the bounds to null when the scrim disappears
DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } }
- val minScrimTop = with(density) { ShadeHeader.Dimensions.CollapsedHeight.toPx() }
+ // Top position if the scrim, when it is fully expanded.
+ val minScrimTop = ShadeHeader.Dimensions.CollapsedHeight
// The minimum offset for the scrim. The scrim is considered fully expanded when it
// is at this offset.
- val minScrimOffset: () -> Float = { minScrimTop - maxScrimTop() }
+ val minScrimOffset: () -> Float = { with(density) { minScrimTop.toPx() } - maxScrimTop() }
// The height of the scrim visible on screen when it is in its resting (collapsed) state.
val minVisibleScrimHeight: () -> Float = {
@@ -563,6 +564,7 @@ fun ContentScope.NotificationScrollingStack(
}
.thenIf(shouldShowScrim) { Modifier.background(scrimBackgroundColor) }
.thenIf(shouldFillMaxSize) { Modifier.fillMaxSize() }
+ .thenIf(supportNestedScrolling) { Modifier.padding(bottom = minScrimTop) }
.debugBackground(viewModel, DEBUG_BOX_COLOR)
) {
Column(
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index a53c6b29338f..aefe83b781e0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -239,7 +239,7 @@ private fun SceneScope.ShadeScene(
modifier = modifier,
shadeSession = shadeSession,
)
- is ShadeMode.Dual -> error("Dual shade is not yet implemented!")
+ is ShadeMode.Dual -> error("Dual shade is implemented separately as an overlay.")
}
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
index 8777ff924bc1..495fdafad26d 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateSharedAsState.kt
@@ -434,7 +434,8 @@ private class AnimatedStateImpl<T, Delta>(
if (element != null) {
layoutImpl.elements[element]?.let { element ->
elementState(
- layoutImpl.state.transitionStates,
+ listOf(layoutImpl.state.transitionStates),
+ elementKey = element.key,
isInContent = { it in element.stateByContent },
)
as? TransitionState.Transition
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
index 16b4322411ac..b0d9fcd4344b 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt
@@ -45,7 +45,11 @@ import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.Constraints
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.unit.round
+import androidx.compose.ui.util.fastAll
+import androidx.compose.ui.util.fastAny
import androidx.compose.ui.util.fastCoerceIn
+import androidx.compose.ui.util.fastForEach
+import androidx.compose.ui.util.fastForEachIndexed
import androidx.compose.ui.util.fastForEachReversed
import androidx.compose.ui.util.lerp
import com.android.compose.animation.scene.content.Content
@@ -92,7 +96,17 @@ internal class Element(val key: ElementKey) {
/** The last and target state of this element in a given content. */
@Stable
- class State(val content: ContentKey) {
+ class State(
+ /**
+ * A list of contents where this element state finds itself in. The last content is the
+ * content of the STL which is actually responsible to compose and place this element. The
+ * other contents (if any) are the ancestors. The ancestors do not actually place this
+ * element but the element is part of the ancestors scene as part of a NestedSTL. The state
+ * can be accessed by ancestor transitions to read the properties of this element to compute
+ * transformations.
+ */
+ val contents: List<ContentKey>
+ ) {
/**
* The *target* state of this element in this content, i.e. the state of this element when
* we are idle on this content.
@@ -158,7 +172,8 @@ internal fun Modifier.element(
// layout/drawing.
// TODO(b/341072461): Revert this and read the current transitions in ElementNode directly once
// we can ensure that SceneTransitionLayoutImpl will compose new contents first.
- val currentTransitionStates = layoutImpl.state.transitionStates
+ val currentTransitionStates = getAllNestedTransitionStates(layoutImpl)
+
return thenIf(layoutImpl.state.isElevationPossible(content.key, key)) {
Modifier.maybeElevateInContent(layoutImpl, content, key, currentTransitionStates)
}
@@ -166,11 +181,26 @@ internal fun Modifier.element(
.testTag(key.testTag)
}
+/**
+ * Returns the transition states of all ancestors + the transition state of the current STL. The
+ * last element is the transition state of the local STL (the one with the highest nestingDepth).
+ *
+ * @return Each transition state of a STL is a List and this is a list of all the states.
+ */
+internal fun getAllNestedTransitionStates(
+ layoutImpl: SceneTransitionLayoutImpl
+): List<List<TransitionState>> {
+ return buildList {
+ layoutImpl.ancestors.fastForEach { add(it.layoutImpl.state.transitionStates) }
+ add(layoutImpl.state.transitionStates)
+ }
+}
+
private fun Modifier.maybeElevateInContent(
layoutImpl: SceneTransitionLayoutImpl,
content: Content,
key: ElementKey,
- transitionStates: List<TransitionState>,
+ transitionStates: List<List<TransitionState>>,
): Modifier {
fun isSharedElement(
stateByContent: Map<ContentKey, Element.State>,
@@ -192,12 +222,12 @@ private fun Modifier.maybeElevateInContent(
content.containerState,
enabled = {
val stateByContent = layoutImpl.elements.getValue(key).stateByContent
- val state = elementState(transitionStates, isInContent = { it in stateByContent })
+ val state = elementState(transitionStates, key, isInContent = { it in stateByContent })
state is TransitionState.Transition &&
state.transformationSpec
.transformations(key, content.key)
- .shared
+ ?.shared
?.transformation
?.elevateInContent == content.key &&
isSharedElement(stateByContent, state) &&
@@ -218,7 +248,7 @@ private fun Modifier.maybeElevateInContent(
*/
internal data class ElementModifier(
internal val layoutImpl: SceneTransitionLayoutImpl,
- private val currentTransitionStates: List<TransitionState>,
+ private val currentTransitionStates: List<List<TransitionState>>,
internal val content: Content,
internal val key: ElementKey,
) : ModifierNodeElement<ElementNode>() {
@@ -232,7 +262,7 @@ internal data class ElementModifier(
internal class ElementNode(
private var layoutImpl: SceneTransitionLayoutImpl,
- private var currentTransitionStates: List<TransitionState>,
+ private var currentTransitionStates: List<List<TransitionState>>,
private var content: Content,
private var key: ElementKey,
) : Modifier.Node(), DrawModifierNode, ApproachLayoutModifierNode, TraversableNode {
@@ -257,10 +287,15 @@ internal class ElementNode(
_element = element
addToRenderAuthority(element)
if (!element.stateByContent.contains(content.key)) {
- val elementState = Element.State(content.key)
+ val contents = buildList {
+ layoutImpl.ancestors.fastForEach { add(it.inContent) }
+ add(content.key)
+ }
+
+ val elementState = Element.State(contents)
element.stateByContent[content.key] = elementState
- layoutImpl.ancestorContentKeys.forEach { element.stateByContent[it] = elementState }
+ layoutImpl.ancestors.fastForEach { element.stateByContent[it.inContent] = elementState }
}
}
@@ -273,7 +308,7 @@ internal class ElementNode(
// this element was composed multiple times in the same content.
val nCodeLocations = stateInContent.nodes.size
if (nCodeLocations != 1 || !stateInContent.nodes.contains(this@ElementNode)) {
- error("$key was composed $nCodeLocations times in ${stateInContent.content}")
+ error("$key was composed $nCodeLocations times in ${stateInContent.contents}")
}
}
}
@@ -288,12 +323,12 @@ internal class ElementNode(
}
private fun addToRenderAuthority(element: Element) {
- val nestingDepth = layoutImpl.ancestorContentKeys.size
+ val nestingDepth = layoutImpl.ancestors.size
element.renderAuthority[nestingDepth] = content.key
}
private fun removeFromRenderAuthority() {
- val nestingDepth = layoutImpl.ancestorContentKeys.size
+ val nestingDepth = layoutImpl.ancestors.size
if (element.renderAuthority[nestingDepth] == content.key) {
element.renderAuthority.remove(nestingDepth)
}
@@ -305,7 +340,7 @@ internal class ElementNode(
fun update(
layoutImpl: SceneTransitionLayoutImpl,
- currentTransitionStates: List<TransitionState>,
+ currentTransitionStates: List<List<TransitionState>>,
content: Content,
key: ElementKey,
) {
@@ -326,7 +361,7 @@ internal class ElementNode(
override fun isMeasurementApproachInProgress(lookaheadSize: IntSize): Boolean {
// TODO(b/324191441): Investigate whether making this check more complex (checking if this
// element is shared or transformed) would lead to better performance.
- return layoutImpl.state.isTransitioning()
+ return isAnyStateTransitioning()
}
override fun Placeable.PlacementScope.isPlacementApproachInProgress(
@@ -334,7 +369,12 @@ internal class ElementNode(
): Boolean {
// TODO(b/324191441): Investigate whether making this check more complex (checking if this
// element is shared or transformed) would lead to better performance.
- return layoutImpl.state.isTransitioning()
+ return isAnyStateTransitioning()
+ }
+
+ private fun isAnyStateTransitioning(): Boolean {
+ return layoutImpl.state.isTransitioning() ||
+ layoutImpl.ancestors.fastAny { it.layoutImpl.state.isTransitioning() }
}
@ExperimentalComposeUiApi
@@ -372,7 +412,7 @@ internal class ElementNode(
// This is the case if for example a transition between two overlays is ongoing where
// sharedElement isn't part of either but the element is still rendered as part of
// the underlying scene that is currently not being transitioned.
- val currentState = currentTransitionStates.last()
+ val currentState = currentTransitionStates.last().last()
val shouldPlaceInThisContent =
elementContentWhenIdle(
layoutImpl,
@@ -448,7 +488,7 @@ internal class ElementNode(
element,
transition,
contentValue = { it.targetOffset },
- transformation = { it.offset },
+ transformation = { it?.offset },
currentValue = { currentOffset },
isSpecified = { it != Offset.Unspecified },
::lerp,
@@ -592,8 +632,7 @@ internal class ElementNode(
}
}
- pruneForContent(stateInContent.content)
- layoutImpl.ancestorContentKeys.forEach { content -> pruneForContent(content) }
+ stateInContent.contents.fastForEach { pruneForContent(it) }
}
}
}
@@ -602,9 +641,10 @@ internal class ElementNode(
private fun elementState(
layoutImpl: SceneTransitionLayoutImpl,
element: Element,
- transitionStates: List<TransitionState>,
+ transitionStates: List<List<TransitionState>>,
): TransitionState? {
- val state = elementState(transitionStates, isInContent = { it in element.stateByContent })
+ val state =
+ elementState(transitionStates, element.key, isInContent = { it in element.stateByContent })
val transition = state as? TransitionState.Transition
val previousTransition = element.lastTransition
@@ -625,23 +665,48 @@ private fun elementState(
}
internal inline fun elementState(
- transitionStates: List<TransitionState>,
+ transitionStates: List<List<TransitionState>>,
+ elementKey: ElementKey,
isInContent: (ContentKey) -> Boolean,
): TransitionState? {
- val lastState = transitionStates.last()
- if (lastState is TransitionState.Idle) {
- check(transitionStates.size == 1)
- return lastState
- }
+ // transitionStates is a list of all ancestor transition states + transitionState of the local
+ // STL. By traversing the list in normal order we by default prioritize the transitionState of
+ // the highest ancestor if it is running and has a transformation for this element.
+ transitionStates.fastForEachIndexed { index, states ->
+ if (index < transitionStates.size - 1) {
+ // Check if any ancestor runs a transition that has a transformation for the element
+ states.fastForEachReversed { state ->
+ if (
+ state is TransitionState.Transition &&
+ (state.transformationSpec.hasTransformation(
+ elementKey,
+ state.fromContent,
+ ) ||
+ state.transformationSpec.hasTransformation(elementKey, state.toContent))
+ ) {
+ return state
+ }
+ }
+ } else {
+ // the last state of the list, is the state of the local STL
+ val lastState = states.last()
+ if (lastState is TransitionState.Idle) {
+ check(states.size == 1)
+ return lastState
+ }
- // Find the last transition with a content that contains the element.
- transitionStates.fastForEachReversed { state ->
- val transition = state as TransitionState.Transition
- if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
- return transition
+ // Find the last transition with a content that contains the element.
+ states.fastForEachReversed { state ->
+ val transition = state as TransitionState.Transition
+ if (isInContent(transition.fromContent) || isInContent(transition.toContent)) {
+ return transition
+ }
+ }
}
}
-
+ // We are running a transition where both from and to don't contain the element. The element
+ // may still be rendered as e.g. it can be part of a idle scene where two overlays are currently
+ // transitioning above it.
return null
}
@@ -706,7 +771,7 @@ private fun prepareInterruption(
stateInContent.alphaInterruptionDelta = 0f
stateInContent.scaleInterruptionDelta = Scale.Zero
- if (!shouldPlaceElement(layoutImpl, stateInContent.content, element, transition)) {
+ if (!shouldPlaceElement(layoutImpl, stateInContent.contents.last(), element, transition)) {
stateInContent.offsetBeforeInterruption = Offset.Unspecified
stateInContent.alphaBeforeInterruption = Element.AlphaUnspecified
stateInContent.scaleBeforeInterruption = Scale.Unspecified
@@ -720,7 +785,7 @@ private fun prepareInterruption(
}
/**
- * Reconcile the state of [element] in the formContent and toContent of [transition] so that the
+ * Reconcile the state of [element] in the fromContent and toContent of [transition] so that the
* values before interruption have their expected values, taking shared transitions into account.
*
* @return the unique state this element had during [transition], `null` if it had multiple
@@ -878,7 +943,7 @@ private inline fun <T> setPlacementInterruptionDelta(
// If the element is shared, also set the delta on the other content so that it is used by that
// content if we start overscrolling it and change the content where the element is placed.
val otherContent =
- if (stateInContent.content == transition.fromContent) transition.toContent
+ if (stateInContent.contents.last() == transition.fromContent) transition.toContent
else transition.fromContent
val otherContentState = element.stateByContent[otherContent] ?: return
if (isSharedElementEnabled(element.key, transition)) {
@@ -916,7 +981,8 @@ private fun shouldPlaceElement(
if (
content != transition.fromContent &&
content != transition.toContent &&
- (!isReplacingOverlay || content != transition.currentScene)
+ (!isReplacingOverlay || content != transition.currentScene) &&
+ transitionDoesNotInvolveAncestorContent(layoutImpl, transition)
) {
return false
}
@@ -938,6 +1004,15 @@ private fun shouldPlaceElement(
return shouldPlaceSharedElement(layoutImpl, content, element.key, transition)
}
+private fun transitionDoesNotInvolveAncestorContent(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: TransitionState.Transition,
+): Boolean {
+ return layoutImpl.ancestors.fastAll {
+ it.inContent != transition.fromContent && it.inContent != transition.toContent
+ }
+}
+
/**
* Whether the element is opaque or not.
*
@@ -968,7 +1043,7 @@ private fun isElementOpaque(
return true
}
- return transition.transformationSpec.transformations(element.key, content.key).alpha == null
+ return transition.transformationSpec.transformations(element.key, content.key)?.alpha == null
}
/**
@@ -992,7 +1067,7 @@ private fun elementAlpha(
element,
transition,
contentValue = { 1f },
- transformation = { it.alpha },
+ transformation = { it?.alpha },
currentValue = { 1f },
isSpecified = { true },
::lerp,
@@ -1060,7 +1135,7 @@ private fun measure(
element,
transition,
contentValue = { it.targetSize },
- transformation = { it.size },
+ transformation = { it?.size },
currentValue = { measurable.measure(constraints).also { maybePlaceable = it }.size() },
isSpecified = { it != Element.SizeUnspecified },
::lerp,
@@ -1093,7 +1168,6 @@ private fun measure(
)
},
)
-
return measurable.measure(
Constraints.fixed(
interruptedSize.width.coerceAtLeast(0),
@@ -1117,7 +1191,7 @@ private fun ContentDrawScope.getDrawScale(
element,
transition,
contentValue = { Scale.Default },
- transformation = { it.drawScale },
+ transformation = { it?.drawScale },
currentValue = { Scale.Default },
isSpecified = { true },
::lerp,
@@ -1205,7 +1279,8 @@ private inline fun <T> computeValue(
element: Element,
transition: TransitionState.Transition?,
contentValue: (Element.State) -> T,
- transformation: (ElementTransformations) -> TransformationWithRange<PropertyTransformation<T>>?,
+ transformation:
+ (ElementTransformations?) -> TransformationWithRange<PropertyTransformation<T>>?,
currentValue: () -> T,
isSpecified: (T) -> Boolean,
lerp: (T, T, Float) -> T,
@@ -1230,7 +1305,7 @@ private inline fun <T> computeValue(
return contentValue(currentContentState)
}
- val currentContent = currentContentState.content
+ val currentContent = currentContentState.contents.last()
// The element is shared: interpolate between the value in fromContent and the value in
// toContent.
@@ -1265,23 +1340,52 @@ private inline fun <T> computeValue(
}
}
+ // The content for which we compute the transformation. Note that this is not necessarily
+ // [currentContent] because [currentContent] could be a different content than the transition
+ // fromContent or toContent during interruptions or when a ancestor transition is running.
+ val content: ContentKey
// Get the transformed value, i.e. the target value at the beginning (for entering elements) or
// end (for leaving elements) of the transition.
- val contentState =
- checkNotNull(
- when {
- isSharedElement && currentContent == fromContent -> fromState
- isSharedElement -> toState
- currentSceneState != null && currentContent == transition.currentScene ->
- currentSceneState
- else -> fromState ?: toState
+ val contentState: Element.State
+ when {
+ isSharedElement -> {
+ content = currentContent
+ contentState = currentContentState
+ }
+ isAncestorTransition(layoutImpl, transition) -> {
+ if (
+ fromState != null &&
+ transition.transformationSpec.hasTransformation(element.key, fromContent)
+ ) {
+ content = fromContent
+ contentState = fromState
+ } else if (
+ toState != null &&
+ transition.transformationSpec.hasTransformation(element.key, toContent)
+ ) {
+ content = toContent
+ contentState = toState
+ } else {
+ throw IllegalStateException(
+ "Ancestor transition is active but no transformation " +
+ "spec was found. The ancestor transition should have only been selected " +
+ "when a transformation for that element and content was defined."
+ )
}
- )
-
- // The content for which we compute the transformation. Note that this is not necessarily
- // [currentContent] because [currentContent] could be a different content than the transition
- // fromContent or toContent during interruptions.
- val content = contentState.content
+ }
+ currentSceneState != null && currentContent == transition.currentScene -> {
+ content = currentContent
+ contentState = currentSceneState
+ }
+ fromState != null -> {
+ content = fromContent
+ contentState = fromState
+ }
+ else -> {
+ content = toContent
+ contentState = toState!!
+ }
+ }
val transformationWithRange =
transformation(transition.transformationSpec.transformations(element.key, content))
@@ -1437,6 +1541,8 @@ private inline fun <T> computeValue(
when {
content == toContent -> true
content == fromContent -> false
+ isAncestorTransition(layoutImpl, transition) ->
+ isEnteringAncestorTransition(layoutImpl, transition)
content == transition.currentScene -> toState == null
else -> content == toContent
}
@@ -1447,6 +1553,22 @@ private inline fun <T> computeValue(
}
}
+private fun isAncestorTransition(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: TransitionState.Transition,
+): Boolean {
+ return layoutImpl.ancestors.fastAny {
+ it.inContent == transition.fromContent || it.inContent == transition.toContent
+ }
+}
+
+private fun isEnteringAncestorTransition(
+ layoutImpl: SceneTransitionLayoutImpl,
+ transition: TransitionState.Transition
+): Boolean {
+ return layoutImpl.ancestors.fastAny { it.inContent == transition.toContent }
+}
+
private inline fun <T> PropertyTransformation<T>.requireInterpolatedTransformation(
element: Element,
transition: TransitionState.Transition,
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
index 388456e8893a..c10a48567b22 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt
@@ -163,7 +163,7 @@ private class MovableElementScopeImpl(
// Important: Like in Modifier.element(), we read the transition states during
// composition then pass them to Layout to make sure that composition sees new states
// before layout and drawing.
- val transitionStates = layoutImpl.state.transitionStates
+ val transitionStates = getAllNestedTransitionStates(layoutImpl)
Layout { _, _ ->
// No need to measure or place anything.
val size =
@@ -186,7 +186,7 @@ private fun shouldComposeMovableElement(
element: MovableElementKey,
): Boolean {
return when (
- val elementState = movableElementState(element, layoutImpl.state.transitionStates)
+ val elementState = movableElementState(element, getAllNestedTransitionStates(layoutImpl))
) {
null ->
movableElementContentWhenIdle(layoutImpl, element, layoutImpl.state.transitionState) ==
@@ -221,10 +221,14 @@ private fun shouldComposeMoveableElement(
private fun movableElementState(
element: MovableElementKey,
- transitionStates: List<TransitionState>,
+ transitionStates: List<List<TransitionState>>,
): TransitionState? {
val contents = element.contentPicker.contents
- return elementState(transitionStates, isInContent = { contents.contains(it) })
+ return elementState(
+ transitionStates,
+ elementKey = element,
+ isInContent = { contents.contains(it) },
+ )
}
private fun movableElementContentWhenIdle(
@@ -245,7 +249,7 @@ private fun placeholderContentSize(
content: ContentKey,
element: Element,
elementKey: MovableElementKey,
- transitionStates: List<TransitionState>,
+ transitionStates: List<List<TransitionState>>,
): IntSize {
// If the content of the movable element was already composed in this scene before, use that
// target size.
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
index ce385abea627..7b30a2a475e3 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt
@@ -698,7 +698,7 @@ internal fun SceneTransitionLayoutForTesting(
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
- ancestorContentKeys: List<ContentKey> = emptyList(),
+ ancestors: List<Ancestor> = remember { emptyList() },
lookaheadScope: LookaheadScope? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
@@ -715,7 +715,7 @@ internal fun SceneTransitionLayoutForTesting(
builder = builder,
animationScope = animationScope,
elements = sharedElementMap,
- ancestorContentKeys = ancestorContentKeys,
+ ancestors = ancestors,
lookaheadScope = lookaheadScope,
)
.also { onLayoutImpl?.invoke(it) }
@@ -738,9 +738,9 @@ internal fun SceneTransitionLayoutForTesting(
"when creating it, which is not supported"
)
}
- if (layoutImpl.ancestorContentKeys != ancestorContentKeys) {
+ if (layoutImpl.ancestors != ancestors) {
error(
- "This SceneTransitionLayout was bound to a different ancestorContents that was " +
+ "This SceneTransitionLayout was bound to a different ancestors that was " +
"used when creating it, which is not supported"
)
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index e1cecc750d3d..e5bdc92b5762 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -57,6 +57,18 @@ import kotlinx.coroutines.launch
/** The type for the content of movable elements. */
internal typealias MovableElementContent = @Composable (@Composable () -> Unit) -> Unit
+internal data class Ancestor(
+ val layoutImpl: SceneTransitionLayoutImpl,
+
+ /**
+ * This is the content in which the corresponding descendant of this ancestor appears in.
+ *
+ * Example: When A is the root and has two scenes SA and SB and SB contains a NestedSTL called
+ * B. Then A is the ancestor of B and inContent is SB.
+ */
+ val inContent: ContentKey,
+)
+
@Stable
internal class SceneTransitionLayoutImpl(
internal val state: MutableSceneTransitionLayoutStateImpl,
@@ -83,16 +95,17 @@ internal class SceneTransitionLayoutImpl(
internal val elements: MutableMap<ElementKey, Element> = mutableMapOf(),
/**
- * When this STL is a [NestedSceneTransitionLayout], this is a list of [ContentKey]s of where
- * this STL is composed in within its ancestors.
+ * When this STL is a [NestedSceneTransitionLayout], this is a list of [Ancestor]s which
+ * provides a reference to the ancestor STLs and indicates where this STL is composed in within
+ * its ancestors.
*
* The root STL holds an emptyList. With each nesting level the parent is supposed to add
* exactly one scene to the list, therefore the size of this list is equal to the nesting depth
* of this STL.
*
- * This is used to know in which content of the ancestors a sharedElement appears in.
+ * This is used to enable transformations and shared elements across NestedSTLs.
*/
- internal val ancestorContentKeys: List<ContentKey> = emptyList(),
+ internal val ancestors: List<Ancestor> = emptyList(),
lookaheadScope: LookaheadScope? = null,
) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
index 6479e69a2aac..756d71c1b5cf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt
@@ -266,19 +266,26 @@ internal class TransformationSpecImpl(
override val distance: UserActionDistance?,
override val transformationMatchers: List<TransformationMatcher>,
) : TransformationSpec {
- private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations>>()
+ private val cache = mutableMapOf<ElementKey, MutableMap<ContentKey, ElementTransformations?>>()
- internal fun transformations(element: ElementKey, content: ContentKey): ElementTransformations {
+ internal fun transformations(
+ element: ElementKey,
+ content: ContentKey,
+ ): ElementTransformations? {
return cache
.getOrPut(element) { mutableMapOf() }
.getOrPut(content) { computeTransformations(element, content) }
}
+ internal fun hasTransformation(element: ElementKey, content: ContentKey): Boolean {
+ return transformations(element, content) != null
+ }
+
/** Filter [transformationMatchers] to compute the [ElementTransformations] of [element]. */
private fun computeTransformations(
element: ElementKey,
content: ContentKey,
- ): ElementTransformations {
+ ): ElementTransformations? {
var shared: TransformationWithRange<SharedElementTransformation>? = null
var offset: TransformationWithRange<PropertyTransformation<Offset>>? = null
var size: TransformationWithRange<PropertyTransformation<IntSize>>? = null
@@ -338,7 +345,13 @@ internal class TransformationSpecImpl(
}
}
- return ElementTransformations(shared, offset, size, drawScale, alpha)
+ return if (
+ shared == null && offset == null && size == null && drawScale == null && alpha == null
+ ) {
+ null
+ } else {
+ ElementTransformations(shared, offset, size, drawScale, alpha)
+ }
}
private fun throwIfNotNull(
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt
index 9de297f3ad5a..ed3a5cac8184 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt
@@ -81,8 +81,9 @@ internal fun sharedElementTransformation(
): TransformationWithRange<SharedElementTransformation>? {
val transformationSpec = transition.transformationSpec
val sharedInFromContent =
- transformationSpec.transformations(element, transition.fromContent).shared
- val sharedInToContent = transformationSpec.transformations(element, transition.toContent).shared
+ transformationSpec.transformations(element, transition.fromContent)?.shared
+ val sharedInToContent =
+ transformationSpec.transformations(element, transition.toContent)?.shared
// The sharedElement() transformation must either be null or be the same in both contents.
if (sharedInFromContent != sharedInToContent) {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
index 3716df5b5a35..8c5a72738a41 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt
@@ -31,6 +31,7 @@ import androidx.compose.ui.layout.approachLayout
import androidx.compose.ui.platform.testTag
import androidx.compose.ui.unit.IntSize
import androidx.compose.ui.zIndex
+import com.android.compose.animation.scene.Ancestor
import com.android.compose.animation.scene.AnimatedState
import com.android.compose.animation.scene.ContentKey
import com.android.compose.animation.scene.ContentScope
@@ -181,16 +182,17 @@ internal class ContentScopeImpl(
modifier: Modifier,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
+ val ancestors =
+ remember(layoutImpl, contentKey, layoutImpl.ancestors) {
+ layoutImpl.ancestors + Ancestor(layoutImpl, contentKey)
+ }
SceneTransitionLayoutForTesting(
state,
modifier,
onLayoutImpl = null,
builder = builder,
sharedElementMap = layoutImpl.elements,
- ancestorContentKeys =
- remember(layoutImpl.ancestorContentKeys, contentKey) {
- layoutImpl.ancestorContentKeys + contentKey
- },
+ ancestors = ancestors,
lookaheadScope = layoutImpl.lookaheadScope,
)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
index c69129b38bdd..676903274b38 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt
@@ -2208,6 +2208,7 @@ class ElementTest {
}
@Test
+ @Ignore("b/363964445")
fun interruption_considerPreviousUniqueState() {
@Composable
fun SceneScope.Foo(modifier: Modifier = Modifier) {
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
new file mode 100644
index 000000000000..0da422bcb696
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedElementTransformationTest.kt
@@ -0,0 +1,410 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene.transformation
+
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.size
+import androidx.compose.runtime.Composable
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.alpha
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.test.SemanticsNodeInteraction
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+import androidx.compose.ui.test.isNotDisplayed
+import androidx.compose.ui.test.junit4.createComposeRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.dp
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.ElementKey
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.Scale
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.SceneTransitionLayout
+import com.android.compose.animation.scene.SceneTransitions
+import com.android.compose.animation.scene.TestScenes
+import com.android.compose.animation.scene.testNestedTransition
+import com.android.compose.animation.scene.testing.lastAlphaForTesting
+import com.android.compose.animation.scene.testing.lastScaleForTesting
+import com.android.compose.animation.scene.transitions
+import com.android.compose.test.assertSizeIsEqualTo
+import com.google.common.truth.Truth.assertThat
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NestedElementTransformationTest {
+ @get:Rule val rule = createComposeRule()
+
+ private object Scenes {
+ val NestedSceneA = SceneKey("NestedSceneA")
+ val NestedSceneB = SceneKey("NestedSceneB")
+ val NestedNestedSceneA = SceneKey("NestedNestedSceneA")
+ val NestedNestedSceneB = SceneKey("NestedNestedSceneB")
+ }
+
+ // Variants are named: nestingDepth + sceneNameSuffix
+ private val elementVariant0A =
+ TestElement(ElementKey("0A"), 0.dp, 0.dp, 100.dp, 100.dp, Color.Red)
+ private val elementVariant0B =
+ TestElement(ElementKey("0B"), 100.dp, 100.dp, 20.dp, 20.dp, Color.Cyan)
+ private val elementVariant1A =
+ TestElement(ElementKey("1A"), 40.dp, 80.dp, 60.dp, 20.dp, Color.Blue)
+ private val elementVariant1B =
+ TestElement(ElementKey("1B"), 80.dp, 40.dp, 140.dp, 180.dp, Color.Yellow)
+ private val elementVariant2A =
+ TestElement(ElementKey("2A"), 120.dp, 240.dp, 20.dp, 140.dp, Color.Green)
+ private val elementVariant2B =
+ TestElement(ElementKey("2B"), 200.dp, 320.dp, 40.dp, 70.dp, Color.Magenta)
+
+ private class TestElement(
+ val key: ElementKey,
+ val x: Dp,
+ val y: Dp,
+ val width: Dp,
+ val height: Dp,
+ val color: Color = Color.Black,
+ val alpha: Float = 0.8f,
+ )
+
+ @Composable
+ private fun ContentScope.TestElement(element: TestElement) {
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier.offset(element.x, element.y)
+ .element(element.key)
+ .size(element.width, element.height)
+ .background(element.color)
+ .alpha(element.alpha)
+ )
+ }
+ }
+
+ private fun createState(
+ startScene: SceneKey,
+ transitions: SceneTransitions = SceneTransitions.Empty,
+ ): MutableSceneTransitionLayoutState {
+ return rule.runOnUiThread { MutableSceneTransitionLayoutState(startScene, transitions) }
+ }
+
+ private val threeNestedStls:
+ @Composable
+ (states: List<MutableSceneTransitionLayoutState>) -> Unit =
+ { states ->
+ SceneTransitionLayout(states[0]) {
+ scene(TestScenes.SceneA, content = { TestElement(elementVariant0A) })
+ scene(
+ TestScenes.SceneB,
+ content = {
+ Box(Modifier.fillMaxSize()) {
+ TestElement(elementVariant0B)
+ NestedSceneTransitionLayout(states[1], modifier = Modifier) {
+ scene(Scenes.NestedSceneA) {
+ Box(Modifier.fillMaxSize()) {
+ TestElement(elementVariant1A)
+ NestedSceneTransitionLayout(
+ state = states[2],
+ modifier = Modifier,
+ ) {
+ scene(Scenes.NestedNestedSceneA) {
+ TestElement(elementVariant2A)
+ }
+ scene(Scenes.NestedNestedSceneB) {
+ TestElement(elementVariant2B)
+ }
+ }
+ }
+ }
+ scene(Scenes.NestedSceneB) { TestElement(elementVariant1B) }
+ }
+ }
+ },
+ )
+ }
+ }
+
+ @Test
+ fun transitionInNestedNestedStl_transitionsOut() {
+ rule.testNestedTransition(
+ states =
+ listOf(
+ createState(TestScenes.SceneB),
+ createState(Scenes.NestedSceneA),
+ createState(
+ Scenes.NestedNestedSceneA,
+ transitions {
+ from(from = Scenes.NestedNestedSceneA, to = Scenes.NestedNestedSceneB) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 100.dp, y = 50.dp)
+ scaleSize(elementVariant2A.key, width = 2f, height = 0.5f)
+ scaleDraw(elementVariant2A.key, scaleX = 4f, scaleY = 0.25f)
+ fade(elementVariant2A.key)
+ }
+ },
+ ),
+ ),
+ transitionLayout = threeNestedStls,
+ changeState = { it[2].setTargetScene(Scenes.NestedNestedSceneB, this) },
+ ) {
+ before { onElement(elementVariant2A.key).assertElementVariant(elementVariant2A) }
+ atAllFrames(4) {
+ onElement(elementVariant2A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant2A.x, elementVariant2A.x + 100.dp),
+ interpolate(elementVariant2A.y, elementVariant2A.y + 50.dp),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant2A.width, elementVariant2A.width * 2f),
+ interpolate(elementVariant2A.height, elementVariant2A.height * 0.5f),
+ )
+ val semanticNode = onElement(elementVariant2A.key).fetchSemanticsNode()
+ assertThat(semanticNode.lastAlphaForTesting).isEqualTo(interpolate(1f, 0f))
+ assertThat(semanticNode.lastScaleForTesting)
+ .isEqualTo(interpolate(Scale(1f, 1f), Scale(4f, 0.25f)))
+ }
+ after { onElement(elementVariant2A.key).isNotDisplayed() }
+ }
+ }
+
+ @Test
+ fun transitionInNestedNestedStl_transitionsIn() {
+ rule.testNestedTransition(
+ states =
+ listOf(
+ createState(TestScenes.SceneB),
+ createState(Scenes.NestedSceneA),
+ createState(
+ Scenes.NestedNestedSceneB,
+ transitions {
+ from(from = Scenes.NestedNestedSceneB) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 100.dp, y = 50.dp)
+ scaleSize(elementVariant2A.key, width = 2f, height = 0.5f)
+ }
+ },
+ ),
+ ),
+ transitionLayout = threeNestedStls,
+ changeState = { it[2].setTargetScene(Scenes.NestedNestedSceneA, this) },
+ ) {
+ before { onElement(elementVariant2A.key).isNotDisplayed() }
+ atAllFrames(4) {
+ onElement(elementVariant2A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant2A.x + 100.dp, elementVariant2A.x),
+ interpolate(elementVariant2A.y + 50.dp, elementVariant2A.y),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant2A.width * 2f, elementVariant2A.width),
+ interpolate(elementVariant2A.height * 0.5f, elementVariant2A.height),
+ )
+ }
+ after { onElement(elementVariant2A.key).assertElementVariant(elementVariant2A) }
+ }
+ }
+
+ @Test
+ fun transitionInNestedStl_elementInNestedNestedStl_transitionsIn() {
+ rule.testNestedTransition(
+ states =
+ listOf(
+ createState(TestScenes.SceneB),
+ createState(
+ Scenes.NestedSceneB,
+ transitions {
+ from(from = Scenes.NestedSceneB, to = Scenes.NestedSceneA) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 100.dp, y = 50.dp)
+ scaleSize(elementVariant2A.key, width = 2f, height = 0.5f)
+ }
+ },
+ ),
+ createState(Scenes.NestedNestedSceneA),
+ ),
+ transitionLayout = threeNestedStls,
+ changeState = { it[1].setTargetScene(Scenes.NestedSceneA, this) },
+ ) {
+ before { onElement(elementVariant2A.key).isNotDisplayed() }
+ atAllFrames(4) {
+ onElement(elementVariant2A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant2A.x + 100.dp, elementVariant2A.x),
+ interpolate(elementVariant2A.y + 50.dp, elementVariant2A.y),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant2A.width * 2f, elementVariant2A.width),
+ interpolate(elementVariant2A.height * 0.5f, elementVariant2A.height),
+ )
+ }
+ after { onElement(elementVariant2A.key).assertElementVariant(elementVariant2A) }
+ }
+ }
+
+ @Test
+ fun transitionInRootStl_elementsInAllLayers_transitionInAndOut() {
+ rule.testNestedTransition(
+ states =
+ listOf(
+ createState(
+ TestScenes.SceneB,
+ transitions {
+ to(to = TestScenes.SceneA) {
+ spec = tween(16 * 4, easing = LinearEasing)
+
+ // transitions out
+ translate(elementVariant2A.key, x = 100.dp, y = 50.dp)
+ scaleSize(elementVariant2A.key, width = 2f, height = 0.5f)
+
+ // transitions out
+ translate(elementVariant0B.key, x = 200.dp, y = 20.dp)
+ scaleSize(elementVariant0B.key, width = 3f, height = 0.2f)
+
+ // transitions out
+ translate(elementVariant1A.key, x = 300.dp, y = 10.dp)
+ scaleSize(elementVariant1A.key, width = 4f, height = 0.1f)
+
+ // transitions in
+ translate(elementVariant0A.key, x = 400.dp, y = 40.dp)
+ scaleSize(elementVariant0A.key, width = 0.5f, height = 2f)
+ }
+ },
+ ),
+ createState(Scenes.NestedSceneA),
+ createState(Scenes.NestedNestedSceneA),
+ ),
+ transitionLayout = threeNestedStls,
+ changeState = { it[0].setTargetScene(TestScenes.SceneA, this) },
+ ) {
+ before {
+ onElement(elementVariant2A.key).assertElementVariant(elementVariant2A)
+ onElement(elementVariant0B.key).assertElementVariant(elementVariant0B)
+ onElement(elementVariant1A.key).assertElementVariant(elementVariant1A)
+ onElement(elementVariant0A.key).isNotDisplayed()
+ }
+ atAllFrames(4) {
+ onElement(elementVariant2A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant2A.x, elementVariant2A.x + 100.dp),
+ interpolate(elementVariant2A.y, elementVariant2A.y + 50.dp),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant2A.width, elementVariant2A.width * 2f),
+ interpolate(elementVariant2A.height, elementVariant2A.height * 0.5f),
+ )
+
+ onElement(elementVariant0B.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant0B.x, elementVariant0B.x + 200.dp),
+ interpolate(elementVariant0B.y, elementVariant0B.y + 20.dp),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant0B.width, elementVariant0B.width * 3f),
+ interpolate(elementVariant0B.height, elementVariant0B.height * 0.2f),
+ )
+
+ onElement(elementVariant1A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant1A.x, elementVariant1A.x + 300.dp),
+ interpolate(elementVariant1A.y, elementVariant1A.y + 10.dp),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant1A.width, elementVariant1A.width * 4f),
+ interpolate(elementVariant1A.height, elementVariant1A.height * 0.1f),
+ )
+
+ onElement(elementVariant0A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant0A.x + 400.dp, elementVariant0A.x),
+ interpolate(elementVariant0A.y + 40.dp, elementVariant0A.y),
+ )
+ .assertSizeIsEqualTo(
+ interpolate(elementVariant0A.width * 0.5f, elementVariant0A.width),
+ interpolate(elementVariant0A.height * 2f, elementVariant0A.height),
+ )
+ }
+ after {
+ onElement(elementVariant2A.key).isNotDisplayed()
+ onElement(elementVariant0B.key).isNotDisplayed()
+ onElement(elementVariant1A.key).isNotDisplayed()
+ onElement(elementVariant0A.key).assertElementVariant(elementVariant0A)
+ }
+ }
+ }
+
+ @Test
+ fun transitionInMultipleStls_rootIsTakingControl() {
+ rule.testNestedTransition(
+ states =
+ listOf(
+ createState(
+ TestScenes.SceneB,
+ transitions {
+ to(to = TestScenes.SceneA) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 100.dp, y = 50.dp)
+ }
+ },
+ ),
+ createState(
+ Scenes.NestedSceneA,
+ transitions {
+ to(to = Scenes.NestedSceneB) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 200.dp, y = 150.dp)
+ }
+ },
+ ),
+ createState(
+ Scenes.NestedNestedSceneA,
+ transitions {
+ to(to = Scenes.NestedNestedSceneB) {
+ spec = tween(16 * 4, easing = LinearEasing)
+ translate(elementVariant2A.key, x = 300.dp, y = 250.dp)
+ }
+ },
+ ),
+ ),
+ transitionLayout = threeNestedStls,
+ changeState = {
+ it[2].setTargetScene(Scenes.NestedNestedSceneB, this)
+ it[1].setTargetScene(Scenes.NestedSceneB, this)
+ it[0].setTargetScene(TestScenes.SceneA, this)
+ },
+ ) {
+ before { onElement(elementVariant2A.key).assertElementVariant(elementVariant2A) }
+ atAllFrames(4) {
+ onElement(elementVariant2A.key)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant2A.x, elementVariant2A.x + 100.dp),
+ interpolate(elementVariant2A.y, elementVariant2A.y + 50.dp),
+ )
+ }
+ after { onElement(elementVariant2A.key).isNotDisplayed() }
+ }
+ }
+
+ private fun SemanticsNodeInteraction.assertElementVariant(variant: TestElement) {
+ assertPositionInRootIsEqualTo(variant.x, variant.y)
+ assertSizeIsEqualTo(variant.width, variant.height)
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
index c6ef8cff1a66..d8b713625681 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
@@ -61,7 +61,7 @@ class NestedSharedElementTest {
val NestedNestedSceneB = SceneKey("NestedNestedSceneB")
}
- private val elementVariant1 = SharedElement(0.dp, 0.dp, 100.dp, 100.dp, Color.Red)
+ private val elementVariant1 = SharedElement(100.dp, 100.dp, 100.dp, 100.dp, Color.Red)
private val elementVariant2 = SharedElement(40.dp, 80.dp, 60.dp, 20.dp, Color.Blue)
private val elementVariant3 = SharedElement(80.dp, 40.dp, 140.dp, 180.dp, Color.Yellow)
private val elementVariant4 = SharedElement(120.dp, 240.dp, 20.dp, 140.dp, Color.Green)
@@ -223,7 +223,8 @@ class NestedSharedElementTest {
// In SceneA, Foo leaves to the left edge.
translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left, false)
- // We can't reference the element inside the NestedSTL as of today
+ // In NestedSceneA, Foo comes in from the top edge.
+ translate(TestElements.Foo.inScene(Scenes.NestedSceneA), Edge.Top, false)
},
) {
before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
@@ -234,6 +235,11 @@ class NestedSharedElementTest {
elementVariant1.y,
)
.assertSizeIsEqualTo(elementVariant1.width, elementVariant1.height)
+ onElement(TestElements.Foo, scene = Scenes.NestedSceneA)
+ .assertPositionInRootIsEqualTo(
+ elementVariant2.x,
+ interpolate(0.dp, elementVariant2.y),
+ )
}
after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
index 47c10f5ab3a3..0dd08d92eb41 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt
@@ -18,11 +18,13 @@ package com.android.compose.animation.scene.transformation
import androidx.compose.animation.core.LinearEasing
import androidx.compose.animation.core.tween
+import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.offset
import androidx.compose.foundation.layout.size
import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.test.assertIsNotDisplayed
import androidx.compose.ui.test.assertPositionInRootIsEqualTo
import androidx.compose.ui.test.junit4.createComposeRule
@@ -47,11 +49,21 @@ class SharedElementTest {
rule.testTransition(
fromSceneContent = {
// Foo is at (10, 50) with a size of (20, 80).
- Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo).size(20.dp, 80.dp))
+ Box(
+ Modifier.offset(10.dp, 50.dp)
+ .element(TestElements.Foo)
+ .size(20.dp, 80.dp)
+ .background(Color.Red)
+ )
},
toSceneContent = {
// Foo is at (50, 70) with a size of (10, 40).
- Box(Modifier.offset(50.dp, 70.dp).element(TestElements.Foo).size(10.dp, 40.dp))
+ Box(
+ Modifier.offset(50.dp, 70.dp)
+ .element(TestElements.Foo)
+ .size(10.dp, 40.dp)
+ .background(Color.Blue)
+ )
},
transition = {
spec = tween(16 * 4, easing = LinearEasing)
@@ -88,13 +100,23 @@ class SharedElementTest {
fromSceneContent = {
Box(Modifier.fillMaxSize()) {
// Foo is at (10, 50).
- Box(Modifier.offset(10.dp, 50.dp).element(TestElements.Foo))
+ Box(
+ Modifier.offset(10.dp, 50.dp)
+ .element(TestElements.Foo)
+ .size(20.dp)
+ .background(Color.Red)
+ )
}
},
toSceneContent = {
Box(Modifier.fillMaxSize()) {
// Foo is at (50, 60).
- Box(Modifier.offset(50.dp, 60.dp).element(TestElements.Foo))
+ Box(
+ Modifier.offset(50.dp, 60.dp)
+ .element(TestElements.Foo)
+ .size(20.dp)
+ .background(Color.Blue)
+ )
}
},
transition = {
@@ -104,7 +126,11 @@ class SharedElementTest {
sharedElement(TestElements.Foo, enabled = false)
// In SceneA, Foo leaves to the left edge.
- translate(TestElements.Foo.inScene(TestScenes.SceneA), Edge.Left)
+ translate(
+ TestElements.Foo.inScene(TestScenes.SceneA),
+ Edge.Left,
+ startsOutsideLayoutBounds = false,
+ )
// In SceneB, Foo comes from the bottom edge.
translate(TestElements.Foo.inScene(TestScenes.SceneB), Edge.Bottom)
diff --git a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
index 124b61e45ed6..bc160fc02498 100644
--- a/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
+++ b/packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt
@@ -25,6 +25,7 @@ import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.rememberCoroutineScope
import androidx.compose.ui.Alignment
import androidx.compose.ui.Modifier
+import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
@@ -63,6 +64,9 @@ interface TransitionTestBuilder {
* Important: [timestamp] must be a multiple of 16 (the duration of a frame on the JVM/Android).
* There is no intermediary state between `t` and `t + 16` , so testing transitions outside of
* `t = 0`, `t = 16`, `t = 32`, etc does not make sense.
+ *
+ * @param builder the builder can run assertions and is passed the CoroutineScope such that the
+ * test can start transitions at any desired point in time.
*/
fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
@@ -85,7 +89,7 @@ interface TransitionTestBuilder {
}
@TransitionTestDsl
-interface TransitionTestAssertionScope {
+interface TransitionTestAssertionScope : CoroutineScope {
/**
* Assert on [element].
*
@@ -312,6 +316,20 @@ fun ComposeContentTestRule.testTransition(
)
}
+fun ComposeContentTestRule.testNestedTransition(
+ states: List<MutableSceneTransitionLayoutState>,
+ changeState: CoroutineScope.(states: List<MutableSceneTransitionLayoutState>) -> Unit,
+ transitionLayout: @Composable (states: List<MutableSceneTransitionLayoutState>) -> Unit,
+ builder: TransitionTestBuilder.() -> Unit,
+) {
+ testTransition(
+ state = states[0],
+ changeState = { changeState(states) },
+ transitionLayout = { transitionLayout(states) },
+ builder = builder,
+ )
+}
+
/** Test the transition from [state] to [to]. */
fun ComposeContentTestRule.testTransition(
state: MutableSceneTransitionLayoutState,
@@ -319,9 +337,15 @@ fun ComposeContentTestRule.testTransition(
transitionLayout: @Composable (state: MutableSceneTransitionLayoutState) -> Unit,
builder: TransitionTestBuilder.() -> Unit,
) {
- val test = transitionTest(builder)
+ lateinit var coroutineScope: CoroutineScope
+ setContent {
+ coroutineScope = rememberCoroutineScope()
+ transitionLayout(state)
+ }
+
val assertionScope =
- object : AutoTransitionTestAssertionScope {
+ object : AutoTransitionTestAssertionScope, CoroutineScope by coroutineScope {
+
var progress = 0f
override fun onElement(
@@ -338,6 +362,16 @@ fun ComposeContentTestRule.testTransition(
from is Int && to is Int -> lerp(from, to, progress)
from is Long && to is Long -> lerp(from, to, progress)
from is Dp && to is Dp -> lerp(from, to, progress)
+ from is Scale && to is Scale ->
+ Scale(
+ lerp(from.scaleX, to.scaleX, progress),
+ lerp(from.scaleY, to.scaleY, progress),
+ interpolate(from.pivot, to.pivot),
+ )
+
+ from is Offset && to is Offset ->
+ Offset(lerp(from.x, to.x, progress), lerp(from.y, to.y, progress))
+
else ->
throw UnsupportedOperationException(
"Interpolation not supported for this type"
@@ -347,14 +381,9 @@ fun ComposeContentTestRule.testTransition(
}
}
- lateinit var coroutineScope: CoroutineScope
- setContent {
- coroutineScope = rememberCoroutineScope()
- transitionLayout(state)
- }
-
// Wait for the UI to be idle then test the before state.
waitForIdle()
+ val test = transitionTest(builder)
test.before(assertionScope)
// Manually advance the clock to the start of the animation.
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
index 4a5ebd057835..aa71b84d7bbc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/wmshell/TestableBubbleController.java
@@ -25,7 +25,6 @@ import android.view.WindowManager;
import com.android.internal.statusbar.IStatusBarService;
import com.android.wm.shell.ShellTaskOrganizer;
-import com.android.wm.shell.WindowManagerShellWrapper;
import com.android.wm.shell.bubbles.BubbleController;
import com.android.wm.shell.bubbles.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
@@ -33,6 +32,8 @@ import com.android.wm.shell.bubbles.BubbleLogger;
import com.android.wm.shell.bubbles.BubblePositioner;
import com.android.wm.shell.bubbles.properties.BubbleProperties;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -62,7 +63,8 @@ public class TestableBubbleController extends BubbleController {
BubbleDataRepository dataRepository,
IStatusBarService statusBarService,
WindowManager windowManager,
- WindowManagerShellWrapper windowManagerShellWrapper,
+ DisplayInsetsController displayInsetsController,
+ DisplayImeController displayImeController,
UserManager userManager,
LauncherApps launcherApps,
BubbleLogger bubbleLogger,
@@ -81,8 +83,8 @@ public class TestableBubbleController extends BubbleController {
BubbleProperties bubbleProperties) {
super(context, shellInit, shellCommandHandler, shellController, data, Runnable::run,
floatingContentCoordinator, dataRepository, statusBarService, windowManager,
- windowManagerShellWrapper, userManager, launcherApps, bubbleLogger,
- taskStackListener, shellTaskOrganizer, positioner, displayController,
+ displayInsetsController, displayImeController, userManager, launcherApps,
+ bubbleLogger, taskStackListener, shellTaskOrganizer, positioner, displayController,
oneHandedOptional, dragAndDropController, shellMainExecutor, shellMainHandler,
new SyncExecutor(), taskViewTransitions, transitions,
syncQueue, wmService, bubbleProperties);
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 70a94f9f9a59..6994a55cdbcd 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -782,6 +782,13 @@
<!-- The top margin for the notification children container in its non-expanded form. -->
<dimen name="notification_children_container_margin_top">48dp</dimen>
+ <!-- The spacing between the notification children container in its non-expanded form, and the
+ header text above it, scaling with text size. This value is chosen so that, taking into
+ account the text spacing for both the text in the top line and the text in the container,
+ the distance between them is 4dp with the default screen configuration (and will grow
+ accordingly for larger font sizes). -->
+ <dimen name="notification_2025_children_container_margin_top">@*android:dimen/notification_2025_content_margin_top</dimen>
+
<!-- The height of the gap between adjacent notification sections. -->
<dimen name="notification_section_divider_height">@dimen/notification_side_paddings</dimen>
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java
index 92338ef3773c..1a068c4229c9 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/AmbientVolumeSlider.java
@@ -45,16 +45,31 @@ public class AmbientVolumeSlider extends LinearLayout {
new Slider.OnSliderTouchListener() {
@Override
public void onStartTrackingTouch(@NonNull Slider slider) {
+ mTrackingTouch = true;
}
@Override
public void onStopTrackingTouch(@NonNull Slider slider) {
+ mTrackingTouch = false;
final int value = Math.round(slider.getValue());
for (OnChangeListener listener : mChangeListeners) {
listener.onValueChange(AmbientVolumeSlider.this, value);
}
}
};
+ private final Slider.OnChangeListener mSliderChangeListener = new Slider.OnChangeListener() {
+ @Override
+ public void onValueChange(@NonNull Slider slider, float value, boolean fromUser) {
+ if (fromUser && !mTrackingTouch) {
+ final int roundedValue = Math.round(value);
+ for (OnChangeListener listener : mChangeListeners) {
+ listener.onValueChange(AmbientVolumeSlider.this, roundedValue);
+ }
+ }
+ }
+ };
+ private boolean mTrackingTouch = false;
+
public AmbientVolumeSlider(@Nullable Context context) {
this(context, /* attrs= */ null);
}
@@ -76,6 +91,7 @@ public class AmbientVolumeSlider extends LinearLayout {
mTitle = requireViewById(R.id.ambient_volume_slider_title);
mSlider = requireViewById(R.id.ambient_volume_slider);
mSlider.addOnSliderTouchListener(mSliderTouchListener);
+ mSlider.addOnChangeListener(mSliderChangeListener);
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
index df43ff19de86..3ccf5063cd68 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationTemplateViewWrapper.java
@@ -16,6 +16,7 @@
package com.android.systemui.statusbar.notification.row.wrapper;
+import static android.app.Flags.notificationsRedesignTemplates;
import static android.view.View.VISIBLE;
import static com.android.systemui.statusbar.notification.row.ExpandableNotificationRow.DEFAULT_HEADER_VISIBLE_AMOUNT;
@@ -149,10 +150,15 @@ public class NotificationTemplateViewWrapper extends NotificationHeaderViewWrapp
}
}, TRANSFORMING_VIEW_TEXT);
- mFullHeaderTranslation = ctx.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.notification_content_margin)
- - ctx.getResources().getDimensionPixelSize(
- com.android.internal.R.dimen.notification_content_margin_top);
+ int contentMargin = ctx.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin);
+ int contentMarginTop =
+ notificationsRedesignTemplates()
+ ? Notification.Builder.getContentMarginTop(ctx,
+ com.android.internal.R.dimen.notification_2025_content_margin_top)
+ : ctx.getResources().getDimensionPixelSize(
+ com.android.internal.R.dimen.notification_content_margin_top);
+ mFullHeaderTranslation = contentMargin - contentMarginTop;
}
@MainThread
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
index 00cd8ce87738..9fb7fad52bb6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainer.java
@@ -168,8 +168,10 @@ public class NotificationChildrenContainer extends ViewGroup
mDividerHeight = res.getDimensionPixelOffset(
R.dimen.notification_children_container_divider_height);
mDividerAlpha = res.getFloat(R.dimen.notification_divider_alpha);
- mNotificationHeaderMargin = res.getDimensionPixelOffset(
- R.dimen.notification_children_container_margin_top);
+ mNotificationHeaderMargin = notificationsRedesignTemplates()
+ ? Notification.Builder.getContentMarginTop(getContext(),
+ R.dimen.notification_2025_children_container_margin_top)
+ : res.getDimensionPixelOffset(R.dimen.notification_children_container_margin_top);
mNotificationTopPadding = res.getDimensionPixelOffset(
R.dimen.notification_children_container_top_padding);
mHeaderHeight = notificationsRedesignTemplates()
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
index 192d66c44aa0..af12d0119c58 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -167,7 +167,6 @@ import com.android.systemui.util.settings.SystemSettings;
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.Flags;
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.BubbleData;
import com.android.wm.shell.bubbles.BubbleDataRepository;
@@ -184,6 +183,8 @@ import com.android.wm.shell.bubbles.StackEducationView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.bubbles.properties.BubbleProperties;
import com.android.wm.shell.common.DisplayController;
+import com.android.wm.shell.common.DisplayImeController;
+import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.FloatingContentCoordinator;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
@@ -325,7 +326,9 @@ public class BubblesTest extends SysuiTestCase {
@Mock
private LauncherApps mLauncherApps;
@Mock
- private WindowManagerShellWrapper mWindowManagerShellWrapper;
+ private DisplayInsetsController mDisplayInsetsController;
+ @Mock
+ private DisplayImeController mDisplayImeController;
@Mock
private BubbleLogger mBubbleLogger;
@Mock
@@ -503,7 +506,7 @@ public class BubblesTest extends SysuiTestCase {
mContext,
mock(NotificationManager.class),
mock(NotificationSettingsInteractor.class)
- );
+ );
interruptionDecisionProvider.start();
mShellTaskOrganizer = new ShellTaskOrganizer(mock(ShellInit.class),
@@ -523,7 +526,8 @@ public class BubblesTest extends SysuiTestCase {
mDataRepository,
mStatusBarService,
mWindowManager,
- mWindowManagerShellWrapper,
+ mDisplayInsetsController,
+ mDisplayImeController,
mUserManager,
mLauncherApps,
mBubbleLogger,
@@ -1430,9 +1434,12 @@ public class BubblesTest extends SysuiTestCase {
mPositioner,
mBubbleController.getStackView(),
new BubbleIconFactory(mContext,
- mContext.getResources().getDimensionPixelSize(com.android.wm.shell.R.dimen.bubble_size),
- mContext.getResources().getDimensionPixelSize(com.android.wm.shell.R.dimen.bubble_badge_size),
- mContext.getResources().getColor(com.android.launcher3.icons.R.color.important_conversation),
+ mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_size),
+ mContext.getResources().getDimensionPixelSize(
+ com.android.wm.shell.R.dimen.bubble_badge_size),
+ mContext.getResources().getColor(
+ com.android.launcher3.icons.R.color.important_conversation),
mContext.getResources().getDimensionPixelSize(
com.android.internal.R.dimen.importance_ring_stroke_width)),
bubble,
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
index a8d5696e8c77..c384b5434bce 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDevice.java
@@ -1345,7 +1345,10 @@ abstract class HdmiCecLocalDevice extends HdmiLocalDevice {
iter.remove();
}
if (mPendingActionClearedCallback != null) {
- mPendingActionClearedCallback.onCleared(this);
+ PendingActionClearedCallback callback = mPendingActionClearedCallback;
+ // To prevent from calling the callback again during handling the callback itself.
+ mPendingActionClearedCallback = null;
+ callback.onCleared(this);
}
}