diff options
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"/> |