summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
-rw-r--r--core/java/android/hardware/camera2/CameraManager.java23
-rw-r--r--core/java/android/provider/Settings.java9
-rw-r--r--core/java/android/security/responsible_apis_flags.aconfig8
-rw-r--r--core/java/android/widget/Button.java4
-rw-r--r--core/java/android/window/flags/lse_desktop_experience.aconfig14
-rw-r--r--core/res/res/layout/side_fps_toast.xml4
-rw-r--r--libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml10
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java12
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt14
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java7
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java11
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java13
-rw-r--r--libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java25
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransitionStateHolderTest.kt2
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt44
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt46
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt3
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java68
-rw-r--r--libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java5
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java3
-rw-r--r--packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java1
-rw-r--r--packages/SystemUI/aconfig/biometrics_framework.aconfig7
-rw-r--r--packages/SystemUI/aconfig/systemui.aconfig10
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt36
-rw-r--r--packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt50
-rw-r--r--packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt234
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Element.kt165
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/MovableElement.kt42
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayout.kt39
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt55
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt113
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/content/Content.kt23
-rw-r--r--packages/SystemUI/compose/scene/src/com/android/compose/ui/util/IntIndexedMap.kt73
-rw-r--r--packages/SystemUI/compose/scene/tests/AndroidManifest.xml3
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt283
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/SharedElementTest.kt57
-rw-r--r--packages/SystemUI/compose/scene/tests/src/com/android/compose/ui/util/IntIndexMapTest.kt92
-rw-r--r--packages/SystemUI/compose/scene/tests/utils/src/com/android/compose/animation/scene/TestTransition.kt90
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt1
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java4
-rw-r--r--packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java19
-rw-r--r--packages/SystemUI/plugin/src/com/android/systemui/plugins/AuthContextPlugin.kt85
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java6
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java12
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java9
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt4
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt13
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/plugins/AuthContextPlugins.kt41
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt7
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt2
-rw-r--r--packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt28
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt5
-rw-r--r--packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt29
-rw-r--r--packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt25
-rw-r--r--packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt43
-rw-r--r--packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java10
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt5
-rw-r--r--packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java2
-rw-r--r--services/core/java/com/android/server/am/ActiveServices.java4
-rw-r--r--services/core/java/com/android/server/am/ActivityManagerService.java4
-rw-r--r--services/core/java/com/android/server/media/quality/MediaQualityService.java102
-rw-r--r--services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java2
-rw-r--r--services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java54
-rw-r--r--services/core/java/com/android/server/wm/CameraStateMonitor.java47
-rw-r--r--services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java47
-rw-r--r--services/tests/security/intrusiondetection/Android.bp4
-rw-r--r--services/tests/security/intrusiondetection/AndroidManifest.xml6
-rw-r--r--services/tests/security/intrusiondetection/AndroidTest.xml1
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java38
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/Android.bp42
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/AndroidManifest.xml24
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/LocalIntrusionDetectionEventTransport.java58
-rw-r--r--services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/TestLoggingService.java (renamed from services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestLoggingService.java)26
-rw-r--r--services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java58
-rw-r--r--tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Android.bp1
-rw-r--r--tests/FlickerTests/AppClose/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/FlickerService/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/IME/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Notification/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt29
-rw-r--r--tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml2
-rw-r--r--tests/FlickerTests/Rotation/AndroidTestTemplate.xml2
92 files changed, 2181 insertions, 449 deletions
diff --git a/core/java/android/hardware/camera2/CameraManager.java b/core/java/android/hardware/camera2/CameraManager.java
index 266efb7b759c..aba2345f28d8 100644
--- a/core/java/android/hardware/camera2/CameraManager.java
+++ b/core/java/android/hardware/camera2/CameraManager.java
@@ -1699,17 +1699,18 @@ public final class CameraManager {
}
if (context != null) {
- final ActivityManager activityManager =
- context.getSystemService(ActivityManager.class);
- for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
- final TaskInfo taskInfo = appTask.getTaskInfo();
- final int freeformCameraCompatMode =
- taskInfo.appCompatTaskInfo.cameraCompatTaskInfo.freeformCameraCompatMode;
- if (freeformCameraCompatMode != 0
- && taskInfo.topActivity != null
- && taskInfo.topActivity.getPackageName().equals(packageName)) {
- // WindowManager has requested rotation override.
- return getRotationOverrideForCompatFreeform(freeformCameraCompatMode);
+ final ActivityManager activityManager = context.getSystemService(ActivityManager.class);
+ if (activityManager != null) {
+ for (ActivityManager.AppTask appTask : activityManager.getAppTasks()) {
+ final TaskInfo taskInfo = appTask.getTaskInfo();
+ final int freeformCameraCompatMode = taskInfo.appCompatTaskInfo
+ .cameraCompatTaskInfo.freeformCameraCompatMode;
+ if (freeformCameraCompatMode != 0
+ && taskInfo.topActivity != null
+ && taskInfo.topActivity.getPackageName().equals(packageName)) {
+ // WindowManager has requested rotation override.
+ return getRotationOverrideForCompatFreeform(freeformCameraCompatMode);
+ }
}
}
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index d5b525884ac1..8d054f4b1750 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -6374,6 +6374,14 @@ public final class Settings {
public static final String LOCALE_PREFERENCES = "locale_preferences";
/**
+ * User can change the region from region settings. This records user's preferred region.
+ *
+ * E.g. : if user's locale is en-US, this will record US
+ * @hide
+ */
+ public static final String PREFERRED_REGION = "preferred_region";
+
+ /**
* Setting to enable camera flash notification feature.
* <ul>
* <li> 0 = Off
@@ -6547,6 +6555,7 @@ public final class Settings {
PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING);
PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON);
+ PRIVATE_SETTINGS.add(PREFERRED_REGION);
}
/**
diff --git a/core/java/android/security/responsible_apis_flags.aconfig b/core/java/android/security/responsible_apis_flags.aconfig
index a5c837b88fa4..2007a5f43f41 100644
--- a/core/java/android/security/responsible_apis_flags.aconfig
+++ b/core/java/android/security/responsible_apis_flags.aconfig
@@ -96,6 +96,14 @@ flag {
}
flag {
+ name: "prevent_intent_redirect_show_toast_if_nested_keys_not_collected"
+ namespace: "responsible_apis"
+ description: "Prevent intent redirect attacks by showing a toast if not yet collected"
+ bug: "361143368"
+ is_fixed_read_only: true
+}
+
+flag {
name: "prevent_intent_redirect_throw_exception_if_nested_keys_not_collected"
namespace: "responsible_apis"
description: "Prevent intent redirect attacks by throwing exception if the intent does not collect nested keys"
diff --git a/core/java/android/widget/Button.java b/core/java/android/widget/Button.java
index 0bf6380eb904..eb3b76873a8f 100644
--- a/core/java/android/widget/Button.java
+++ b/core/java/android/widget/Button.java
@@ -134,6 +134,7 @@ public class Button extends TextView {
// 1. app target sdk is 36 or above.
// 2. feature flag rolled-out.
// 3. device is a watch.
+ // 4. button uses Theme.DeviceDefault.
// getButtonDefaultStyleAttr and getButtonDefaultStyleRes works together to alter the UI
// while considering the conditions above.
// Their results are mutual exclusive. i.e. when conditions above are all true,
@@ -229,6 +230,7 @@ public class Button extends TextView {
private static boolean useWearMaterial3Style(Context context) {
return Flags.useWearMaterial3Ui() && CompatChanges.isChangeEnabled(WEAR_MATERIAL3_BUTTON)
- && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH);
+ && context.getPackageManager().hasSystemFeature(PackageManager.FEATURE_WATCH)
+ && context.getThemeResId() == com.android.internal.R.style.Theme_DeviceDefault;
}
}
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 8019e6791cf1..7faa5d702d6b 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -242,13 +242,6 @@ flag {
}
flag {
- name: "enable_desktop_windowing_app_handle_education_integration"
- namespace: "lse_desktop_experience"
- description: "Enables desktop windowing app handle education and integrates new APIs"
- bug: "380272815"
-}
-
-flag {
name: "enable_desktop_windowing_transitions"
namespace: "lse_desktop_experience"
description: "Enables desktop windowing transition & motion polish changes"
@@ -304,6 +297,13 @@ flag {
}
flag {
+ name: "enable_desktop_windowing_app_to_web_education_integration"
+ namespace: "lse_desktop_experience"
+ description: "Enables desktop windowing App-to-Web education and integrates new APIs"
+ bug: "380272815"
+}
+
+flag {
name: "enable_minimize_button"
namespace: "lse_desktop_experience"
description: "Adds a minimize button the the caption bar"
diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml
index 78299ab0ea99..7bb6fcfa2ae0 100644
--- a/core/res/res/layout/side_fps_toast.xml
+++ b/core/res/res/layout/side_fps_toast.xml
@@ -25,7 +25,7 @@
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="6"
- android:paddingBottom="10dp"
+ android:paddingBottom="16dp"
android:text="@string/fp_power_button_enrollment_title"
android:textColor="@color/side_fps_text_color"
android:paddingLeft="20dp"/>
@@ -37,7 +37,7 @@
android:layout_height="wrap_content"
android:layout_width="0dp"
android:layout_weight="3"
- android:paddingBottom="10dp"
+ android:paddingBottom="16dp"
android:text="@string/fp_power_button_enrollment_button_text"
style="?android:attr/buttonBarNegativeButtonStyle"
android:textColor="@color/side_fps_button_color"
diff --git a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
index 3dbf7542ac6e..fcf74e3c1936 100644
--- a/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
+++ b/libs/WindowManager/Shell/res/layout/desktop_mode_app_header.xml
@@ -46,15 +46,19 @@
<TextView
android:id="@+id/application_name"
android:layout_width="0dp"
- android:layout_height="20dp"
- android:maxWidth="86dp"
+ android:layout_height="wrap_content"
+ android:maxWidth="130dp"
android:textAppearance="@android:style/TextAppearance.Material.Title"
android:textSize="14sp"
android:textFontWeight="500"
- android:lineHeight="20dp"
+ android:lineHeight="20sp"
android:layout_gravity="center_vertical"
android:layout_weight="1"
android:layout_marginStart="8dp"
+ android:singleLine="true"
+ android:ellipsize="none"
+ android:requiresFadingEdge="horizontal"
+ android:fadingEdgeLength="28dp"
android:clickable="false"
android:focusable="false"
tools:text="Gmail"/>
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 b82496e45415..3b53c3fbe03f 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
@@ -274,8 +274,11 @@ public class BubbleController implements ConfigurationChangeListener,
private final DragAndDropController mDragAndDropController;
/** Used to send bubble events to launcher. */
private Bubbles.BubbleStateListener mBubbleStateListener;
- /** Used to track previous navigation mode to detect switch to buttons navigation. */
- private boolean mIsPrevNavModeGestures;
+ /**
+ * Used to track previous navigation mode to detect switch to buttons navigation. Set to
+ * true to switch the bubble bar to the opposite side for 3 nav buttons mode on device boot.
+ */
+ private boolean mIsPrevNavModeGestures = true;
/** Used to send updates to the views from {@link #mBubbleDataListener}. */
private BubbleViewCallback mBubbleViewCallback;
@@ -357,7 +360,6 @@ public class BubbleController implements ConfigurationChangeListener,
}
};
mExpandedViewManager = BubbleExpandedViewManager.fromBubbleController(this);
- mIsPrevNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
}
private void registerOneHandedState(OneHandedController oneHanded) {
@@ -593,9 +595,9 @@ public class BubbleController implements ConfigurationChangeListener,
if (mBubbleStateListener != null) {
boolean isCurrentNavModeGestures = ContextUtils.isGestureNavigationMode(mContext);
if (mIsPrevNavModeGestures && !isCurrentNavModeGestures) {
- BubbleBarLocation navButtonsLocation = ContextUtils.isRtl(mContext)
+ BubbleBarLocation bubbleBarLocation = ContextUtils.isRtl(mContext)
? BubbleBarLocation.RIGHT : BubbleBarLocation.LEFT;
- mBubblePositioner.setBubbleBarLocation(navButtonsLocation);
+ mBubblePositioner.setBubbleBarLocation(bubbleBarLocation);
}
mIsPrevNavModeGestures = isCurrentNavModeGestures;
BubbleBarUpdate update = mBubbleData.getInitialStateForBubbleBar();
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
index b50716ad07a3..8b830e769c70 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserver.kt
@@ -22,6 +22,7 @@ import android.view.SurfaceControl
import android.window.TransitionInfo
import com.android.internal.protolog.ProtoLog
import com.android.window.flags.Flags.appCompatRefactoring
+import com.android.wm.shell.common.transition.TransitionStateHolder
import com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_APP_COMPAT
import com.android.wm.shell.shared.TransitionUtil.isClosingType
import com.android.wm.shell.sysui.ShellInit
@@ -33,7 +34,8 @@ import com.android.wm.shell.transition.Transitions
class LetterboxTransitionObserver(
shellInit: ShellInit,
private val transitions: Transitions,
- private val letterboxController: LetterboxController
+ private val letterboxController: LetterboxController,
+ private val transitionStateHolder: TransitionStateHolder
) : Transitions.TransitionObserver {
companion object {
@@ -71,11 +73,11 @@ class LetterboxTransitionObserver(
change.endAbsBounds.height()
)
with(letterboxController) {
- if (isClosingType(change.mode)) {
- destroyLetterboxSurface(
- key,
- startTransaction
- )
+ // TODO(b/380274087) Handle return to home from a recents transition.
+ if (isClosingType(change.mode) &&
+ !transitionStateHolder.isRecentsTransitionRunning()) {
+ // For the other types of close we need to check the recents.
+ destroyLetterboxSurface(key, finishTransaction)
} else {
val isTopActivityLetterboxed = ti.appCompatTaskInfo.isTopActivityLetterboxed
if (isTopActivityLetterboxed) {
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 974535385334..860431a80851 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
@@ -68,6 +68,7 @@ import com.android.wm.shell.common.MultiInstanceHelper;
import com.android.wm.shell.common.ShellExecutor;
import com.android.wm.shell.common.SyncTransactionQueue;
import com.android.wm.shell.common.TaskStackListenerImpl;
+import com.android.wm.shell.common.transition.TransitionStateHolder;
import com.android.wm.shell.compatui.letterbox.LetterboxCommandHandler;
import com.android.wm.shell.compatui.letterbox.LetterboxController;
import com.android.wm.shell.compatui.letterbox.LetterboxTransitionObserver;
@@ -1316,9 +1317,11 @@ public abstract class WMShellModule {
static LetterboxTransitionObserver provideLetterboxTransitionObserver(
@NonNull ShellInit shellInit,
@NonNull Transitions transitions,
- @NonNull LetterboxController letterboxController
+ @NonNull LetterboxController letterboxController,
+ @NonNull TransitionStateHolder transitionStateHolder
) {
- return new LetterboxTransitionObserver(shellInit, transitions, letterboxController);
+ return new LetterboxTransitionObserver(shellInit, transitions, letterboxController,
+ transitionStateHolder);
}
@WMSingleton
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
index 982fda0ddf36..aa954fbe5669 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/CaptionWindowDecoration.java
@@ -194,6 +194,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
@VisibleForTesting
static void updateRelayoutParams(
RelayoutParams relayoutParams,
+ @NonNull Context context,
ActivityManager.RunningTaskInfo taskInfo,
boolean applyStartTransactionOnDraw,
boolean shouldSetTaskVisibilityPositionAndCrop,
@@ -206,9 +207,11 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
relayoutParams.mRunningTaskInfo = taskInfo;
relayoutParams.mLayoutResId = R.layout.caption_window_decor;
relayoutParams.mCaptionHeightId = getCaptionHeightIdStatic(taskInfo.getWindowingMode());
- relayoutParams.mShadowRadiusId = hasGlobalFocus
- ? R.dimen.freeform_decor_shadow_focused_thickness
- : R.dimen.freeform_decor_shadow_unfocused_thickness;
+ relayoutParams.mShadowRadius = hasGlobalFocus
+ ? context.getResources().getDimensionPixelSize(
+ R.dimen.freeform_decor_shadow_focused_thickness)
+ : context.getResources().getDimensionPixelSize(
+ R.dimen.freeform_decor_shadow_unfocused_thickness);
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop;
relayoutParams.mIsCaptionVisible = taskInfo.isFreeform()
@@ -251,7 +254,7 @@ public class CaptionWindowDecoration extends WindowDecoration<WindowDecorLinearL
final SurfaceControl oldDecorationSurface = mDecorationContainerSurface;
final WindowContainerTransaction wct = new WindowContainerTransaction();
- updateRelayoutParams(mRelayoutParams, taskInfo, applyStartTransactionOnDraw,
+ updateRelayoutParams(mRelayoutParams, mContext, taskInfo, applyStartTransactionOnDraw,
shouldSetTaskVisibilityPositionAndCrop, mIsStatusBarVisible,
mIsKeyguardVisibleAndOccluded,
mDisplayController.getInsetsState(taskInfo.displayId), hasGlobalFocus,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
index 96cc559a64ae..5eb031218ee1 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecoration.java
@@ -980,10 +980,15 @@ public class DesktopModeWindowDecoration extends WindowDecoration<WindowDecorLin
relayoutParams.mInputFeatures
|= WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL;
}
- if (DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ hasGlobalFocus)) {
- relayoutParams.mShadowRadiusId = hasGlobalFocus
- ? R.dimen.freeform_decor_shadow_focused_thickness
- : R.dimen.freeform_decor_shadow_unfocused_thickness;
+ if (isAppHeader
+ && DesktopModeStatus.useWindowShadow(/* isFocusedWindow= */ hasGlobalFocus)) {
+ relayoutParams.mShadowRadius = hasGlobalFocus
+ ? context.getResources().getDimensionPixelSize(
+ R.dimen.freeform_decor_shadow_focused_thickness)
+ : context.getResources().getDimensionPixelSize(
+ R.dimen.freeform_decor_shadow_unfocused_thickness);
+ } else {
+ relayoutParams.mShadowRadius = INVALID_SHADOW_RADIUS;
}
relayoutParams.mApplyStartTransactionOnDraw = applyStartTransactionOnDraw;
relayoutParams.mSetTaskVisibilityPositionAndCrop = shouldSetTaskVisibilityPositionAndCrop;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
index 852eee5f6672..584ee39ab317 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/windowdecor/WindowDecoration.java
@@ -17,7 +17,6 @@
package com.android.wm.shell.windowdecor;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
-import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
import static android.content.res.Configuration.DENSITY_DPI_UNDEFINED;
import static android.view.WindowInsets.Type.captionBar;
import static android.view.WindowInsets.Type.mandatorySystemGestures;
@@ -110,6 +109,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
* Invalid corner radius that signifies that corner radius should not be set.
*/
static final int INVALID_CORNER_RADIUS = -1;
+ /**
+ * Invalid corner radius that signifies that shadow radius should not be set.
+ */
+ static final int INVALID_SHADOW_RADIUS = -1;
/**
* System-wide context. Only used to create context with overridden configurations.
@@ -439,16 +442,10 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
.setPosition(mTaskSurface, taskPosition.x, taskPosition.y);
}
- float shadowRadius;
- if (mTaskInfo.getWindowingMode() == WINDOWING_MODE_FULLSCREEN) {
- // Shadow is not needed for fullscreen tasks
- shadowRadius = 0;
- } else {
- shadowRadius =
- loadDimension(mDecorWindowContext.getResources(), params.mShadowRadiusId);
+ if (params.mShadowRadius != INVALID_SHADOW_RADIUS) {
+ startT.setShadowRadius(mTaskSurface, params.mShadowRadius);
+ finishT.setShadowRadius(mTaskSurface, params.mShadowRadius);
}
- startT.setShadowRadius(mTaskSurface, shadowRadius);
- finishT.setShadowRadius(mTaskSurface, shadowRadius);
if (params.mSetTaskVisibilityPositionAndCrop) {
startT.show(mTaskSurface);
@@ -851,8 +848,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
@InsetsSource.Flags int mInsetSourceFlags;
final Region mDisplayExclusionRegion = Region.obtain();
- int mShadowRadiusId;
- int mCornerRadius;
+ int mShadowRadius = INVALID_SHADOW_RADIUS;
+ int mCornerRadius = INVALID_CORNER_RADIUS;
int mCaptionTopPadding;
boolean mIsCaptionVisible;
@@ -874,8 +871,8 @@ public abstract class WindowDecoration<T extends View & TaskFocusStateConsumer>
mInsetSourceFlags = 0;
mDisplayExclusionRegion.setEmpty();
- mShadowRadiusId = Resources.ID_NULL;
- mCornerRadius = 0;
+ mShadowRadius = INVALID_SHADOW_RADIUS;
+ mCornerRadius = INVALID_SHADOW_RADIUS;
mCaptionTopPadding = 0;
mIsCaptionVisible = false;
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
index 706c63244890..1de47df78853 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-legacy/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
index 7df1675f541c..34d001c858f6 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/flicker-service/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
index 7df1675f541c..34d001c858f6 100644
--- a/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/e2e/splitscreen/platinum/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
index d87c1795cf7b..9c1a8f17aeee 100644
--- a/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/appcompat/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
index 99969e71238a..02b2cec8dbdb 100644
--- a/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/bubble/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
index 19c3e4048d69..9f32d68559e7 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/AndroidTestTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
index 7505860709e9..34e4e744dae7 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
+++ b/libs/WindowManager/Shell/tests/flicker/pip/csuiteDefaultTemplate.xml
@@ -48,6 +48,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransitionStateHolderTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransitionStateHolderTest.kt
index 7b1d27a8b823..64772d037383 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransitionStateHolderTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/common/transition/TransitionStateHolderTest.kt
@@ -18,7 +18,6 @@ package com.android.wm.shell.common.transition
import android.testing.AndroidTestingRunner
import androidx.test.filters.SmallTest
-import com.android.server.testutils.any
import com.android.wm.shell.TestShellExecutor
import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.recents.RecentsTransitionStateListener
@@ -35,6 +34,7 @@ import org.junit.runner.RunWith
import org.mockito.ArgumentCaptor
import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
+import org.mockito.kotlin.any
import org.mockito.kotlin.never
/**
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
index 9c6afcb8be63..07bfefe0b275 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/compatui/letterbox/LetterboxTransitionObserverTest.kt
@@ -25,9 +25,12 @@ import android.testing.AndroidTestingRunner
import android.view.SurfaceControl
import android.view.WindowManager.TRANSIT_CLOSE
import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn
import com.android.window.flags.Flags
import com.android.wm.shell.ShellTestCase
import com.android.wm.shell.common.ShellExecutor
+import com.android.wm.shell.common.transition.TransitionStateHolder
+import com.android.wm.shell.recents.RecentsTransitionHandler
import com.android.wm.shell.sysui.ShellInit
import com.android.wm.shell.transition.Transitions
import com.android.wm.shell.util.TransitionObserverInputBuilder
@@ -37,6 +40,7 @@ import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.any
+import org.mockito.kotlin.doReturn
import org.mockito.kotlin.eq
import org.mockito.kotlin.mock
import org.mockito.kotlin.never
@@ -154,21 +158,38 @@ class LetterboxTransitionObserverTest : ShellTestCase() {
}
@Test
- fun `When closing change letterbox surface destroy is triggered`() {
+ fun `When closing change with no recents running letterbox surfaces are destroyed`() {
runTestScenario { r ->
executeTransitionObserverTest(observerFactory = r.observerFactory) {
r.invokeShellInit()
inputBuilder {
buildTransitionInfo()
+ r.configureRecentsState(running = false)
r.createClosingChange(inputBuilder = this)
}
validateOutput {
r.destroyEventDetected(expected = true)
- r.creationEventDetected(expected = false)
- r.visibilityEventDetected(expected = false, visible = false)
- r.updateSurfaceBoundsEventDetected(expected = false)
+ }
+ }
+ }
+ }
+
+ @Test
+ fun `When closing change and recents are running letterbox surfaces are not destroyed`() {
+ runTestScenario { r ->
+ executeTransitionObserverTest(observerFactory = r.observerFactory) {
+ r.invokeShellInit()
+
+ inputBuilder {
+ buildTransitionInfo()
+ r.createClosingChange(inputBuilder = this)
+ r.configureRecentsState(running = true)
+ }
+
+ validateOutput {
+ r.destroyEventDetected(expected = false)
}
}
}
@@ -197,6 +218,7 @@ class LetterboxTransitionObserverTest : ShellTestCase() {
private val transitions: Transitions
private val letterboxController: LetterboxController
private val letterboxObserver: LetterboxTransitionObserver
+ private val transitionStateHolder: TransitionStateHolder
val observerFactory: () -> LetterboxTransitionObserver
@@ -205,8 +227,16 @@ class LetterboxTransitionObserverTest : ShellTestCase() {
shellInit = ShellInit(executor)
transitions = mock<Transitions>()
letterboxController = mock<LetterboxController>()
+ transitionStateHolder =
+ TransitionStateHolder(shellInit, mock<RecentsTransitionHandler>())
+ spyOn(transitionStateHolder)
letterboxObserver =
- LetterboxTransitionObserver(shellInit, transitions, letterboxController)
+ LetterboxTransitionObserver(
+ shellInit,
+ transitions,
+ letterboxController,
+ transitionStateHolder
+ )
observerFactory = { letterboxObserver }
}
@@ -218,6 +248,10 @@ class LetterboxTransitionObserverTest : ShellTestCase() {
verify(transitions, expected.asMode()).registerObserver(observer())
}
+ fun configureRecentsState(running: Boolean) {
+ doReturn(running).`when`(transitionStateHolder).isRecentsTransitionRunning()
+ }
+
fun creationEventDetected(
expected: Boolean,
displayId: Int = DISPLAY_ID,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt
index 0e15668a05a7..a328b5b2bb6b 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/util/TransitionObserverTestUtils.kt
@@ -77,6 +77,30 @@ class TransitionObserverTestContext : TransitionObserverTestStep {
validateObj.validate()
}
+ fun validateOnMerged(
+ validate:
+ TransitionObserverOnTransitionMergedValidation.() -> Unit
+ ) {
+ val validateObj = TransitionObserverOnTransitionMergedValidation()
+ transitionObserver.onTransitionMerged(
+ validateObj.playing,
+ validateObj.merged
+ )
+ validateObj.validate()
+ }
+
+ fun validateOnFinished(
+ validate:
+ TransitionObserverOnTransitionFinishedValidation.() -> Unit
+ ) {
+ val validateObj = TransitionObserverOnTransitionFinishedValidation()
+ transitionObserver.onTransitionFinished(
+ transitionReadyInput.transition,
+ validateObj.aborted
+ )
+ validateObj.validate()
+ }
+
fun invokeObservable() {
transitionObserver.onTransitionReady(
transitionReadyInput.transition,
@@ -162,6 +186,28 @@ class TransitionObserverInputBuilder : TransitionObserverTestStep {
class TransitionObserverResultValidation : TransitionObserverTestStep
/**
+ * Phase responsible for the execution of validation methods after the
+ * [TransitionObservable#onTransitionMerged] has been executed.
+ */
+class TransitionObserverOnTransitionMergedValidation : TransitionObserverTestStep {
+ val merged = mock<IBinder>()
+ val playing = mock<IBinder>()
+
+ init {
+ spyOn(merged)
+ spyOn(playing)
+ }
+}
+
+/**
+ * Phase responsible for the execution of validation methods after the
+ * [TransitionObservable#onTransitionFinished] has been executed.
+ */
+class TransitionObserverOnTransitionFinishedValidation : TransitionObserverTestStep {
+ var aborted: Boolean = false
+}
+
+/**
* Allows to run a test about a specific [TransitionObserver] passing the specific
* implementation and input value as parameters for the [TransitionObserver#onTransitionReady]
* method.
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
index 59141ca39487..b856a28e54db 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/CaptionWindowDecorationTests.kt
@@ -48,6 +48,7 @@ class CaptionWindowDecorationTests : ShellTestCase() {
CaptionWindowDecoration.updateRelayoutParams(
relayoutParams,
+ mContext,
taskInfo,
true,
false,
@@ -71,6 +72,7 @@ class CaptionWindowDecorationTests : ShellTestCase() {
CaptionWindowDecoration.updateRelayoutParams(
relayoutParams,
+ mContext,
taskInfo,
true,
false,
@@ -90,6 +92,7 @@ class CaptionWindowDecorationTests : ShellTestCase() {
val relayoutParams = WindowDecoration.RelayoutParams()
CaptionWindowDecoration.updateRelayoutParams(
relayoutParams,
+ mContext,
taskInfo,
true,
false,
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
index e390fbbd751f..03c7c9857d8f 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorationTests.java
@@ -61,7 +61,6 @@ import android.content.pm.ActivityInfo;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
-import android.content.res.Resources;
import android.content.res.TypedArray;
import android.graphics.PointF;
import android.graphics.Rect;
@@ -295,8 +294,9 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
- public void updateRelayoutParams_noSysPropFlagsSet_windowShadowsAreEnabled() {
+ public void updateRelayoutParams_noSysPropFlagsSet_windowShadowsAreSetForFreeform() {
final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FREEFORM);
RelayoutParams relayoutParams = new RelayoutParams();
DesktopModeWindowDecoration.updateRelayoutParams(
@@ -309,7 +309,46 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
/* hasGlobalFocus= */ true,
mExclusionRegion);
- assertThat(relayoutParams.mShadowRadiusId).isNotEqualTo(Resources.ID_NULL);
+ assertThat(relayoutParams.mShadowRadius)
+ .isNotEqualTo(WindowDecoration.INVALID_SHADOW_RADIUS);
+ }
+
+ @Test
+ public void updateRelayoutParams_noSysPropFlagsSet_windowShadowsAreNotSetForFullscreen() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_FULLSCREEN);
+ RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState(),
+ /* hasGlobalFocus= */ true,
+ mExclusionRegion);
+
+ assertThat(relayoutParams.mShadowRadius).isEqualTo(WindowDecoration.INVALID_SHADOW_RADIUS);
+ }
+
+ @Test
+ public void updateRelayoutParams_noSysPropFlagsSet_windowShadowsAreNotSetForSplit() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams, mContext, taskInfo, /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState(),
+ /* hasGlobalFocus= */ true,
+ mExclusionRegion);
+
+ assertThat(relayoutParams.mShadowRadius).isEqualTo(WindowDecoration.INVALID_SHADOW_RADIUS);
}
@Test
@@ -359,6 +398,29 @@ public class DesktopModeWindowDecorationTests extends ShellTestCase {
}
@Test
+ public void updateRelayoutParams_noSysPropFlagsSet_roundedCornersNotSetForSplit() {
+ final ActivityManager.RunningTaskInfo taskInfo = createTaskInfo(/* visible= */ true);
+ taskInfo.configuration.windowConfiguration.setWindowingMode(WINDOWING_MODE_MULTI_WINDOW);
+ fillRoundedCornersResources(/* fillValue= */ 30);
+ RelayoutParams relayoutParams = new RelayoutParams();
+
+ DesktopModeWindowDecoration.updateRelayoutParams(
+ relayoutParams,
+ mTestableContext,
+ taskInfo,
+ /* applyStartTransactionOnDraw= */ true,
+ /* shouldSetTaskPositionAndCrop */ false,
+ /* isStatusBarVisible */ true,
+ /* isKeyguardVisibleAndOccluded */ false,
+ /* inFullImmersiveMode */ false,
+ new InsetsState(),
+ /* hasGlobalFocus= */ true,
+ mExclusionRegion);
+
+ assertThat(relayoutParams.mCornerRadius).isEqualTo(INVALID_CORNER_RADIUS);
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_APP_HEADER_WITH_TASK_DENSITY)
public void updateRelayoutParams_appHeader_usesTaskDensity() {
final int systemDensity = mTestableContext.getOrCreateTestableResources().getResources()
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
index 534803db5fe0..04b2be0b1a25 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/WindowDecorationTests.java
@@ -114,6 +114,7 @@ public class WindowDecorationTests extends ShellTestCase {
private static final Rect TASK_BOUNDS = new Rect(100, 300, 400, 400);
private static final Point TASK_POSITION_IN_PARENT = new Point(40, 60);
private static final int CORNER_RADIUS = 20;
+ private static final int SHADOW_RADIUS = 10;
private static final int STATUS_BAR_INSET_SOURCE_ID = 0;
@Rule
@@ -162,7 +163,7 @@ public class WindowDecorationTests extends ShellTestCase {
mRelayoutParams.mLayoutResId = 0;
mRelayoutParams.mCaptionHeightId = R.dimen.test_freeform_decor_caption_height;
mCaptionMenuWidthId = R.dimen.test_freeform_decor_caption_menu_width;
- mRelayoutParams.mShadowRadiusId = R.dimen.test_window_decor_shadow_radius;
+ mRelayoutParams.mShadowRadius = SHADOW_RADIUS;
mRelayoutParams.mCornerRadius = CORNER_RADIUS;
when(mMockDisplayController.getDisplay(Display.DEFAULT_DISPLAY))
@@ -280,7 +281,7 @@ public class WindowDecorationTests extends ShellTestCase {
verify(mMockSurfaceControlStartT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
verify(mMockSurfaceControlFinishT).setCornerRadius(mMockTaskSurface, CORNER_RADIUS);
- verify(mMockSurfaceControlStartT).setShadowRadius(mMockTaskSurface, 10);
+ verify(mMockSurfaceControlStartT).setShadowRadius(mMockTaskSurface, SHADOW_RADIUS);
assertEquals(300, mRelayoutResult.mWidth);
assertEquals(100, mRelayoutResult.mHeight);
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 8e7180c4dc8d..935ea2549d49 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -119,7 +119,8 @@ public class SystemSettings {
Settings.System.SCREEN_FLASH_NOTIFICATION_COLOR,
Settings.System.NOTIFICATION_COOLDOWN_ENABLED,
Settings.System.NOTIFICATION_COOLDOWN_ALL,
- Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED
+ Settings.System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED,
+ Settings.System.PREFERRED_REGION
));
if (Flags.backUpSmoothDisplayAndForcePeakRefreshRate()) {
settings.add(Settings.System.PEAK_REFRESH_RATE);
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index cfc7743f0a8d..9938139293fd 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -265,5 +265,6 @@ public class SystemSettingsValidators {
VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.NOTIFICATION_COOLDOWN_ALL, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.NOTIFICATION_COOLDOWN_VIBRATE_UNLOCKED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(System.PREFERRED_REGION, ANY_STRING_VALIDATOR);
}
}
diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig
index 10d7352da7dc..e3f5378175d2 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -12,3 +12,10 @@ flag {
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "cont_auth_plugin"
+ namespace: "biometrics_framework"
+ description: "Plugin and related API hooks for contextual auth plugins"
+ bug: "373600589"
+}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 20b83e10e821..0d654d9c8f67 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -1201,6 +1201,16 @@ flag {
}
flag {
+ name: "communal_hub_use_thread_pool_for_widgets"
+ namespace: "systemui"
+ description: "Use a dedicated thread pool executor for loading widgets on glanceable hub"
+ bug: "369412569"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "communal_standalone_support"
namespace: "systemui"
description: "Support communal features without a dock"
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
index a1f0c146c507..38f09988e7a7 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/ActivityTransitionAnimator.kt
@@ -867,9 +867,6 @@ constructor(
) {
// Raise closing task to "above" layer so it isn't covered.
t.setLayer(target.leash, aboveLayers - i)
- } else if (TransitionUtil.isOpeningType(change.mode)) {
- // Put into the "below" layer space.
- t.setLayer(target.leash, belowLayers - i)
}
} else if (TransitionInfo.isIndependent(change, info)) {
// Root tasks
@@ -1150,7 +1147,7 @@ constructor(
// If a [controller.windowAnimatorState] exists, treat this like a takeover.
takeOverAnimationInternal(
window,
- startWindowState = null,
+ startWindowStates = null,
startTransaction = null,
callback,
)
@@ -1165,23 +1162,22 @@ constructor(
callback: IRemoteAnimationFinishedCallback?,
) {
val window = setUpAnimation(apps, callback) ?: return
- val startWindowState = startWindowStates[apps!!.indexOf(window)]
- takeOverAnimationInternal(window, startWindowState, startTransaction, callback)
+ takeOverAnimationInternal(window, startWindowStates, startTransaction, callback)
}
private fun takeOverAnimationInternal(
window: RemoteAnimationTarget,
- startWindowState: WindowAnimationState?,
+ startWindowStates: Array<WindowAnimationState>?,
startTransaction: SurfaceControl.Transaction?,
callback: IRemoteAnimationFinishedCallback?,
) {
val useSpring =
- !controller.isLaunching && startWindowState != null && startTransaction != null
+ !controller.isLaunching && startWindowStates != null && startTransaction != null
startAnimation(
window,
navigationBar = null,
useSpring,
- startWindowState,
+ startWindowStates,
startTransaction,
callback,
)
@@ -1291,7 +1287,7 @@ constructor(
window: RemoteAnimationTarget,
navigationBar: RemoteAnimationTarget? = null,
useSpring: Boolean = false,
- startingWindowState: WindowAnimationState? = null,
+ startingWindowStates: Array<WindowAnimationState>? = null,
startTransaction: SurfaceControl.Transaction? = null,
iCallback: IRemoteAnimationFinishedCallback? = null,
) {
@@ -1337,7 +1333,6 @@ constructor(
val isExpandingFullyAbove =
transitionAnimator.isExpandingFullyAbove(controller.transitionContainer, endState)
- val windowState = startingWindowState ?: controller.windowAnimatorState
// We animate the opening window and delegate the view expansion to [this.controller].
val delegate = this.controller
@@ -1360,6 +1355,18 @@ constructor(
}
}
+ // The states are sorted matching the changes inside the transition info.
+ // Using this info, the RemoteAnimationTargets are created, with their
+ // prefixOrderIndex fields in reverse order to that of changes. To extract
+ // the right state, we need to invert again.
+ val windowState =
+ if (startingWindowStates != null) {
+ startingWindowStates[
+ startingWindowStates.size - window.prefixOrderIndex]
+ } else {
+ controller.windowAnimatorState
+ }
+
// TODO(b/323863002): use the timestamp and velocity to update the initial
// position.
val bounds = windowState?.bounds
@@ -1448,6 +1455,12 @@ constructor(
delegate.onTransitionAnimationProgress(state, progress, linearProgress)
}
}
+ val windowState =
+ if (startingWindowStates != null) {
+ startingWindowStates[startingWindowStates.size - window.prefixOrderIndex]
+ } else {
+ controller.windowAnimatorState
+ }
val velocityPxPerS =
if (longLivedReturnAnimationsEnabled() && windowState?.velocityPxPerMs != null) {
val xVelocityPxPerS = windowState.velocityPxPerMs.x * 1000
@@ -1466,7 +1479,6 @@ constructor(
fadeWindowBackgroundLayer = !controller.isBelowAnimatingWindow,
drawHole = !controller.isBelowAnimatingWindow,
startVelocity = velocityPxPerS,
- startFrameTime = windowState?.timestamp ?: -1,
)
}
diff --git a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
index 4e889e946a5f..e2bc4095e1b5 100644
--- a/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
+++ b/packages/SystemUI/animation/src/com/android/systemui/animation/TransitionAnimator.kt
@@ -27,8 +27,6 @@ import android.graphics.drawable.GradientDrawable
import android.util.FloatProperty
import android.util.Log
import android.util.MathUtils
-import android.util.TimeUtils
-import android.view.Choreographer
import android.view.View
import android.view.ViewGroup
import android.view.ViewGroupOverlay
@@ -368,7 +366,6 @@ class TransitionAnimator(
@get:VisibleForTesting val springY: SpringAnimation,
@get:VisibleForTesting val springScale: SpringAnimation,
private val springState: SpringState,
- private val startFrameTime: Long,
private val onAnimationStart: Runnable,
) : Animation {
@get:VisibleForTesting
@@ -377,42 +374,6 @@ class TransitionAnimator(
override fun start() {
onAnimationStart.run()
-
- // If no start frame time is provided, we start the springs normally.
- if (startFrameTime < 0) {
- startSprings()
- return
- }
-
- // This function is not guaranteed to be called inside a frame. We try to access the
- // frame time immediately, but if we're not inside a frame this will throw an exception.
- // We must then post a callback to be run at the beginning of the next frame.
- try {
- initAndStartSprings(Choreographer.getInstance().frameTime)
- } catch (_: IllegalStateException) {
- Choreographer.getInstance().postFrameCallback { frameTimeNanos ->
- initAndStartSprings(frameTimeNanos / TimeUtils.NANOS_PER_MS)
- }
- }
- }
-
- private fun initAndStartSprings(frameTime: Long) {
- // Initialize the spring as if it had started at the time that its start state
- // was created.
- springX.doAnimationFrame(startFrameTime)
- springY.doAnimationFrame(startFrameTime)
- springScale.doAnimationFrame(startFrameTime)
- // Move the spring time forward to the current frame, so it updates its internal state
- // following the initial momentum over the elapsed time.
- springX.doAnimationFrame(frameTime)
- springY.doAnimationFrame(frameTime)
- springScale.doAnimationFrame(frameTime)
- // Actually start the spring. We do this after the previous calls because the framework
- // doesn't like it when you call doAnimationFrame() after start() with an earlier time.
- startSprings()
- }
-
- private fun startSprings() {
springX.start()
springY.start()
springScale.start()
@@ -510,9 +471,7 @@ class TransitionAnimator(
* is true.
*
* If [startVelocity] (expressed in pixels per second) is not null, a multi-spring animation
- * using it for the initial momentum will be used instead of the default interpolators. In this
- * case, [startFrameTime] (if non-negative) represents the frame time at which the springs
- * should be started.
+ * using it for the initial momentum will be used instead of the default interpolators.
*/
fun startAnimation(
controller: Controller,
@@ -521,7 +480,6 @@ class TransitionAnimator(
fadeWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
startVelocity: PointF? = null,
- startFrameTime: Long = -1,
): Animation {
if (!controller.isLaunching) assertReturnAnimations()
if (startVelocity != null) assertLongLivedReturnAnimations()
@@ -544,7 +502,6 @@ class TransitionAnimator(
fadeWindowBackgroundLayer,
drawHole,
startVelocity,
- startFrameTime,
)
.apply { start() }
}
@@ -558,7 +515,6 @@ class TransitionAnimator(
fadeWindowBackgroundLayer: Boolean = true,
drawHole: Boolean = false,
startVelocity: PointF? = null,
- startFrameTime: Long = -1,
): Animation {
val transitionContainer = controller.transitionContainer
val transitionContainerOverlay = transitionContainer.overlay
@@ -581,7 +537,6 @@ class TransitionAnimator(
startState,
endState,
startVelocity,
- startFrameTime,
windowBackgroundLayer,
transitionContainer,
transitionContainerOverlay,
@@ -767,7 +722,6 @@ class TransitionAnimator(
startState: State,
endState: State,
startVelocity: PointF,
- startFrameTime: Long,
windowBackgroundLayer: GradientDrawable,
transitionContainer: View,
transitionContainerOverlay: ViewGroupOverlay,
@@ -958,7 +912,7 @@ class TransitionAnimator(
}
}
- return MultiSpringAnimation(springX, springY, springScale, springState, startFrameTime) {
+ return MultiSpringAnimation(springX, springY, springScale, springState) {
onAnimationStart(
controller,
isExpandingFullyAbove,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
new file mode 100644
index 000000000000..e3310780afd7
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/ResponsiveLazyHorizontalGrid.kt
@@ -0,0 +1,234 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.communal.ui.compose
+
+import android.content.res.Configuration
+import androidx.compose.foundation.OverscrollEffect
+import androidx.compose.foundation.gestures.FlingBehavior
+import androidx.compose.foundation.gestures.ScrollableDefaults
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.BoxWithConstraints
+import androidx.compose.foundation.layout.PaddingValues
+import androidx.compose.foundation.layout.calculateEndPadding
+import androidx.compose.foundation.layout.calculateStartPadding
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.lazy.grid.GridCells
+import androidx.compose.foundation.lazy.grid.LazyGridScope
+import androidx.compose.foundation.lazy.grid.LazyGridState
+import androidx.compose.foundation.lazy.grid.LazyHorizontalGrid
+import androidx.compose.foundation.lazy.grid.rememberLazyGridState
+import androidx.compose.foundation.rememberOverscrollEffect
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.remember
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.platform.LocalConfiguration
+import androidx.compose.ui.platform.LocalLayoutDirection
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.DpSize
+import androidx.compose.ui.unit.IntSize
+import androidx.compose.ui.unit.coerceAtMost
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.times
+
+/**
+ * Renders a responsive [LazyHorizontalGrid] with dynamic columns and rows. Each cell will maintain
+ * the specified aspect ratio, but is otherwise resizeable in order to best fill the available
+ * space.
+ */
+@Composable
+fun ResponsiveLazyHorizontalGrid(
+ cellAspectRatio: Float,
+ modifier: Modifier = Modifier,
+ state: LazyGridState = rememberLazyGridState(),
+ minContentPadding: PaddingValues = PaddingValues(0.dp),
+ minHorizontalArrangement: Dp = 0.dp,
+ minVerticalArrangement: Dp = 0.dp,
+ flingBehavior: FlingBehavior = ScrollableDefaults.flingBehavior(),
+ userScrollEnabled: Boolean = true,
+ overscrollEffect: OverscrollEffect? = rememberOverscrollEffect(),
+ content: LazyGridScope.(sizeInfo: SizeInfo) -> Unit,
+) {
+ check(cellAspectRatio > 0f) { "Aspect ratio must be greater than 0, but was $cellAspectRatio" }
+ check(minHorizontalArrangement.value >= 0f && minVerticalArrangement.value >= 0f) {
+ "Horizontal and vertical arrangements must be non-negative, but were " +
+ "$minHorizontalArrangement and $minVerticalArrangement, respectively."
+ }
+ BoxWithConstraints(modifier) {
+ val gridSize = rememberGridSize(maxWidth = maxWidth, maxHeight = maxHeight)
+ val layoutDirection = LocalLayoutDirection.current
+
+ val minStartPadding = minContentPadding.calculateStartPadding(layoutDirection)
+ val minEndPadding = minContentPadding.calculateEndPadding(layoutDirection)
+ val minTopPadding = minContentPadding.calculateTopPadding()
+ val minBottomPadding = minContentPadding.calculateBottomPadding()
+ val minHorizontalPadding = minStartPadding + minEndPadding
+ val minVerticalPadding = minTopPadding + minBottomPadding
+
+ // Determine the maximum allowed cell width and height based on the available width and
+ // height, and the desired number of columns and rows.
+ val maxCellWidth =
+ calculateCellSize(
+ availableSpace = maxWidth,
+ padding = minHorizontalPadding,
+ numCells = gridSize.width,
+ cellSpacing = minHorizontalArrangement,
+ )
+ val maxCellHeight =
+ calculateCellSize(
+ availableSpace = maxHeight,
+ padding = minVerticalPadding,
+ numCells = gridSize.height,
+ cellSpacing = minVerticalArrangement,
+ )
+
+ // Constrain the max size to the desired aspect ratio.
+ val finalSize =
+ calculateClosestSize(
+ maxWidth = maxCellWidth,
+ maxHeight = maxCellHeight,
+ aspectRatio = cellAspectRatio,
+ )
+
+ // Determine how much space in each dimension we've used up, and how much we have left as
+ // extra space. Distribute the extra space evenly along the content padding.
+ val usedWidth =
+ calculateUsedSpace(
+ cellSize = finalSize.width,
+ numCells = gridSize.width,
+ padding = minHorizontalPadding,
+ cellSpacing = minHorizontalArrangement,
+ )
+ .coerceAtMost(maxWidth)
+ val usedHeight =
+ calculateUsedSpace(
+ cellSize = finalSize.height,
+ numCells = gridSize.height,
+ padding = minVerticalPadding,
+ cellSpacing = minVerticalArrangement,
+ )
+ .coerceAtMost(maxHeight)
+ val extraWidth = maxWidth - usedWidth
+ val extraHeight = maxHeight - usedHeight
+
+ val finalContentPadding =
+ PaddingValues(
+ start = minStartPadding + extraWidth / 2,
+ end = minEndPadding + extraWidth / 2,
+ top = minTopPadding + extraHeight / 2,
+ bottom = minBottomPadding + extraHeight / 2,
+ )
+
+ LazyHorizontalGrid(
+ rows = GridCells.Fixed(gridSize.height),
+ modifier = Modifier.fillMaxSize(),
+ state = state,
+ contentPadding = finalContentPadding,
+ horizontalArrangement = Arrangement.spacedBy(minHorizontalArrangement),
+ verticalArrangement = Arrangement.spacedBy(minVerticalArrangement),
+ flingBehavior = flingBehavior,
+ userScrollEnabled = userScrollEnabled,
+ overscrollEffect = overscrollEffect,
+ ) {
+ content(
+ SizeInfo(
+ cellSize = finalSize,
+ contentPadding = finalContentPadding,
+ horizontalArrangement = minHorizontalArrangement,
+ verticalArrangement = minVerticalArrangement,
+ maxHeight = maxHeight,
+ )
+ )
+ }
+ }
+}
+
+private fun calculateCellSize(availableSpace: Dp, padding: Dp, numCells: Int, cellSpacing: Dp): Dp =
+ (availableSpace - padding - cellSpacing * (numCells - 1)) / numCells
+
+private fun calculateUsedSpace(cellSize: Dp, numCells: Int, padding: Dp, cellSpacing: Dp): Dp =
+ cellSize * numCells + padding + (numCells - 1) * cellSpacing
+
+private fun calculateClosestSize(maxWidth: Dp, maxHeight: Dp, aspectRatio: Float): DpSize {
+ return if (maxWidth / maxHeight > aspectRatio) {
+ // Target is too wide, shrink width
+ DpSize(maxHeight * aspectRatio, maxHeight)
+ } else {
+ // Target is too tall, shrink height
+ DpSize(maxWidth, maxWidth / aspectRatio)
+ }
+}
+
+/**
+ * Provides size info of the responsive grid, since the size is dynamic.
+ *
+ * @property cellSize The size of each cell in the grid.
+ * @property contentPadding The final content padding of the grid.
+ * @property horizontalArrangement The space between columns in the grid.
+ * @property verticalArrangement The space between rows in the grid.
+ * @property availableHeight The maximum height an item in the grid may occupy.
+ */
+data class SizeInfo(
+ val cellSize: DpSize,
+ val contentPadding: PaddingValues,
+ val horizontalArrangement: Dp,
+ val verticalArrangement: Dp,
+ private val maxHeight: Dp,
+) {
+ val availableHeight: Dp
+ get() =
+ maxHeight -
+ contentPadding.calculateBottomPadding() -
+ contentPadding.calculateTopPadding()
+}
+
+@Composable
+private fun rememberGridSize(maxWidth: Dp, maxHeight: Dp): IntSize {
+ val configuration = LocalConfiguration.current
+ val orientation = configuration.orientation
+
+ return remember(orientation, maxWidth, maxHeight) {
+ if (orientation == Configuration.ORIENTATION_PORTRAIT) {
+ IntSize(
+ width = calculateNumCellsWidth(maxWidth),
+ height = calculateNumCellsHeight(maxHeight),
+ )
+ } else {
+ // In landscape we invert the rows/columns to ensure we match the same area as portrait.
+ // This keeps the number of elements in the grid consistent when changing orientation.
+ IntSize(
+ width = calculateNumCellsHeight(maxWidth),
+ height = calculateNumCellsWidth(maxHeight),
+ )
+ }
+ }
+}
+
+private fun calculateNumCellsWidth(width: Dp) =
+ // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
+ when {
+ width >= 840.dp -> 3
+ width >= 600.dp -> 2
+ else -> 1
+ }
+
+private fun calculateNumCellsHeight(height: Dp) =
+ // See https://developer.android.com/develop/ui/views/layout/use-window-size-classes
+ when {
+ height >= 900.dp -> 3
+ height >= 480.dp -> 2
+ else -> 1
+ }
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 eb2a01632095..e819bfd18578 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
@@ -53,10 +53,10 @@ import com.android.compose.animation.scene.content.state.TransitionState
import com.android.compose.animation.scene.transformation.CustomPropertyTransformation
import com.android.compose.animation.scene.transformation.InterpolatedPropertyTransformation
import com.android.compose.animation.scene.transformation.PropertyTransformation
-import com.android.compose.animation.scene.transformation.SharedElementTransformation
import com.android.compose.animation.scene.transformation.TransformationWithRange
import com.android.compose.modifiers.thenIf
import com.android.compose.ui.graphics.drawInContainer
+import com.android.compose.ui.util.IntIndexedMap
import com.android.compose.ui.util.lerp
import kotlin.math.roundToInt
import kotlinx.coroutines.launch
@@ -70,6 +70,14 @@ internal class Element(val key: ElementKey) {
val stateByContent = SnapshotStateMap<ContentKey, State>()
/**
+ * A sorted map of nesting depth (key) to content key (value). For shared elements it is used to
+ * determine which content this element should be rendered by. The nesting depth refers to the
+ * number of STLs nested within each other, starting at 0 for the parent STL and increasing by
+ * one for each nested [NestedSceneTransitionLayout].
+ */
+ val renderAuthority = IntIndexedMap<ContentKey>()
+
+ /**
* The last transition that was used when computing the state (size, position and alpha) of this
* element in any content, or `null` if it was last laid out when idle.
*/
@@ -232,9 +240,8 @@ internal class ElementNode(
private val element: Element
get() = _element!!
- private var _stateInContent: Element.State? = null
private val stateInContent: Element.State
- get() = _stateInContent!!
+ get() = element.stateByContent.getValue(content.key)
override val traverseKey: Any = ElementTraverseKey
@@ -248,9 +255,13 @@ internal class ElementNode(
val element =
layoutImpl.elements[key] ?: Element(key).also { layoutImpl.elements[key] = it }
_element = element
- _stateInContent =
- element.stateByContent[content.key]
- ?: Element.State(content.key).also { element.stateByContent[content.key] = it }
+ addToRenderAuthority(element)
+ if (!element.stateByContent.contains(content.key)) {
+ val elementState = Element.State(content.key)
+ element.stateByContent[content.key] = elementState
+
+ layoutImpl.ancestorContentKeys.forEach { element.stateByContent[it] = elementState }
+ }
}
private fun addNodeToContentState() {
@@ -272,8 +283,20 @@ internal class ElementNode(
removeNodeFromContentState()
maybePruneMaps(layoutImpl, element, stateInContent)
+ removeFromRenderAuthority()
_element = null
- _stateInContent = null
+ }
+
+ private fun addToRenderAuthority(element: Element) {
+ val nestingDepth = layoutImpl.ancestorContentKeys.size
+ element.renderAuthority[nestingDepth] = content.key
+ }
+
+ private fun removeFromRenderAuthority() {
+ val nestingDepth = layoutImpl.ancestorContentKeys.size
+ if (element.renderAuthority[nestingDepth] == content.key) {
+ element.renderAuthority.remove(nestingDepth)
+ }
}
private fun removeNodeFromContentState() {
@@ -346,15 +369,17 @@ internal class ElementNode(
val elementState = elementState(layoutImpl, element, currentTransitionStates)
if (elementState == null) {
// If the element is not part of any transition, place it normally in its idle scene.
+ // 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 placeInThisContent =
+ val shouldPlaceInThisContent =
elementContentWhenIdle(
layoutImpl,
currentState,
isInContent = { it in element.stateByContent },
) == content.key
-
- return if (placeInThisContent) {
+ return if (shouldPlaceInThisContent) {
placeNormally(measurable, constraints)
} else {
doNotPlace(measurable, constraints)
@@ -536,7 +561,9 @@ internal class ElementNode(
stateInContent.clearLastPlacementValues()
traverseDescendants(ElementTraverseKey) { node ->
- (node as ElementNode)._stateInContent?.clearLastPlacementValues()
+ if ((node as ElementNode)._element != null) {
+ node.stateInContent.clearLastPlacementValues()
+ }
TraversableNode.Companion.TraverseDescendantsAction.ContinueTraversal
}
}
@@ -569,22 +596,30 @@ internal class ElementNode(
element: Element,
stateInContent: Element.State,
) {
- // If element is not composed in this content anymore, remove the content values. This
- // works because [onAttach] is called before [onDetach], so if an element is moved from
- // the UI tree we will first add the new code location then remove the old one.
- if (
- stateInContent.nodes.isEmpty() &&
- element.stateByContent[stateInContent.content] == stateInContent
- ) {
- element.stateByContent.remove(stateInContent.content)
-
- // If the element is not composed in any content, remove it from the elements map.
+ fun pruneForContent(contentKey: ContentKey) {
+ // If element is not composed in this content anymore, remove the content values.
+ // This works because [onAttach] is called before [onDetach], so if an element is
+ // moved from the UI tree we will first add the new code location then remove the
+ // old one.
if (
- element.stateByContent.isEmpty() && layoutImpl.elements[element.key] == element
+ stateInContent.nodes.isEmpty() &&
+ element.stateByContent[contentKey] == stateInContent
) {
- layoutImpl.elements.remove(element.key)
+ element.stateByContent.remove(contentKey)
+
+ // If the element is not composed in any content, remove it from the elements
+ // map.
+ if (
+ element.stateByContent.isEmpty() &&
+ layoutImpl.elements[element.key] == element
+ ) {
+ layoutImpl.elements.remove(element.key)
+ }
}
}
+
+ pruneForContent(stateInContent.content)
+ layoutImpl.ancestorContentKeys.forEach { content -> pruneForContent(content) }
}
}
}
@@ -890,12 +925,13 @@ private fun shouldPlaceElement(
val transition =
when (elementState) {
is TransitionState.Idle -> {
- return content ==
- elementContentWhenIdle(
- layoutImpl,
- elementState,
- isInContent = { it in element.stateByContent },
- )
+ return element.shouldBeRenderedBy(content) &&
+ content ==
+ elementContentWhenIdle(
+ layoutImpl,
+ elementState,
+ isInContent = { it in element.stateByContent },
+ )
}
is TransitionState.Transition -> elementState
}
@@ -925,76 +961,7 @@ private fun shouldPlaceElement(
return true
}
- return shouldPlaceOrComposeSharedElement(
- layoutImpl,
- content,
- element.key,
- transition,
- isInContent = { it in element.stateByContent },
- )
-}
-
-internal inline fun shouldPlaceOrComposeSharedElement(
- layoutImpl: SceneTransitionLayoutImpl,
- content: ContentKey,
- element: ElementKey,
- transition: TransitionState.Transition,
- isInContent: (ContentKey) -> Boolean,
-): Boolean {
- val overscrollContent = transition.currentOverscrollSpec?.content
- if (overscrollContent != null) {
- return when (transition) {
- // If we are overscrolling between scenes, only place/compose the element in the
- // overscrolling scene.
- is TransitionState.Transition.ChangeScene -> content == overscrollContent
-
- // If we are overscrolling an overlay, place/compose the element if [content] is the
- // overscrolling content or if [content] is the current scene and the overscrolling
- // overlay does not contain the element.
- is TransitionState.Transition.ReplaceOverlay,
- is TransitionState.Transition.ShowOrHideOverlay ->
- content == overscrollContent ||
- (content == transition.currentScene && !isInContent(overscrollContent))
- }
- }
-
- val scenePicker = element.contentPicker
- val pickedScene =
- scenePicker.contentDuringTransition(
- element = element,
- transition = transition,
- fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
- toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
- )
-
- return pickedScene == content
-}
-
-private fun isSharedElementEnabled(
- element: ElementKey,
- transition: TransitionState.Transition,
-): Boolean {
- return sharedElementTransformation(element, transition)?.transformation?.enabled ?: true
-}
-
-internal fun sharedElementTransformation(
- element: ElementKey,
- transition: TransitionState.Transition,
-): TransformationWithRange<SharedElementTransformation>? {
- val transformationSpec = transition.transformationSpec
- val sharedInFromContent =
- 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) {
- error(
- "Different sharedElement() transformations matched $element " +
- "(from=$sharedInFromContent to=$sharedInToContent)"
- )
- }
-
- return sharedInFromContent
+ return shouldPlaceSharedElement(layoutImpl, content, element.key, 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 509a16c5a704..17510c732e65 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
@@ -196,18 +196,54 @@ private fun shouldComposeMovableElement(
is TransitionState.Transition -> {
// During transitions, always compose movable elements in the scene picked by their
// content picker.
- val contents = element.contentPicker.contents
- shouldPlaceOrComposeSharedElement(
+ shouldComposeMoveableElement(
layoutImpl,
content,
element,
elementState,
- isInContent = { contents.contains(it) },
+ element.contentPicker.contents,
)
}
}
}
+private fun shouldComposeMoveableElement(
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: ContentKey,
+ elementKey: ElementKey,
+ transition: TransitionState.Transition,
+ containingContents: Set<ContentKey>,
+): Boolean {
+ val overscrollContent = transition.currentOverscrollSpec?.content
+ if (overscrollContent != null) {
+ return when (transition) {
+ // If we are overscrolling between scenes, only place/compose the element in the
+ // overscrolling scene.
+ is TransitionState.Transition.ChangeScene -> content == overscrollContent
+
+ // If we are overscrolling an overlay, place/compose the element if [content] is the
+ // overscrolling content or if [content] is the current scene and the overscrolling
+ // overlay does not contain the element.
+ is TransitionState.Transition.ReplaceOverlay,
+ is TransitionState.Transition.ShowOrHideOverlay ->
+ content == overscrollContent ||
+ (content == transition.currentScene &&
+ !containingContents.contains(overscrollContent))
+ }
+ }
+
+ val scenePicker = elementKey.contentPicker
+ val pickedScene =
+ scenePicker.contentDuringTransition(
+ element = elementKey,
+ transition = transition,
+ fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
+ toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
+ )
+
+ return pickedScene == content
+}
+
private fun movableElementState(
element: MovableElementKey,
transitionStates: List<TransitionState>,
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 d3ddb5003469..a91a5058a436 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
@@ -28,6 +28,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.input.nestedscroll.NestedScrollConnection
import androidx.compose.ui.input.pointer.PointerType
+import androidx.compose.ui.layout.LookaheadScope
import androidx.compose.ui.platform.LocalDensity
import androidx.compose.ui.platform.LocalLayoutDirection
import androidx.compose.ui.unit.Density
@@ -68,7 +69,7 @@ fun SceneTransitionLayout(
swipeDetector,
transitionInterceptionThreshold,
onLayoutImpl = null,
- builder,
+ builder = builder,
)
}
@@ -261,6 +262,18 @@ interface BaseContentScope : ElementStateScope {
* lists keep a constant size during transitions even if its elements are growing/shrinking.
*/
fun Modifier.noResizeDuringTransitions(): Modifier
+
+ /**
+ * A [NestedSceneTransitionLayout] will share its elements with its ancestor STLs therefore
+ * enabling sharedElement transitions between them.
+ */
+ // TODO(b/380070506): Add more parameters when default params are supported in Kotlin 2.0.21
+ @Composable
+ fun NestedSceneTransitionLayout(
+ state: SceneTransitionLayoutState,
+ modifier: Modifier,
+ builder: SceneTransitionLayoutScope.() -> Unit,
+ )
}
typealias SceneScope = ContentScope
@@ -677,6 +690,9 @@ internal fun SceneTransitionLayoutForTesting(
swipeDetector: SwipeDetector = DefaultSwipeDetector,
transitionInterceptionThreshold: Float = 0f,
onLayoutImpl: ((SceneTransitionLayoutImpl) -> Unit)? = null,
+ sharedElementMap: MutableMap<ElementKey, Element> = remember { mutableMapOf() },
+ ancestorContentKeys: List<ContentKey> = emptyList(),
+ lookaheadScope: LookaheadScope? = null,
builder: SceneTransitionLayoutScope.() -> Unit,
) {
val density = LocalDensity.current
@@ -691,6 +707,9 @@ internal fun SceneTransitionLayoutForTesting(
transitionInterceptionThreshold = transitionInterceptionThreshold,
builder = builder,
animationScope = animationScope,
+ elements = sharedElementMap,
+ ancestorContentKeys = ancestorContentKeys,
+ lookaheadScope = lookaheadScope,
)
.also { onLayoutImpl?.invoke(it) }
}
@@ -706,6 +725,24 @@ internal fun SceneTransitionLayoutForTesting(
" that was used when creating it, which is not supported"
)
}
+ if (layoutImpl.elements != sharedElementMap) {
+ error(
+ "This SceneTransitionLayout was bound to a different elements map that was used " +
+ "when creating it, which is not supported"
+ )
+ }
+ if (layoutImpl.ancestorContentKeys != ancestorContentKeys) {
+ error(
+ "This SceneTransitionLayout was bound to a different ancestorContents that was " +
+ "used when creating it, which is not supported"
+ )
+ }
+ if (lookaheadScope != null && layoutImpl.lookaheadScope != lookaheadScope) {
+ error(
+ "This SceneTransitionLayout was bound to a different lookaheadScope that was " +
+ "used when creating it, which is not supported"
+ )
+ }
layoutImpl.density = density
layoutImpl.layoutDirection = layoutDirection
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 b916b0b45e41..bdc1461f06c9 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
@@ -70,7 +70,39 @@ internal class SceneTransitionLayoutImpl(
* animations.
*/
internal val animationScope: CoroutineScope,
+
+ /**
+ * The map of [Element]s.
+ *
+ * Important: [Element]s from this map should never be accessed during composition because the
+ * Elements are added when the associated Modifier.element() node is attached to the Modifier
+ * tree, i.e. after composition.
+ */
+ 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.
+ *
+ * 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.
+ */
+ internal val ancestorContentKeys: List<ContentKey> = emptyList(),
+ lookaheadScope: LookaheadScope? = null,
) {
+
+ /**
+ * The [LookaheadScope] of this layout, that can be used to compute offsets relative to the
+ * layout. For [NestedSceneTransitionLayout]s this scope is the scope of the root STL, such that
+ * offset computations can be shared among all children.
+ */
+ private var _lookaheadScope: LookaheadScope? = lookaheadScope
+ internal val lookaheadScope: LookaheadScope
+ get() = _lookaheadScope!!
+
/**
* The map of [Scene]s.
*
@@ -89,15 +121,6 @@ internal class SceneTransitionLayoutImpl(
get() = _overlays ?: SnapshotStateMap<OverlayKey, Overlay>().also { _overlays = it }
/**
- * The map of [Element]s.
- *
- * Important: [Element]s from this map should never be accessed during composition because the
- * Elements are added when the associated Modifier.element() node is attached to the Modifier
- * tree, i.e. after composition.
- */
- internal val elements = mutableMapOf<ElementKey, Element>()
-
- /**
* The map of contents of movable elements.
*
* Note that given that this map is mutated directly during a composition, it has to be a
@@ -138,13 +161,6 @@ internal class SceneTransitionLayoutImpl(
_userActionDistanceScope = it
}
- /**
- * The [LookaheadScope] of this layout, that can be used to compute offsets relative to the
- * layout.
- */
- internal lateinit var lookaheadScope: LookaheadScope
- private set
-
internal var lastSize: IntSize = IntSize.Zero
init {
@@ -347,7 +363,12 @@ internal class SceneTransitionLayoutImpl(
.then(LayoutElement(layoutImpl = this))
) {
LookaheadScope {
- lookaheadScope = this
+ if (_lookaheadScope == null) {
+ // We can't init this in a SideEffect as other NestedSTLs are already calling
+ // this during composition. However, when composition is canceled
+ // SceneTransitionLayoutImpl is discarded as well. So it's fine to do this here.
+ _lookaheadScope = this
+ }
BackHandler()
Scenes()
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
new file mode 100644
index 000000000000..599a152a23bd
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SharedElement.kt
@@ -0,0 +1,113 @@
+/*
+ * 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.compose.animation.scene
+
+import com.android.compose.animation.scene.content.state.TransitionState
+import com.android.compose.animation.scene.transformation.SharedElementTransformation
+import com.android.compose.animation.scene.transformation.TransformationWithRange
+
+/**
+ * Whether this element should be rendered by the given [content]. This method returns true only for
+ * exactly one content at any given time.
+ */
+internal fun Element.shouldBeRenderedBy(content: ContentKey): Boolean {
+ // The current strategy is that always the content with the lowest nestingDepth has authority.
+ // This content is supposed to render the shared element because this is also the level at which
+ // the transition is running. If the [renderAuthority.size] is 1 it means that that this element
+ // is currently composed only in one nesting level, which means that the render authority
+ // is determined by "classic" shared element code.
+ return renderAuthority.size == 1 || renderAuthority.first() == content
+}
+
+/**
+ * Whether this element is currently composed in multiple [SceneTransitionLayout]s.
+ *
+ * Note: Shared elements across [NestedSceneTransitionLayout]s side-by-side are not supported.
+ */
+internal fun Element.isPresentInMultipleStls(): Boolean {
+ return renderAuthority.size > 1
+}
+
+internal fun shouldPlaceSharedElement(
+ layoutImpl: SceneTransitionLayoutImpl,
+ content: ContentKey,
+ elementKey: ElementKey,
+ transition: TransitionState.Transition,
+): Boolean {
+ val element = layoutImpl.elements.getValue(elementKey)
+ if (element.isPresentInMultipleStls()) {
+ // If the element is present in multiple STLs we require the highest STL to render it and
+ // we don't want contentPicker to potentially return false for the highest STL.
+ return element.shouldBeRenderedBy(content)
+ }
+
+ val overscrollContent = transition.currentOverscrollSpec?.content
+ if (overscrollContent != null) {
+ return when (transition) {
+ // If we are overscrolling between scenes, only place/compose the element in the
+ // overscrolling scene.
+ is TransitionState.Transition.ChangeScene -> content == overscrollContent
+
+ // If we are overscrolling an overlay, place/compose the element if [content] is the
+ // overscrolling content or if [content] is the current scene and the overscrolling
+ // overlay does not contain the element.
+ is TransitionState.Transition.ReplaceOverlay,
+ is TransitionState.Transition.ShowOrHideOverlay ->
+ content == overscrollContent ||
+ (content == transition.currentScene &&
+ overscrollContent !in element.stateByContent)
+ }
+ }
+
+ val scenePicker = elementKey.contentPicker
+ val pickedScene =
+ scenePicker.contentDuringTransition(
+ element = elementKey,
+ transition = transition,
+ fromContentZIndex = layoutImpl.content(transition.fromContent).zIndex,
+ toContentZIndex = layoutImpl.content(transition.toContent).zIndex,
+ )
+
+ return pickedScene == content
+}
+
+internal fun isSharedElementEnabled(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+): Boolean {
+ return sharedElementTransformation(element, transition)?.transformation?.enabled ?: true
+}
+
+internal fun sharedElementTransformation(
+ element: ElementKey,
+ transition: TransitionState.Transition,
+): TransformationWithRange<SharedElementTransformation>? {
+ val transformationSpec = transition.transformationSpec
+ val sharedInFromContent =
+ 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) {
+ error(
+ "Different sharedElement() transformations matched $element " +
+ "(from=$sharedInFromContent to=$sharedInToContent)"
+ )
+ }
+
+ return sharedInFromContent
+}
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 255a16c6de6b..8c4cd8c93b87 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
@@ -23,6 +23,7 @@ import androidx.compose.runtime.Stable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableFloatStateOf
import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.layout.approachLayout
@@ -41,7 +42,9 @@ import com.android.compose.animation.scene.MovableElement
import com.android.compose.animation.scene.MovableElementContentScope
import com.android.compose.animation.scene.MovableElementKey
import com.android.compose.animation.scene.NestedScrollBehavior
+import com.android.compose.animation.scene.SceneTransitionLayoutForTesting
import com.android.compose.animation.scene.SceneTransitionLayoutImpl
+import com.android.compose.animation.scene.SceneTransitionLayoutScope
import com.android.compose.animation.scene.SceneTransitionLayoutState
import com.android.compose.animation.scene.SharedValueType
import com.android.compose.animation.scene.UserAction
@@ -175,4 +178,24 @@ internal class ContentScopeImpl(
override fun Modifier.noResizeDuringTransitions(): Modifier {
return noResizeDuringTransitions(layoutState = layoutImpl.state)
}
+
+ @Composable
+ override fun NestedSceneTransitionLayout(
+ state: SceneTransitionLayoutState,
+ modifier: Modifier,
+ builder: SceneTransitionLayoutScope.() -> Unit,
+ ) {
+ SceneTransitionLayoutForTesting(
+ state,
+ modifier,
+ onLayoutImpl = null,
+ builder = builder,
+ sharedElementMap = layoutImpl.elements,
+ ancestorContentKeys =
+ remember(layoutImpl.ancestorContentKeys, contentKey) {
+ layoutImpl.ancestorContentKeys + contentKey
+ },
+ lookaheadScope = layoutImpl.lookaheadScope,
+ )
+ }
}
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/IntIndexedMap.kt b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/IntIndexedMap.kt
new file mode 100644
index 000000000000..1b5341b8048a
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/ui/util/IntIndexedMap.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.compose.ui.util
+
+/**
+ * This is a custom implementation that resembles a SortedMap<Int, T> but is based on a simple
+ * ArrayList to avoid the allocation overhead and boxing.
+ *
+ * It can only hold positive keys and 0 and it is only efficient for small keys (0 - ~100), but
+ * therefore provides fast operations for small keys.
+ */
+internal class IntIndexedMap<T> {
+ private val arrayList = ArrayList<T?>()
+ private var _size = 0
+ val size
+ get() = _size
+
+ /** Returns the value at [key] or null if the key is not present. */
+ operator fun get(key: Int): T? {
+ if (key < 0 || key >= arrayList.size) return null
+ return arrayList[key]
+ }
+
+ /**
+ * Sets the value at [key] to [value]. If [key] is larger than the current size of the map, this
+ * operation may take up to O(key) time and space. Therefore this data structure is only
+ * efficient for small [key] sizes.
+ */
+ operator fun set(key: Int, value: T?) {
+ if (key < 0)
+ throw UnsupportedOperationException("This map can only hold positive keys and 0.")
+ if (key < arrayList.size) {
+ if (arrayList[key] != null && value == null) _size--
+ if (arrayList[key] == null && value != null) _size++
+ arrayList[key] = value
+ } else {
+ if (value == null) return
+ while (key > arrayList.size) {
+ arrayList.add(null)
+ }
+ _size++
+ arrayList.add(value)
+ }
+ }
+
+ /** Remove value at [key] */
+ fun remove(key: Int) {
+ if (key >= arrayList.size) return
+ this[key] = null
+ }
+
+ /** Get the [value] with the smallest [key] of the map. */
+ fun first(): T {
+ for (i in 0 until arrayList.size) {
+ return arrayList[i] ?: continue
+ }
+ throw NoSuchElementException("The map is empty.")
+ }
+}
diff --git a/packages/SystemUI/compose/scene/tests/AndroidManifest.xml b/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
index 174ad30a8f1d..2b76d7ba267e 100644
--- a/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
+++ b/packages/SystemUI/compose/scene/tests/AndroidManifest.xml
@@ -18,7 +18,8 @@
package="com.android.compose.animation.scene.tests" >
<uses-permission android:name="android.permission.WRITE_SECURE_SETTINGS" />
- <application>
+ <application
+ android:theme="@android:style/Theme.NoTitleBar.Fullscreen">
<uses-library android:name="android.test.runner" />
</application>
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
new file mode 100644
index 000000000000..c6ef8cff1a66
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/transformation/NestedSharedElementTest.kt
@@ -0,0 +1,283 @@
+/*
+ * 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.assertIsNotDisplayed
+import androidx.compose.ui.test.assertPositionInRootIsEqualTo
+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.AutoTransitionTestAssertionScope
+import com.android.compose.animation.scene.ContentScope
+import com.android.compose.animation.scene.Default4FrameLinearTransition
+import com.android.compose.animation.scene.Edge
+import com.android.compose.animation.scene.MutableSceneTransitionLayoutState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.TestElements
+import com.android.compose.animation.scene.TestScenes
+import com.android.compose.animation.scene.inScene
+import com.android.compose.animation.scene.testTransition
+import com.android.compose.animation.scene.transitions
+import com.android.compose.test.assertSizeIsEqualTo
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@RunWith(AndroidJUnit4::class)
+class NestedSharedElementTest {
+ @get:Rule val rule = createComposeRule()
+
+ private object Scenes {
+ val NestedSceneA = SceneKey("NestedSceneA")
+ val NestedSceneB = SceneKey("NestedSceneB")
+ val NestedNestedSceneA = SceneKey("NestedNestedSceneA")
+ val NestedNestedSceneB = SceneKey("NestedNestedSceneB")
+ }
+
+ private val elementVariant1 = SharedElement(0.dp, 0.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)
+
+ private class SharedElement(
+ 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.SharedElement(element: SharedElement) {
+ Box(Modifier.fillMaxSize()) {
+ Box(
+ Modifier.offset(element.x, element.y)
+ .element(TestElements.Foo)
+ .size(element.width, element.height)
+ .background(element.color)
+ .alpha(element.alpha)
+ )
+ }
+ }
+
+ private val contentWithSharedElement: @Composable ContentScope.() -> Unit = {
+ SharedElement(elementVariant1)
+ }
+
+ private val nestedState: MutableSceneTransitionLayoutState =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ Scenes.NestedSceneA,
+ transitions {
+ from(
+ from = Scenes.NestedSceneA,
+ to = Scenes.NestedSceneB,
+ builder = Default4FrameLinearTransition,
+ )
+ },
+ )
+ }
+
+ private val nestedNestedState: MutableSceneTransitionLayoutState =
+ rule.runOnUiThread {
+ MutableSceneTransitionLayoutState(
+ Scenes.NestedNestedSceneA,
+ transitions {
+ from(
+ from = Scenes.NestedNestedSceneA,
+ to = Scenes.NestedNestedSceneB,
+ builder = Default4FrameLinearTransition,
+ )
+ },
+ )
+ }
+
+ private val nestedStlWithSharedElement: @Composable ContentScope.() -> Unit = {
+ NestedSceneTransitionLayout(nestedState, modifier = Modifier) {
+ scene(Scenes.NestedSceneA) { SharedElement(elementVariant2) }
+ scene(Scenes.NestedSceneB) { SharedElement(elementVariant3) }
+ }
+ }
+
+ private val nestedNestedStlWithSharedElement: @Composable ContentScope.() -> Unit = {
+ NestedSceneTransitionLayout(nestedState, modifier = Modifier) {
+ scene(Scenes.NestedSceneA) {
+ NestedSceneTransitionLayout(state = nestedNestedState, modifier = Modifier) {
+ scene(Scenes.NestedNestedSceneA) { SharedElement(elementVariant4) }
+ scene(Scenes.NestedNestedSceneB) { SharedElement(elementVariant3) }
+ }
+ }
+ scene(Scenes.NestedSceneB) { SharedElement(elementVariant2) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElementTransition_fromNestedSTLtoParentSTL() {
+ rule.testTransition(
+ fromSceneContent = nestedStlWithSharedElement,
+ toSceneContent = contentWithSharedElement,
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
+
+ onElement(TestElements.Foo, TestScenes.SceneB)
+ .assertBetweenElementVariants(elementVariant2, elementVariant1, this)
+ }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElementTransition_fromParentSTLtoNestedSTL() {
+ rule.testTransition(
+ fromSceneContent = contentWithSharedElement,
+ toSceneContent = nestedStlWithSharedElement,
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
+
+ onElement(TestElements.Foo, TestScenes.SceneA)
+ .assertBetweenElementVariants(elementVariant1, elementVariant2, this)
+ }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElementTransition_fromParentSTLtoNestedNestedSTL() {
+ rule.testTransition(
+ fromSceneContent = contentWithSharedElement,
+ toSceneContent = nestedNestedStlWithSharedElement,
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, TestScenes.SceneB).assertIsNotDisplayed()
+
+ onElement(TestElements.Foo, TestScenes.SceneA)
+ .assertBetweenElementVariants(elementVariant1, elementVariant4, this)
+ }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant4) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElementTransition_fromNestedNestedSTLtoNestedSTL() {
+ rule.testTransition(
+ fromSceneContent = nestedNestedStlWithSharedElement,
+ toSceneContent = { Box(modifier = Modifier.fillMaxSize()) },
+ changeState = { nestedState.setTargetScene(Scenes.NestedSceneB, this) },
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant4) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
+ onElement(TestElements.Foo, Scenes.NestedNestedSceneA).assertIsNotDisplayed()
+
+ onElement(TestElements.Foo, Scenes.NestedSceneB)
+ .assertBetweenElementVariants(elementVariant4, elementVariant2, this)
+ }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElement_sharedElementTransitionIsDisabled() {
+ rule.testTransition(
+ fromSceneContent = contentWithSharedElement,
+ toSceneContent = nestedStlWithSharedElement,
+ transition = {
+ spec = tween(16 * 4, easing = LinearEasing)
+
+ // Disable the shared element animation.
+ sharedElement(TestElements.Foo, enabled = false)
+
+ // 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
+ },
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant1) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, scene = TestScenes.SceneA)
+ .assertPositionInRootIsEqualTo(
+ interpolate(elementVariant1.x, 0.dp),
+ elementVariant1.y,
+ )
+ .assertSizeIsEqualTo(elementVariant1.width, elementVariant1.height)
+ }
+ after { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ }
+ }
+
+ @Test
+ fun nestedSharedElementTransition_transitionInsideNestedStl() {
+ rule.testTransition(
+ layoutModifier = Modifier.fillMaxSize(),
+ fromSceneContent = nestedStlWithSharedElement,
+ toSceneContent = contentWithSharedElement,
+ changeState = { nestedState.setTargetScene(Scenes.NestedSceneB, animationScope = this) },
+ ) {
+ before { onElement(TestElements.Foo).assertElementVariant(elementVariant2) }
+ atAllFrames(4) {
+ onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
+
+ onElement(TestElements.Foo, scene = Scenes.NestedSceneB)
+ .assertBetweenElementVariants(elementVariant2, elementVariant3, this)
+ }
+ after {
+ onElement(TestElements.Foo, Scenes.NestedSceneA).assertIsNotDisplayed()
+ onElement(TestElements.Foo).assertElementVariant(elementVariant3)
+ }
+ }
+ }
+
+ private fun SemanticsNodeInteraction.assertElementVariant(variant: SharedElement) {
+ assertPositionInRootIsEqualTo(variant.x, variant.y)
+ assertSizeIsEqualTo(variant.width, variant.height)
+ }
+
+ private fun SemanticsNodeInteraction.assertBetweenElementVariants(
+ from: SharedElement,
+ to: SharedElement,
+ assertScope: AutoTransitionTestAssertionScope,
+ ) {
+ assertPositionInRootIsEqualTo(
+ assertScope.interpolate(from.x, to.x),
+ assertScope.interpolate(from.y, to.y),
+ )
+ assertSizeIsEqualTo(
+ assertScope.interpolate(from.width, to.width),
+ assertScope.interpolate(from.height, to.height),
+ )
+ }
+}
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 2e3a934c2701..47c10f5ab3a3 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
@@ -62,35 +62,14 @@ class SharedElementTest {
onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp)
onElement(TestElements.Foo).assertSizeIsEqualTo(20.dp, 80.dp)
}
- at(0) {
- // Shared elements are by default placed and drawn only in the scene with highest
- // zIndex.
+ atAllFrames(4) {
onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- .assertSizeIsEqualTo(20.dp, 80.dp)
- }
- at(16) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(20.dp, 55.dp)
- .assertSizeIsEqualTo(17.5.dp, 70.dp)
- }
- at(32) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
- onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(30.dp, 60.dp)
- .assertSizeIsEqualTo(15.dp, 60.dp)
- }
- at(48) {
- onElement(TestElements.Foo, TestScenes.SceneA).assertIsNotDisplayed()
-
onElement(TestElements.Foo, TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(40.dp, 65.dp)
- .assertSizeIsEqualTo(12.5.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(
+ interpolate(10.dp, 50.dp),
+ interpolate(50.dp, 70.dp),
+ )
+ .assertSizeIsEqualTo(interpolate(20.dp, 10.dp), interpolate(80.dp, 40.dp))
}
after {
onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 70.dp)
@@ -132,29 +111,11 @@ class SharedElementTest {
},
) {
before { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(10.dp, 50.dp) }
- at(0) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(10.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 100.dp)
- }
- at(16) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(7.5.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 90.dp)
- }
- at(32) {
- onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(5.dp, 50.dp)
- onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 80.dp)
- }
- at(48) {
+ atAllFrames(4) {
onElement(TestElements.Foo, scene = TestScenes.SceneA)
- .assertPositionInRootIsEqualTo(2.5.dp, 50.dp)
+ .assertPositionInRootIsEqualTo(interpolate(10.dp, 0.dp), 50.dp)
onElement(TestElements.Foo, scene = TestScenes.SceneB)
- .assertPositionInRootIsEqualTo(50.dp, 70.dp)
+ .assertPositionInRootIsEqualTo(50.dp, interpolate(100.dp, 60.dp))
}
after { onElement(TestElements.Foo).assertPositionInRootIsEqualTo(50.dp, 60.dp) }
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/ui/util/IntIndexMapTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/ui/util/IntIndexMapTest.kt
new file mode 100644
index 000000000000..d7a9b9007be0
--- /dev/null
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/ui/util/IntIndexMapTest.kt
@@ -0,0 +1,92 @@
+/*
+ * 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.compose.ui.util
+
+import com.google.common.truth.Truth.assertThat
+import org.junit.Test
+
+class IntIndexMapTest {
+
+ @Test
+ fun testSetGetFirstAndSize() {
+ val map = IntIndexedMap<String>()
+
+ // Write first element at 10
+ map[10] = "1"
+ assertThat(map[10]).isEqualTo("1")
+ assertThat(map.size).isEqualTo(1)
+ assertThat(map.first()).isEqualTo("1")
+
+ // Write same element to same index
+ map[10] = "1"
+ assertThat(map[10]).isEqualTo("1")
+ assertThat(map.size).isEqualTo(1)
+
+ // Writing into larger index
+ map[12] = "2"
+ assertThat(map[12]).isEqualTo("2")
+ assertThat(map.size).isEqualTo(2)
+ assertThat(map.first()).isEqualTo("1")
+
+ // Overwriting existing index
+ map[10] = "3"
+ assertThat(map[10]).isEqualTo("3")
+ assertThat(map.size).isEqualTo(2)
+ assertThat(map.first()).isEqualTo("3")
+
+ // Writing into smaller index
+ map[0] = "4"
+ assertThat(map[0]).isEqualTo("4")
+ assert(map.size == 3)
+ assertThat(map.first()).isEqualTo("4")
+
+ // Writing null into non-null index
+ map[0] = null
+ assertThat(map[0]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(2)
+ assertThat(map.first()).isEqualTo("3")
+
+ // Writing null into smaller null index
+ map[1] = null
+ assertThat(map[1]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(2)
+
+ // Writing null into larger null index
+ map[15] = null
+ assertThat(map[15]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(2)
+
+ // Remove existing element
+ map.remove(12)
+ assertThat(map[12]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(1)
+
+ // Remove non-existing element
+ map.remove(17)
+ assertThat(map[17]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(1)
+
+ // Remove all elements
+ assertThat(map.first()).isEqualTo("3")
+ map.remove(10)
+ map.remove(10)
+ map.remove(0)
+ assertThat(map.size).isEqualTo(0)
+ assertThat(map[10]).isEqualTo(null)
+ assertThat(map.size).isEqualTo(0)
+ }
+}
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 0d2fcfc0b790..124b61e45ed6 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
@@ -16,6 +16,8 @@
package com.android.compose.animation.scene
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.Composable
@@ -27,6 +29,9 @@ import androidx.compose.ui.semantics.SemanticsNode
import androidx.compose.ui.test.SemanticsNodeInteraction
import androidx.compose.ui.test.SemanticsNodeInteractionsProvider
import androidx.compose.ui.test.junit4.ComposeContentTestRule
+import androidx.compose.ui.unit.Dp
+import androidx.compose.ui.unit.lerp
+import androidx.compose.ui.util.lerp
import kotlinx.coroutines.CoroutineScope
import platform.test.motion.MotionTestRule
import platform.test.motion.RecordedMotion
@@ -62,6 +67,16 @@ interface TransitionTestBuilder {
fun at(timestamp: Long, builder: TransitionTestAssertionScope.() -> Unit)
/**
+ * Run the same assertion for all frames of a transition.
+ *
+ * @param totalFrames needs to be the exact number of frames of the transition that is run,
+ * otherwise the passed progress will be incorrect. That is the duration in ms divided by 16.
+ * @param builder is passed a progress Float which can be used to calculate values for the
+ * specific frame. Or use [AutoTransitionTestAssertionScope.interpolate].
+ */
+ fun atAllFrames(totalFrames: Int, builder: AutoTransitionTestAssertionScope.(Float) -> Unit)
+
+ /**
* Assert on the state of the layout after the transition finished.
*
* This should be called maximum once, after [before] or [at] is called.
@@ -82,6 +97,16 @@ interface TransitionTestAssertionScope {
fun onElement(element: ElementKey, scene: SceneKey? = null): SemanticsNodeInteraction
}
+interface AutoTransitionTestAssertionScope : TransitionTestAssertionScope {
+
+ /** Linear interpolate [from] and [to] with the current progress of the transition. */
+ fun <T> interpolate(from: T, to: T): T
+}
+
+val Default4FrameLinearTransition: TransitionBuilder.() -> Unit = {
+ spec = tween(16 * 4, easing = LinearEasing)
+}
+
/**
* Test the transition between [fromSceneContent] and [toSceneContent] at different points in time.
*
@@ -90,10 +115,13 @@ interface TransitionTestAssertionScope {
fun ComposeContentTestRule.testTransition(
fromSceneContent: @Composable ContentScope.() -> Unit,
toSceneContent: @Composable ContentScope.() -> Unit,
- transition: TransitionBuilder.() -> Unit,
+ transition: TransitionBuilder.() -> Unit = Default4FrameLinearTransition,
layoutModifier: Modifier = Modifier,
fromScene: SceneKey = TestScenes.SceneA,
toScene: SceneKey = TestScenes.SceneB,
+ changeState: CoroutineScope.(MutableSceneTransitionLayoutState) -> Unit = { state ->
+ state.setTargetScene(toScene, animationScope = this)
+ },
builder: TransitionTestBuilder.() -> Unit,
) {
testTransition(
@@ -104,7 +132,7 @@ fun ComposeContentTestRule.testTransition(
transitions { from(fromScene, to = toScene, builder = transition) },
)
},
- to = toScene,
+ changeState = changeState,
transitionLayout = { state ->
SceneTransitionLayout(state, layoutModifier) {
scene(fromScene, content = fromSceneContent)
@@ -293,13 +321,30 @@ fun ComposeContentTestRule.testTransition(
) {
val test = transitionTest(builder)
val assertionScope =
- object : TransitionTestAssertionScope {
+ object : AutoTransitionTestAssertionScope {
+ var progress = 0f
+
override fun onElement(
element: ElementKey,
scene: SceneKey?,
): SemanticsNodeInteraction {
return onNode(isElement(element, scene))
}
+
+ override fun <T> interpolate(from: T, to: T): T {
+ @Suppress("UNCHECKED_CAST")
+ return when {
+ from is Float && to is Float -> lerp(from, to, progress)
+ 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)
+ else ->
+ throw UnsupportedOperationException(
+ "Interpolation not supported for this type"
+ )
+ }
+ as T
+ }
}
lateinit var coroutineScope: CoroutineScope
@@ -321,14 +366,28 @@ fun ComposeContentTestRule.testTransition(
mainClock.advanceTimeByFrame()
waitForIdle()
+ var currentTime = 0L
// Test the assertions at specific points in time.
test.timestamps.forEach { tsAssertion ->
if (tsAssertion.timestampDelta > 0L) {
mainClock.advanceTimeBy(tsAssertion.timestampDelta)
waitForIdle()
+ currentTime += tsAssertion.timestampDelta.toInt()
}
- tsAssertion.assertion(assertionScope)
+ assertionScope.progress = tsAssertion.progress
+ try {
+ tsAssertion.assertion(assertionScope, tsAssertion.progress)
+ } catch (assertionError: AssertionError) {
+ if (assertionScope.progress > 0) {
+ throw AssertionError(
+ "Transition assertion failed at ${currentTime}ms " +
+ "at progress: ${assertionScope.progress}f",
+ assertionError,
+ )
+ }
+ throw assertionError
+ }
}
// Go to the end state and test it.
@@ -371,7 +430,25 @@ private fun transitionTest(builder: TransitionTestBuilder.() -> Unit): Transitio
val delta = timestamp - currentTimestamp
currentTimestamp = timestamp
- timestamps.add(TimestampAssertion(delta, builder))
+ timestamps.add(TimestampAssertion(delta, { builder() }, 0f))
+ }
+
+ override fun atAllFrames(
+ totalFrames: Int,
+ builder: AutoTransitionTestAssertionScope.(Float) -> Unit,
+ ) {
+ check(after == null) { "atFrames(...) {} must be called before after {}" }
+ check(currentTimestamp == 0L) {
+ "atFrames(...) can't be called multiple times or after at(...)"
+ }
+
+ for (frame in 0 until totalFrames) {
+ val timestamp = frame * 16L
+ val delta = timestamp - currentTimestamp
+ val progress = frame.toFloat() / totalFrames
+ currentTimestamp = timestamp
+ timestamps.add(TimestampAssertion(delta, builder, progress))
+ }
}
override fun after(builder: TransitionTestAssertionScope.() -> Unit) {
@@ -396,5 +473,6 @@ private class TransitionTest(
private class TimestampAssertion(
val timestampDelta: Long,
- val assertion: TransitionTestAssertionScope.() -> Unit,
+ val assertion: AutoTransitionTestAssertionScope.(Float) -> Unit,
+ val progress: Float,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index 91f9cce5b69b..b8d4bb4b8e77 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -667,6 +667,7 @@ open class AuthContainerViewTest : SysuiTestCase() {
faceProps,
wakefulnessLifecycle,
userManager,
+ null /* authContextPlugins */,
lockPatternUtils,
interactionJankMonitor,
{ promptSelectorInteractor },
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 2dcbdc80f695..2817f5573865 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -117,6 +117,7 @@ import org.mockito.junit.MockitoRule;
import java.util.ArrayList;
import java.util.List;
+import java.util.Optional;
import java.util.Random;
@RunWith(AndroidJUnit4.class)
@@ -1187,7 +1188,8 @@ public class AuthControllerTest extends SysuiTestCase {
TestableAuthController(Context context) {
super(context, null /* applicationCoroutineScope */,
mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
- mFingerprintManager, mFaceManager, () -> mUdfpsController, mDisplayManager,
+ mFingerprintManager, mFaceManager, Optional.empty(),
+ () -> mUdfpsController, mDisplayManager,
mWakefulnessLifecycle, mUserManager, mLockPatternUtils, () -> mUdfpsLogger,
() -> mLogContextInteractor, () -> mPromptSelectionInteractor,
() -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
index 21c6583d4e84..aeea99be40dd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsControllerTest.java
@@ -365,6 +365,25 @@ public class UdfpsControllerTest extends SysuiTestCase {
}
@Test
+ public void showUdfpsOverlay_invokedTwice_doesNotNotifyListenerSecondTime() throws RemoteException {
+ mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
+ BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+ mFgExecutor.runAllReady();
+
+ verify(mFingerprintManager).onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN,
+ TEST_REQUEST_ID, mOpticalProps.sensorId);
+
+ reset(mFingerprintManager);
+
+ // Second attempt should do nothing
+ mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
+ BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
+ mFgExecutor.runAllReady();
+ verify(mFingerprintManager, never()).onUdfpsUiEvent(FingerprintManager.UDFPS_UI_OVERLAY_SHOWN,
+ TEST_REQUEST_ID, mOpticalProps.sensorId);
+ }
+
+ @Test
public void testSubscribesToOrientationChangesWhenShowingOverlay() throws Exception {
mOverlayController.showUdfpsOverlay(TEST_REQUEST_ID, mOpticalProps.sensorId,
BiometricRequestConstants.REASON_AUTH_KEYGUARD, mUdfpsOverlayControllerCallback);
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/AuthContextPlugin.kt b/packages/SystemUI/plugin/src/com/android/systemui/plugins/AuthContextPlugin.kt
new file mode 100644
index 000000000000..773c2a2d5e78
--- /dev/null
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/AuthContextPlugin.kt
@@ -0,0 +1,85 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.plugins
+
+import android.os.IBinder
+import android.view.View
+import com.android.systemui.plugins.annotations.ProvidesInterface
+
+/**
+ * Plugin for experimental "Contextual Auth" features.
+ *
+ * These plugins will get raw access to low-level events about the user's environment, such as
+ * moving in/out of trusted locations, connection status of trusted devices, auth attempts, etc.
+ * They will also receive callbacks related to system events & transitions to enable prototypes on
+ * sensitive surfaces like lock screen and BiometricPrompt.
+ *
+ * Note to rebuild the plugin jar run: m PluginDummyLib
+ */
+@ProvidesInterface(action = AuthContextPlugin.ACTION, version = AuthContextPlugin.VERSION)
+interface AuthContextPlugin : Plugin {
+
+ /**
+ * Called in the background when the plugin is enabled.
+ *
+ * This is a good time to ask your friendly [saucier] to cook up something special. The
+ * [Plugin.onCreate] can also be used for initialization.
+ */
+ fun activated(saucier: Saucier)
+
+ /**
+ * Called when a [SensitiveSurface] is first shown.
+ *
+ * This may be called repeatedly if the state of the surface changes after it is shown. For
+ * example, [SensitiveSurface.BiometricPrompt.isCredential] will change if the user falls back
+ * to a credential-based auth method.
+ */
+ fun onShowingSensitiveSurface(surface: SensitiveSurface)
+
+ /**
+ * Called when a [SensitiveSurface] sensitive surface is hidden.
+ *
+ * This method may still be called without [onShowingSensitiveSurface] in cases of rapid
+ * dismissal and plugins implementations should typically be idempotent.
+ */
+ fun onHidingSensitiveSurface(surface: SensitiveSurface)
+
+ companion object {
+ /** Plugin action. */
+ const val ACTION = "com.android.systemui.action.PLUGIN_AUTH_CONTEXT"
+ /** Plugin version. */
+ const val VERSION = 1
+ }
+
+ /** Information about a sensitive surface in the framework, which the Plugin may augment. */
+ sealed interface SensitiveSurface {
+
+ /** Information about the BiometricPrompt that is being shown to the user. */
+ data class BiometricPrompt(val view: View? = null, val isCredential: Boolean = false) :
+ SensitiveSurface
+
+ /** Information about bouncer. */
+ data class LockscreenBouncer(val view: View? = null) : SensitiveSurface
+ }
+
+ /** Ask for the special. */
+ interface Saucier {
+
+ /** What [flavor] would you like? */
+ fun getSauce(flavor: String): IBinder?
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index b491c94db151..f6b6655dca4d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -64,6 +64,7 @@ import com.android.internal.jank.InteractionJankMonitor;
import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.biometrics.AuthController.ScaleFactorProvider;
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.plugins.AuthContextPlugins;
import com.android.systemui.biometrics.shared.model.BiometricModalities;
import com.android.systemui.biometrics.shared.model.PromptKind;
import com.android.systemui.biometrics.ui.CredentialView;
@@ -132,6 +133,7 @@ public class AuthContainerView extends LinearLayout
private final int mEffectiveUserId;
private final IBinder mWindowToken = new Binder();
private final ViewCaptureAwareWindowManager mWindowManager;
+ @Nullable private final AuthContextPlugins mAuthContextPlugins;
private final Interpolator mLinearOutSlowIn;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
@@ -289,6 +291,7 @@ public class AuthContainerView extends LinearLayout
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@NonNull UserManager userManager,
+ @Nullable AuthContextPlugins authContextPlugins,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
@NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
@@ -306,6 +309,7 @@ public class AuthContainerView extends LinearLayout
WindowManager wm = getContext().getSystemService(WindowManager.class);
mWindowManager = new ViewCaptureAwareWindowManager(wm, lazyViewCapture,
enableViewCaptureTracing());
+ mAuthContextPlugins = authContextPlugins;
mWakefulnessLifecycle = wakefulnessLifecycle;
mApplicationCoroutineScope = applicationCoroutineScope;
@@ -446,7 +450,7 @@ public class AuthContainerView extends LinearLayout
final CredentialViewModel vm = mCredentialViewModelProvider.get();
vm.setAnimateContents(animateContents);
((CredentialView) mCredentialView).init(vm, this, mPanelController, animatePanel,
- mBiometricCallback);
+ mBiometricCallback, mAuthContextPlugins);
mLayout.addView(mCredentialView);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index a5bd559dcbf2..4faf6ff9f596 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -21,6 +21,7 @@ import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FINGERPRIN
import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_REAR;
import static android.view.Display.INVALID_DISPLAY;
+import static com.android.systemui.Flags.contAuthPlugin;
import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
import android.annotation.NonNull;
@@ -74,6 +75,7 @@ import com.android.internal.widget.LockPatternUtils;
import com.android.systemui.CoreStartable;
import com.android.systemui.biometrics.domain.interactor.LogContextInteractor;
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
+import com.android.systemui.biometrics.plugins.AuthContextPlugins;
import com.android.systemui.biometrics.shared.model.UdfpsOverlayParams;
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel;
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel;
@@ -108,6 +110,7 @@ import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Optional;
import java.util.Set;
import javax.inject.Inject;
@@ -139,6 +142,7 @@ public class AuthController implements
private final ActivityTaskManager mActivityTaskManager;
@Nullable private final FingerprintManager mFingerprintManager;
@Nullable private final FaceManager mFaceManager;
+ @Nullable private final AuthContextPlugins mContextPlugins;
private final Provider<UdfpsController> mUdfpsControllerFactory;
private final CoroutineScope mApplicationCoroutineScope;
private Job mBiometricContextListenerJob = null;
@@ -717,6 +721,7 @@ public class AuthController implements
@NonNull WindowManager windowManager,
@Nullable FingerprintManager fingerprintManager,
@Nullable FaceManager faceManager,
+ Optional<AuthContextPlugins> contextPlugins,
Provider<UdfpsController> udfpsControllerFactory,
@NonNull DisplayManager displayManager,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
@@ -744,6 +749,7 @@ public class AuthController implements
mActivityTaskManager = activityTaskManager;
mFingerprintManager = fingerprintManager;
mFaceManager = faceManager;
+ mContextPlugins = contAuthPlugin() ? contextPlugins.orElse(null) : null;
mUdfpsControllerFactory = udfpsControllerFactory;
mUdfpsLogger = udfpsLogger;
mDisplayManager = displayManager;
@@ -858,6 +864,10 @@ public class AuthController implements
mActivityTaskManager.registerTaskStackListener(mTaskStackListener);
mOrientationListener.enable();
updateSensorLocations();
+
+ if (mContextPlugins != null) {
+ mContextPlugins.activate();
+ }
}
@Override
@@ -1350,7 +1360,7 @@ public class AuthController implements
config.mSensorIds = sensorIds;
config.mScaleProvider = this::getScaleFactor;
return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps,
- wakefulnessLifecycle, userManager, lockPatternUtils,
+ wakefulnessLifecycle, userManager, mContextPlugins, lockPatternUtils,
mInteractionJankMonitor, mPromptSelectorInteractor, viewModel,
mCredentialViewModelProvider, bgExecutor, mVibratorHelper,
mLazyViewCapture, mMSDLPlayer);
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
index 2863e29c9a34..a9133e45e93f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsController.java
@@ -814,6 +814,11 @@ public class UdfpsController implements DozeReceiver, Dumpable {
private void showUdfpsOverlay(@NonNull UdfpsControllerOverlay overlay) {
mExecution.assertIsMainThread();
+ if (mOverlay != null) {
+ Log.d(TAG, "showUdfpsOverlay | the overlay is already showing");
+ return;
+ }
+
mOverlay = overlay;
final int requestReason = overlay.getRequestReason();
if (requestReason == REASON_AUTH_KEYGUARD
@@ -823,7 +828,7 @@ public class UdfpsController implements DozeReceiver, Dumpable {
return;
}
if (overlay.show(this, mOverlayParams)) {
- Log.v(TAG, "showUdfpsOverlay | adding window reason=" + requestReason);
+ Log.d(TAG, "showUdfpsOverlay | adding window reason=" + requestReason);
mOnFingerDown = false;
mAttemptedToDismissKeyguard = false;
mOrientationListener.enable();
@@ -832,7 +837,7 @@ public class UdfpsController implements DozeReceiver, Dumpable {
overlay.getRequestId(), mSensorProps.sensorId);
}
} else {
- Log.v(TAG, "showUdfpsOverlay | the overlay is already showing");
+ Log.d(TAG, "showUdfpsOverlay | the overlay is already showing");
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
index 2593cebb14d0..51eb13947d40 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsControllerOverlay.kt
@@ -41,6 +41,7 @@ import android.view.WindowManager
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.AccessibilityManager.TouchExplorationStateChangeListener
import androidx.annotation.VisibleForTesting
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.keyguard.KeyguardUpdateMonitor
import com.android.systemui.animation.ActivityTransitionAnimator
@@ -73,7 +74,6 @@ import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
-import com.android.app.tracing.coroutines.launchTraced as launch
private const val TAG = "UdfpsControllerOverlay"
@@ -245,7 +245,7 @@ constructor(
return true
}
- Log.v(TAG, "showUdfpsOverlay | the overlay is already showing")
+ Log.d(TAG, "showUdfpsOverlay | the overlay is already showing")
return false
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
index e2a8a691b1fd..60ce17721b42 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/dagger/BiometricsModule.kt
@@ -36,6 +36,7 @@ import com.android.systemui.biometrics.data.repository.FingerprintPropertyReposi
import com.android.systemui.biometrics.data.repository.FingerprintPropertyRepositoryImpl
import com.android.systemui.biometrics.data.repository.PromptRepository
import com.android.systemui.biometrics.data.repository.PromptRepositoryImpl
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.biometrics.udfps.BoundingBoxOverlapDetector
import com.android.systemui.biometrics.udfps.EllipseOverlapDetector
import com.android.systemui.biometrics.udfps.OverlapDetector
@@ -58,7 +59,7 @@ import javax.inject.Qualifier
/** Dagger module for all things biometric. */
@Module
interface BiometricsModule {
- /** Starts AuthController. */
+ /** Starts AuthController. */
@Binds
@IntoMap
@ClassKey(AuthController::class)
@@ -103,8 +104,9 @@ interface BiometricsModule {
@SysUISingleton
fun displayStateRepository(impl: DisplayStateRepositoryImpl): DisplayStateRepository
- @BindsOptionalOf
- fun deviceEntryUnlockTrackerViewBinder(): DeviceEntryUnlockTrackerViewBinder
+ @BindsOptionalOf fun authContextPlugins(): AuthContextPlugins
+
+ @BindsOptionalOf fun deviceEntryUnlockTrackerViewBinder(): DeviceEntryUnlockTrackerViewBinder
companion object {
/** Background [Executor] for HAL related operations. */
@@ -117,8 +119,7 @@ interface BiometricsModule {
@Provides fun providesUdfpsUtils(): UdfpsUtils = UdfpsUtils()
- @Provides
- fun provideIconProvider(context: Context): IconProvider = IconProvider(context)
+ @Provides fun provideIconProvider(context: Context): IconProvider = IconProvider(context)
@Provides
@SysUISingleton
@@ -136,7 +137,7 @@ interface BiometricsModule {
EllipseOverlapDetectorParams(
minOverlap = values[3],
targetSize = values[2],
- stepSize = values[4].toInt()
+ stepSize = values[4].toInt(),
)
)
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/plugins/AuthContextPlugins.kt b/packages/SystemUI/src/com/android/systemui/biometrics/plugins/AuthContextPlugins.kt
new file mode 100644
index 000000000000..ca38e9869ed1
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/plugins/AuthContextPlugins.kt
@@ -0,0 +1,41 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.biometrics.plugins
+
+import com.android.systemui.plugins.AuthContextPlugin
+import com.android.systemui.plugins.PluginManager
+
+/** Wrapper interface for registering & forwarding events to all available [AuthContextPlugin]s. */
+interface AuthContextPlugins {
+ /** Finds and actives all plugins via SysUI's [PluginManager] (should be called at startup). */
+ fun activate()
+
+ /**
+ * Interact with all registered plugins.
+ *
+ * The provided [block] will be repeated for each available plugin.
+ */
+ suspend fun use(block: (AuthContextPlugin) -> Unit)
+
+ /**
+ * Like [use] but when no existing coroutine context is available.
+ *
+ * The [block] will be run on SysUI's general background context and can, optionally, be
+ * confined to [runOnMain] (defaults to a background thread).
+ */
+ fun useInBackground(runOnMain: Boolean = false, block: (AuthContextPlugin) -> Unit)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
index b28733f5cc55..dad140f00cee 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPasswordView.kt
@@ -11,6 +11,7 @@ import android.view.accessibility.AccessibilityManager
import android.widget.LinearLayout
import android.widget.TextView
import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
import com.android.systemui.biometrics.ui.binder.Spaghetti
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
@@ -33,6 +34,7 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) :
panelViewController: AuthPanelController,
animatePanel: Boolean,
legacyCallback: Spaghetti.Callback,
+ plugins: AuthContextPlugins?,
) {
CredentialViewBinder.bind(
this,
@@ -40,7 +42,8 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) :
viewModel,
panelViewController,
animatePanel,
- legacyCallback
+ legacyCallback,
+ plugins,
)
}
@@ -78,7 +81,7 @@ class CredentialPasswordView(context: Context, attrs: AttributeSet?) :
0,
statusBarInsets.top,
0,
- if (keyboardInsets.bottom == 0) navigationInsets.bottom else keyboardInsets.bottom
+ if (keyboardInsets.bottom == 0) navigationInsets.bottom else keyboardInsets.bottom,
)
return WindowInsets.CONSUMED
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
index d9d286fe7035..e80a79ba1641 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialPatternView.kt
@@ -8,6 +8,7 @@ import android.view.WindowInsets
import android.view.WindowInsets.Type
import android.widget.LinearLayout
import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.biometrics.ui.binder.CredentialViewBinder
import com.android.systemui.biometrics.ui.binder.Spaghetti
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
@@ -23,6 +24,7 @@ class CredentialPatternView(context: Context, attrs: AttributeSet?) :
panelViewController: AuthPanelController,
animatePanel: Boolean,
legacyCallback: Spaghetti.Callback,
+ plugins: AuthContextPlugins?,
) {
CredentialViewBinder.bind(
this,
@@ -30,7 +32,8 @@ class CredentialPatternView(context: Context, attrs: AttributeSet?) :
viewModel,
panelViewController,
animatePanel,
- legacyCallback
+ legacyCallback,
+ plugins,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
index e2f98958ab55..f3e49175538f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/CredentialView.kt
@@ -1,6 +1,7 @@
package com.android.systemui.biometrics.ui
import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.biometrics.ui.binder.Spaghetti
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
@@ -29,5 +30,6 @@ sealed interface CredentialView {
panelViewController: AuthPanelController,
animatePanel: Boolean,
legacyCallback: Spaghetti.Callback,
+ plugins: AuthContextPlugins?,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
index 39543e78f784..10b12117a3a9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -9,18 +9,21 @@ import android.widget.TextView
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.systemui.biometrics.AuthPanelController
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.biometrics.ui.CredentialPasswordView
import com.android.systemui.biometrics.ui.CredentialPatternView
import com.android.systemui.biometrics.ui.CredentialView
import com.android.systemui.biometrics.ui.viewmodel.CredentialViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.plugins.AuthContextPlugin
import com.android.systemui.res.R
import kotlinx.coroutines.Job
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.onEach
-import com.android.app.tracing.coroutines.launchTraced as launch
private const val ANIMATE_CREDENTIAL_INITIAL_DURATION_MS = 150
@@ -42,6 +45,7 @@ object CredentialViewBinder {
panelViewController: AuthPanelController,
animatePanel: Boolean,
legacyCallback: Spaghetti.Callback,
+ plugins: AuthContextPlugins?,
maxErrorDuration: Long = 3_000L,
requestFocusForInput: Boolean = true,
) {
@@ -72,6 +76,10 @@ object CredentialViewBinder {
}
repeatOnLifecycle(Lifecycle.State.STARTED) {
+ if (plugins != null) {
+ launch { plugins.notifyShowingBPCredential(view) }
+ }
+
// show prompt metadata
launch {
viewModel.header.collect { header ->
@@ -136,6 +144,12 @@ object CredentialViewBinder {
host.onCredentialAttemptsRemaining(info.remaining!!, info.message)
}
}
+
+ try {
+ awaitCancellation()
+ } catch (_: Throwable) {
+ plugins?.notifyHidingBPCredential()
+ }
}
}
@@ -172,3 +186,15 @@ private var TextView.textOrHide: String?
text = if (gone) "" else value
}
get() = text?.toString()
+
+private suspend fun AuthContextPlugins.notifyShowingBPCredential(view: View) = use { plugin ->
+ plugin.onShowingSensitiveSurface(
+ AuthContextPlugin.SensitiveSurface.BiometricPrompt(view = view, isCredential = true)
+ )
+}
+
+private fun AuthContextPlugins.notifyHidingBPCredential() = useInBackground { plugin ->
+ plugin.onHidingSensitiveSurface(
+ AuthContextPlugin.SensitiveSurface.BiometricPrompt(isCredential = true)
+ )
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
index 12f06bbd4f5e..8a4cc63f65fb 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/BouncerViewBinder.kt
@@ -3,6 +3,8 @@ package com.android.systemui.bouncer.ui.binder
import android.view.ViewGroup
import com.android.keyguard.KeyguardMessageAreaController
import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.Flags.contAuthPlugin
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor
import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
@@ -17,6 +19,7 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition
import com.android.systemui.log.BouncerLogger
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import dagger.Lazy
+import java.util.Optional
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -60,6 +63,7 @@ class BouncerViewBinder
constructor(
private val legacyBouncerDependencies: Lazy<LegacyBouncerDependencies>,
private val composeBouncerDependencies: Lazy<ComposeBouncerDependencies>,
+ private val contextPlugins: Optional<AuthContextPlugins>,
) {
fun bind(view: ViewGroup) {
if (ComposeBouncerFlags.isOnlyComposeBouncerEnabled()) {
@@ -85,6 +89,7 @@ constructor(
deps.bouncerMessageInteractor,
deps.bouncerLogger,
deps.selectedUserInteractor,
+ if (contAuthPlugin()) contextPlugins.orElse(null) else null,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
index 71eda0c19e6f..434a9ce58c3b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/binder/KeyguardBouncerViewBinder.kt
@@ -22,11 +22,13 @@ import android.view.ViewGroup
import android.window.OnBackAnimationCallback
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launchTraced as launch
import com.android.keyguard.KeyguardMessageAreaController
import com.android.keyguard.KeyguardSecurityContainerController
import com.android.keyguard.KeyguardSecurityModel
import com.android.keyguard.KeyguardSecurityView
import com.android.keyguard.dagger.KeyguardBouncerComponent
+import com.android.systemui.biometrics.plugins.AuthContextPlugins
import com.android.systemui.bouncer.domain.interactor.BouncerMessageInteractor
import com.android.systemui.bouncer.shared.constants.KeyguardBouncerConstants.EXPANSION_VISIBLE
import com.android.systemui.bouncer.ui.BouncerViewDelegate
@@ -35,10 +37,10 @@ import com.android.systemui.keyguard.ui.viewmodel.PrimaryBouncerToGoneTransition
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.log.BouncerLogger
import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.plugins.AuthContextPlugin
import com.android.systemui.user.domain.interactor.SelectedUserInteractor
import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.filter
-import com.android.app.tracing.coroutines.launchTraced as launch
/** Binds the bouncer container to its view model. */
object KeyguardBouncerViewBinder {
@@ -52,6 +54,7 @@ object KeyguardBouncerViewBinder {
bouncerMessageInteractor: BouncerMessageInteractor,
bouncerLogger: BouncerLogger,
selectedUserInteractor: SelectedUserInteractor,
+ plugins: AuthContextPlugins?,
) {
// Builds the KeyguardSecurityContainerController from bouncer view group.
val securityContainerController: KeyguardSecurityContainerController =
@@ -94,7 +97,7 @@ object KeyguardBouncerViewBinder {
override fun setDismissAction(
onDismissAction: ActivityStarter.OnDismissAction?,
- cancelAction: Runnable?
+ cancelAction: Runnable?,
) {
securityContainerController.setOnDismissAction(onDismissAction, cancelAction)
}
@@ -138,7 +141,7 @@ object KeyguardBouncerViewBinder {
it.bindMessageView(
bouncerMessageInteractor,
messageAreaControllerFactory,
- bouncerLogger
+ bouncerLogger,
)
}
} else {
@@ -149,6 +152,13 @@ object KeyguardBouncerViewBinder {
securityContainerController.reset()
securityContainerController.onPause()
}
+ plugins?.apply {
+ if (isShowing) {
+ notifyBouncerShowing(view)
+ } else {
+ notifyBouncerGone()
+ }
+ }
}
}
@@ -209,7 +219,7 @@ object KeyguardBouncerViewBinder {
securityContainerController.showMessage(
it.message,
it.colorStateList,
- /* animated= */ true
+ /* animated= */ true,
)
viewModel.onMessageShown()
}
@@ -233,8 +243,19 @@ object KeyguardBouncerViewBinder {
awaitCancellation()
} finally {
viewModel.setBouncerViewDelegate(null)
+ plugins?.notifyBouncerGone()
}
}
}
}
}
+
+private suspend fun AuthContextPlugins.notifyBouncerShowing(view: View) = use { plugin ->
+ plugin.onShowingSensitiveSurface(
+ AuthContextPlugin.SensitiveSurface.LockscreenBouncer(view = view)
+ )
+}
+
+private fun AuthContextPlugins.notifyBouncerGone() = useInBackground { plugin ->
+ plugin.onHidingSensitiveSurface(AuthContextPlugin.SensitiveSurface.LockscreenBouncer())
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt
index cc6007b400f7..50d86a24be96 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/util/WidgetViewFactory.kt
@@ -20,6 +20,7 @@ import android.content.Context
import android.os.Bundle
import android.util.SizeF
import com.android.app.tracing.coroutines.withContextTraced as withContext
+import com.android.systemui.Flags
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.GlanceableHubMultiUserHelper
import com.android.systemui.communal.widgets.AppWidgetHostListenerDelegate
@@ -30,6 +31,9 @@ import com.android.systemui.communal.widgets.WidgetInteractionHandler
import com.android.systemui.dagger.qualifiers.UiBackground
import dagger.Lazy
import java.util.concurrent.Executor
+import java.util.concurrent.LinkedBlockingQueue
+import java.util.concurrent.ThreadPoolExecutor
+import java.util.concurrent.TimeUnit
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -53,7 +57,11 @@ constructor(
withContext("$TAG#createWidget", uiBgContext) {
val view =
CommunalAppWidgetHostView(context, interactionHandler).apply {
- setExecutor(uiBgExecutor)
+ if (Flags.communalHubUseThreadPoolForWidgets()) {
+ setExecutor(widgetExecutor)
+ } else {
+ setExecutor(uiBgExecutor)
+ }
setAppWidget(model.appWidgetId, model.providerInfo)
}
@@ -90,5 +98,20 @@ constructor(
private companion object {
const val TAG = "WidgetViewFactory"
+
+ val poolSize = Runtime.getRuntime().availableProcessors().coerceAtLeast(2)
+
+ /**
+ * This executor is used for widget inflation. Parameters match what launcher uses. See
+ * [com.android.launcher3.util.Executors.THREAD_POOL_EXECUTOR].
+ */
+ val widgetExecutor =
+ ThreadPoolExecutor(
+ /*corePoolSize*/ poolSize,
+ /*maxPoolSize*/ poolSize,
+ /*keepAlive*/ 1,
+ /*unit*/ TimeUnit.SECONDS,
+ /*workQueue*/ LinkedBlockingQueue(),
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt
new file mode 100644
index 000000000000..3067ccbb7cea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/flags/QsInCompose.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.qs.flags
+
+import com.android.systemui.flags.RefactorFlagUtils
+import com.android.systemui.shade.shared.flag.DualShade
+
+/**
+ * Object to help check if the new QS ui should be used. This is true if either [QSComposeFragment]
+ * or [DualShade] are enabled.
+ */
+object QsInCompose {
+
+ /**
+ * This is not a real flag name, but a representation of the allowed flag names. Should not be
+ * used with test annotations.
+ */
+ private val flagName = "${QSComposeFragment.FLAG_NAME}|${DualShade.FLAG_NAME}"
+
+ @JvmStatic
+ inline val isEnabled: Boolean
+ get() = QSComposeFragment.isEnabled || DualShade.isEnabled
+
+ @JvmStatic
+ fun isUnexpectedlyInLegacyMode() =
+ RefactorFlagUtils.isUnexpectedlyInLegacyMode(isEnabled, flagName)
+
+ @JvmStatic fun assertInLegacyMode() = RefactorFlagUtils.assertInLegacyMode(isEnabled, flagName)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
index 8c004c4d3adf..6844f053cd21 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/BrightnessDialog.java
@@ -48,7 +48,7 @@ import com.android.internal.logging.nano.MetricsProto.MetricsEvent;
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel;
import com.android.systemui.compose.ComposeInitializer;
import com.android.systemui.dagger.qualifiers.Main;
-import com.android.systemui.qs.flags.QSComposeFragment;
+import com.android.systemui.qs.flags.QsInCompose;
import com.android.systemui.res.R;
import com.android.systemui.shade.domain.interactor.ShadeInteractor;
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper;
@@ -96,7 +96,7 @@ public class BrightnessDialog extends Activity {
super.onCreate(savedInstanceState);
setWindowAttributes();
View view;
- if (!QSComposeFragment.isEnabled()) {
+ if (!QsInCompose.isEnabled()) {
setContentView(R.layout.brightness_mirror_container);
view = findViewById(R.id.brightness_mirror_container);
setDialogContent((FrameLayout) view);
@@ -140,7 +140,7 @@ public class BrightnessDialog extends Activity {
window.getDecorView();
window.setLayout(WRAP_CONTENT, WRAP_CONTENT);
getTheme().applyStyle(R.style.Theme_SystemUI_QuickSettings, false);
- if (QSComposeFragment.isEnabled()) {
+ if (QsInCompose.isEnabled()) {
window.getDecorView().addOnAttachStateChangeListener(
new View.OnAttachStateChangeListener() {
@Override
@@ -217,7 +217,7 @@ public class BrightnessDialog extends Activity {
@Override
protected void onStart() {
super.onStart();
- if (!QSComposeFragment.isEnabled()) {
+ if (!QsInCompose.isEnabled()) {
mBrightnessController.registerCallbacks();
}
MetricsLogger.visible(this, MetricsEvent.BRIGHTNESS_DIALOG);
@@ -241,7 +241,7 @@ public class BrightnessDialog extends Activity {
protected void onStop() {
super.onStop();
MetricsLogger.hidden(this, MetricsEvent.BRIGHTNESS_DIALOG);
- if (!QSComposeFragment.isEnabled()) {
+ if (!QsInCompose.isEnabled()) {
mBrightnessController.unregisterCallbacks();
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
index f7059e244084..a64ff321cd4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/settings/brightness/BrightnessDialogTest.kt
@@ -31,6 +31,7 @@ import com.android.systemui.activity.SingleActivityFactory
import com.android.systemui.brightness.ui.viewmodel.BrightnessSliderViewModel
import com.android.systemui.brightness.ui.viewmodel.brightnessSliderViewModelFactory
import com.android.systemui.qs.flags.QSComposeFragment
+import com.android.systemui.qs.flags.QsInCompose
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.policy.AccessibilityManagerWrapper
@@ -70,8 +71,8 @@ class BrightnessDialogTest(val flags: FlagsParameterization) : SysuiTestCase() {
mSetFlagsRule.setFlagsParameterization(flags)
}
- val viewId by lazy {
- if (QSComposeFragment.isEnabled) {
+ private val viewId by lazy {
+ if (QsInCompose.isEnabled) {
R.id.brightness_dialog_slider
} else {
R.id.brightness_mirror_container
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 af14edd10f5f..a1c9022e18bd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/wmshell/BubblesTest.java
@@ -141,6 +141,7 @@ import com.android.systemui.statusbar.notification.collection.NotificationEntryB
import com.android.systemui.statusbar.notification.collection.notifcollection.CommonNotifCollection;
import com.android.systemui.statusbar.notification.collection.notifcollection.NotifCollectionListener;
import com.android.systemui.statusbar.notification.collection.render.NotificationVisibilityProvider;
+import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.notification.interruption.AvalancheProvider;
import com.android.systemui.statusbar.notification.interruption.KeyguardNotificationVisibilityProvider;
import com.android.systemui.statusbar.notification.interruption.NotificationInterruptLogger;
@@ -154,7 +155,6 @@ import com.android.systemui.statusbar.phone.KeyguardBypassController;
import com.android.systemui.statusbar.policy.BatteryController;
import com.android.systemui.statusbar.policy.ConfigurationController;
import com.android.systemui.statusbar.policy.DeviceProvisionedController;
-import com.android.systemui.statusbar.notification.headsup.HeadsUpManager;
import com.android.systemui.statusbar.policy.KeyguardStateController;
import com.android.systemui.statusbar.policy.SensitiveNotificationProtectionController;
import com.android.systemui.statusbar.policy.ZenModeController;
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 71cbc10074d6..a58d850e042f 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -5845,7 +5845,7 @@ public final class ActiveServices {
if (r.inSharedIsolatedProcess) {
app = mAm.mProcessList.getSharedIsolatedProcess(procName, r.appInfo.uid,
r.appInfo.packageName);
- if (app != null) {
+ if (app != null && !app.isKilled()) {
final IApplicationThread thread = app.getThread();
final int pid = app.getPid();
final UidRecord uidRecord = app.getUidRecord();
@@ -5870,6 +5870,8 @@ public final class ActiveServices {
// If a dead object exception was thrown -- fall through to
// restart the application.
}
+ } else {
+ app = null;
}
} else {
// If this service runs in an isolated process, then each time
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index cced0383c063..78dee3169161 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -132,7 +132,7 @@ import static android.provider.Settings.Global.DEBUG_APP;
import static android.provider.Settings.Global.WAIT_FOR_DEBUGGER;
import static android.security.Flags.preventIntentRedirect;
import static android.security.Flags.preventIntentRedirectCollectNestedKeysOnServerIfNotCollected;
-import static android.security.Flags.preventIntentRedirectShowToast;
+import static android.security.Flags.preventIntentRedirectShowToastIfNestedKeysNotCollected;
import static android.security.Flags.preventIntentRedirectThrowExceptionIfNestedKeysNotCollected;
import static android.util.FeatureFlagUtils.SETTINGS_ENABLE_MONITOR_PHANTOM_PROCS;
import static android.view.Display.INVALID_DISPLAY;
@@ -19336,7 +19336,7 @@ public class ActivityManagerService extends IActivityManager.Stub
"[IntentRedirect] The intent does not have its nested keys collected as a "
+ "preparation for creating intent creator tokens. Intent: "
+ intent + "; creatorPackage: " + creatorPackage);
- if (preventIntentRedirectShowToast()) {
+ if (preventIntentRedirectShowToastIfNestedKeysNotCollected()) {
UiThread.getHandler().post(
() -> Toast.makeText(mContext,
"Nested keys not collected. go/report-bug-intentRedir to report a"
diff --git a/services/core/java/com/android/server/media/quality/MediaQualityService.java b/services/core/java/com/android/server/media/quality/MediaQualityService.java
index 52433a568750..af329070ec22 100644
--- a/services/core/java/com/android/server/media/quality/MediaQualityService.java
+++ b/services/core/java/com/android/server/media/quality/MediaQualityService.java
@@ -42,6 +42,7 @@ import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Locale;
+import java.util.stream.Collectors;
/**
* This service manage picture profile and sound profile for TV setting. Also communicates with the
@@ -90,6 +91,7 @@ public class MediaQualityService extends SystemService {
public void updatePictureProfile(String id, PictureProfile pp) {
// TODO: implement
}
+
@Override
public void removePictureProfile(String id) {
// TODO: implement
@@ -220,11 +222,9 @@ public class MediaQualityService extends SystemService {
String [] column = {BaseParameters.PARAMETER_NAME};
List<PictureProfile> pictureProfiles = getPictureProfilesBasedOnConditions(column,
null, null);
- List<String> packageNames = new ArrayList<>();
- for (PictureProfile pictureProfile: pictureProfiles) {
- packageNames.add(pictureProfile.getName());
- }
- return packageNames;
+ return pictureProfiles.stream()
+ .map(PictureProfile::getName)
+ .collect(Collectors.toList());
}
private List<PictureProfile> getPictureProfilesBasedOnConditions(String[] columns,
@@ -283,21 +283,107 @@ public class MediaQualityService extends SystemService {
@Override
public SoundProfile getSoundProfile(int type, String id) {
- return null;
+ SQLiteDatabase db = mMediaQualityDbHelper.getReadableDatabase();
+
+ String selection = BaseParameters.PARAMETER_ID + " = ?";
+ String[] selectionArguments = {id};
+
+ try (
+ Cursor cursor = db.query(
+ mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+ getAllSoundProfileColumns(),
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ int count = cursor.getCount();
+ if (count == 0) {
+ return null;
+ }
+ if (count > 1) {
+ Log.wtf(TAG, String.format(Locale.US, "%d entries found for id=%s"
+ + " in %s. Should only ever be 0 or 1.", count, id,
+ mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME));
+ return null;
+ }
+ cursor.moveToFirst();
+ return getSoundProfileFromCursor(cursor);
+ }
}
+
@Override
public List<SoundProfile> getSoundProfilesByPackage(String packageName) {
- return new ArrayList<>();
+ String selection = BaseParameters.PARAMETER_PACKAGE + " = ?";
+ String[] selectionArguments = {packageName};
+ return getSoundProfilesBasedOnConditions(getAllSoundProfileColumns(), selection,
+ selectionArguments);
}
+
@Override
public List<SoundProfile> getAvailableSoundProfiles() {
return new ArrayList<>();
}
+
@Override
public List<String> getSoundProfilePackageNames() {
- return new ArrayList<>();
+ String [] column = {BaseParameters.PARAMETER_NAME};
+ List<SoundProfile> soundProfiles = getSoundProfilesBasedOnConditions(column,
+ null, null);
+ return soundProfiles.stream()
+ .map(SoundProfile::getName)
+ .collect(Collectors.toList());
+ }
+
+ private String[] getAllSoundProfileColumns() {
+ return new String[]{
+ BaseParameters.PARAMETER_ID,
+ BaseParameters.PARAMETER_NAME,
+ BaseParameters.PARAMETER_INPUT_ID,
+ BaseParameters.PARAMETER_PACKAGE,
+ mMediaQualityDbHelper.SETTINGS
+ };
+ }
+
+ private SoundProfile getSoundProfileFromCursor(Cursor cursor) {
+ String returnId = cursor.getString(
+ cursor.getColumnIndexOrThrow(BaseParameters.PARAMETER_ID));
+ int type = cursor.getInt(
+ cursor.getColumnIndexOrThrow(BaseParameters.PARAMETER_TYPE));
+ String name = cursor.getString(
+ cursor.getColumnIndexOrThrow(BaseParameters.PARAMETER_NAME));
+ String inputId = cursor.getString(
+ cursor.getColumnIndexOrThrow(BaseParameters.PARAMETER_INPUT_ID));
+ String packageName = cursor.getString(
+ cursor.getColumnIndexOrThrow(BaseParameters.PARAMETER_PACKAGE));
+ String settings = cursor.getString(
+ cursor.getColumnIndexOrThrow(mMediaQualityDbHelper.SETTINGS));
+ return new SoundProfile(returnId, type, name, inputId, packageName,
+ jsonToBundle(settings));
}
+ private List<SoundProfile> getSoundProfilesBasedOnConditions(String[] columns,
+ String selection, String[] selectionArguments) {
+ SQLiteDatabase db = mMediaQualityDbHelper.getReadableDatabase();
+
+ try (
+ Cursor cursor = db.query(
+ mMediaQualityDbHelper.SOUND_QUALITY_TABLE_NAME,
+ columns,
+ selection,
+ selectionArguments,
+ /*groupBy=*/ null,
+ /*having=*/ null,
+ /*orderBy=*/ null)
+ ) {
+ List<SoundProfile> soundProfiles = new ArrayList<>();
+ while (cursor.moveToNext()) {
+ soundProfiles.add(getSoundProfileFromCursor(cursor));
+ }
+ return soundProfiles;
+ }
+ }
@Override
public void registerPictureProfileCallback(final IPictureProfileCallback callback) {
diff --git a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
index 2cc08c327b71..a15360760e32 100644
--- a/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
+++ b/services/core/java/com/android/server/security/advancedprotection/AdvancedProtectionService.java
@@ -199,7 +199,7 @@ public class AdvancedProtectionService extends IAdvancedProtectionService.Stub
}
void sendCallbackAdded(boolean enabled, IAdvancedProtectionCallback callback) {
- Message.obtain(mHandler, MODE_CHANGED, /*enabled*/ enabled ? 1 : 0, /*unused*/ -1,
+ Message.obtain(mHandler, CALLBACK_ADDED, /*enabled*/ enabled ? 1 : 0, /*unused*/ -1,
/*callback*/ callback)
.sendToTarget();
}
diff --git a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
index 506477f67bfc..cb95b3655c61 100644
--- a/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
+++ b/services/core/java/com/android/server/wm/CameraCompatFreeformPolicy.java
@@ -65,6 +65,9 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
@NonNull
private final CameraStateMonitor mCameraStateMonitor;
+ // TODO(b/380840084): Consider moving this to the CameraStateMonitor, and keeping track of
+ // all current camera activities, especially when the camera access is switching from one app to
+ // another.
@Nullable
private Task mCameraTask;
@@ -123,8 +126,7 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
}
@Override
- public void onCameraOpened(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ public void onCameraOpened(@NonNull ActivityRecord cameraActivity) {
// Do not check orientation outside of the config recompute, as the app's orientation intent
// might be obscured by a fullscreen override. Especially for apps which have a camera
// functionality which is not the main focus of the app: while most of the app might work
@@ -136,18 +138,15 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
return;
}
- cameraActivity.recomputeConfiguration();
- cameraActivity.getTask().dispatchTaskInfoChangedIfNeeded(/* force= */ true);
- cameraActivity.ensureActivityConfiguration(/* ignoreVisibility= */ false);
+ mCameraTask = cameraActivity.getTask();
+ updateAndDispatchCameraConfiguration();
}
@Override
- public boolean onCameraClosed(@NonNull String cameraId) {
+ public boolean canCameraBeClosed(@NonNull String cameraId) {
// Top activity in the same task as the camera activity, or `null` if the task is
// closed.
- final ActivityRecord topActivity = mCameraTask != null
- ? mCameraTask.getTopActivity(/* isFinishing */ false, /* includeOverlays */ false)
- : null;
+ final ActivityRecord topActivity = getTopActivityFromCameraTask();
if (topActivity != null) {
if (isActivityForCameraIdRefreshing(topActivity, cameraId)) {
ProtoLog.v(WmProtoLogGroups.WM_DEBUG_STATES,
@@ -157,10 +156,36 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
return false;
}
}
- mCameraTask = null;
return true;
}
+ @Override
+ public void onCameraClosed() {
+ // Top activity in the same task as the camera activity, or `null` if the task is
+ // closed.
+ final ActivityRecord topActivity = getTopActivityFromCameraTask();
+ // Only clean up if the camera is not running - this close signal could be from switching
+ // cameras (e.g. back to front camera, and vice versa).
+ if (topActivity == null || !mCameraStateMonitor.isCameraRunningForActivity(topActivity)) {
+ updateAndDispatchCameraConfiguration();
+ mCameraTask = null;
+ }
+ }
+
+ private void updateAndDispatchCameraConfiguration() {
+ if (mCameraTask == null) {
+ return;
+ }
+ final ActivityRecord activity = getTopActivityFromCameraTask();
+ if (activity != null) {
+ activity.recomputeConfiguration();
+ mCameraTask.dispatchTaskInfoChangedIfNeeded(/* force= */ true);
+ activity.ensureActivityConfiguration(/* ignoreVisibility= */ true);
+ } else {
+ mCameraTask.dispatchTaskInfoChangedIfNeeded(/* force= */ true);
+ }
+ }
+
boolean shouldCameraCompatControlOrientation(@NonNull ActivityRecord activity) {
return isCameraRunningAndWindowingModeEligible(activity);
}
@@ -262,10 +287,17 @@ final class CameraCompatFreeformPolicy implements CameraStateMonitor.CameraCompa
&& !activity.isEmbedded();
}
+ @Nullable
+ private ActivityRecord getTopActivityFromCameraTask() {
+ return mCameraTask != null
+ ? mCameraTask.getTopActivity(/* isFinishing */ false, /* includeOverlays */ false)
+ : null;
+ }
+
private boolean isActivityForCameraIdRefreshing(@NonNull ActivityRecord topActivity,
@NonNull String cameraId) {
if (!isTreatmentEnabledForActivity(topActivity, /* checkOrientation= */ true)
- || mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
+ || !mCameraStateMonitor.isCameraWithIdRunningForActivity(topActivity, cameraId)) {
return false;
}
return topActivity.mAppCompatController.getAppCompatCameraOverrides().isRefreshRequested();
diff --git a/services/core/java/com/android/server/wm/CameraStateMonitor.java b/services/core/java/com/android/server/wm/CameraStateMonitor.java
index 3b6e30ab2a6d..3aa355869d85 100644
--- a/services/core/java/com/android/server/wm/CameraStateMonitor.java
+++ b/services/core/java/com/android/server/wm/CameraStateMonitor.java
@@ -67,6 +67,10 @@ class CameraStateMonitor {
// when camera connection is closed and we need to clean up our records.
private final CameraIdPackageNameBiMapping mCameraIdPackageBiMapping =
new CameraIdPackageNameBiMapping();
+ // TODO(b/380840084): Consider making this a set of CameraId/PackageName pairs. This is to
+ // keep track of camera-closed signals when apps are switching camera access, so that the policy
+ // can restore app configuration when an app closes camera (e.g. loses camera access due to
+ // another app).
private final Set<String> mScheduledToBeRemovedCameraIdSet = new ArraySet<>();
// TODO(b/336474959): should/can this go in the compat listeners?
@@ -163,15 +167,14 @@ class CameraStateMonitor {
if (cameraActivity == null || cameraActivity.getTask() == null) {
return;
}
- notifyListenersCameraOpened(cameraActivity, cameraId);
+ notifyListenersCameraOpened(cameraActivity);
}
}
- private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ private void notifyListenersCameraOpened(@NonNull ActivityRecord cameraActivity) {
for (int i = 0; i < mCameraStateListeners.size(); i++) {
CameraCompatStateListener listener = mCameraStateListeners.get(i);
- listener.onCameraOpened(cameraActivity, cameraId);
+ listener.onCameraOpened(cameraActivity);
}
}
@@ -224,11 +227,11 @@ class CameraStateMonitor {
// Already reconnected to this camera, no need to clean up.
return;
}
-
- final boolean closeSuccessfulForAllListeners = notifyListenersCameraClosed(cameraId);
- if (closeSuccessfulForAllListeners) {
+ final boolean canClose = checkCanCloseForAllListeners(cameraId);
+ if (canClose) {
// Finish cleaning up.
mCameraIdPackageBiMapping.removeCameraId(cameraId);
+ notifyListenersCameraClosed();
} else {
// Not ready to process closure yet - the camera activity might be refreshing.
// Try again later.
@@ -238,15 +241,21 @@ class CameraStateMonitor {
}
/**
- * @return {@code false} if any listeners have reported issues processing the close.
+ * @return {@code false} if any listener has reported that they cannot process camera close now.
*/
- private boolean notifyListenersCameraClosed(@NonNull String cameraId) {
- boolean closeSuccessfulForAllListeners = true;
+ private boolean checkCanCloseForAllListeners(@NonNull String cameraId) {
for (int i = 0; i < mCameraStateListeners.size(); i++) {
- closeSuccessfulForAllListeners &= mCameraStateListeners.get(i).onCameraClosed(cameraId);
+ if (!mCameraStateListeners.get(i).canCameraBeClosed(cameraId)) {
+ return false;
+ }
}
+ return true;
+ }
- return closeSuccessfulForAllListeners;
+ private void notifyListenersCameraClosed() {
+ for (int i = 0; i < mCameraStateListeners.size(); i++) {
+ mCameraStateListeners.get(i).onCameraClosed();
+ }
}
// TODO(b/335165310): verify that this works in multi instance and permission dialogs.
@@ -297,14 +306,18 @@ class CameraStateMonitor {
/**
* Notifies the compat listener that an activity has opened camera.
*/
- // TODO(b/336474959): try to decouple `cameraId` from the listeners.
- void onCameraOpened(@NonNull ActivityRecord cameraActivity, @NonNull String cameraId);
+ void onCameraOpened(@NonNull ActivityRecord cameraActivity);
/**
- * Notifies the compat listener that camera is closed.
+ * Checks whether a listener is ready to do a cleanup when camera is closed.
*
- * @return true if cleanup has been successful - the notifier might try again if false.
+ * <p>The notifier might try again if false is returned.
*/
// TODO(b/336474959): try to decouple `cameraId` from the listeners.
- boolean onCameraClosed(@NonNull String cameraId);
+ boolean canCameraBeClosed(@NonNull String cameraId);
+
+ /**
+ * Notifies the compat listener that camera is closed.
+ */
+ void onCameraClosed();
}
}
diff --git a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
index 0ccc0fe80b52..3c199dba565b 100644
--- a/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
+++ b/services/core/java/com/android/server/wm/DisplayRotationCompatPolicy.java
@@ -71,6 +71,9 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
@NonNull
private final ActivityRefresher mActivityRefresher;
+ // TODO(b/380840084): Consider moving this to the CameraStateMonitor, and keeping track of
+ // all current camera activities, especially when the camera access is switching from one app to
+ // another.
@Nullable
private Task mCameraTask;
@@ -327,8 +330,7 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
}
@Override
- public void onCameraOpened(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ public void onCameraOpened(@NonNull ActivityRecord cameraActivity) {
mCameraTask = cameraActivity.getTask();
// Checking whether an activity in fullscreen rather than the task as this camera
// compat treatment doesn't cover activity embedding.
@@ -374,16 +376,9 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
}
@Override
- public boolean onCameraClosed(@NonNull String cameraId) {
- final ActivityRecord topActivity;
- if (Flags.cameraCompatFullscreenPickSameTaskActivity()) {
- topActivity = mCameraTask != null ? mCameraTask.getTopActivity(
- /* includeFinishing= */ true, /* includeOverlays= */ false) : null;
- } else {
- topActivity = mDisplayContent.topRunningActivity(/* considerKeyguardState= */ true);
- }
+ public boolean canCameraBeClosed(@NonNull String cameraId) {
+ final ActivityRecord topActivity = getTopActivity();
- mCameraTask = null;
if (topActivity == null) {
return true;
}
@@ -399,6 +394,23 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
return false;
}
}
+ return true;
+ }
+
+ @Override
+ public void onCameraClosed() {
+ final ActivityRecord topActivity = getTopActivity();
+
+ // Only clean up if the camera is not running - this close signal could be from switching
+ // cameras (e.g. back to front camera, and vice versa).
+ if (topActivity == null || !mCameraStateMonitor.isCameraRunningForActivity(topActivity)) {
+ // Call after getTopActivity(), as that method might use the activity from mCameraTask.
+ mCameraTask = null;
+ }
+
+ if (topActivity == null) {
+ return;
+ }
ProtoLog.v(WM_DEBUG_ORIENTATION,
"Display id=%d is notified that Camera is closed, updating rotation.",
@@ -406,11 +418,10 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
// Checking whether an activity in fullscreen rather than the task as this camera compat
// treatment doesn't cover activity embedding.
if (topActivity.getWindowingMode() != WINDOWING_MODE_FULLSCREEN) {
- return true;
+ return;
}
recomputeConfigurationForCameraCompatIfNeeded(topActivity);
mDisplayContent.updateOrientation();
- return true;
}
// TODO(b/336474959): Do we need cameraId here?
@@ -430,6 +441,16 @@ final class DisplayRotationCompatPolicy implements CameraStateMonitor.CameraComp
}
}
+ @Nullable
+ private ActivityRecord getTopActivity() {
+ if (Flags.cameraCompatFullscreenPickSameTaskActivity()) {
+ return mCameraTask != null ? mCameraTask.getTopActivity(
+ /* includeFinishing= */ true, /* includeOverlays= */ false) : null;
+ } else {
+ return mDisplayContent.topRunningActivity(/* considerKeyguardState= */ true);
+ }
+ }
+
/**
* @return {@code true} if the configuration needs to be recomputed after a camera state update.
*/
diff --git a/services/tests/security/intrusiondetection/Android.bp b/services/tests/security/intrusiondetection/Android.bp
index 86852a7da019..8d674b14feac 100644
--- a/services/tests/security/intrusiondetection/Android.bp
+++ b/services/tests/security/intrusiondetection/Android.bp
@@ -23,12 +23,16 @@ android_test {
"frameworks-base-testutils",
"junit",
"platform-test-annotations",
+ "servicestests-utils",
"services.core",
"truth",
"Nene",
"Harrier",
"TestApp",
],
+ data: [
+ ":TestIntrusionDetectionApp",
+ ],
platform_apis: true,
diff --git a/services/tests/security/intrusiondetection/AndroidManifest.xml b/services/tests/security/intrusiondetection/AndroidManifest.xml
index 40299ffe4b59..b30710d9bcbe 100644
--- a/services/tests/security/intrusiondetection/AndroidManifest.xml
+++ b/services/tests/security/intrusiondetection/AndroidManifest.xml
@@ -31,10 +31,12 @@
<action android:name="android.app.action.DEVICE_ADMIN_ENABLED"/>
</intent-filter>
</receiver>
- <service android:name="com.android.server.security.intrusiondetection.TestLoggingService"
- android:exported="true"/>
</application>
+ <queries>
+ <package android:name="com.android.coretests.apps.testapp" />
+ </queries>
+
<instrumentation android:name="androidx.test.runner.AndroidJUnitRunner"
android:targetPackage="com.android.server.security.intrusiondetection.tests"
android:label="Frameworks IntrusionDetection Services Tests"/>
diff --git a/services/tests/security/intrusiondetection/AndroidTest.xml b/services/tests/security/intrusiondetection/AndroidTest.xml
index 42cb9e3236e0..6489dea4a508 100644
--- a/services/tests/security/intrusiondetection/AndroidTest.xml
+++ b/services/tests/security/intrusiondetection/AndroidTest.xml
@@ -20,6 +20,7 @@
<target_preparer class="com.android.tradefed.targetprep.suite.SuiteApkInstaller">
<option name="cleanup-apks" value="true"/>
<option name="test-file-name" value="IntrusionDetectionServiceTests.apk"/>
+ <option name="test-file-name" value="TestIntrusionDetectionApp.apk"/>
<option name="install-arg" value="-t" />
</target_preparer>
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
index ab6da552a9ae..e505ebeb2946 100644
--- a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/IntrusionDetectionServiceTest.java
@@ -70,9 +70,9 @@ import com.android.bedstead.nene.exceptions.NeneException;
import com.android.bedstead.permissions.CommonPermissions;
import com.android.bedstead.permissions.PermissionContext;
import com.android.bedstead.permissions.annotations.EnsureHasPermission;
+import com.android.coretests.apps.testapp.LocalIntrusionDetectionEventTransport;
+import com.android.internal.infra.AndroidFuture;
import com.android.server.ServiceThread;
-import com.android.server.security.intrusiondetection.TestLoggingService;
-import com.android.server.security.intrusiondetection.TestLoggingService.LocalBinder;
import org.junit.Before;
import org.junit.Ignore;
@@ -118,13 +118,16 @@ public class IntrusionDetectionServiceTest {
private IntrusionDetectionEventTransportConnection mIntrusionDetectionEventTransportConnection;
private DataAggregator mDataAggregator;
private IntrusionDetectionService mIntrusionDetectionService;
+ private IBinder mService;
private TestLooper mTestLooper;
private Looper mLooper;
private TestLooper mTestLooperOfDataAggregator;
private Looper mLooperOfDataAggregator;
private FakePermissionEnforcer mPermissionEnforcer;
- private TestLoggingService mService;
private boolean mBoundToLoggingService = false;
+ private static final String TEST_PKG =
+ "com.android.coretests.apps.testapp";
+ private static final String TEST_SERVICE = TEST_PKG + ".TestLoggingService";
@BeforeClass
public static void setDeviceOwner() {
@@ -575,8 +578,8 @@ public class IntrusionDetectionServiceTest {
}
@Test
- public void test_StartBackupTransportService() {
- final String TAG = "test_StartBackupTransportService";
+ public void test_StartIntrusionDetectionEventTransportService() {
+ final String TAG = "test_StartIntrusionDetectionEventTransportService";
ServiceConnection serviceConnection = null;
assertEquals(false, mBoundToLoggingService);
@@ -598,17 +601,14 @@ public class IntrusionDetectionServiceTest {
private ServiceConnection startTestService() throws SecurityException, InterruptedException {
final String TAG = "startTestService";
final CountDownLatch latch = new CountDownLatch(1);
+ LocalIntrusionDetectionEventTransport transport =
+ new LocalIntrusionDetectionEventTransport();
ServiceConnection serviceConnection = new ServiceConnection() {
- // Called when the connection with the service is established.
+ // Called when connection with the service is established.
@Override
public void onServiceConnected(ComponentName className, IBinder service) {
- // Because we have bound to an explicit
- // service that is running in our own process, we can
- // cast its IBinder to a concrete class and directly access it.
- Log.d(TAG, "onServiceConnected");
- LocalBinder binder = (LocalBinder) service;
- mService = binder.getService();
+ mService = transport.getBinder();
mBoundToLoggingService = true;
latch.countDown();
}
@@ -618,14 +618,24 @@ public class IntrusionDetectionServiceTest {
public void onServiceDisconnected(ComponentName className) {
Log.d(TAG, "onServiceDisconnected");
mBoundToLoggingService = false;
- latch.countDown();
}
};
- Intent intent = new Intent(mContext, TestLoggingService.class);
+ Intent intent = new Intent();
+ intent.setComponent(new ComponentName(TEST_PKG, TEST_SERVICE));
mContext.bindService(intent, serviceConnection, Context.BIND_AUTO_CREATE);
latch.await(5, TimeUnit.SECONDS);
+ // call the methods on the transport object
+ IntrusionDetectionEvent event =
+ new IntrusionDetectionEvent(new SecurityEvent(123, new byte[15]));
+ List<IntrusionDetectionEvent> events = new ArrayList<>();
+ events.add(event);
+ assertTrue(transport.initialize());
+ assertTrue(transport.addData(events));
+ assertTrue(transport.release());
+ assertEquals(1, transport.getEvents().size());
+
return serviceConnection;
}
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/Android.bp b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/Android.bp
new file mode 100644
index 000000000000..ca5952b140c1
--- /dev/null
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/Android.bp
@@ -0,0 +1,42 @@
+// Copyright (C) 2017 The Android Open Source Project
+//
+// Licensed under the Apache License, Version 2.0 (the "License");
+// you may not use this file except in compliance with the License.
+// You may obtain a copy of the License at
+//
+// http://www.apache.org/licenses/LICENSE-2.0
+//
+// Unless required by applicable law or agreed to in writing, software
+// distributed under the License is distributed on an "AS IS" BASIS,
+// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+// See the License for the specific language governing permissions and
+// limitations under the License.
+
+package {
+ default_team: "trendy_team_platform_security",
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+android_test_helper_app {
+ name: "TestIntrusionDetectionApp",
+
+ static_libs: [
+ "frameworks-base-testutils",
+ "services.core",
+ "servicestests-utils",
+ ],
+
+ srcs: ["**/*.java"],
+
+ platform_apis: true,
+ certificate: "platform",
+ dxflags: ["--multi-dex"],
+ optimize: {
+ enabled: false,
+ },
+}
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/AndroidManifest.xml b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/AndroidManifest.xml
new file mode 100644
index 000000000000..7cc75ab70571
--- /dev/null
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/AndroidManifest.xml
@@ -0,0 +1,24 @@
+<?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.
+-->
+
+<manifest xmlns:android="http://schemas.android.com/apk/res/android"
+ package="com.android.coretests.apps.testapp">
+
+ <application>
+ <service android:name=".TestLoggingService"
+ android:exported="true" />
+ </application>
+</manifest> \ No newline at end of file
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/LocalIntrusionDetectionEventTransport.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/LocalIntrusionDetectionEventTransport.java
new file mode 100644
index 000000000000..f0012da44fa4
--- /dev/null
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/LocalIntrusionDetectionEventTransport.java
@@ -0,0 +1,58 @@
+/*
+ * 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.coretests.apps.testapp;
+
+import android.security.intrusiondetection.IntrusionDetectionEvent;
+import android.security.intrusiondetection.IntrusionDetectionEventTransport;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * A class that extends {@link IntrusionDetectionEventTransport} to provide a
+ * local transport mechanism for testing purposes. This implementation overrides
+ * the {@link #initialize()}, {@link #addData(List)}, and {@link #release()} methods
+ * to manage events locally within the test environment.
+ *
+ * For now, the implementation returns true for all methods since we don't
+ * have a real data source to send events to.
+ */
+public class LocalIntrusionDetectionEventTransport extends IntrusionDetectionEventTransport {
+ private List<IntrusionDetectionEvent> mEvents = new ArrayList<>();
+
+ @Override
+ public boolean initialize() {
+ return true;
+ }
+
+ @Override
+ public boolean addData(List<IntrusionDetectionEvent> events) {
+ mEvents.addAll(events);
+ return true;
+ }
+
+ @Override
+ public boolean release() {
+ return true;
+ }
+
+ public List<IntrusionDetectionEvent> getEvents() {
+ return mEvents;
+ }
+} \ No newline at end of file
diff --git a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestLoggingService.java b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/TestLoggingService.java
index 486140b03fbc..e4bf987402fd 100644
--- a/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestLoggingService.java
+++ b/services/tests/security/intrusiondetection/src/com/android/server/security/intrusiondetection/TestApp/src/com/android/coretests/apps/testapp/TestLoggingService.java
@@ -14,37 +14,27 @@
* limitations under the License.
*/
-package com.android.server.security.intrusiondetection;
+package com.android.coretests.apps.testapp;
import android.app.Service;
import android.content.Intent;
-import android.os.Binder;
import android.os.IBinder;
import android.os.Process;
-import android.util.Log;
+
+import com.android.internal.infra.AndroidFuture;
public class TestLoggingService extends Service {
private static final String TAG = "TestLoggingService";
+ private LocalIntrusionDetectionEventTransport mLocalIntrusionDetectionEventTransport;
- // Binder given to clients.
- private final IBinder binder = new LocalBinder();
-
- /**
- * Class used for the client Binder. Because we know this service always
- * runs in the same process as its clients, we don't need to deal with IPC.
- */
- public class LocalBinder extends Binder {
- TestLoggingService getService() {
- // Return this instance of TestLoggingService so clients
- // can call public methods.
- return TestLoggingService.this;
- }
+ public TestLoggingService() {
+ mLocalIntrusionDetectionEventTransport = new LocalIntrusionDetectionEventTransport();
}
+ // Binder given to clients.
@Override
public IBinder onBind(Intent intent) {
- // Return the binder for the service
- return binder;
+ return mLocalIntrusionDetectionEventTransport.getBinder();
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
index ad80f82c8ea8..4810c7fc32d2 100644
--- a/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/CameraStateMonitorTests.java
@@ -22,6 +22,8 @@ import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyLong;
@@ -137,6 +139,14 @@ public final class CameraStateMonitorTests extends WindowTestsBase {
}
@Test
+ public void testOnCameraOpened_listenerAdded_cameraRegistersAsOpenedDuringTheCallback() {
+ mCameraStateMonitor.addCameraStateListener(mListener);
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ assertTrue(mListener.mIsCameraOpened);
+ }
+
+ @Test
public void testOnCameraOpened_cameraClosed_notifyCameraClosed() {
mCameraStateMonitor.addCameraStateListener(mListener);
// Listener returns true on `onCameraOpened`.
@@ -144,10 +154,21 @@ public final class CameraStateMonitorTests extends WindowTestsBase {
mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ assertEquals(1, mListener.mCheckCanCloseCounter);
assertEquals(1, mListener.mOnCameraClosedCounter);
}
@Test
+ public void testOnCameraOpenedAndClosed_cameraRegistersAsClosedDuringTheCallback() {
+ mCameraStateMonitor.addCameraStateListener(mListener);
+ // Listener returns true on `onCameraOpened`.
+ mCameraAvailabilityCallback.onCameraOpened(CAMERA_ID_1, TEST_PACKAGE_1);
+
+ mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
+ assertFalse(mListener.mIsCameraOpened);
+ }
+
+ @Test
public void testOnCameraOpened_listenerCannotCloseYet_notifyCameraClosedAgain() {
mCameraStateMonitor.addCameraStateListener(mListenerCannotClose);
// Listener returns true on `onCameraOpened`.
@@ -155,7 +176,8 @@ public final class CameraStateMonitorTests extends WindowTestsBase {
mCameraAvailabilityCallback.onCameraClosed(CAMERA_ID_1);
- assertEquals(2, mListenerCannotClose.mOnCameraClosedCounter);
+ assertEquals(2, mListenerCannotClose.mCheckCanCloseCounter);
+ assertEquals(1, mListenerCannotClose.mOnCameraClosedCounter);
}
@Test
@@ -197,39 +219,49 @@ public final class CameraStateMonitorTests extends WindowTestsBase {
CameraStateMonitor.CameraCompatStateListener {
int mOnCameraOpenedCounter = 0;
+ int mCheckCanCloseCounter = 0;
int mOnCameraClosedCounter = 0;
- private boolean mOnCameraClosedReturnValue = true;
+ boolean mIsCameraOpened;
+
+ private boolean mCheckCanCloseReturnValue = true;
/**
- * @param simulateUnsuccessfulCloseOnce When false, returns `true` on every
- * `onCameraClosed`. When true, returns `false` on the
- * first `onCameraClosed` callback, and `true on the
+ * @param simulateCannotCloseOnce When false, returns `true` on every
+ * `checkCanClose`. When true, returns `false` on the
+ * first `checkCanClose` callback, and `true on the
* subsequent calls. This fake implementation tests the
* retry mechanism in {@link CameraStateMonitor}.
*/
- FakeCameraCompatStateListener(boolean simulateUnsuccessfulCloseOnce) {
- mOnCameraClosedReturnValue = !simulateUnsuccessfulCloseOnce;
+ FakeCameraCompatStateListener(boolean simulateCannotCloseOnce) {
+ mCheckCanCloseReturnValue = !simulateCannotCloseOnce;
}
@Override
- public void onCameraOpened(@NonNull ActivityRecord cameraActivity,
- @NonNull String cameraId) {
+ public void onCameraOpened(@NonNull ActivityRecord cameraActivity) {
mOnCameraOpenedCounter++;
+ mIsCameraOpened = mCameraStateMonitor.isCameraRunningForActivity(cameraActivity);
}
@Override
- public boolean onCameraClosed(@NonNull String cameraId) {
- mOnCameraClosedCounter++;
- boolean returnValue = mOnCameraClosedReturnValue;
+ public boolean canCameraBeClosed(@NonNull String cameraId) {
+ mCheckCanCloseCounter++;
+ final boolean returnValue = mCheckCanCloseReturnValue;
// If false, return false only the first time, so it doesn't fall in the infinite retry
// loop.
- mOnCameraClosedReturnValue = true;
+ mCheckCanCloseReturnValue = true;
return returnValue;
}
+ @Override
+ public void onCameraClosed() {
+ mOnCameraClosedCounter++;
+ mIsCameraOpened = mCameraStateMonitor.isCameraRunningForActivity(mActivity);
+ }
+
void resetCounters() {
mOnCameraOpenedCounter = 0;
+ mCheckCanCloseCounter = 0;
mOnCameraClosedCounter = 0;
}
}
diff --git a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
index 8b65efdfb5f9..685ae9a5fef2 100644
--- a/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/ActivityEmbedding/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Android.bp b/tests/FlickerTests/Android.bp
index 1e997b386faa..f44eacbaafbf 100644
--- a/tests/FlickerTests/Android.bp
+++ b/tests/FlickerTests/Android.bp
@@ -41,6 +41,7 @@ java_defaults {
"platform-test-annotations",
"wm-flicker-common-app-helpers",
"wm-shell-flicker-utils",
+ "systemui-tapl",
],
data: [":FlickerTestApp"],
}
diff --git a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
index 3382c1e227b3..5f92d7fe830b 100644
--- a/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppClose/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
index e941e79faea3..1b90e99a8ba2 100644
--- a/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/AppLaunch/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
index 4e06dca17fe2..ffdbb02984a7 100644
--- a/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/FlickerService/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/IME/AndroidTestTemplate.xml b/tests/FlickerTests/IME/AndroidTestTemplate.xml
index 0cadd68597b6..12670cda74b2 100644
--- a/tests/FlickerTests/IME/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/IME/AndroidTestTemplate.xml
@@ -47,6 +47,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Notification/AndroidTestTemplate.xml b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
index f32e8bed85ef..e2ac5a9579ae 100644
--- a/tests/FlickerTests/Notification/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Notification/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
index ad70757a9a4d..da90c4f624d2 100644
--- a/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
+++ b/tests/FlickerTests/Notification/src/com/android/server/wm/flicker/notification/OpenAppFromNotificationWarmTest.kt
@@ -16,6 +16,8 @@
package com.android.server.wm.flicker.notification
+import android.platform.systemui_tapl.controller.NotificationIdentity
+import android.platform.systemui_tapl.ui.Root
import android.platform.test.annotations.Postsubmit
import android.platform.test.annotations.Presubmit
import android.platform.test.rule.DisableNotificationCooldownSettingRule
@@ -28,8 +30,6 @@ import android.tools.helpers.wakeUpAndGoToHomeScreen
import android.tools.traces.component.ComponentNameMatcher
import android.view.WindowInsets
import android.view.WindowManager
-import androidx.test.uiautomator.By
-import androidx.test.uiautomator.Until
import com.android.server.wm.flicker.helpers.NotificationAppHelper
import com.android.server.wm.flicker.helpers.setRotation
import com.android.server.wm.flicker.navBarLayerIsVisibleAtEnd
@@ -87,8 +87,9 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) :
.withWindowSurfaceDisappeared(ComponentNameMatcher.NOTIFICATION_SHADE)
.waitForAndVerify()
}
+
protected fun FlickerTestData.openAppFromNotification() {
- doOpenAppAndWait(startY = 10, endY = 3 * device.displayHeight / 4, steps = 25)
+ doOpenAppAndWait()
}
protected fun FlickerTestData.openAppFromLockNotification() {
@@ -101,25 +102,27 @@ open class OpenAppFromNotificationWarmTest(flicker: LegacyFlickerTest) :
WindowInsets.Type.statusBars() or WindowInsets.Type.displayCutout()
)
- doOpenAppAndWait(startY = insets.top + 100, endY = device.displayHeight / 2, steps = 4)
+ doOpenAppAndWait()
}
- protected fun FlickerTestData.doOpenAppAndWait(startY: Int, endY: Int, steps: Int) {
- // Swipe down to show the notification shade
- val x = device.displayWidth / 2
- device.swipe(x, startY, x, endY, steps)
- device.waitForIdle(2000)
- instrumentation.uiAutomation.syncInputTransactions()
+ protected fun FlickerTestData.doOpenAppAndWait() {
+ val shade = Root.get().openNotificationShade()
// Launch the activity by clicking the notification
+ // Post notification and ensure that it's collapsed
val notification =
- device.wait(Until.findObject(By.text("Flicker Test Notification")), 2000L)
- notification?.click() ?: error("Notification not found")
- instrumentation.uiAutomation.syncInputTransactions()
+ shade.notificationStack.findNotification(
+ NotificationIdentity(
+ type = NotificationIdentity.Type.BY_TEXT,
+ text = "Flicker Test Notification",
+ )
+ )
+ notification.clickToApp()
// Wait for the app to launch
wmHelper.StateSyncBuilder().withFullScreenApp(testApp).waitForAndVerify()
}
+
@Presubmit @Test override fun appWindowBecomesVisible() = appWindowBecomesVisible_warmStart()
@Presubmit @Test override fun appLayerBecomesVisible() = appLayerBecomesVisible_warmStart()
diff --git a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
index 68ae4f1f7f4f..1a4feb6e9eca 100644
--- a/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/QuickSwitch/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>
diff --git a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
index ec186723b4a4..481a8bb66fee 100644
--- a/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
+++ b/tests/FlickerTests/Rotation/AndroidTestTemplate.xml
@@ -45,6 +45,8 @@
<target_preparer class="com.android.tradefed.targetprep.RunCommandTargetPreparer">
<option name="test-user-token" value="%TEST_USER%"/>
<option name="run-command" value="rm -rf /data/user/%TEST_USER%/files/*"/>
+ <!-- Disable AOD -->
+ <option name="run-command" value="settings put secure doze_always_on 0"/>
<option name="run-command" value="settings put secure show_ime_with_hard_keyboard 1"/>
<option name="run-command" value="settings put system show_touches 1"/>
<option name="run-command" value="settings put system pointer_location 1"/>