diff options
169 files changed, 3346 insertions, 954 deletions
diff --git a/AconfigFlags.bp b/AconfigFlags.bp index ab5d503eac62..0ccdf37f0c2c 100644 --- a/AconfigFlags.bp +++ b/AconfigFlags.bp @@ -1043,12 +1043,20 @@ aconfig_declarations { name: "device_policy_aconfig_flags", package: "android.app.admin.flags", container: "system", + exportable: true, srcs: [ "core/java/android/app/admin/flags/flags.aconfig", ], } java_aconfig_library { + name: "device_policy_exported_aconfig_flags_lib", + aconfig_declarations: "device_policy_aconfig_flags", + defaults: ["framework-minus-apex-aconfig-java-defaults"], + mode: "exported", +} + +java_aconfig_library { name: "device_policy_aconfig_flags_lib", aconfig_declarations: "device_policy_aconfig_flags", defaults: ["framework-minus-apex-aconfig-java-defaults"], diff --git a/core/java/android/app/ITaskStackListener.aidl b/core/java/android/app/ITaskStackListener.aidl index 3c6ff2865d04..f2228f94ff01 100644 --- a/core/java/android/app/ITaskStackListener.aidl +++ b/core/java/android/app/ITaskStackListener.aidl @@ -145,6 +145,11 @@ oneway interface ITaskStackListener { void onTaskSnapshotChanged(int taskId, in TaskSnapshot snapshot); /** + * Called when a task snapshot become invalidated. + */ + void onTaskSnapshotInvalidated(int taskId); + + /** * Reports that an Activity received a back key press when there were no additional activities * on the back stack. * diff --git a/core/java/android/app/TaskStackListener.java b/core/java/android/app/TaskStackListener.java index 0290cee94dc3..36f61fd3ef59 100644 --- a/core/java/android/app/TaskStackListener.java +++ b/core/java/android/app/TaskStackListener.java @@ -178,6 +178,9 @@ public abstract class TaskStackListener extends ITaskStackListener.Stub { } @Override + public void onTaskSnapshotInvalidated(int taskId) { } + + @Override public void onBackPressedOnTaskRoot(RunningTaskInfo taskInfo) throws RemoteException { } diff --git a/core/java/android/app/admin/DeviceAdminInfo.java b/core/java/android/app/admin/DeviceAdminInfo.java index 9ef8b38666c6..46c9e781bed1 100644 --- a/core/java/android/app/admin/DeviceAdminInfo.java +++ b/core/java/android/app/admin/DeviceAdminInfo.java @@ -21,6 +21,7 @@ import static android.app.admin.flags.Flags.FLAG_HEADLESS_DEVICE_OWNER_SINGLE_US import android.annotation.FlaggedApi; import android.annotation.IntDef; import android.annotation.NonNull; +import android.app.admin.flags.Flags; import android.compat.annotation.UnsupportedAppUsage; import android.content.ComponentName; import android.content.Context; @@ -176,6 +177,10 @@ public final class DeviceAdminInfo implements Parcelable { * provisioned into "affiliated" mode when on a Headless System User Mode device. * * <p>This mode adds a Profile Owner to all users other than the user the Device Owner is on. + * + * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * DPCs should set the value of attribute "headless-device-owner-mode" inside the + * "headless-system-user" tag as "affiliated". */ public static final int HEADLESS_DEVICE_OWNER_MODE_AFFILIATED = 1; @@ -185,6 +190,10 @@ public final class DeviceAdminInfo implements Parcelable { * * <p>This mode only allows a single secondary user on the device blocking the creation of * additional secondary users. + * + * <p>Starting from Android version {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM}, + * DPCs should set the value of attribute "headless-device-owner-mode" inside the + * "headless-system-user" tag as "single_user". */ @FlaggedApi(FLAG_HEADLESS_DEVICE_OWNER_SINGLE_USER_ENABLED) public static final int HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER = 2; @@ -383,17 +392,30 @@ public final class DeviceAdminInfo implements Parcelable { } mSupportsTransferOwnership = true; } else if (tagName.equals("headless-system-user")) { - String deviceOwnerModeStringValue = - parser.getAttributeValue(null, "device-owner-mode"); + String deviceOwnerModeStringValue = null; + if (Flags.headlessSingleUserCompatibilityFix()) { + deviceOwnerModeStringValue = parser.getAttributeValue( + null, "headless-device-owner-mode"); + } + if (deviceOwnerModeStringValue == null) { + deviceOwnerModeStringValue = + parser.getAttributeValue(null, "device-owner-mode"); + } - if (deviceOwnerModeStringValue.equalsIgnoreCase("unsupported")) { + if ("unsupported".equalsIgnoreCase(deviceOwnerModeStringValue)) { mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_UNSUPPORTED; - } else if (deviceOwnerModeStringValue.equalsIgnoreCase("affiliated")) { + } else if ("affiliated".equalsIgnoreCase(deviceOwnerModeStringValue)) { mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_AFFILIATED; - } else if (deviceOwnerModeStringValue.equalsIgnoreCase("single_user")) { + } else if ("single_user".equalsIgnoreCase(deviceOwnerModeStringValue)) { mHeadlessDeviceOwnerMode = HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; } else { - throw new XmlPullParserException("headless-system-user mode must be valid"); + if (Flags.headlessSingleUserCompatibilityFix()) { + Log.e(TAG, "Unknown headless-system-user mode: " + + deviceOwnerModeStringValue); + } else { + throw new XmlPullParserException( + "headless-system-user mode must be valid"); + } } } } diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig index 18914e120d52..83daa4524696 100644 --- a/core/java/android/app/admin/flags/flags.aconfig +++ b/core/java/android/app/admin/flags/flags.aconfig @@ -303,3 +303,24 @@ flag { purpose: PURPOSE_BUGFIX } } + +flag { + name: "headless_single_user_compatibility_fix" + namespace: "enterprise" + description: "Fix for compatibility issue introduced from using single_user mode on pre-Android V builds" + bug: "338050276" + is_exported: true + metadata { + purpose: PURPOSE_BUGFIX + } +} + +flag { + name: "headless_single_min_target_sdk" + namespace: "enterprise" + description: "Only allow DPCs targeting Android V to provision into single user mode" + bug: "338588825" + metadata { + purpose: PURPOSE_BUGFIX + } +} diff --git a/core/java/android/content/pm/flags.aconfig b/core/java/android/content/pm/flags.aconfig index 205f1e9c1f5c..45591d79ee00 100644 --- a/core/java/android/content/pm/flags.aconfig +++ b/core/java/android/content/pm/flags.aconfig @@ -248,3 +248,11 @@ flag { bug: "316916801" is_fixed_read_only: true } + +flag { + name: "package_restart_query_disabled_by_default" + namespace: "package_manager_service" + description: "Feature flag to register broadcast receiver only support package restart query." + bug: "300309050" + is_fixed_read_only: true +} diff --git a/core/java/android/view/InputWindowHandle.java b/core/java/android/view/InputWindowHandle.java index de5fc7f3e358..58ef5efe846f 100644 --- a/core/java/android/view/InputWindowHandle.java +++ b/core/java/android/view/InputWindowHandle.java @@ -67,7 +67,7 @@ public final class InputWindowHandle { InputConfig.SPY, InputConfig.INTERCEPTS_STYLUS, InputConfig.CLONE, - InputConfig.SENSITIVE_FOR_TRACING, + InputConfig.SENSITIVE_FOR_PRIVACY, }) public @interface InputConfigFlags {} diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index 1d843757489e..ca8eaf9bccd0 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -12696,9 +12696,11 @@ public final class ViewRootImpl implements ViewParent, return; } + boolean traceFrameRate = false; try { if (mLastPreferredFrameRate != preferredFrameRate) { - if (Trace.isTagEnabled(Trace.TRACE_TAG_VIEW)) { + traceFrameRate = Trace.isTagEnabled(Trace.TRACE_TAG_VIEW); + if (traceFrameRate) { Trace.traceBegin( Trace.TRACE_TAG_VIEW, "ViewRootImpl#setFrameRate " + preferredFrameRate + " compatibility " @@ -12713,7 +12715,9 @@ public final class ViewRootImpl implements ViewParent, } catch (Exception e) { Log.e(mTag, "Unable to set frame rate", e); } finally { - Trace.traceEnd(Trace.TRACE_TAG_VIEW); + if (traceFrameRate) { + Trace.traceEnd(Trace.TRACE_TAG_VIEW); + } } } diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java index 0bc2430f8805..f22e8f583e1a 100644 --- a/core/java/android/view/WindowManager.java +++ b/core/java/android/view/WindowManager.java @@ -4365,7 +4365,8 @@ public interface WindowManager extends ViewManager { public static final int INPUT_FEATURE_SPY = 1 << 2; /** - * Input feature used to indicate that this window is sensitive for tracing. + * Input feature used to indicate that this window is privacy sensitive. This may be used + * to redact input interactions from tracing or screen mirroring. * <p> * A window that uses {@link LayoutParams#FLAG_SECURE} will automatically be treated as * a sensitive for input tracing, but this input feature can be set on windows that don't @@ -4378,7 +4379,7 @@ public interface WindowManager extends ViewManager { * * @hide */ - public static final int INPUT_FEATURE_SENSITIVE_FOR_TRACING = 1 << 3; + public static final int INPUT_FEATURE_SENSITIVE_FOR_PRIVACY = 1 << 3; /** * An internal annotation for flags that can be specified to {@link #inputFeatures}. @@ -4392,7 +4393,7 @@ public interface WindowManager extends ViewManager { INPUT_FEATURE_NO_INPUT_CHANNEL, INPUT_FEATURE_DISABLE_USER_ACTIVITY, INPUT_FEATURE_SPY, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, }) public @interface InputFeatureFlags { } diff --git a/core/java/com/android/internal/content/PackageMonitor.java b/core/java/com/android/internal/content/PackageMonitor.java index 7ac553c56bf7..3af1dd7a28e4 100644 --- a/core/java/com/android/internal/content/PackageMonitor.java +++ b/core/java/com/android/internal/content/PackageMonitor.java @@ -22,6 +22,7 @@ import android.compat.annotation.UnsupportedAppUsage; import android.content.Context; import android.content.Intent; import android.content.IntentFilter; +import android.content.pm.Flags; import android.content.pm.PackageManager; import android.net.Uri; import android.os.Bundle; @@ -68,7 +69,8 @@ public abstract class PackageMonitor extends android.content.BroadcastReceiver { @UnsupportedAppUsage public PackageMonitor() { - this(true); + // If the feature flag is enabled, set mSupportsPackageRestartQuery to false by default + this(!Flags.packageRestartQueryDisabledByDefault()); } /** diff --git a/core/res/res/layout/side_fps_toast.xml b/core/res/res/layout/side_fps_toast.xml index 96860b050393..2c35c9b888cf 100644 --- a/core/res/res/layout/side_fps_toast.xml +++ b/core/res/res/layout/side_fps_toast.xml @@ -18,28 +18,26 @@ <LinearLayout xmlns:android="http://schemas.android.com/apk/res/android" android:layout_width="wrap_content" android:layout_height="wrap_content" - android:minWidth="350dp" android:layout_gravity="center" + android:minWidth="350dp" android:background="@color/side_fps_toast_background"> <TextView - android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_weight="6" android:text="@string/fp_power_button_enrollment_title" - android:singleLine="true" - android:ellipsize="end" android:textColor="@color/side_fps_text_color" android:paddingLeft="20dp"/> <Space - android:layout_width="wrap_content" - android:layout_height="match_parent" - android:layout_weight="1"/> + android:layout_width="5dp" + android:layout_height="match_parent" /> <Button android:id="@+id/turn_off_screen" - android:layout_width="wrap_content" android:layout_height="wrap_content" + android:layout_width="0dp" + android:layout_weight="3" android:text="@string/fp_power_button_enrollment_button_text" - android:paddingRight="20dp" style="?android:attr/buttonBarNegativeButtonStyle" android:textColor="@color/side_fps_button_color" - android:maxLines="1"/> + /> </LinearLayout>
\ No newline at end of file diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index 5e900f773a65..27b756d46b12 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -842,7 +842,8 @@ that created the task, and therefore there will only be one instance of this activity in a task. In contrast to the {@code singleTask} launch mode, this activity can be started in multiple instances in different tasks if the - {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set.--> + {@code FLAG_ACTIVITY_MULTIPLE_TASK} or {@code FLAG_ACTIVITY_NEW_DOCUMENT} is set. + This enum value is introduced in API level 31. --> <enum name="singleInstancePerTask" value="4" /> </attr> <!-- Specify the orientation an activity should be run in. If not diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java index e38038e38c25..59092d48bcaa 100644 --- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java +++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitController.java @@ -3137,11 +3137,9 @@ public class SplitController implements JetpackTaskFragmentOrganizer.TaskFragmen private static EmbeddedActivityWindowInfo translateActivityWindowInfo( @NonNull Activity activity, @NonNull ActivityWindowInfo activityWindowInfo) { final boolean isEmbedded = activityWindowInfo.isEmbedded(); - final Rect activityBounds = new Rect(activity.getResources().getConfiguration() - .windowConfiguration.getBounds()); final Rect taskBounds = new Rect(activityWindowInfo.getTaskBounds()); final Rect activityStackBounds = new Rect(activityWindowInfo.getTaskFragmentBounds()); - return new EmbeddedActivityWindowInfo(activity, isEmbedded, activityBounds, taskBounds, + return new EmbeddedActivityWindowInfo(activity, isEmbedded, taskBounds, activityStackBounds); } diff --git a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java index 8bc3a300136a..a52587778fa8 100644 --- a/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java +++ b/libs/WindowManager/Jetpack/tests/unittest/src/androidx/window/extensions/embedding/SplitControllerTest.java @@ -1570,8 +1570,6 @@ public class SplitControllerTest { mSetFlagRule.enableFlags(Flags.FLAG_ACTIVITY_WINDOW_INFO_FLAG); final boolean isEmbedded = true; - final Rect activityBounds = mActivity.getResources().getConfiguration().windowConfiguration - .getBounds(); final Rect taskBounds = new Rect(0, 0, 1000, 2000); final Rect activityStackBounds = new Rect(0, 0, 500, 2000); doReturn(isEmbedded).when(mActivityWindowInfo).isEmbedded(); @@ -1579,7 +1577,7 @@ public class SplitControllerTest { doReturn(activityStackBounds).when(mActivityWindowInfo).getTaskFragmentBounds(); final EmbeddedActivityWindowInfo expected = new EmbeddedActivityWindowInfo(mActivity, - isEmbedded, activityBounds, taskBounds, activityStackBounds); + isEmbedded, taskBounds, activityStackBounds); assertEquals(expected, mSplitController.getEmbeddedActivityWindowInfo(mActivity)); } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java index 12dce5bf70c0..8b2d0ddf2510 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransition.java @@ -45,7 +45,6 @@ import android.window.WindowContainerTransaction; import androidx.annotation.Nullable; import com.android.internal.util.Preconditions; -import com.android.wm.shell.R; import com.android.wm.shell.ShellTaskOrganizer; import com.android.wm.shell.common.pip.PipBoundsAlgorithm; import com.android.wm.shell.common.pip.PipBoundsState; @@ -64,6 +63,9 @@ public class PipTransition extends PipTransitionController implements private static final String TAG = PipTransition.class.getSimpleName(); private static final String PIP_TASK_TOKEN = "pip_task_token"; private static final String PIP_TASK_LEASH = "pip_task_leash"; + private static final String PIP_START_TX = "pip_start_tx"; + private static final String PIP_FINISH_TX = "pip_finish_tx"; + private static final String PIP_DESTINATION_BOUNDS = "pip_dest_bounds"; /** * The fixed start delay in ms when fading out the content overlay from bounds animation. @@ -98,6 +100,8 @@ public class PipTransition extends PipTransitionController implements private WindowContainerToken mPipTaskToken; @Nullable private SurfaceControl mPipLeash; + @Nullable + private Transitions.TransitionFinishCallback mFinishCallback; public PipTransition( Context context, @@ -223,7 +227,6 @@ public class PipTransition extends PipTransitionController implements return startExpandAnimation(info, startTransaction, finishTransaction, finishCallback); } else if (transition == mResizeTransition) { mResizeTransition = null; - mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS); return startResizeAnimation(info, startTransaction, finishTransaction, finishCallback); } @@ -246,31 +249,27 @@ public class PipTransition extends PipTransitionController implements return false; } SurfaceControl pipLeash = pipChange.getLeash(); - Rect destinationBounds = pipChange.getEndAbsBounds(); // Even though the final bounds and crop are applied with finishTransaction since // this is a visible change, we still need to handle the app draw coming in. Snapshot // covering app draw during collection will be removed by startTransaction. So we make - // the crop equal to the final bounds and then scale the leash back to starting bounds. + // the crop equal to the final bounds and then let the current + // animator scale the leash back to starting bounds. + // Note: animator is responsible for applying the startTx but NOT finishTx. startTransaction.setWindowCrop(pipLeash, pipChange.getEndAbsBounds().width(), pipChange.getEndAbsBounds().height()); - startTransaction.setScale(pipLeash, - (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), - (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); - startTransaction.apply(); - finishTransaction.setScale(pipLeash, - (float) mPipBoundsState.getBounds().width() / destinationBounds.width(), - (float) mPipBoundsState.getBounds().height() / destinationBounds.height()); - - // We are done with the transition, but will continue animating leash to final bounds. - finishCallback.onTransitionFinished(null); - - // Animate the pip leash with the new buffer - final int duration = mContext.getResources().getInteger( - R.integer.config_pipResizeAnimationDuration); // TODO: b/275910498 Couple this routine with a new implementation of the PiP animator. - startResizeAnimation(pipLeash, mPipBoundsState.getBounds(), destinationBounds, duration); + // Classes interested in continuing the animation would subscribe to this state update + // getting info such as endBounds, startTx, and finishTx as an extra Bundle once + // animators are in place. Once done state needs to be updated to CHANGED_PIP_BOUNDS. + Bundle extra = new Bundle(); + extra.putParcelable(PIP_START_TX, startTransaction); + extra.putParcelable(PIP_FINISH_TX, finishTransaction); + extra.putParcelable(PIP_DESTINATION_BOUNDS, pipChange.getEndAbsBounds()); + + mFinishCallback = finishCallback; + mPipTransitionState.setState(PipTransitionState.CHANGING_PIP_BOUNDS, extra); return true; } @@ -285,12 +284,17 @@ public class PipTransition extends PipTransitionController implements WindowContainerToken pipTaskToken = pipChange.getContainer(); SurfaceControl pipLeash = pipChange.getLeash(); + if (pipTaskToken == null || pipLeash == null) { + return false; + } + PictureInPictureParams params = pipChange.getTaskInfo().pictureInPictureParams; Rect srcRectHint = params.getSourceRectHint(); Rect startBounds = pipChange.getStartAbsBounds(); Rect destinationBounds = pipChange.getEndAbsBounds(); WindowContainerTransaction finishWct = new WindowContainerTransaction(); + SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); if (PipBoundsAlgorithm.isSourceRectHintValidForEnterPip(srcRectHint, destinationBounds)) { final float scale = (float) destinationBounds.width() / srcRectHint.width(); @@ -316,19 +320,17 @@ public class PipTransition extends PipTransitionController implements .reparent(overlayLeash, pipLeash) .setLayer(overlayLeash, Integer.MAX_VALUE); - if (pipTaskToken != null) { - SurfaceControl.Transaction tx = new SurfaceControl.Transaction(); - tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), - this::onClientDrawAtTransitionEnd) - .setScale(overlayLeash, 1f, 1f) - .setPosition(overlayLeash, - (destinationBounds.width() - overlaySize) / 2f, - (destinationBounds.height() - overlaySize) / 2f); - finishWct.setBoundsChangeTransaction(pipTaskToken, tx); - } + // Overlay needs to be adjusted once a new draw comes in resetting surface transform. + tx.setScale(overlayLeash, 1f, 1f); + tx.setPosition(overlayLeash, (destinationBounds.width() - overlaySize) / 2f, + (destinationBounds.height() - overlaySize) / 2f); } startTransaction.apply(); + tx.addTransactionCommittedListener(mPipScheduler.getMainExecutor(), + this::onClientDrawAtTransitionEnd); + finishWct.setBoundsChangeTransaction(pipTaskToken, tx); + // Note that finishWct should be free of any actual WM state changes; we are using // it for syncing with the client draw after delayed configuration changes are dispatched. finishCallback.onTransitionFinished(finishWct.isEmpty() ? null : finishWct); @@ -412,14 +414,6 @@ public class PipTransition extends PipTransitionController implements return true; } - /** - * TODO: b/275910498 Use a new implementation of the PiP animator here. - */ - private void startResizeAnimation(SurfaceControl leash, Rect startBounds, - Rect endBounds, int duration) { - mPipTransitionState.setState(PipTransitionState.CHANGED_PIP_BOUNDS); - } - // // Various helpers to resolve transition requests and infos // @@ -537,6 +531,15 @@ public class PipTransition extends PipTransitionController implements mPipTransitionState.mPipTaskToken = null; mPipTransitionState.mPinnedTaskLeash = null; break; + case PipTransitionState.CHANGED_PIP_BOUNDS: + // Note: this might not be the end of the animation, rather animator just finished + // adjusting startTx and finishTx and is ready to finishTransition(). The animator + // can still continue playing the leash into the destination bounds after. + if (mFinishCallback != null) { + mFinishCallback.onTransitionFinished(null); + mFinishCallback = null; + } + break; } } } diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java index f7bc622b6195..9a9c59e2fa8e 100644 --- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java +++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTransitionState.java @@ -257,6 +257,7 @@ public class PipTransitionState { private String stateToString() { switch (mState) { case UNDEFINED: return "undefined"; + case SWIPING_TO_PIP: return "swiping_to_pip"; case ENTERING_PIP: return "entering-pip"; case ENTERED_PIP: return "entered-pip"; case CHANGING_PIP_BOUNDS: return "changing-bounds"; diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java index b52faba79ed7..4c76fb02f7d8 100644 --- a/nfc/java/android/nfc/cardemulation/PollingFrame.java +++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java @@ -44,8 +44,15 @@ public final class PollingFrame implements Parcelable{ /** * @hide */ - @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, value = { POLLING_LOOP_TYPE_A, POLLING_LOOP_TYPE_B, - POLLING_LOOP_TYPE_F, POLLING_LOOP_TYPE_OFF, POLLING_LOOP_TYPE_ON }) + @IntDef(prefix = { "POLLING_LOOP_TYPE_"}, + value = { + POLLING_LOOP_TYPE_A, + POLLING_LOOP_TYPE_B, + POLLING_LOOP_TYPE_F, + POLLING_LOOP_TYPE_OFF, + POLLING_LOOP_TYPE_ON, + POLLING_LOOP_TYPE_UNKNOWN + }) @Retention(RetentionPolicy.SOURCE) @FlaggedApi(android.nfc.Flags.FLAG_NFC_READ_POLLING_LOOP) public @interface PollingFrameType {} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt deleted file mode 100644 index 0db01e88c608..000000000000 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsColors.kt +++ /dev/null @@ -1,122 +0,0 @@ -/* - * Copyright (C) 2022 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.settingslib.spa.framework.theme - -import android.content.Context -import android.os.Build -import androidx.annotation.VisibleForTesting -import androidx.compose.runtime.Composable -import androidx.compose.runtime.remember -import androidx.compose.runtime.staticCompositionLocalOf -import androidx.compose.ui.graphics.Color -import androidx.compose.ui.platform.LocalContext - -data class SettingsColorScheme( - val categoryTitle: Color = Color.Unspecified, - val surface: Color = Color.Unspecified, - val surfaceHeader: Color = Color.Unspecified, - val secondaryText: Color = Color.Unspecified, - val primaryContainer: Color = Color.Unspecified, - val onPrimaryContainer: Color = Color.Unspecified, -) - -internal val LocalColorScheme = staticCompositionLocalOf { SettingsColorScheme() } - -@Composable -internal fun settingsColorScheme(isDarkTheme: Boolean): SettingsColorScheme { - val context = LocalContext.current - return remember(isDarkTheme) { - when { - Build.VERSION.SDK_INT >= Build.VERSION_CODES.S -> { - if (isDarkTheme) dynamicDarkColorScheme(context) - else dynamicLightColorScheme(context) - } - isDarkTheme -> darkColorScheme() - else -> lightColorScheme() - } - } -} - -/** - * Creates a light dynamic color scheme. - * - * Use this function to create a color scheme based off the system wallpaper. If the developer - * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a - * light theme variant. - * - * @param context The context required to get system resource data. - */ -@VisibleForTesting -internal fun dynamicLightColorScheme(context: Context): SettingsColorScheme { - val tonalPalette = dynamicTonalPalette(context) - return SettingsColorScheme( - categoryTitle = tonalPalette.primary40, - surface = tonalPalette.neutral99, - surfaceHeader = tonalPalette.neutral90, - secondaryText = tonalPalette.neutralVariant30, - primaryContainer = tonalPalette.primary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -/** - * Creates a dark dynamic color scheme. - * - * Use this function to create a color scheme based off the system wallpaper. If the developer - * changes the wallpaper this color scheme will change accordingly. This dynamic scheme is a dark - * theme variant. - * - * @param context The context required to get system resource data. - */ -@VisibleForTesting -internal fun dynamicDarkColorScheme(context: Context): SettingsColorScheme { - val tonalPalette = dynamicTonalPalette(context) - return SettingsColorScheme( - categoryTitle = tonalPalette.primary90, - surface = tonalPalette.neutral20, - surfaceHeader = tonalPalette.neutral30, - secondaryText = tonalPalette.neutralVariant80, - primaryContainer = tonalPalette.secondary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -@VisibleForTesting -internal fun darkColorScheme(): SettingsColorScheme { - val tonalPalette = tonalPalette() - return SettingsColorScheme( - categoryTitle = tonalPalette.primary90, - surface = tonalPalette.neutral20, - surfaceHeader = tonalPalette.neutral30, - secondaryText = tonalPalette.neutralVariant80, - primaryContainer = tonalPalette.secondary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} - -@VisibleForTesting -internal fun lightColorScheme(): SettingsColorScheme { - val tonalPalette = tonalPalette() - return SettingsColorScheme( - categoryTitle = tonalPalette.primary40, - surface = tonalPalette.neutral99, - surfaceHeader = tonalPalette.neutral90, - secondaryText = tonalPalette.neutralVariant30, - primaryContainer = tonalPalette.primary90, - onPrimaryContainer = tonalPalette.neutral10, - ) -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt index d14b96020a06..d9f82e8c6986 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/framework/theme/SettingsTheme.kt @@ -21,7 +21,6 @@ import androidx.compose.material3.LocalContentColor import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.CompositionLocalProvider -import androidx.compose.runtime.ReadOnlyComposable /** * The Material 3 Theme for Settings. @@ -35,17 +34,9 @@ fun SettingsTheme(content: @Composable () -> Unit) { typography = rememberSettingsTypography(), ) { CompositionLocalProvider( - LocalColorScheme provides settingsColorScheme(isDarkTheme), LocalContentColor provides MaterialTheme.colorScheme.onSurface, ) { content() } } } - -object SettingsTheme { - val colorScheme: SettingsColorScheme - @Composable - @ReadOnlyComposable - get() = LocalColorScheme.current -} diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt index 979cf3bddae6..70d353da496c 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/button/ActionButtons.kt @@ -88,9 +88,9 @@ private fun RowScope.ActionButton(actionButton: ActionButton) { interactionSource = remember(actionButton) { MutableInteractionSource() }, shape = RectangleShape, colors = ButtonDefaults.filledTonalButtonColors( - containerColor = SettingsTheme.colorScheme.surface, - contentColor = SettingsTheme.colorScheme.categoryTitle, - disabledContainerColor = SettingsTheme.colorScheme.surface, + containerColor = MaterialTheme.colorScheme.surface, + contentColor = MaterialTheme.colorScheme.primary, + disabledContainerColor = MaterialTheme.colorScheme.surface, ), contentPadding = PaddingValues(horizontal = 4.dp, vertical = 20.dp), ) { diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt index d08d97eb89db..0546719eb8cd 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/card/SettingsCard.kt @@ -83,7 +83,7 @@ fun SettingsCardContent( Card( shape = CornerExtraSmall, colors = CardDefaults.cardColors( - containerColor = containerColor.takeOrElse { SettingsTheme.colorScheme.surface }, + containerColor = containerColor.takeOrElse { MaterialTheme.colorScheme.surface }, ), modifier = Modifier .fillMaxWidth() diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt index 706bd0a5d099..36cd136602f3 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/CustomizedAppBar.kt @@ -74,7 +74,6 @@ import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.Velocity import androidx.compose.ui.unit.dp import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme import com.android.settingslib.spa.framework.theme.settingsBackground import kotlin.math.abs import kotlin.math.max @@ -142,7 +141,7 @@ private fun Title(title: String, maxLines: Int = Int.MAX_VALUE) { @Composable private fun topAppBarColors() = TopAppBarColors( containerColor = MaterialTheme.colorScheme.settingsBackground, - scrolledContainerColor = SettingsTheme.colorScheme.surfaceHeader, + scrolledContainerColor = MaterialTheme.colorScheme.surfaceVariant, navigationIconContentColor = MaterialTheme.colorScheme.onSurface, titleContentColor = MaterialTheme.colorScheme.onSurface, actionIconContentColor = MaterialTheme.colorScheme.onSurfaceVariant, diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt index 6f2c38caa3bc..60814bf8cc25 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/scaffold/SettingsTab.kt @@ -51,8 +51,8 @@ internal fun SettingsTab( .clip(SettingsShape.CornerMedium) .background( color = lerp( - start = SettingsTheme.colorScheme.primaryContainer, - stop = SettingsTheme.colorScheme.surface, + start = MaterialTheme.colorScheme.primaryContainer, + stop = MaterialTheme.colorScheme.surface, fraction = colorFraction, ), ), @@ -61,8 +61,8 @@ internal fun SettingsTab( text = title, style = MaterialTheme.typography.labelLarge, color = lerp( - start = SettingsTheme.colorScheme.onPrimaryContainer, - stop = SettingsTheme.colorScheme.secondaryText, + start = MaterialTheme.colorScheme.onPrimaryContainer, + stop = MaterialTheme.colorScheme.onSurface, fraction = colorFraction, ), ) diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt index 6aac5bf3839a..48cd145da124 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Category.kt @@ -46,7 +46,7 @@ fun CategoryTitle(title: String) { end = SettingsDimension.itemPaddingEnd, bottom = 8.dp, ), - color = SettingsTheme.colorScheme.categoryTitle, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt index 930d0a1872ab..99b2524f0f9e 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/CopyableBody.kt @@ -37,7 +37,6 @@ import androidx.compose.ui.res.stringResource import androidx.compose.ui.text.AnnotatedString import androidx.compose.ui.unit.DpOffset import com.android.settingslib.spa.framework.theme.SettingsDimension -import com.android.settingslib.spa.framework.theme.SettingsTheme @Composable fun CopyableBody(body: String) { @@ -78,7 +77,7 @@ private fun DropdownMenuTitle(text: String) { top = SettingsDimension.itemPaddingAround, bottom = SettingsDimension.buttonPaddingVertical, ), - color = SettingsTheme.colorScheme.categoryTitle, + color = MaterialTheme.colorScheme.primary, style = MaterialTheme.typography.labelMedium, ) } diff --git a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt index d423d9fe5897..6e5f32ebe545 100644 --- a/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt +++ b/packages/SettingsLib/Spa/spa/src/com/android/settingslib/spa/widget/ui/Text.kt @@ -47,7 +47,6 @@ fun SettingsTitle( modifier = Modifier .padding(vertical = SettingsDimension.paddingTiny) .contentDescription(contentDescription), - color = MaterialTheme.colorScheme.onSurface, style = MaterialTheme.typography.titleMedium.withWeight(useMediumWeight), ) } diff --git a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt b/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt deleted file mode 100644 index f3f89e07814f..000000000000 --- a/packages/SettingsLib/Spa/tests/src/com/android/settingslib/spa/framework/theme/SettingsColorsTest.kt +++ /dev/null @@ -1,65 +0,0 @@ -/* - * Copyright (C) 2022 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.settingslib.spa.framework.theme - -import android.content.Context -import androidx.test.core.app.ApplicationProvider -import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.google.common.truth.Truth.assertThat -import org.junit.Test -import org.junit.runner.RunWith -import androidx.compose.ui.graphics.Color - -@RunWith(AndroidJUnit4::class) -class SettingsColorsTest { - private val context: Context = ApplicationProvider.getApplicationContext() - - @Test - fun testDynamicTheme() { - // The dynamic color could be different in different device, just check basic restrictions: - // 1. text color is different with surface color - // 2. primary / spinner color is different with its on-item color - val ls = dynamicLightColorScheme(context) - assertThat(ls.categoryTitle).isNotEqualTo(ls.surface) - assertThat(ls.secondaryText).isNotEqualTo(ls.surface) - assertThat(ls.primaryContainer).isNotEqualTo(ls.onPrimaryContainer) - - val ds = dynamicDarkColorScheme(context) - assertThat(ds.categoryTitle).isNotEqualTo(ds.surface) - assertThat(ds.secondaryText).isNotEqualTo(ds.surface) - assertThat(ds.primaryContainer).isNotEqualTo(ds.onPrimaryContainer) - } - - @Test - fun testStaticTheme() { - val ls = lightColorScheme() - assertThat(ls.categoryTitle).isEqualTo(Color(red = 103, green = 80, blue = 164)) - assertThat(ls.surface).isEqualTo(Color(red = 255, green = 251, blue = 254)) - assertThat(ls.surfaceHeader).isEqualTo(Color(red = 230, green = 225, blue = 229)) - assertThat(ls.secondaryText).isEqualTo(Color(red = 73, green = 69, blue = 79)) - assertThat(ls.primaryContainer).isEqualTo(Color(red = 234, green = 221, blue = 255)) - assertThat(ls.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31)) - - val ds = darkColorScheme() - assertThat(ds.categoryTitle).isEqualTo(Color(red = 234, green = 221, blue = 255)) - assertThat(ds.surface).isEqualTo(Color(red = 49, green = 48, blue = 51)) - assertThat(ds.surfaceHeader).isEqualTo(Color(red = 72, green = 70, blue = 73)) - assertThat(ds.secondaryText).isEqualTo(Color(red = 202, green = 196, blue = 208)) - assertThat(ds.primaryContainer).isEqualTo(Color(red = 232, green = 222, blue = 248)) - assertThat(ds.onPrimaryContainer).isEqualTo(Color(red = 28, green = 27, blue = 31)) - } -} diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt index 68da1431c594..bededf03a0f4 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/template/app/AppList.kt @@ -31,6 +31,7 @@ import androidx.compose.runtime.remember import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.Dp +import androidx.lifecycle.compose.collectAsStateWithLifecycle import androidx.lifecycle.viewmodel.compose.viewModel import com.android.settingslib.spa.framework.compose.LifecycleEffect import com.android.settingslib.spa.framework.compose.LogCompositions @@ -49,7 +50,6 @@ import com.android.settingslib.spaprivileged.model.app.AppListViewModel import com.android.settingslib.spaprivileged.model.app.AppRecord import com.android.settingslib.spaprivileged.model.app.IAppListViewModel import com.android.settingslib.spaprivileged.model.app.userId -import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.flow.MutableStateFlow private const val TAG = "AppList" @@ -95,9 +95,9 @@ internal fun <T : AppRecord> AppListInput<T>.AppListImpl( LogCompositions(TAG, config.userIds.toString()) val viewModel = viewModelSupplier() Column(Modifier.fillMaxSize()) { - val optionsState = viewModel.spinnerOptionsFlow.collectAsState(null, Dispatchers.IO) + val optionsState = viewModel.spinnerOptionsFlow.collectAsStateWithLifecycle(null) SpinnerOptions(optionsState, viewModel.optionFlow) - val appListData = viewModel.appListDataFlow.collectAsState(null, Dispatchers.IO) + val appListData = viewModel.appListDataFlow.collectAsStateWithLifecycle(null) listModel.AppListWidget(appListData, header, bottomPadding, noItemMessage) } } diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt index 5a6c0a1bf275..dd7c0368bf4b 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/framework/compose/DisposableBroadcastReceiverAsUserTest.kt @@ -27,8 +27,6 @@ import androidx.compose.ui.platform.LocalLifecycleOwner import androidx.compose.ui.test.junit4.createComposeRule import androidx.lifecycle.testing.TestLifecycleOwner import androidx.test.ext.junit.runners.AndroidJUnit4 -import com.android.settingslib.spa.testutils.delay -import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test import org.junit.runner.RunWith @@ -71,9 +69,8 @@ class DisposableBroadcastReceiverAsUserTest { DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {} } } - composeTestRule.delay() - assertThat(registeredBroadcastReceiver).isNotNull() + composeTestRule.waitUntil { registeredBroadcastReceiver != null } } @Test @@ -91,9 +88,8 @@ class DisposableBroadcastReceiverAsUserTest { } registeredBroadcastReceiver!!.onReceive(context, Intent()) - composeTestRule.delay() - assertThat(onReceiveIsCalled).isTrue() + composeTestRule.waitUntil { onReceiveIsCalled } } private companion object { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt index 70b38feae9d5..cd747cc142c1 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsGlobalBooleanTest.kt @@ -102,7 +102,8 @@ class SettingsGlobalBooleanTest { delay(100) value = true - assertThat(listDeferred.await()).containsExactly(false, true).inOrder() + assertThat(listDeferred.await()) + .containsAtLeast(false, true).inOrder() } private companion object { diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt index 29a89be87acd..ecc92f8f8d5c 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/settingsprovider/SettingsSecureBooleanTest.kt @@ -102,7 +102,8 @@ class SettingsSecureBooleanTest { delay(100) value = true - assertThat(listDeferred.await()).containsExactly(false, true).inOrder() + assertThat(listDeferred.await()) + .containsAtLeast(false, true).inOrder() } private companion object { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java index 4e52c77f27b4..cb6a93002ea7 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDeviceManager.java @@ -176,6 +176,22 @@ public class CachedBluetoothDeviceManager { } /** + * Sync device status of the pair of the hearing aid if needed. + * + * @param device the remote device + */ + public synchronized void syncDeviceWithinHearingAidSetIfNeeded(CachedBluetoothDevice device, + int state, int profileId) { + if (profileId == BluetoothProfile.HAP_CLIENT + || profileId == BluetoothProfile.HEARING_AID + || profileId == BluetoothProfile.CSIP_SET_COORDINATOR) { + if (state == BluetoothProfile.STATE_CONNECTED) { + mHearingAidDeviceManager.syncDeviceIfNeeded(device); + } + } + } + + /** * Search for existing sub device {@link CachedBluetoothDevice}. * * @param device the address of the Bluetooth device diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java index 1069b715d946..9bf42f9396e3 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/HearingAidDeviceManager.java @@ -15,6 +15,8 @@ */ package com.android.settingslib.bluetooth; +import android.bluetooth.BluetoothCsipSetCoordinator; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothLeAudio; import android.bluetooth.BluetoothProfile; @@ -108,6 +110,10 @@ public class HearingAidDeviceManager { return hiSyncId != BluetoothHearingAid.HI_SYNC_ID_INVALID; } + private boolean isValidGroupId(int groupId) { + return groupId != BluetoothCsipSetCoordinator.GROUP_ID_INVALID; + } + private CachedBluetoothDevice getCachedDevice(long hiSyncId) { for (int i = mCachedDevices.size() - 1; i >= 0; i--) { CachedBluetoothDevice cachedDevice = mCachedDevices.get(i); @@ -258,6 +264,27 @@ public class HearingAidDeviceManager { } } + void syncDeviceIfNeeded(CachedBluetoothDevice device) { + final LocalBluetoothProfileManager profileManager = mBtManager.getProfileManager(); + final HapClientProfile hap = profileManager.getHapClientProfile(); + // Sync preset if device doesn't support synchronization on the remote side + if (hap != null && !hap.supportsSynchronizedPresets(device.getDevice())) { + final CachedBluetoothDevice mainDevice = findMainDevice(device); + if (mainDevice != null) { + int mainPresetIndex = hap.getActivePresetIndex(mainDevice.getDevice()); + int presetIndex = hap.getActivePresetIndex(device.getDevice()); + if (mainPresetIndex != BluetoothHapClient.PRESET_INDEX_UNAVAILABLE + && mainPresetIndex != presetIndex) { + if (DEBUG) { + Log.d(TAG, "syncing preset from " + presetIndex + "->" + + mainPresetIndex + ", device=" + device); + } + hap.selectPreset(device.getDevice(), mainPresetIndex); + } + } + } + } + private void setAudioRoutingConfig(CachedBluetoothDevice device) { AudioDeviceAttributes hearingDeviceAttributes = mRoutingHelper.getMatchedHearingDeviceAttributes(device); @@ -326,7 +353,19 @@ public class HearingAidDeviceManager { } CachedBluetoothDevice findMainDevice(CachedBluetoothDevice device) { + if (device == null || mCachedDevices == null) { + return null; + } + for (CachedBluetoothDevice cachedDevice : mCachedDevices) { + if (isValidGroupId(cachedDevice.getGroupId())) { + Set<CachedBluetoothDevice> memberSet = cachedDevice.getMemberDevice(); + for (CachedBluetoothDevice memberDevice : memberSet) { + if (memberDevice != null && memberDevice.equals(device)) { + return cachedDevice; + } + } + } if (isValidHiSyncId(cachedDevice.getHiSyncId())) { CachedBluetoothDevice subDevice = cachedDevice.getSubDevice(); if (subDevice != null && subDevice.equals(device)) { diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java index 4055986e8a57..8dfeb559a8b5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManager.java @@ -408,6 +408,8 @@ public class LocalBluetoothProfileManager { boolean needDispatchProfileConnectionState = true; if (cachedDevice.getHiSyncId() != BluetoothHearingAid.HI_SYNC_ID_INVALID || cachedDevice.getGroupId() != BluetoothCsipSetCoordinator.GROUP_ID_INVALID) { + mDeviceManager.syncDeviceWithinHearingAidSetIfNeeded(cachedDevice, newState, + mProfile.getProfileId()); needDispatchProfileConnectionState = !mDeviceManager .onProfileConnectionStateChangedIfProcessed(cachedDevice, newState, mProfile.getProfileId()); diff --git a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt index 68f471dd4e4f..d198136447a5 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt @@ -45,14 +45,13 @@ val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?> .buffer(capacity = Channel.CONFLATED) /** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */ -val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?> +val MediaSessionManager.defaultRemoteSessionChanged: Flow<MediaSession.Token?> get() = callbackFlow { val callback = object : MediaSessionManager.RemoteSessionCallback { - override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) { - launch { send(sessionToken) } - } + override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) = + Unit override fun onDefaultRemoteSessionChanged( sessionToken: MediaSession.Token? diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt index e4ac9fe686a3..195ccfcd328d 100644 --- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/MediaControllerRepository.kt @@ -21,6 +21,7 @@ import android.media.session.MediaSessionManager import com.android.settingslib.bluetooth.LocalBluetoothManager import com.android.settingslib.bluetooth.headsetAudioModeChanges import com.android.settingslib.media.session.activeMediaChanges +import com.android.settingslib.media.session.defaultRemoteSessionChanged import com.android.settingslib.volume.shared.AudioManagerEventsReceiver import com.android.settingslib.volume.shared.model.AudioManagerEvent import kotlin.coroutines.CoroutineContext @@ -59,6 +60,9 @@ class MediaControllerRepositoryImpl( override val activeSessions: StateFlow<List<MediaController>> = merge( + mediaSessionManager.defaultRemoteSessionChanged.map { + mediaSessionManager.getActiveSessions(null) + }, mediaSessionManager.activeMediaChanges.filterNotNull(), localBluetoothManager?.headsetAudioModeChanges?.map { mediaSessionManager.getActiveSessions(null) diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java index aa5a2984e70c..1f0da90ed028 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/HearingAidDeviceManagerTest.java @@ -72,14 +72,18 @@ public class HearingAidDeviceManagerTest { @Rule public MockitoRule mMockitoRule = MockitoJUnit.rule(); - private final static long HISYNCID1 = 10; - private final static long HISYNCID2 = 11; - private final static String DEVICE_NAME_1 = "TestName_1"; - private final static String DEVICE_NAME_2 = "TestName_2"; - private final static String DEVICE_ALIAS_1 = "TestAlias_1"; - private final static String DEVICE_ALIAS_2 = "TestAlias_2"; - private final static String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; - private final static String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; + private static final long HISYNCID1 = 10; + private static final long HISYNCID2 = 11; + private static final int GROUP_ID_1 = 20; + private static final int GROUP_ID_2 = 21; + private static final int PRESET_INDEX_1 = 1; + private static final int PRESET_INDEX_2 = 2; + private static final String DEVICE_NAME_1 = "TestName_1"; + private static final String DEVICE_NAME_2 = "TestName_2"; + private static final String DEVICE_ALIAS_1 = "TestAlias_1"; + private static final String DEVICE_ALIAS_2 = "TestAlias_2"; + private static final String DEVICE_ADDRESS_1 = "AA:BB:CC:DD:EE:11"; + private static final String DEVICE_ADDRESS_2 = "AA:BB:CC:DD:EE:22"; private final BluetoothClass DEVICE_CLASS = createBtClass(BluetoothClass.Device.AUDIO_VIDEO_HANDSFREE); private final Context mContext = ApplicationProvider.getApplicationContext(); @@ -706,14 +710,73 @@ public class HearingAidDeviceManagerTest { } @Test - public void findMainDevice() { + public void findMainDevice_sameHiSyncId() { when(mCachedDevice1.getHiSyncId()).thenReturn(HISYNCID1); when(mCachedDevice2.getHiSyncId()).thenReturn(HISYNCID1); mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); mCachedDevice1.setSubDevice(mCachedDevice2); - assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)). - isEqualTo(mCachedDevice1); + assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo( + mCachedDevice1); + } + + @Test + public void findMainDevice_sameGroupId() { + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + assertThat(mHearingAidDeviceManager.findMainDevice(mCachedDevice2)).isEqualTo( + mCachedDevice1); + } + + @Test + public void syncDeviceWithinSet_synchronized_differentPresetIndex_shouldNotSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(true); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(true); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1); + + verify(mHapClientProfile, never()).selectPreset(any(), anyInt()); + } + + @Test + public void syncDeviceWithinSet_unsynchronized_samePresetIndex_shouldNotSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice1); + + verify(mHapClientProfile, never()).selectPreset(any(), anyInt()); + } + + @Test + public void syncDeviceWithinSet_unsynchronized_differentPresetIndex_shouldSync() { + when(mHapClientProfile.getActivePresetIndex(mDevice1)).thenReturn(PRESET_INDEX_1); + when(mHapClientProfile.getActivePresetIndex(mDevice2)).thenReturn(PRESET_INDEX_2); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice1)).thenReturn(false); + when(mHapClientProfile.supportsSynchronizedPresets(mDevice2)).thenReturn(false); + when(mCachedDevice1.getGroupId()).thenReturn(GROUP_ID_1); + when(mCachedDevice2.getGroupId()).thenReturn(GROUP_ID_2); + mCachedDeviceManager.mCachedDevices.add(mCachedDevice1); + mCachedDevice1.addMemberDevice(mCachedDevice2); + + mHearingAidDeviceManager.syncDeviceIfNeeded(mCachedDevice2); + + verify(mHapClientProfile).selectPreset(mDevice2, PRESET_INDEX_1); } private HearingAidInfo getLeftAshaHearingAidInfo(long hiSyncId) { diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java index cef083584744..6ff90ba4b391 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/LocalBluetoothProfileManagerTest.java @@ -28,6 +28,7 @@ import static org.mockito.Mockito.when; import android.bluetooth.BluetoothA2dp; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothDevice; +import android.bluetooth.BluetoothHapClient; import android.bluetooth.BluetoothHeadset; import android.bluetooth.BluetoothHearingAid; import android.bluetooth.BluetoothPan; @@ -55,7 +56,9 @@ import java.util.List; @RunWith(RobolectricTestRunner.class) @Config(shadows = {ShadowBluetoothAdapter.class}) public class LocalBluetoothProfileManagerTest { - private final static long HISYNCID = 10; + private static final long HISYNCID = 10; + + private static final int GROUP_ID = 1; @Mock private LocalBluetoothManager mBtManager; @Mock @@ -201,7 +204,8 @@ public class LocalBluetoothProfileManagerTest { * CachedBluetoothDeviceManager method */ @Test - public void stateChangedHandler_receiveHAPConnectionStateChanged_shouldDispatchDeviceManager() { + public void + stateChangedHandler_receiveHearingAidConnectionStateChanged_dispatchDeviceManager() { mShadowBluetoothAdapter.setSupportedProfiles(generateList( new int[] {BluetoothProfile.HEARING_AID})); mProfileManager.updateLocalProfiles(); @@ -219,6 +223,28 @@ public class LocalBluetoothProfileManagerTest { } /** + * Verify BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED with uuid intent will dispatch + * to {@link CachedBluetoothDeviceManager} method + */ + @Test + public void stateChangedHandler_receiveHapClientConnectionStateChanged_dispatchDeviceManager() { + mShadowBluetoothAdapter.setSupportedProfiles(generateList( + new int[] {BluetoothProfile.HAP_CLIENT})); + mProfileManager.updateLocalProfiles(); + when(mCachedBluetoothDevice.getGroupId()).thenReturn(GROUP_ID); + + mIntent = new Intent(BluetoothHapClient.ACTION_HAP_CONNECTION_STATE_CHANGED); + mIntent.putExtra(BluetoothDevice.EXTRA_DEVICE, mDevice); + mIntent.putExtra(BluetoothProfile.EXTRA_PREVIOUS_STATE, BluetoothProfile.STATE_CONNECTING); + mIntent.putExtra(BluetoothProfile.EXTRA_STATE, BluetoothProfile.STATE_CONNECTED); + + mContext.sendBroadcast(mIntent); + + verify(mDeviceManager).syncDeviceWithinHearingAidSetIfNeeded(mCachedBluetoothDevice, + BluetoothProfile.STATE_CONNECTED, BluetoothProfile.HAP_CLIENT); + } + + /** * Verify BluetoothPan.ACTION_CONNECTION_STATE_CHANGED intent with uuid will dispatch to * profile connection state changed callback */ diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp index 65c570840368..ed62ce7e27ed 100644 --- a/packages/SystemUI/Android.bp +++ b/packages/SystemUI/Android.bp @@ -362,6 +362,7 @@ android_library { "device_state_flags_lib", "kotlinx_coroutines_android", "kotlinx_coroutines", + "kotlinx_coroutines_guava", "//frameworks/libs/systemui:iconloader_base", "SystemUI-tags", "SystemUI-proto", @@ -382,6 +383,7 @@ android_library { "androidx.compose.material_material-icons-extended", "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", + "device_policy_aconfig_flags_lib", ], libs: [ "keepanno-annotations", @@ -541,6 +543,7 @@ android_library { "androidx.activity_activity-compose", "androidx.compose.animation_animation-graphics", "TraceurCommon", + "kotlinx_coroutines_guava", ], } @@ -622,6 +625,7 @@ android_app { "//frameworks/libs/systemui:compilelib", "SystemUI-tests-base", "androidx.compose.runtime_runtime", + "SystemUI-core", ], libs: [ "keepanno-annotations", diff --git a/packages/SystemUI/OWNERS b/packages/SystemUI/OWNERS index 796e3914f3c1..d2e5a13adfce 100644 --- a/packages/SystemUI/OWNERS +++ b/packages/SystemUI/OWNERS @@ -4,13 +4,13 @@ set noparent dsandler@android.com -aaronjli@google.com achalke@google.com acul@google.com adamcohen@google.com aioana@google.com alexflo@google.com andonian@google.com +amiko@google.com aroederer@google.com arteiro@google.com asc@google.com @@ -39,7 +39,6 @@ hwwang@google.com hyunyoungs@google.com ikateryna@google.com iyz@google.com -jamesoleary@google.com jbolinger@google.com jdemeulenaere@google.com jeffdq@google.com @@ -82,6 +81,7 @@ pixel@google.com pomini@google.com princedonkor@google.com rahulbanerjee@google.com +rgl@google.com roosa@google.com saff@google.com santie@google.com @@ -110,6 +110,3 @@ yeinj@google.com yuandizhou@google.com yurilin@google.com zakcohen@google.com - -#Android TV -rgl@google.com diff --git a/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt new file mode 100644 index 000000000000..3d7401d8f263 --- /dev/null +++ b/packages/SystemUI/compose/facade/enabled/src/com/android/systemui/scene/QuickSettingsShadeSceneModule.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.scene + +import com.android.systemui.qs.ui.composable.QuickSettingsShadeScene +import com.android.systemui.scene.shared.model.Scene +import dagger.Binds +import dagger.Module +import dagger.multibindings.IntoSet + +@Module +interface QuickSettingsShadeSceneModule { + + @Binds @IntoSet fun quickSettingsShade(scene: QuickSettingsShadeScene): Scene +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt index 2ba78cfd7785..fdf82ca026b1 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationScrimNestedScrollConnection.kt @@ -30,11 +30,15 @@ import com.android.compose.nestedscroll.PriorityNestedScrollConnection */ fun NotificationScrimNestedScrollConnection( scrimOffset: () -> Float, - onScrimOffsetChanged: (Float) -> Unit, + snapScrimOffset: (Float) -> Unit, + animateScrimOffset: (Float) -> Unit, minScrimOffset: () -> Float, maxScrimOffset: Float, contentHeight: () -> Float, minVisibleScrimHeight: () -> Float, + isCurrentGestureOverscroll: () -> Boolean, + onStart: (Float) -> Unit = {}, + onStop: (Float) -> Unit = {}, ): PriorityNestedScrollConnection { return PriorityNestedScrollConnection( orientation = Orientation.Vertical, @@ -49,7 +53,7 @@ fun NotificationScrimNestedScrollConnection( // scrolling down and content is done scrolling to top. After that, the scrim // needs to collapse; collapse the scrim until it is at the maxScrimOffset. canStartPostScroll = { offsetAvailable, _ -> - offsetAvailable > 0 && scrimOffset() < maxScrimOffset + offsetAvailable > 0 && (scrimOffset() < maxScrimOffset || isCurrentGestureOverscroll()) }, canStartPostFling = { false }, canContinueScroll = { @@ -57,7 +61,7 @@ fun NotificationScrimNestedScrollConnection( minScrimOffset() < currentHeight && currentHeight < maxScrimOffset }, canScrollOnFling = true, - onStart = { /* do nothing */}, + onStart = { offsetAvailable -> onStart(offsetAvailable) }, onScroll = { offsetAvailable -> val currentHeight = scrimOffset() val amountConsumed = @@ -68,10 +72,16 @@ fun NotificationScrimNestedScrollConnection( val amountLeft = minScrimOffset() - currentHeight offsetAvailable.coerceAtLeast(amountLeft) } - onScrimOffsetChanged(currentHeight + amountConsumed) + snapScrimOffset(currentHeight + amountConsumed) amountConsumed }, // Don't consume the velocity on pre/post fling - onStop = { 0f }, + onStop = { velocityAvailable -> + onStop(velocityAvailable) + if (scrimOffset() < minScrimOffset()) { + animateScrimOffset(minScrimOffset()) + } + 0f + }, ) } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt index 6e987bd03483..16ae5b1e1562 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt @@ -18,6 +18,7 @@ package com.android.systemui.notifications.ui.composable import android.util.Log +import androidx.compose.animation.core.Animatable import androidx.compose.foundation.background import androidx.compose.foundation.gestures.scrollBy import androidx.compose.foundation.layout.Box @@ -39,8 +40,8 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue -import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -77,6 +78,7 @@ import com.android.systemui.statusbar.notification.stack.ui.viewmodel.Notificati import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel import kotlin.math.roundToInt +import kotlinx.coroutines.launch object Notifications { object Elements { @@ -159,11 +161,13 @@ fun SceneScope.NotificationScrollingStack( shouldPunchHoleBehindScrim: Boolean, modifier: Modifier = Modifier, ) { + val coroutineScope = rememberCoroutineScope() val density = LocalDensity.current val screenCornerRadius = LocalScreenCornerRadius.current val scrimCornerRadius = dimensionResource(R.dimen.notification_scrim_corner_radius) val scrollState = rememberScrollState() val syntheticScroll = viewModel.syntheticScroll.collectAsState(0f) + val isCurrentGestureOverscroll = viewModel.isCurrentGestureOverscroll.collectAsState(false) val expansionFraction by viewModel.expandFraction.collectAsState(0f) val navBarHeight = @@ -180,7 +184,7 @@ fun SceneScope.NotificationScrollingStack( // When fully expanded (scrimOffset = minScrimOffset), its top bound is at minScrimStartY, // which is equal to the height of the Shade Header. Thus, when the scrim is fully expanded, the // entire height of the scrim is visible on screen. - val scrimOffset = remember { mutableStateOf(0f) } + val scrimOffset = remember { Animatable(0f) } // set the bounds to null when the scrim disappears DisposableEffect(Unit) { onDispose { viewModel.onScrimBoundsChanged(null) } } @@ -204,7 +208,7 @@ fun SceneScope.NotificationScrollingStack( // expanded, reset scrim offset. LaunchedEffect(stackHeight, scrimOffset) { snapshotFlow { stackHeight.value < minVisibleScrimHeight() && scrimOffset.value < 0f } - .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.value = 0f } + .collect { shouldCollapse -> if (shouldCollapse) scrimOffset.snapTo(0f) } } // if we receive scroll delta from NSSL, offset the scrim and placeholder accordingly. @@ -214,7 +218,7 @@ fun SceneScope.NotificationScrollingStack( val minOffset = minScrimOffset() if (scrimOffset.value > minOffset) { val remainingDelta = (minOffset - (scrimOffset.value - delta)).coerceAtLeast(0f) - scrimOffset.value = (scrimOffset.value - delta).coerceAtLeast(minOffset) + scrimOffset.snapTo((scrimOffset.value - delta).coerceAtLeast(minOffset)) if (remainingDelta > 0f) { scrollState.scrollBy(remainingDelta) } @@ -296,20 +300,30 @@ fun SceneScope.NotificationScrollingStack( modifier = Modifier.verticalNestedScrollToScene( topBehavior = NestedScrollBehavior.EdgeWithPreview, + isExternalOverscrollGesture = { isCurrentGestureOverscroll.value } ) .nestedScroll( remember( scrimOffset, maxScrimTop, minScrimTop, + isCurrentGestureOverscroll, ) { NotificationScrimNestedScrollConnection( scrimOffset = { scrimOffset.value }, - onScrimOffsetChanged = { scrimOffset.value = it }, + snapScrimOffset = { value -> + coroutineScope.launch { scrimOffset.snapTo(value) } + }, + animateScrimOffset = { value -> + coroutineScope.launch { scrimOffset.animateTo(value) } + }, minScrimOffset = minScrimOffset, maxScrimOffset = 0f, contentHeight = { stackHeight.value }, minVisibleScrimHeight = minVisibleScrimHeight, + isCurrentGestureOverscroll = { + isCurrentGestureOverscroll.value + }, ) } ) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt index 1f0340805c95..1c675e339941 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt @@ -26,10 +26,10 @@ import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel import com.android.systemui.scene.shared.model.Scenes import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.shade.ui.composable.OverlayShade -import com.android.systemui.shade.ui.viewmodel.NotificationsShadeSceneViewModel import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import javax.inject.Inject import kotlinx.coroutines.flow.StateFlow diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt new file mode 100644 index 000000000000..636c6c3b7d14 --- /dev/null +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt @@ -0,0 +1,71 @@ +/* + * 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.ui.composable + +import androidx.compose.foundation.layout.Arrangement +import androidx.compose.foundation.layout.padding +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import com.android.compose.animation.scene.SceneScope +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.scene.ui.composable.ComposableScene +import com.android.systemui.shade.ui.composable.OverlayShade +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import javax.inject.Inject +import kotlinx.coroutines.flow.StateFlow + +@SysUISingleton +class QuickSettingsShadeScene +@Inject +constructor( + viewModel: QuickSettingsShadeSceneViewModel, + private val overlayShadeViewModel: OverlayShadeViewModel, +) : ComposableScene { + + override val key = Scenes.QuickSettingsShade + + override val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + viewModel.destinationScenes + + @Composable + override fun SceneScope.Content( + modifier: Modifier, + ) { + OverlayShade( + viewModel = overlayShadeViewModel, + modifier = modifier, + horizontalArrangement = Arrangement.End, + ) { + Text( + text = "Quick settings grid", + modifier = Modifier.padding(QuickSettingsShade.Dimensions.Padding) + ) + } + } +} + +object QuickSettingsShade { + object Dimensions { + val Padding = 16.dp + } +} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt index 6f2ed8178801..ded63a107e70 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt @@ -86,7 +86,10 @@ constructor( modifier = Modifier.fillMaxWidth().height(80.dp).semantics { liveRegion = LiveRegionMode.Polite - this.onClick(label = clickLabel) { false } + this.onClick(label = clickLabel) { + viewModel.onBarClick(null) + true + } }, color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 9f5ab3c0e284..a46f4e5fef1a 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -66,7 +66,9 @@ fun VolumeSlider( // provide a not animated value to the a11y because it fails to announce the // settled value when it changes rapidly. - progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange) + if (state.isEnabled) { + progressBarRangeInfo = ProgressBarRangeInfo(state.value, state.valueRange) + } setProgress { targetValue -> val targetDirection = when { diff --git a/packages/SystemUI/compose/scene/OWNERS b/packages/SystemUI/compose/scene/OWNERS index 33a59c2bcab3..dac37eeb3e8c 100644 --- a/packages/SystemUI/compose/scene/OWNERS +++ b/packages/SystemUI/compose/scene/OWNERS @@ -2,12 +2,13 @@ set noparent # Bug component: 1184816 +amiko@google.com jdemeulenaere@google.com omarmt@google.com # SysUI Dr No's. # Don't send reviews here. -dsandler@android.com cinek@google.com +dsandler@android.com juliacr@google.com pixel@google.com
\ No newline at end of file diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt index 6b289f3c66a3..b5e93131f828 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt @@ -47,8 +47,11 @@ internal fun CoroutineScope.animateToScene( } return when (transitionState) { - is TransitionState.Idle -> animate(layoutState, target, transitionKey) + is TransitionState.Idle -> + animate(layoutState, target, transitionKey, isInitiatedByUserInput = false) is TransitionState.Transition -> { + val isInitiatedByUserInput = transitionState.isInitiatedByUserInput + // A transition is currently running: first check whether `transition.toScene` or // `transition.fromScene` is the same as our target scene, in which case the transition // can be accelerated or reversed to end up in the target state. @@ -68,8 +71,14 @@ internal fun CoroutineScope.animateToScene( } else { // The transition is in progress: start the canned animation at the same // progress as it was in. - // TODO(b/290184746): Also take the current velocity into account. - animate(layoutState, target, transitionKey, startProgress = progress) + animate( + layoutState, + target, + transitionKey, + isInitiatedByUserInput, + initialProgress = progress, + initialVelocity = transitionState.progressVelocity, + ) } } else if (transitionState.fromScene == target) { // There is a transition from [target] to another scene: simply animate the same @@ -83,19 +92,52 @@ internal fun CoroutineScope.animateToScene( layoutState.finishTransition(transitionState, target) null } else { - // TODO(b/290184746): Also take the current velocity into account. animate( layoutState, target, transitionKey, - startProgress = progress, + isInitiatedByUserInput, + initialProgress = progress, + initialVelocity = transitionState.progressVelocity, reversed = true, ) } } else { // Generic interruption; the current transition is neither from or to [target]. - // TODO(b/290930950): Better handle interruptions here. - animate(layoutState, target, transitionKey) + val interruptionResult = + layoutState.transitions.interruptionHandler.onInterruption( + transitionState, + target, + ) + ?: DefaultInterruptionHandler.onInterruption(transitionState, target) + + val animateFrom = interruptionResult.animateFrom + if ( + animateFrom != transitionState.toScene && + animateFrom != transitionState.fromScene + ) { + error( + "InterruptionResult.animateFrom must be either the fromScene " + + "(${transitionState.fromScene.debugName}) or the toScene " + + "(${transitionState.toScene.debugName}) of the interrupted transition." + ) + } + + // If we were A => B and that we are now animating A => C, add a transition B => A + // to the list of transitions so that B "disappears back to A". + val chain = interruptionResult.chain + if (chain && animateFrom != transitionState.currentScene) { + animateToScene(layoutState, animateFrom, transitionKey = null) + } + + animate( + layoutState, + target, + transitionKey, + isInitiatedByUserInput, + fromScene = animateFrom, + chain = chain, + ) } } } @@ -103,32 +145,31 @@ internal fun CoroutineScope.animateToScene( private fun CoroutineScope.animate( layoutState: BaseSceneTransitionLayoutState, - target: SceneKey, + targetScene: SceneKey, transitionKey: TransitionKey?, - startProgress: Float = 0f, + isInitiatedByUserInput: Boolean, + initialProgress: Float = 0f, + initialVelocity: Float = 0f, reversed: Boolean = false, + fromScene: SceneKey = layoutState.transitionState.currentScene, + chain: Boolean = true, ): TransitionState.Transition { - val fromScene = layoutState.transitionState.currentScene - val isUserInput = - (layoutState.transitionState as? TransitionState.Transition)?.isInitiatedByUserInput - ?: false - val targetProgress = if (reversed) 0f else 1f val transition = if (reversed) { OneOffTransition( - fromScene = target, + fromScene = targetScene, toScene = fromScene, - currentScene = target, - isInitiatedByUserInput = isUserInput, + currentScene = targetScene, + isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, ) } else { OneOffTransition( fromScene = fromScene, - toScene = target, - currentScene = target, - isInitiatedByUserInput = isUserInput, + toScene = targetScene, + currentScene = targetScene, + isInitiatedByUserInput = isInitiatedByUserInput, isUserInputOngoing = false, ) } @@ -136,7 +177,7 @@ private fun CoroutineScope.animate( // Change the current layout state to start this new transition. This will compute the // TransformationSpec associated to this transition, which we need to initialize the Animatable // that will actually animate it. - layoutState.startTransition(transition, transitionKey) + layoutState.startTransition(transition, transitionKey, chain) // The transition now contains the transformation spec that we should use to instantiate the // Animatable. @@ -144,19 +185,19 @@ private fun CoroutineScope.animate( val visibilityThreshold = (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold val animatable = - Animatable(startProgress, visibilityThreshold = visibilityThreshold).also { + Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also { transition.animatable = it } // Animate the progress to its target value. transition.job = - launch { animatable.animateTo(targetProgress, animationSpec) } + launch { animatable.animateTo(targetProgress, animationSpec, initialVelocity) } .apply { invokeOnCompletion { // Settle the state to Idle(target). Note that this will do nothing if this // transition was replaced/interrupted by another one, and this also runs if // this coroutine is cancelled, i.e. if [this] coroutine scope is cancelled. - layoutState.finishTransition(transition, target) + layoutState.finishTransition(transition, targetScene) } } @@ -185,6 +226,9 @@ private class OneOffTransition( override val progress: Float get() = animatable.value + override val progressVelocity: Float + get() = animatable.velocity + override fun finish(): Job = job } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt index f78ed2fdcaf6..67589909ac03 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/DraggableHandler.kt @@ -579,6 +579,18 @@ private class SwipeTransition( return offset / distance } + override val progressVelocity: Float + get() { + val animatable = offsetAnimation?.animatable ?: return 0f + val distance = distance() + if (distance == DistanceUnspecified) { + return 0f + } + + val velocityInDistanceUnit = animatable.velocity + return velocityInDistanceUnit / distance.absoluteValue + } + override val isInitiatedByUserInput = true override var bouncingScene: SceneKey? = null @@ -865,6 +877,7 @@ internal class NestedScrollHandlerImpl( private val orientation: Orientation, private val topOrLeftBehavior: NestedScrollBehavior, private val bottomOrRightBehavior: NestedScrollBehavior, + private val isExternalOverscrollGesture: () -> Boolean, ) { private val layoutState = layoutImpl.state private val draggableHandler = layoutImpl.draggableHandler(orientation) @@ -920,7 +933,8 @@ internal class NestedScrollHandlerImpl( return PriorityNestedScrollConnection( orientation = orientation, canStartPreScroll = { offsetAvailable, offsetBeforeStart -> - canChangeScene = offsetBeforeStart == 0f + canChangeScene = + if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val canInterceptSwipeTransition = canChangeScene && @@ -950,7 +964,8 @@ internal class NestedScrollHandlerImpl( else -> return@PriorityNestedScrollConnection false } - val isZeroOffset = offsetBeforeStart == 0f + val isZeroOffset = + if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f val canStart = when (behavior) { 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 ca643231e874..20742ee77fff 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 @@ -329,10 +329,9 @@ private fun elementTransition( if (transition == null && previousTransition != null) { // The transition was just finished. - element.sceneStates.values.forEach { sceneState -> - sceneState.offsetInterruptionDelta = Offset.Zero - sceneState.scaleInterruptionDelta = Scale.Zero - sceneState.alphaInterruptionDelta = 0f + element.sceneStates.values.forEach { + it.clearValuesBeforeInterruption() + it.clearInterruptionDeltas() } } @@ -375,12 +374,22 @@ private fun prepareInterruption(element: Element) { sceneState.scaleBeforeInterruption = lastScale sceneState.alphaBeforeInterruption = lastAlpha - sceneState.offsetInterruptionDelta = Offset.Zero - sceneState.scaleInterruptionDelta = Scale.Zero - sceneState.alphaInterruptionDelta = 0f + sceneState.clearInterruptionDeltas() } } +private fun Element.SceneState.clearInterruptionDeltas() { + offsetInterruptionDelta = Offset.Zero + scaleInterruptionDelta = Scale.Zero + alphaInterruptionDelta = 0f +} + +private fun Element.SceneState.clearValuesBeforeInterruption() { + offsetBeforeInterruption = Offset.Unspecified + scaleBeforeInterruption = Scale.Unspecified + alphaBeforeInterruption = Element.AlphaUnspecified +} + /** * Compute what [value] should be if we take the * [interruption progress][TransitionState.Transition.interruptionProgress] of [transition] into @@ -744,7 +753,11 @@ private fun ApproachMeasureScope.place( // No need to place the element in this scene if we don't want to draw it anyways. if (!shouldPlaceElement(layoutImpl, scene, element, transition)) { sceneState.lastOffset = Offset.Unspecified - sceneState.offsetBeforeInterruption = Offset.Unspecified + sceneState.lastScale = Scale.Unspecified + sceneState.lastAlpha = Element.AlphaUnspecified + + sceneState.clearValuesBeforeInterruption() + sceneState.clearInterruptionDeltas() return } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.kt new file mode 100644 index 000000000000..54c64fd721ec --- /dev/null +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/InterruptionHandler.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.compose.animation.scene + +/** + * A handler to specify how a transition should be interrupted. + * + * @see DefaultInterruptionHandler + * @see SceneTransitionsBuilder.interruptionHandler + */ +interface InterruptionHandler { + /** + * This function is called when [interrupted] is interrupted: it is currently animating between + * [interrupted.fromScene] and [interrupted.toScene], and we will now animate to + * [newTargetScene]. + * + * If this returns `null`, then the [default behavior][DefaultInterruptionHandler] will be used: + * we will animate from [interrupted.currentScene] and chaining will be enabled (see + * [InterruptionResult] for more information about chaining). + * + * @see InterruptionResult + */ + fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey, + ): InterruptionResult? +} + +/** + * The result of an interruption that specifies how we should handle a transition A => B now that we + * have to animate to C. + * + * For instance, if the interrupted transition was A => B and currentScene = B: + * - animateFrom = B && chain = true => there will be 2 transitions running in parallel, A => B and + * B => C. + * - animateFrom = A && chain = true => there will be 2 transitions running in parallel, B => A and + * A => C. + * - animateFrom = B && chain = false => there will be 1 transition running, B => C. + * - animateFrom = A && chain = false => there will be 1 transition running, A => C. + */ +class InterruptionResult( + /** + * The scene we should animate from when transitioning to C. + * + * Important: This **must** be either [TransitionState.Transition.fromScene] or + * [TransitionState.Transition.toScene] of the transition that was interrupted. + */ + val animateFrom: SceneKey, + + /** + * Whether chaining is enabled, i.e. if the new transition to C should run in parallel with the + * previous one(s) or if it should be the only remaining transition that is running. + */ + val chain: Boolean = true, +) + +/** + * The default interruption handler: we animate from [TransitionState.Transition.currentScene] and + * chaining is enabled. + */ +object DefaultInterruptionHandler : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey, + ): InterruptionResult { + return InterruptionResult( + animateFrom = interrupted.currentScene, + chain = true, + ) + } +} diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt index 5a2f85ad163c..1fa6b3f7d6c0 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/NestedScrollToScene.kt @@ -75,6 +75,7 @@ internal fun Modifier.nestedScrollToScene( orientation: Orientation, topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ) = this then NestedScrollToSceneElement( @@ -82,6 +83,7 @@ internal fun Modifier.nestedScrollToScene( orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) private data class NestedScrollToSceneElement( @@ -89,6 +91,7 @@ private data class NestedScrollToSceneElement( private val orientation: Orientation, private val topOrLeftBehavior: NestedScrollBehavior, private val bottomOrRightBehavior: NestedScrollBehavior, + private val isExternalOverscrollGesture: () -> Boolean, ) : ModifierNodeElement<NestedScrollToSceneNode>() { override fun create() = NestedScrollToSceneNode( @@ -96,6 +99,7 @@ private data class NestedScrollToSceneElement( orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) override fun update(node: NestedScrollToSceneNode) { @@ -104,6 +108,7 @@ private data class NestedScrollToSceneElement( orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) } @@ -121,6 +126,7 @@ private class NestedScrollToSceneNode( orientation: Orientation, topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ) : DelegatingNode() { private var priorityNestedScrollConnection: PriorityNestedScrollConnection = scenePriorityNestedScrollConnection( @@ -128,6 +134,7 @@ private class NestedScrollToSceneNode( orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) private var nestedScrollNode: DelegatableNode = @@ -150,6 +157,7 @@ private class NestedScrollToSceneNode( orientation: Orientation, topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ) { // Clean up the old nested scroll connection priorityNestedScrollConnection.reset() @@ -162,6 +170,7 @@ private class NestedScrollToSceneNode( orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) nestedScrollNode = nestedScrollModifierNode( @@ -177,11 +186,13 @@ private fun scenePriorityNestedScrollConnection( orientation: Orientation, topOrLeftBehavior: NestedScrollBehavior, bottomOrRightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ) = NestedScrollHandlerImpl( layoutImpl = layoutImpl, orientation = orientation, topOrLeftBehavior = topOrLeftBehavior, bottomOrRightBehavior = bottomOrRightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) .connection diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt index 339868c9fbc9..6fef33c797d9 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/Scene.kt @@ -141,23 +141,27 @@ internal class SceneScopeImpl( override fun Modifier.horizontalNestedScrollToScene( leftBehavior: NestedScrollBehavior, rightBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ): Modifier = nestedScrollToScene( layoutImpl = layoutImpl, orientation = Orientation.Horizontal, topOrLeftBehavior = leftBehavior, bottomOrRightBehavior = rightBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) override fun Modifier.verticalNestedScrollToScene( topBehavior: NestedScrollBehavior, - bottomBehavior: NestedScrollBehavior + bottomBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: () -> Boolean, ): Modifier = nestedScrollToScene( layoutImpl = layoutImpl, orientation = Orientation.Vertical, topOrLeftBehavior = topBehavior, bottomOrRightBehavior = bottomBehavior, + isExternalOverscrollGesture = isExternalOverscrollGesture, ) override fun Modifier.noResizeDuringTransitions(): Modifier { 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 c7c874c1185d..11e711ace971 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 @@ -250,6 +250,7 @@ interface BaseSceneScope : ElementStateScope { fun Modifier.horizontalNestedScrollToScene( leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier /** @@ -262,6 +263,7 @@ interface BaseSceneScope : ElementStateScope { fun Modifier.verticalNestedScrollToScene( topBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, bottomBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview, + isExternalOverscrollGesture: () -> Boolean = { false }, ): Modifier /** diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt index 5fda77a3e0ae..7f94f0d88c5e 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutState.kt @@ -227,6 +227,9 @@ sealed interface TransitionState { */ abstract val progress: Float + /** The current velocity of [progress], in progress units. */ + abstract val progressVelocity: Float + /** Whether the transition was triggered by user input rather than being programmatic. */ abstract val isInitiatedByUserInput: Boolean @@ -422,13 +425,18 @@ internal abstract class BaseSceneTransitionLayoutState( } /** - * Start a new [transition], instantly interrupting any ongoing transition if there was one. + * Start a new [transition]. + * + * If [chain] is `true`, then the transitions will simply be added to [currentTransitions] and + * will run in parallel to the current transitions. If [chain] is `false`, then the list of + * [currentTransitions] will be cleared and [transition] will be the only running transition. * * Important: you *must* call [finishTransition] once the transition is finished. */ internal fun startTransition( transition: TransitionState.Transition, transitionKey: TransitionKey?, + chain: Boolean = true, ) { // Compute the [TransformationSpec] when the transition starts. val fromScene = transition.fromScene @@ -471,26 +479,10 @@ internal abstract class BaseSceneTransitionLayoutState( finishTransition(currentState, currentState.currentScene) } - // Check that we don't have too many concurrent transitions. - if (transitionStates.size >= MAX_CONCURRENT_TRANSITIONS) { - Log.wtf( - TAG, - buildString { - appendLine("Potential leak detected in SceneTransitionLayoutState!") - appendLine( - " Some transition(s) never called STLState.finishTransition()." - ) - appendLine(" Transitions (size=${transitionStates.size}):") - transitionStates.fastForEach { state -> - val transition = state as TransitionState.Transition - val from = transition.fromScene - val to = transition.toScene - val indicator = - if (finishedTransitions.contains(transition)) "x" else " " - appendLine(" [$indicator] $from => $to ($transition)") - } - } - ) + val tooManyTransitions = transitionStates.size >= MAX_CONCURRENT_TRANSITIONS + val clearCurrentTransitions = !chain || tooManyTransitions + if (clearCurrentTransitions) { + if (tooManyTransitions) logTooManyTransitions() // Force finish all transitions. while (currentTransitions.isNotEmpty()) { @@ -511,6 +503,24 @@ internal abstract class BaseSceneTransitionLayoutState( } } + private fun logTooManyTransitions() { + Log.wtf( + TAG, + buildString { + appendLine("Potential leak detected in SceneTransitionLayoutState!") + appendLine(" Some transition(s) never called STLState.finishTransition().") + appendLine(" Transitions (size=${transitionStates.size}):") + transitionStates.fastForEach { state -> + val transition = state as TransitionState.Transition + val from = transition.fromScene + val to = transition.toScene + val indicator = if (finishedTransitions.contains(transition)) "x" else " " + appendLine(" [$indicator] $from => $to ($transition)") + } + } + ) + } + private fun cancelActiveTransitionLinks() { for ((link, linkedTransition) in activeTransitionLinks) { link.target.finishTransition(linkedTransition, linkedTransition.currentScene) diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt index b46614397ff4..0f6a1d276578 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitions.kt @@ -44,6 +44,7 @@ internal constructor( internal val defaultSwipeSpec: SpringSpec<Float>, internal val transitionSpecs: List<TransitionSpecImpl>, internal val overscrollSpecs: List<OverscrollSpecImpl>, + internal val interruptionHandler: InterruptionHandler, ) { private val transitionCache = mutableMapOf< @@ -145,6 +146,7 @@ internal constructor( defaultSwipeSpec = DefaultSwipeSpec, transitionSpecs = emptyList(), overscrollSpecs = emptyList(), + interruptionHandler = DefaultInterruptionHandler, ) } } diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt index 6bc397e86cfa..a4682ff2a885 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDsl.kt @@ -40,6 +40,12 @@ interface SceneTransitionsBuilder { var defaultSwipeSpec: SpringSpec<Float> /** + * The [InterruptionHandler] used when transitions are interrupted. Defaults to + * [DefaultInterruptionHandler]. + */ + var interruptionHandler: InterruptionHandler + + /** * Define the default animation to be played when transitioning [to] the specified scene, from * any scene. For the animation specification to apply only when transitioning between two * specific scenes, use [from] instead. diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt index 1c9080fa085d..802ab1f2eebb 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/TransitionDslImpl.kt @@ -47,12 +47,14 @@ internal fun transitionsImpl( return SceneTransitions( impl.defaultSwipeSpec, impl.transitionSpecs, - impl.transitionOverscrollSpecs + impl.transitionOverscrollSpecs, + impl.interruptionHandler, ) } private class SceneTransitionsBuilderImpl : SceneTransitionsBuilder { override var defaultSwipeSpec: SpringSpec<Float> = SceneTransitions.DefaultSwipeSpec + override var interruptionHandler: InterruptionHandler = DefaultInterruptionHandler val transitionSpecs = mutableListOf<TransitionSpecImpl>() val transitionOverscrollSpecs = mutableListOf<OverscrollSpecImpl>() diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt index 73393a1ab0cf..79f126d24561 100644 --- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt +++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/transition/link/LinkedTransition.kt @@ -45,5 +45,8 @@ internal class LinkedTransition( override val progress: Float get() = originalTransition.progress + override val progressVelocity: Float + get() = originalTransition.progressVelocity + override fun finish(): Job = originalTransition.finish() } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt index 1fd1bf4a56e2..8625482d5f71 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/DraggableHandlerTest.kt @@ -32,12 +32,11 @@ import com.android.compose.animation.scene.NestedScrollBehavior.EdgeWithPreview import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC -import com.android.compose.animation.scene.TransitionState.Idle import com.android.compose.animation.scene.TransitionState.Transition +import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.MonotonicClockTestScope import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat -import com.google.common.truth.Truth.assertWithMessage import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.cancelAndJoin import kotlinx.coroutines.launch @@ -103,12 +102,16 @@ class DraggableHandlerTest { val draggableHandler = layoutImpl.draggableHandler(Orientation.Vertical) val horizontalDraggableHandler = layoutImpl.draggableHandler(Orientation.Horizontal) - fun nestedScrollConnection(nestedScrollBehavior: NestedScrollBehavior) = + fun nestedScrollConnection( + nestedScrollBehavior: NestedScrollBehavior, + isExternalOverscrollGesture: Boolean = false + ) = NestedScrollHandlerImpl( layoutImpl = layoutImpl, orientation = draggableHandler.orientation, topOrLeftBehavior = nestedScrollBehavior, bottomOrRightBehavior = nestedScrollBehavior, + isExternalOverscrollGesture = { isExternalOverscrollGesture } ) .connection @@ -145,10 +148,8 @@ class DraggableHandlerTest { } fun assertIdle(currentScene: SceneKey) { - assertThat(transitionState).isInstanceOf(Idle::class.java) - assertWithMessage("currentScene does not match") - .that(transitionState.currentScene) - .isEqualTo(currentScene) + assertThat(transitionState).isIdle() + assertThat(transitionState).hasCurrentScene(currentScene) } fun assertTransition( @@ -158,34 +159,12 @@ class DraggableHandlerTest { progress: Float? = null, isUserInputOngoing: Boolean? = null ) { - assertThat(transitionState).isInstanceOf(Transition::class.java) - val transition = transitionState as Transition - - if (currentScene != null) - assertWithMessage("currentScene does not match") - .that(transition.currentScene) - .isEqualTo(currentScene) - - if (fromScene != null) - assertWithMessage("fromScene does not match") - .that(transition.fromScene) - .isEqualTo(fromScene) - - if (toScene != null) - assertWithMessage("toScene does not match") - .that(transition.toScene) - .isEqualTo(toScene) - - if (progress != null) - assertWithMessage("progress does not match") - .that(transition.progress) - .isWithin(0f) // returns true when comparing 0.0f with -0.0f - .of(progress) - - if (isUserInputOngoing != null) - assertWithMessage("isUserInputOngoing does not match") - .that(transition.isUserInputOngoing) - .isEqualTo(isUserInputOngoing) + val transition = assertThat(transitionState).isTransition() + currentScene?.let { assertThat(transition).hasCurrentScene(it) } + fromScene?.let { assertThat(transition).hasFromScene(it) } + toScene?.let { assertThat(transition).hasToScene(it) } + progress?.let { assertThat(transition).hasProgress(it) } + isUserInputOngoing?.let { assertThat(transition).hasIsUserInputOngoing(it) } } fun onDragStarted( @@ -801,6 +780,26 @@ class DraggableHandlerTest { } @Test + fun flingAfterScrollStartedByExternalOverscrollGesture() = runGestureTest { + val nestedScroll = + nestedScrollConnection( + nestedScrollBehavior = EdgeWithPreview, + isExternalOverscrollGesture = true + ) + + // scroll not consumed in child + nestedScroll.scroll( + available = downOffset(fractionOfScreen = 0.1f), + ) + + // scroll offsetY10 is all available for parents + nestedScroll.scroll(available = downOffset(fractionOfScreen = 0.1f)) + assertTransition(SceneA) + + nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) + } + + @Test fun beforeNestedScrollStart_stop_shouldBeIgnored() = runGestureTest { val nestedScroll = nestedScrollConnection(nestedScrollBehavior = EdgeWithPreview) nestedScroll.preFling(available = Velocity(0f, velocityThreshold)) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt index 92e1b2cd030c..e19dc965a394 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/ElementTest.kt @@ -20,7 +20,6 @@ import androidx.compose.animation.core.LinearEasing import androidx.compose.animation.core.Spring import androidx.compose.animation.core.spring import androidx.compose.animation.core.tween -import androidx.compose.foundation.ExperimentalFoundationApi import androidx.compose.foundation.gestures.Orientation import androidx.compose.foundation.gestures.rememberScrollableState import androidx.compose.foundation.gestures.scrollable @@ -43,7 +42,6 @@ import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.setValue import androidx.compose.runtime.snapshotFlow import androidx.compose.ui.Alignment -import androidx.compose.ui.ExperimentalComposeUiApi import androidx.compose.ui.Modifier import androidx.compose.ui.geometry.Offset import androidx.compose.ui.layout.approachLayout @@ -64,6 +62,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.subjects.assertThat import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.launch @@ -78,7 +77,6 @@ class ElementTest { @get:Rule val rule = createComposeRule() @Composable - @OptIn(ExperimentalComposeUiApi::class) private fun SceneScope.Element( key: ElementKey, size: Dp, @@ -496,7 +494,6 @@ class ElementTest { } @Test - @OptIn(ExperimentalFoundationApi::class) fun elementModifierNodeIsRecycledInLazyLayouts() = runTest { val nPages = 2 val pagerState = PagerState(currentPage = 0) { nPages } @@ -654,8 +651,7 @@ class ElementTest { } } - assertThat(state.currentTransition).isNull() - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(state.transitionState).isIdle() // Swipe by half of verticalSwipeDistance. rule.onRoot().performTouchInput { @@ -691,9 +687,9 @@ class ElementTest { val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) - val transition = state.currentTransition + val transition = assertThat(state.transitionState).isTransition() assertThat(transition).isNotNull() - assertThat(transition!!.progress).isEqualTo(0.5f) + assertThat(transition).hasProgress(0.5f) assertThat(animatedFloat).isEqualTo(50f) rule.onRoot().performTouchInput { @@ -702,8 +698,8 @@ class ElementTest { } // Scroll 150% (Scene B overscroll by 50%) - assertThat(transition.progress).isEqualTo(1.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(1.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) // animatedFloat cannot overflow (canOverflow = false) assertThat(animatedFloat).isEqualTo(100f) @@ -714,8 +710,8 @@ class ElementTest { } // Scroll 250% (Scene B overscroll by 150%) - assertThat(transition.progress).isEqualTo(2.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(2.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) assertThat(animatedFloat).isEqualTo(100f) } @@ -766,8 +762,7 @@ class ElementTest { } } - assertThat(state.currentTransition).isNull() - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(state.transitionState).isIdle() val fooElement = rule.onNodeWithTag(TestElements.Foo.testTag, useUnmergedTree = true) fooElement.assertTopPositionInRootIsEqualTo(0.dp) @@ -779,10 +774,9 @@ class ElementTest { moveBy(Offset(0f, touchSlop + layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } - val transition = state.currentTransition - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(transition).isNotNull() - assertThat(transition!!.progress).isEqualTo(-0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasOverscrollSpec() + assertThat(transition).hasProgress(-0.5f) fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 0.5f) rule.onRoot().performTouchInput { @@ -791,8 +785,8 @@ class ElementTest { } // Scroll 150% (Scene B overscroll by 50%) - assertThat(transition.progress).isEqualTo(-1.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(-1.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(overscrollTranslateY * 1.5f) } @@ -825,13 +819,12 @@ class ElementTest { moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } - val transition = state.currentTransition - assertThat(transition).isNotNull() + val transition = assertThat(state.transitionState).isTransition() assertThat(animatedFloat).isEqualTo(100f) // Scroll 150% (100% scroll + 50% overscroll) - assertThat(transition!!.progress).isEqualTo(1.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(1.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 0.5f) assertThat(animatedFloat).isEqualTo(100f) @@ -841,8 +834,8 @@ class ElementTest { } // Scroll 250% (100% scroll + 150% overscroll) - assertThat(transition.progress).isEqualTo(2.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(2.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * 1.5f) assertThat(animatedFloat).isEqualTo(100f) } @@ -882,13 +875,11 @@ class ElementTest { moveBy(Offset(0f, layoutHeight.toPx() * 0.5f), delayMillis = 1_000) } - val transition = state.currentTransition - assertThat(transition).isNotNull() - transition as TransitionState.HasOverscrollProperties + val transition = assertThat(state.transitionState).isTransition() // Scroll 150% (100% scroll + 50% overscroll) - assertThat(transition.progress).isEqualTo(1.5f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() + assertThat(transition).hasProgress(1.5f) + assertThat(transition).hasOverscrollSpec() fooElement.assertTopPositionInRootIsEqualTo(layoutHeight * (transition.progress - 1f)) assertThat(animatedFloat).isEqualTo(100f) @@ -900,8 +891,8 @@ class ElementTest { rule.waitUntil(timeoutMillis = 10_000) { transition.progress < 1f } assertThat(transition.progress).isLessThan(1f) - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(transition.bouncingScene).isEqualTo(transition.toScene) + assertThat(transition).hasOverscrollSpec() + assertThat(transition).hasBouncingScene(transition.toScene) assertThat(animatedFloat).isEqualTo(100f) } @@ -980,13 +971,13 @@ class ElementTest { val transitions = state.currentTransitions assertThat(transitions).hasSize(2) - assertThat(transitions[0].fromScene).isEqualTo(SceneA) - assertThat(transitions[0].toScene).isEqualTo(SceneB) - assertThat(transitions[0].progress).isEqualTo(0f) + assertThat(transitions[0]).hasFromScene(SceneA) + assertThat(transitions[0]).hasToScene(SceneB) + assertThat(transitions[0]).hasProgress(0f) - assertThat(transitions[1].fromScene).isEqualTo(SceneB) - assertThat(transitions[1].toScene).isEqualTo(SceneC) - assertThat(transitions[1].progress).isEqualTo(0f) + assertThat(transitions[1]).hasFromScene(SceneB) + assertThat(transitions[1]).hasToScene(SceneC) + assertThat(transitions[1]).hasProgress(0f) // First frame: both are at x = 0dp. For the whole transition, Foo is at y = 0dp and Bar is // at y = layoutSize - elementSoze = 100dp. @@ -1049,24 +1040,30 @@ class ElementTest { Box(modifier.element(TestElements.Foo).size(fooSize)) } + lateinit var layoutImpl: SceneTransitionLayoutImpl rule.setContent { - SceneTransitionLayout(state, Modifier.size(layoutSize)) { + SceneTransitionLayoutForTesting( + state, + Modifier.size(layoutSize), + onLayoutImpl = { layoutImpl = it }, + ) { // In scene A, Foo is aligned at the TopStart. scene(SceneA) { Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopStart)) } } + // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming + // from B. We put it before (below) scene B so that we can check that interruptions + // values and deltas are properly cleared once all transitions are done. + scene(SceneC) { + Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } + } + // In scene B, Foo is aligned at the TopEnd, so it moves horizontally when coming // from A. scene(SceneB) { Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.TopEnd)) } } - - // In scene C, Foo is aligned at the BottomEnd, so it moves vertically when coming - // from B. - scene(SceneC) { - Box(Modifier.fillMaxSize()) { Foo(Modifier.align(Alignment.BottomEnd)) } - } } } @@ -1115,7 +1112,7 @@ class ElementTest { // Interruption progress is at 100% and bToC is at 0%, so Foo should be at the same offset // as right before the interruption. rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo(offsetInAToB.x, offsetInAToB.y) // Move the transition forward at 30% and set the interruption progress to 50%. @@ -1130,7 +1127,7 @@ class ElementTest { ) rule.waitForIdle() rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo( offsetInBToCWithInterruption.x, offsetInBToCWithInterruption.y, @@ -1140,7 +1137,24 @@ class ElementTest { bToCProgress = 1f interruptionProgress = 0f rule - .onNode(isElement(TestElements.Foo, SceneC)) + .onNode(isElement(TestElements.Foo, SceneB)) .assertPositionInRootIsEqualTo(offsetInC.x, offsetInC.y) + + // Manually finish the transition. + state.finishTransition(aToB, SceneB) + state.finishTransition(bToC, SceneC) + rule.waitForIdle() + assertThat(state.transitionState).isIdle() + + // The interruption values should be unspecified and deltas should be set to zero. + val foo = layoutImpl.elements.getValue(TestElements.Foo) + assertThat(foo.sceneStates.keys).containsExactly(SceneC) + val stateInC = foo.sceneStates.getValue(SceneC) + assertThat(stateInC.offsetBeforeInterruption).isEqualTo(Offset.Unspecified) + assertThat(stateInC.scaleBeforeInterruption).isEqualTo(Scale.Unspecified) + assertThat(stateInC.alphaBeforeInterruption).isEqualTo(Element.AlphaUnspecified) + assertThat(stateInC.offsetInterruptionDelta).isEqualTo(Offset.Zero) + assertThat(stateInC.scaleInterruptionDelta).isEqualTo(Scale.Zero) + assertThat(stateInC.alphaInterruptionDelta).isEqualTo(0f) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt new file mode 100644 index 000000000000..85d4165b4bf6 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/InterruptionHandlerTest.kt @@ -0,0 +1,210 @@ +/* + * 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 androidx.compose.animation.core.tween +import androidx.compose.ui.test.junit4.createComposeRule +import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.subjects.assertThat +import com.android.compose.test.runMonotonicClockTest +import com.google.common.truth.Correspondence +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.launch +import org.junit.Rule +import org.junit.Test +import org.junit.runner.RunWith + +@RunWith(AndroidJUnit4::class) +class InterruptionHandlerTest { + @get:Rule val rule = createComposeRule() + + @Test + fun default() = runMonotonicClockTest { + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { /* default interruption handler */}, + ) + + state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // A to B. + Triple(SceneA, SceneB, SceneB), + + // B to C. + Triple(SceneB, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun chainingDisabled() = runMonotonicClockTest { + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { + // Handler that animates from currentScene (default) but disables chaining. + interruptionHandler = + object : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey + ): InterruptionResult { + return InterruptionResult( + animateFrom = interrupted.currentScene, + chain = false, + ) + } + } + }, + ) + + state.setTargetScene(SceneB, coroutineScope = this) + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // B to C. + Triple(SceneB, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun animateFromOtherScene() = runMonotonicClockTest { + val duration = 500 + val state = + MutableSceneTransitionLayoutState( + SceneA, + transitions { + // Handler that animates from the scene that is not currentScene. + interruptionHandler = + object : InterruptionHandler { + override fun onInterruption( + interrupted: TransitionState.Transition, + newTargetScene: SceneKey + ): InterruptionResult { + return InterruptionResult( + animateFrom = + if (interrupted.currentScene == interrupted.toScene) { + interrupted.fromScene + } else { + interrupted.toScene + } + ) + } + } + + from(SceneA, to = SceneB) { spec = tween(duration) } + }, + ) + + // Animate to B and advance the transition a little bit so that progress > visibility + // threshold and that reversing from B back to A won't immediately snap to A. + state.setTargetScene(SceneB, coroutineScope = this) + testScheduler.advanceTimeBy(duration / 2L) + + state.setTargetScene(SceneC, coroutineScope = this) + + assertThat(state.currentTransitions) + .comparingElementsUsing(FromToCurrentTriple) + .containsExactly( + // Initial transition A to B. This transition will never be consumed by anyone given + // that it has the same (from, to) pair as the next transition. + Triple(SceneA, SceneB, SceneB), + + // Initial transition reversed, B back to A. + Triple(SceneA, SceneB, SceneA), + + // A to C. + Triple(SceneA, SceneC, SceneC), + ) + .inOrder() + } + + @Test + fun animateToFromScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {}) + + // Fake a transition from A to B that has a non 0 velocity. + val progressVelocity = 1f + val aToB = + transition( + from = SceneA, + to = SceneB, + current = { SceneB }, + // Progress must be > visibility threshold otherwise we will directly snap to A. + progress = { 0.5f }, + progressVelocity = { progressVelocity }, + onFinish = { launch {} }, + ) + state.startTransition(aToB, transitionKey = null) + + // Animate back to A. The previous transition is reversed, i.e. it has the same (from, to) + // pair, and its velocity is used when animating the progress back to 0. + val bToA = checkNotNull(state.setTargetScene(SceneA, coroutineScope = this)) + testScheduler.runCurrent() + assertThat(bToA).hasFromScene(SceneA) + assertThat(bToA).hasToScene(SceneB) + assertThat(bToA).hasCurrentScene(SceneA) + assertThat(bToA).hasProgressVelocity(progressVelocity) + } + + @Test + fun animateToToScene() = runMonotonicClockTest { + val state = MutableSceneTransitionLayoutStateImpl(SceneA, transitions {}) + + // Fake a transition from A to B with current scene = A that has a non 0 velocity. + val progressVelocity = -1f + val aToB = + transition( + from = SceneA, + to = SceneB, + current = { SceneA }, + progressVelocity = { progressVelocity }, + onFinish = { launch {} }, + ) + state.startTransition(aToB, transitionKey = null) + + // Animate to B. The previous transition is reversed, i.e. it has the same (from, to) pair, + // and its velocity is used when animating the progress to 1. + val bToA = checkNotNull(state.setTargetScene(SceneB, coroutineScope = this)) + testScheduler.runCurrent() + assertThat(bToA).hasFromScene(SceneA) + assertThat(bToA).hasToScene(SceneB) + assertThat(bToA).hasCurrentScene(SceneB) + assertThat(bToA).hasProgressVelocity(progressVelocity) + } + + companion object { + val FromToCurrentTriple = + Correspondence.transforming( + { transition: TransitionState.Transition? -> + Triple(transition?.fromScene, transition?.toScene, transition?.currentScene) + }, + "(from, to, current) triple" + ) + } +} diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt index 224ffe29a1b8..9523896e5c00 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/MovableElementTest.kt @@ -43,6 +43,7 @@ import androidx.compose.ui.test.onNodeWithText import androidx.compose.ui.test.performClick import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo import com.google.common.truth.Truth.assertThat import org.junit.Rule @@ -157,8 +158,8 @@ class MovableElementTest { fromSceneZIndex: Float, toSceneZIndex: Float ): SceneKey { - assertThat(transition.fromScene).isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + assertThat(transition).hasFromScene(TestScenes.SceneA) + assertThat(transition).hasToScene(TestScenes.SceneB) assertThat(fromSceneZIndex).isEqualTo(0) assertThat(toSceneZIndex).isEqualTo(1) diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt index 93e94f8f95a2..f29d0a78bbb2 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutStateTest.kt @@ -25,6 +25,7 @@ import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC import com.android.compose.animation.scene.TestScenes.SceneD +import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.animation.scene.transition.link.StateLink import com.android.compose.test.runMonotonicClockTest import com.google.common.truth.Truth.assertThat @@ -322,8 +323,8 @@ class SceneTransitionLayoutStateTest { // Go back to A. state.setTargetScene(SceneA, coroutineScope = this) testScheduler.advanceUntilIdle() - assertThat(state.currentTransition).isNull() - assertThat(state.transitionState.currentScene).isEqualTo(SceneA) + assertThat(state.transitionState).isIdle() + assertThat(state.transitionState).hasCurrentScene(SceneA) // Specific transition from A to B. assertThat( @@ -477,23 +478,24 @@ class SceneTransitionLayoutStateTest { overscroll(SceneB, Orientation.Vertical) { fade(TestElements.Foo) } } ) - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneA is NOT defined progress.value = -0.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() progress.value = 1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneB is defined progress.value = 1.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneB) + val overscrollSpec = assertThat(transition).hasOverscrollSpec() + assertThat(overscrollSpec.scene).isEqualTo(SceneB) } @Test @@ -507,23 +509,25 @@ class SceneTransitionLayoutStateTest { overscroll(SceneA, Orientation.Vertical) { fade(TestElements.Foo) } } ) - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneA is defined progress.value = -0.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNotNull() - assertThat(state.currentTransition?.currentOverscrollSpec?.scene).isEqualTo(SceneA) + val overscrollSpec = assertThat(transition).hasOverscrollSpec() + assertThat(overscrollSpec.scene).isEqualTo(SceneA) // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() progress.value = 1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneB is NOT defined progress.value = 1.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() } @Test @@ -534,22 +538,24 @@ class SceneTransitionLayoutStateTest { progress = { progress.value }, sceneTransitions = transitions {} ) - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneA is NOT defined progress.value = -0.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() // scroll from SceneA to SceneB progress.value = 0.5f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() progress.value = 1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() // overscroll for SceneB is NOT defined progress.value = 1.1f - assertThat(state.currentTransition?.currentOverscrollSpec).isNull() + assertThat(transition).hasNoOverscrollSpec() } @Test diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt index 7836581c86e8..692c18bb8ac5 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SceneTransitionLayoutTest.kt @@ -51,6 +51,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4 import com.android.compose.animation.scene.TestScenes.SceneA import com.android.compose.animation.scene.TestScenes.SceneB import com.android.compose.animation.scene.TestScenes.SceneC +import com.android.compose.animation.scene.subjects.assertThat import com.android.compose.test.assertSizeIsEqualTo import com.android.compose.test.subjects.DpOffsetSubject import com.android.compose.test.subjects.assertThat @@ -147,34 +148,34 @@ class SceneTransitionLayoutTest { rule.onNodeWithText("SceneA").assertIsDisplayed() rule.onNodeWithText("SceneB").assertDoesNotExist() rule.onNodeWithText("SceneC").assertDoesNotExist() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Change to scene B. Only that scene is displayed. currentScene = SceneB rule.onNodeWithText("SceneA").assertDoesNotExist() rule.onNodeWithText("SceneB").assertIsDisplayed() rule.onNodeWithText("SceneC").assertDoesNotExist() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneB) } @Test fun testBack() { rule.setContent { TestContent() } - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA) + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) rule.activity.onBackPressed() rule.waitForIdle() - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB) + assertThat(layoutState.transitionState).hasCurrentScene(SceneB) } @Test fun testTransitionState() { rule.setContent { TestContent() } - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // We will advance the clock manually. rule.mainClock.autoAdvance = false @@ -182,45 +183,38 @@ class SceneTransitionLayoutTest { // Change the current scene. Until composition is triggered, this won't change the layout // state. currentScene = SceneB - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // On the next frame, we will recompose because currentScene changed, which will start the // transition (i.e. it will change the transitionState to be a Transition) in a // LaunchedEffect. rule.mainClock.advanceTimeByFrame() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java) - val transition = layoutState.transitionState as TransitionState.Transition - assertThat(transition.fromScene).isEqualTo(SceneA) - assertThat(transition.toScene).isEqualTo(SceneB) - assertThat(transition.progress).isEqualTo(0f) + val transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasProgress(0f) // Then, on the next frame, the animator we started gets its initial value and clock // starting time. We are now at progress = 0f. rule.mainClock.advanceTimeByFrame() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java) - assertThat((layoutState.transitionState as TransitionState.Transition).progress) - .isEqualTo(0f) + assertThat(transition).hasProgress(0f) // The test transition lasts 480ms. 240ms after the start of the transition, we are at // progress = 0.5f. rule.mainClock.advanceTimeBy(TestTransitionDuration / 2) - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java) - assertThat((layoutState.transitionState as TransitionState.Transition).progress) - .isEqualTo(0.5f) + assertThat(transition).hasProgress(0.5f) // (240-16) ms later, i.e. one frame before the transition is finished, we are at // progress=(480-16)/480. rule.mainClock.advanceTimeBy(TestTransitionDuration / 2 - 16) - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Transition::class.java) - assertThat((layoutState.transitionState as TransitionState.Transition).progress) - .isEqualTo((TestTransitionDuration - 16) / 480f) + assertThat(transition).hasProgress((TestTransitionDuration - 16) / 480f) // one frame (16ms) later, the transition is finished and we are in the idle state in scene // B. rule.mainClock.advanceTimeByFrame() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneB) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneB) } @Test @@ -261,8 +255,8 @@ class SceneTransitionLayoutTest { // 100.dp. We pause at the middle of the transition, so it should now be 75.dp given that we // use a linear interpolator. Foo was at (x = layoutSize - 50dp, y = 0) in SceneA and is // going to (x = 0, y = 0), so the offset should now be half what it was. - assertThat((layoutState.transitionState as TransitionState.Transition).progress) - .isEqualTo(0.5f) + var transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) sharedFoo.assertWidthIsEqualTo(75.dp) sharedFoo.assertHeightIsEqualTo(75.dp) sharedFoo.assertPositionInRootIsEqualTo( @@ -290,8 +284,8 @@ class SceneTransitionLayoutTest { val expectedSize = 100.dp + (150.dp - 100.dp) * interpolatedProgress sharedFoo = rule.onNode(isElement(TestElements.Foo, SceneC)) - assertThat((layoutState.transitionState as TransitionState.Transition).progress) - .isEqualTo(interpolatedProgress) + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasProgress(interpolatedProgress) sharedFoo.assertWidthIsEqualTo(expectedSize) sharedFoo.assertHeightIsEqualTo(expectedSize) sharedFoo.assertPositionInRootIsEqualTo(expectedLeft, expectedTop) @@ -305,16 +299,16 @@ class SceneTransitionLayoutTest { // Wait for the transition to C to finish. rule.mainClock.advanceTimeBy(TestTransitionDuration) - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneC) // Go back to scene A. This should happen instantly (once the animation started, i.e. after // 2 frames) given that we use a snap() animation spec. currentScene = SceneA rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeByFrame() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) } @Test @@ -384,7 +378,9 @@ class SceneTransitionLayoutTest { rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeBy(duration / 2) rule.waitForIdle() - assertThat(state.currentTransition?.progress).isEqualTo(0.5f) + + var transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0.5f) // A and B are composed. rule.onNodeWithTag("aRoot").assertExists() @@ -396,7 +392,9 @@ class SceneTransitionLayoutTest { rule.mainClock.advanceTimeByFrame() rule.mainClock.advanceTimeByFrame() rule.waitForIdle() - assertThat(state.currentTransition?.progress).isEqualTo(0f) + + transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasProgress(0f) // A, B and C are composed. rule.onNodeWithTag("aRoot").assertExists() @@ -405,7 +403,7 @@ class SceneTransitionLayoutTest { // Let A => B finish. rule.mainClock.advanceTimeBy(duration / 2L) - assertThat(state.currentTransition?.progress).isEqualTo(0.5f) + assertThat(transition).hasProgress(0.5f) rule.waitForIdle() // B and C are composed. @@ -416,8 +414,8 @@ class SceneTransitionLayoutTest { // Let B => C finish. rule.mainClock.advanceTimeBy(duration / 2L) rule.mainClock.advanceTimeByFrame() - assertThat(state.currentTransition).isNull() rule.waitForIdle() + assertThat(state.transitionState).isIdle() // Only C is composed. rule.onNodeWithTag("aRoot").assertDoesNotExist() diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt index f034c184b794..1dd9322b3ad5 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/SwipeToSceneTest.kt @@ -38,6 +38,9 @@ import androidx.compose.ui.unit.Density import androidx.compose.ui.unit.IntSize import androidx.compose.ui.unit.dp import androidx.test.ext.junit.runners.AndroidJUnit4 +import com.android.compose.animation.scene.TestScenes.SceneA +import com.android.compose.animation.scene.TestScenes.SceneB +import com.android.compose.animation.scene.subjects.assertThat import com.google.common.truth.Truth.assertThat import org.junit.Rule import org.junit.Test @@ -65,7 +68,7 @@ class SwipeToSceneTest { @get:Rule val rule = createComposeRule() private fun layoutState( - initialScene: SceneKey = TestScenes.SceneA, + initialScene: SceneKey = SceneA, transitions: SceneTransitions = EmptyTestTransitions, ) = MutableSceneTransitionLayoutState(initialScene, transitions) @@ -80,22 +83,21 @@ class SwipeToSceneTest { modifier = Modifier.size(LayoutWidth, LayoutHeight).testTag(TestElements.Foo.debugName), ) { scene( - TestScenes.SceneA, + SceneA, userActions = if (swipesEnabled()) mapOf( - Swipe.Left to TestScenes.SceneB, + Swipe.Left to SceneB, Swipe.Down to TestScenes.SceneC, - Swipe.Up to TestScenes.SceneB, + Swipe.Up to SceneB, ) else emptyMap(), ) { Box(Modifier.fillMaxSize()) } scene( - TestScenes.SceneB, - userActions = - if (swipesEnabled()) mapOf(Swipe.Right to TestScenes.SceneA) else emptyMap(), + SceneB, + userActions = if (swipesEnabled()) mapOf(Swipe.Right to SceneA) else emptyMap(), ) { Box(Modifier.fillMaxSize()) } @@ -104,11 +106,10 @@ class SwipeToSceneTest { userActions = if (swipesEnabled()) mapOf( - Swipe.Down to TestScenes.SceneA, - Swipe(SwipeDirection.Down, pointerCount = 2) to TestScenes.SceneB, - Swipe(SwipeDirection.Right, fromSource = Edge.Left) to - TestScenes.SceneB, - Swipe(SwipeDirection.Down, fromSource = Edge.Top) to TestScenes.SceneB, + Swipe.Down to SceneA, + Swipe(SwipeDirection.Down, pointerCount = 2) to SceneB, + Swipe(SwipeDirection.Right, fromSource = Edge.Left) to SceneB, + Swipe(SwipeDirection.Down, fromSource = Edge.Top) to SceneB, ) else emptyMap(), ) { @@ -129,8 +130,8 @@ class SwipeToSceneTest { TestContent(layoutState) } - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Drag left (i.e. from right to left) by 55dp. We pick 55dp here because 56dp is the // positional threshold from which we commit the gesture. @@ -144,31 +145,27 @@ class SwipeToSceneTest { // We should be at a progress = 55dp / LayoutWidth given that we use the layout size in // the gesture axis as swipe distance. - var transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) - assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth) - assertThat(transition.isInitiatedByUserInput).isTrue() + var transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasProgress(55.dp / LayoutWidth) + assertThat(transition).isInitiatedByUserInput() // Release the finger. We should now be animating back to A (currentScene = SceneA) given // that 55dp < positional threshold. rule.onRoot().performTouchInput { up() } - transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) - assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth) - assertThat(transition.isInitiatedByUserInput).isTrue() + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasProgress(55.dp / LayoutWidth) + assertThat(transition).isInitiatedByUserInput() // Wait for the animation to finish. We should now be in scene A. rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Now we do the same but vertically and with a drag distance of 56dp, which is >= // positional threshold. @@ -178,31 +175,27 @@ class SwipeToSceneTest { } // Drag is in progress, so currentScene = SceneA and progress = 56dp / LayoutHeight - transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneC) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) - assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight) - assertThat(transition.isInitiatedByUserInput).isTrue() + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(TestScenes.SceneC) + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasProgress(56.dp / LayoutHeight) + assertThat(transition).isInitiatedByUserInput() // Release the finger. We should now be animating to C (currentScene = SceneC) given // that 56dp >= positional threshold. rule.onRoot().performTouchInput { up() } - transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneC) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC) - assertThat(transition.progress).isEqualTo(56.dp / LayoutHeight) - assertThat(transition.isInitiatedByUserInput).isTrue() + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(TestScenes.SceneC) + assertThat(transition).hasCurrentScene(TestScenes.SceneC) + assertThat(transition).hasProgress(56.dp / LayoutHeight) + assertThat(transition).isInitiatedByUserInput() // Wait for the animation to finish. We should now be in scene C. rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) } @Test @@ -216,8 +209,8 @@ class SwipeToSceneTest { TestContent(layoutState) } - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Swipe left (i.e. from right to left) using a velocity of 124 dp/s. We pick 124 dp/s here // because 125 dp/s is the velocity threshold from which we commit the gesture. We also use @@ -233,18 +226,16 @@ class SwipeToSceneTest { // We should be animating back to A (currentScene = SceneA) given that 124 dp/s < velocity // threshold. - var transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneA) - assertThat(transition.progress).isEqualTo(55.dp / LayoutWidth) + var transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasCurrentScene(SceneA) + assertThat(transition).hasProgress(55.dp / LayoutWidth) // Wait for the animation to finish. We should now be in scene A. rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Now we do the same but vertically and with a swipe velocity of 126dp, which is > // velocity threshold. Note that in theory we could have used 125 dp (= velocity threshold) @@ -259,18 +250,16 @@ class SwipeToSceneTest { } // We should be animating to C (currentScene = SceneC). - transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneA) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneC) - assertThat(transition.currentScene).isEqualTo(TestScenes.SceneC) - assertThat(transition.progress).isEqualTo(55.dp / LayoutHeight) + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(TestScenes.SceneC) + assertThat(transition).hasCurrentScene(TestScenes.SceneC) + assertThat(transition).hasProgress(55.dp / LayoutHeight) // Wait for the animation to finish. We should now be in scene C. rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) } @Test @@ -286,8 +275,8 @@ class SwipeToSceneTest { TestContent(layoutState) } - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) // Swipe down with two fingers. rule.onRoot().performTouchInput { @@ -298,18 +287,16 @@ class SwipeToSceneTest { } // We are transitioning to B because we used 2 fingers. - val transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneC) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + val transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(TestScenes.SceneC) + assertThat(transition).hasToScene(SceneB) // Release the fingers and wait for the animation to end. We are back to C because we only // swiped 10dp. rule.onRoot().performTouchInput { repeat(2) { i -> up(pointerId = i) } } rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) } @Test @@ -325,8 +312,8 @@ class SwipeToSceneTest { TestContent(layoutState) } - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) // Swipe down from the top edge. rule.onRoot().performTouchInput { @@ -335,18 +322,16 @@ class SwipeToSceneTest { } // We are transitioning to B (and not A) because we started from the top edge. - var transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneC) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + var transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(TestScenes.SceneC) + assertThat(transition).hasToScene(SceneB) // Release the fingers and wait for the animation to end. We are back to C because we only // swiped 10dp. rule.onRoot().performTouchInput { up() } rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) // Swipe right from the left edge. rule.onRoot().performTouchInput { @@ -355,18 +340,16 @@ class SwipeToSceneTest { } // We are transitioning to B (and not A) because we started from the left edge. - transition = layoutState.transitionState - assertThat(transition).isInstanceOf(TransitionState.Transition::class.java) - assertThat((transition as TransitionState.Transition).fromScene) - .isEqualTo(TestScenes.SceneC) - assertThat(transition.toScene).isEqualTo(TestScenes.SceneB) + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasFromScene(TestScenes.SceneC) + assertThat(transition).hasToScene(SceneB) // Release the fingers and wait for the animation to end. We are back to C because we only // swiped 10dp. rule.onRoot().performTouchInput { up() } rule.waitForIdle() - assertThat(layoutState.transitionState).isInstanceOf(TransitionState.Idle::class.java) - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneC) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(TestScenes.SceneC) } @Test @@ -380,7 +363,7 @@ class SwipeToSceneTest { layoutState( transitions = transitions { - from(TestScenes.SceneA, to = TestScenes.SceneB) { + from(SceneA, to = SceneB) { distance = FixedDistance(verticalSwipeDistance) } } @@ -395,12 +378,12 @@ class SwipeToSceneTest { modifier = Modifier.size(LayoutWidth, LayoutHeight) ) { scene( - TestScenes.SceneA, - userActions = mapOf(Swipe.Down to TestScenes.SceneB), + SceneA, + userActions = mapOf(Swipe.Down to SceneB), ) { Spacer(Modifier.fillMaxSize()) } - scene(TestScenes.SceneB) { Spacer(Modifier.fillMaxSize()) } + scene(SceneB) { Spacer(Modifier.fillMaxSize()) } } } @@ -413,9 +396,9 @@ class SwipeToSceneTest { } // We should be at 50% - val transition = layoutState.currentTransition + val transition = assertThat(layoutState.transitionState).isTransition() assertThat(transition).isNotNull() - assertThat(transition!!.progress).isEqualTo(0.5f) + assertThat(transition).hasProgress(0.5f) } @Test @@ -434,15 +417,14 @@ class SwipeToSceneTest { } // We should still correctly compute that we are swiping down to scene C. - var transition = layoutState.currentTransition - assertThat(transition).isNotNull() - assertThat(transition?.toScene).isEqualTo(TestScenes.SceneC) + var transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasToScene(TestScenes.SceneC) // Release the finger, animating back to scene A. rule.onRoot().performTouchInput { up() } rule.waitForIdle() - assertThat(layoutState.currentTransition).isNull() - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Swipe up by exactly touchSlop, so that the drag overSlop is 0f. rule.onRoot().performTouchInput { @@ -451,15 +433,14 @@ class SwipeToSceneTest { } // We should still correctly compute that we are swiping up to scene B. - transition = layoutState.currentTransition - assertThat(transition).isNotNull() - assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB) + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasToScene(SceneB) // Release the finger, animating back to scene A. rule.onRoot().performTouchInput { up() } rule.waitForIdle() - assertThat(layoutState.currentTransition).isNull() - assertThat(layoutState.transitionState.currentScene).isEqualTo(TestScenes.SceneA) + assertThat(layoutState.transitionState).isIdle() + assertThat(layoutState.transitionState).hasCurrentScene(SceneA) // Swipe left by exactly touchSlop, so that the drag overSlop is 0f. rule.onRoot().performTouchInput { @@ -468,14 +449,13 @@ class SwipeToSceneTest { } // We should still correctly compute that we are swiping down to scene B. - transition = layoutState.currentTransition - assertThat(transition).isNotNull() - assertThat(transition?.toScene).isEqualTo(TestScenes.SceneB) + transition = assertThat(layoutState.transitionState).isTransition() + assertThat(transition).hasToScene(SceneB) } @Test fun swipeEnabledLater() { - val layoutState = MutableSceneTransitionLayoutState(TestScenes.SceneA) + val layoutState = MutableSceneTransitionLayoutState(SceneA) var swipesEnabled by mutableStateOf(false) var touchSlop = 0f rule.setContent { @@ -509,34 +489,32 @@ class SwipeToSceneTest { fun transitionKey() { val transitionkey = TransitionKey(debugName = "foo") val state = - MutableSceneTransitionLayoutState( - TestScenes.SceneA, + MutableSceneTransitionLayoutStateImpl( + SceneA, transitions { - from(TestScenes.SceneA, to = TestScenes.SceneB) { fade(TestElements.Foo) } - from(TestScenes.SceneA, to = TestScenes.SceneB, key = transitionkey) { + from(SceneA, to = SceneB) { fade(TestElements.Foo) } + from(SceneA, to = SceneB, key = transitionkey) { fade(TestElements.Foo) fade(TestElements.Bar) } } ) - as MutableSceneTransitionLayoutStateImpl var touchSlop = 0f rule.setContent { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(state, Modifier.size(LayoutWidth, LayoutHeight)) { scene( - TestScenes.SceneA, + SceneA, userActions = mapOf( - Swipe.Down to TestScenes.SceneB, - Swipe.Up to - UserActionResult(TestScenes.SceneB, transitionKey = transitionkey) + Swipe.Down to SceneB, + Swipe.Up to UserActionResult(SceneB, transitionKey = transitionkey) ) ) { Box(Modifier.fillMaxSize()) } - scene(TestScenes.SceneB) { Box(Modifier.fillMaxSize()) } + scene(SceneB) { Box(Modifier.fillMaxSize()) } } } @@ -546,12 +524,12 @@ class SwipeToSceneTest { moveBy(Offset(0f, touchSlop), delayMillis = 1_000) } - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() + assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue() assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(1) // Move the pointer up to swipe to scene B using the new transition. rule.onRoot().performTouchInput { moveBy(Offset(0f, -1.dp.toPx()), delayMillis = 1_000) } - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() + assertThat(state.isTransitioning(from = SceneA, to = SceneB)).isTrue() assertThat(state.currentTransition?.transformationSpec?.transformations).hasSize(2) } @@ -567,19 +545,17 @@ class SwipeToSceneTest { // the difference between the bottom of the scene and the bottom of the element, // so that we use the offset and size of the element as well as the size of the // scene. - val fooSize = TestElements.Foo.targetSize(TestScenes.SceneB) ?: return 0f - val fooOffset = TestElements.Foo.targetOffset(TestScenes.SceneB) ?: return 0f - val sceneSize = TestScenes.SceneB.targetSize() ?: return 0f + val fooSize = TestElements.Foo.targetSize(SceneB) ?: return 0f + val fooOffset = TestElements.Foo.targetOffset(SceneB) ?: return 0f + val sceneSize = SceneB.targetSize() ?: return 0f return sceneSize.height - fooOffset.y - fooSize.height } } val state = MutableSceneTransitionLayoutState( - TestScenes.SceneA, - transitions { - from(TestScenes.SceneA, to = TestScenes.SceneB) { distance = swipeDistance } - } + SceneA, + transitions { from(SceneA, to = SceneB) { distance = swipeDistance } } ) val layoutSize = 200.dp @@ -591,10 +567,10 @@ class SwipeToSceneTest { touchSlop = LocalViewConfiguration.current.touchSlop SceneTransitionLayout(state, Modifier.size(layoutSize)) { - scene(TestScenes.SceneA, userActions = mapOf(Swipe.Up to TestScenes.SceneB)) { + scene(SceneA, userActions = mapOf(Swipe.Up to SceneB)) { Box(Modifier.fillMaxSize()) } - scene(TestScenes.SceneB) { + scene(SceneB) { Box(Modifier.fillMaxSize()) { Box(Modifier.offset(y = fooYOffset).element(TestElements.Foo).size(fooSize)) } @@ -611,7 +587,9 @@ class SwipeToSceneTest { } rule.waitForIdle() - assertThat(state.isTransitioning(from = TestScenes.SceneA, to = TestScenes.SceneB)).isTrue() - assertThat(state.currentTransition!!.progress).isWithin(0.01f).of(0.5f) + val transition = assertThat(state.transitionState).isTransition() + assertThat(transition).hasFromScene(SceneA) + assertThat(transition).hasToScene(SceneB) + assertThat(transition).hasProgress(0.5f, tolerance = 0.01f) } } diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt index c49a5b85ebe3..a609be48a225 100644 --- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/Transition.kt @@ -29,6 +29,7 @@ fun transition( to: SceneKey, current: () -> SceneKey = { from }, progress: () -> Float = { 0f }, + progressVelocity: () -> Float = { 0f }, interruptionProgress: () -> Float = { 100f }, isInitiatedByUserInput: Boolean = false, isUserInputOngoing: Boolean = false, @@ -42,6 +43,8 @@ fun transition( get() = current() override val progress: Float get() = progress() + override val progressVelocity: Float + get() = progressVelocity() override val isInitiatedByUserInput: Boolean = isInitiatedByUserInput override val isUserInputOngoing: Boolean = isUserInputOngoing diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt new file mode 100644 index 000000000000..348989218ce9 --- /dev/null +++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/subjects/TransitionStateSubject.kt @@ -0,0 +1,130 @@ +/* + * 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.subjects + +import com.android.compose.animation.scene.OverscrollSpec +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.TransitionState +import com.google.common.truth.Fact.simpleFact +import com.google.common.truth.FailureMetadata +import com.google.common.truth.Subject +import com.google.common.truth.Subject.Factory +import com.google.common.truth.Truth + +/** Assert on a [TransitionState]. */ +fun assertThat(state: TransitionState): TransitionStateSubject { + return Truth.assertAbout(TransitionStateSubject.transitionStates()).that(state) +} + +/** Assert on a [TransitionState.Transition]. */ +fun assertThat(transitions: TransitionState.Transition): TransitionSubject { + return Truth.assertAbout(TransitionSubject.transitions()).that(transitions) +} + +class TransitionStateSubject +private constructor( + metadata: FailureMetadata, + private val actual: TransitionState, +) : Subject(metadata, actual) { + fun hasCurrentScene(sceneKey: SceneKey) { + check("currentScene").that(actual.currentScene).isEqualTo(sceneKey) + } + + fun isIdle(): TransitionState.Idle { + if (actual !is TransitionState.Idle) { + failWithActual(simpleFact("expected to be TransitionState.Idle")) + } + + return actual as TransitionState.Idle + } + + fun isTransition(): TransitionState.Transition { + if (actual !is TransitionState.Transition) { + failWithActual(simpleFact("expected to be TransitionState.Transition")) + } + + return actual as TransitionState.Transition + } + + companion object { + fun transitionStates() = Factory { metadata, actual: TransitionState -> + TransitionStateSubject(metadata, actual) + } + } +} + +class TransitionSubject +private constructor( + metadata: FailureMetadata, + private val actual: TransitionState.Transition, +) : Subject(metadata, actual) { + fun hasCurrentScene(sceneKey: SceneKey) { + check("currentScene").that(actual.currentScene).isEqualTo(sceneKey) + } + + fun hasFromScene(sceneKey: SceneKey) { + check("fromScene").that(actual.fromScene).isEqualTo(sceneKey) + } + + fun hasToScene(sceneKey: SceneKey) { + check("toScene").that(actual.toScene).isEqualTo(sceneKey) + } + + fun hasProgress(progress: Float, tolerance: Float = 0f) { + check("progress").that(actual.progress).isWithin(tolerance).of(progress) + } + + fun hasProgressVelocity(progressVelocity: Float, tolerance: Float = 0f) { + check("progressVelocity") + .that(actual.progressVelocity) + .isWithin(tolerance) + .of(progressVelocity) + } + + fun isInitiatedByUserInput() { + check("isInitiatedByUserInput").that(actual.isInitiatedByUserInput).isTrue() + } + + fun hasIsUserInputOngoing(isUserInputOngoing: Boolean) { + check("isUserInputOngoing").that(actual.isUserInputOngoing).isEqualTo(isUserInputOngoing) + } + + fun hasOverscrollSpec(): OverscrollSpec { + check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNotNull() + return actual.currentOverscrollSpec!! + } + + fun hasNoOverscrollSpec() { + check("currentOverscrollSpec").that(actual.currentOverscrollSpec).isNull() + } + + fun hasBouncingScene(scene: SceneKey) { + if (actual !is TransitionState.HasOverscrollProperties) { + failWithActual(simpleFact("expected to be TransitionState.HasOverscrollProperties")) + } + + check("bouncingScene") + .that((actual as TransitionState.HasOverscrollProperties).bouncingScene) + .isEqualTo(scene) + } + + companion object { + fun transitions() = Factory { metadata, actual: TransitionState.Transition -> + TransitionSubject(metadata, actual) + } + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt index 447c28067200..c8717d881336 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt @@ -17,6 +17,8 @@ package com.android.keyguard +import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags import android.content.res.Configuration import android.media.AudioManager import android.telephony.TelephonyManager @@ -148,6 +150,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { @Mock private lateinit var faceAuthAccessibilityDelegate: FaceAuthAccessibilityDelegate @Mock private lateinit var deviceProvisionedController: DeviceProvisionedController @Mock private lateinit var postureController: DevicePostureController + @Mock private lateinit var devicePolicyManager: DevicePolicyManager @Captor private lateinit var swipeListenerArgumentCaptor: @@ -273,6 +276,7 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { mSelectedUserInteractor, deviceProvisionedController, faceAuthAccessibilityDelegate, + devicePolicyManager, keyguardTransitionInteractor, { primaryBouncerInteractor }, ) { @@ -934,6 +938,45 @@ class KeyguardSecurityContainerControllerTest : SysuiTestCase() { verify(viewFlipperController).asynchronouslyInflateView(any(), any(), any()) } + @Test + fun showAlmostAtWipeDialog_calledOnMainUser_setsCorrectUserType() { + mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + val mainUserId = 10 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ mainUserId, + /* expiringUserId = */ mainUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog(any(), any(), eq(KeyguardSecurityContainer.USER_TYPE_PRIMARY)) + } + + @Test + fun showAlmostAtWipeDialog_calledOnNonMainUser_setsCorrectUserType() { + mSetFlagsRule.enableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) + val secondaryUserId = 10 + val mainUserId = 0 + + underTest.showMessageForFailedUnlockAttempt( + /* userId = */ secondaryUserId, + /* expiringUserId = */ secondaryUserId, + /* mainUserId = */ mainUserId, + /* remainingBeforeWipe = */ 1, + /* failedAttempts = */ 1 + ) + + verify(view) + .showAlmostAtWipeDialog( + any(), + any(), + eq(KeyguardSecurityContainer.USER_TYPE_SECONDARY_USER) + ) + } + private val registeredSwipeListener: KeyguardSecurityContainer.SwipeListener get() { underTest.onViewAttached() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt index 81878aaf4a18..0c5e726e17aa 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractorTest.kt @@ -17,6 +17,8 @@ package com.android.systemui.authentication.domain.interactor import android.app.admin.DevicePolicyManager +import android.app.admin.flags.Flags as DevicePolicyFlags +import android.platform.test.annotations.EnableFlags import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.internal.widget.LockPatternUtils @@ -32,6 +34,8 @@ import com.android.systemui.authentication.shared.model.AuthenticationWipeModel import com.android.systemui.coroutines.collectLastValue import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos +import com.android.systemui.user.data.repository.FakeUserRepository +import com.android.systemui.user.data.repository.fakeUserRepository import com.google.common.truth.Truth.assertThat import kotlin.time.Duration.Companion.seconds import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -410,12 +414,16 @@ class AuthenticationInteractorTest : SysuiTestCase() { } @Test + @EnableFlags(DevicePolicyFlags.FLAG_HEADLESS_SINGLE_USER_FIXES) fun upcomingWipe() = testScope.runTest { val upcomingWipe by collectLastValue(underTest.upcomingWipe) kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin) val correctPin = FakeAuthenticationRepository.DEFAULT_PIN val wrongPin = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 } + kosmos.fakeUserRepository.asMainUser() + kosmos.fakeAuthenticationRepository.profileWithMinFailedUnlockAttemptsForWipe = + FakeUserRepository.MAIN_USER_ID underTest.authenticate(correctPin) assertThat(upcomingWipe).isNull() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt index 20beabb983da..2546f27cb351 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepositoryTest.kt @@ -41,6 +41,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.biometrics.domain.interactor.displayStateInteractor import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.FlowValue import com.android.systemui.coroutines.collectLastValue import com.android.systemui.coroutines.collectValues @@ -144,6 +145,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository private val testScope = kosmos.testScope private val fakeUserRepository = kosmos.fakeUserRepository + private val fakeExecutor = kosmos.fakeExecutor private lateinit var authStatus: FlowValue<FaceAuthenticationStatus?> private lateinit var detectStatus: FlowValue<FaceDetectionStatus?> private lateinit var authRunning: FlowValue<Boolean?> @@ -220,12 +222,12 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { testScope.backgroundScope, testDispatcher, testDispatcher, + fakeExecutor, sessionTracker, uiEventLogger, FaceAuthenticationLogger(logcatLogBuffer("DeviceEntryFaceAuthRepositoryLog")), biometricSettingsRepository, deviceEntryFingerprintAuthRepository, - trustRepository, keyguardRepository, powerInteractor, keyguardInteractor, @@ -292,6 +294,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { fun faceLockoutStatusIsPropagated() = testScope.runTest { initCollectors() + fakeExecutor.runAllReady() verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture()) allPreconditionsToRunFaceAuthAreTrue() @@ -1177,6 +1180,7 @@ class DeviceEntryFaceAuthRepositoryTest : SysuiTestCase() { } private suspend fun TestScope.allPreconditionsToRunFaceAuthAreTrue() { + fakeExecutor.runAllReady() verify(faceManager, atLeastOnce()) .addLockoutResetCallback(faceLockoutResetCallback.capture()) trustRepository.setCurrentUserTrusted(false) diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt index abc684c09f49..5661bd388757 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.notifications.ui.viewmodel import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 @@ -32,6 +32,7 @@ import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticati import com.android.systemui.kosmos.testScope import com.android.systemui.scene.domain.interactor.sceneInteractor import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel import com.android.systemui.testKosmos import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt new file mode 100644 index 000000000000..034c2e9b6789 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt @@ -0,0 +1,124 @@ +/* + * 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.ui.viewmodel + +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.compose.animation.scene.Swipe +import com.android.systemui.SysuiTestCase +import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository +import com.android.systemui.authentication.shared.model.AuthenticationMethodModel +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository +import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.Scenes +import com.android.systemui.shade.ui.viewmodel.quickSettingsShadeSceneViewModel +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper +@EnableSceneContainer +class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val sceneInteractor = kosmos.sceneInteractor + private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + + private val underTest = kosmos.quickSettingsShadeSceneViewModel + + @Test + fun upTransitionSceneKey_deviceLocked_lockscreen() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + lockDevice() + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun upTransitionSceneKey_deviceUnlocked_gone() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + lockDevice() + unlockDevice() + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone) + } + + @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Lockscreen) + } + + @Test + fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() = + testScope.runTest { + val destinationScenes by collectLastValue(underTest.destinationScenes) + kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true) + kosmos.fakeAuthenticationRepository.setAuthenticationMethod( + AuthenticationMethodModel.None + ) + runCurrent() + sceneInteractor.changeScene(Scenes.Gone, "reason") + + assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(Scenes.Gone) + } + + private fun TestScope.lockDevice() { + val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeAuthenticationRepository.setAuthenticationMethod(AuthenticationMethodModel.Pin) + assertThat(deviceUnlockStatus?.isUnlocked).isFalse() + sceneInteractor.changeScene(Scenes.Lockscreen, "reason") + runCurrent() + } + + private fun TestScope.unlockDevice() { + val deviceUnlockStatus by collectLastValue(deviceUnlockedInteractor.deviceUnlockStatus) + + kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus( + SuccessFingerprintAuthenticationStatus(0, true) + ) + assertThat(deviceUnlockStatus?.isUnlocked).isTrue() + sceneInteractor.changeScene(Scenes.Gone, "reason") + runCurrent() + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java index 66f741620e44..d6e3879b899f 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationShadeWindowControllerImplTest.java @@ -20,7 +20,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_ALT_FOCUSABLE_IM; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static com.google.common.truth.Truth.assertThat; @@ -79,12 +79,12 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; import org.mockito.Spy; -import platform.test.runner.parameterized.ParameterizedAndroidJunit4; -import platform.test.runner.parameterized.Parameters; - import java.util.List; import java.util.concurrent.Executor; +import platform.test.runner.parameterized.ParameterizedAndroidJunit4; +import platform.test.runner.parameterized.Parameters; + @RunWith(ParameterizedAndroidJunit4.class) @RunWithLooper(setAsMainLooper = true) @SmallTest @@ -341,7 +341,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) != 0).isTrue(); assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0) .isTrue(); } @@ -353,7 +353,7 @@ public class NotificationShadeWindowControllerImplTest extends SysuiTestCase { verify(mWindowManager).updateViewLayout(any(), mLayoutParameters.capture()); assertThat((mLayoutParameters.getValue().flags & FLAG_SECURE) == 0).isTrue(); assertThat( - (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) + (mLayoutParameters.getValue().inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) == 0) .isTrue(); } diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt new file mode 100644 index 000000000000..35e4047109d5 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationScrimNestedScrollConnectionTest.kt @@ -0,0 +1,249 @@ +/* + * 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.statusbar.notification + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.input.nestedscroll.NestedScrollSource +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.notifications.ui.composable.NotificationScrimNestedScrollConnection +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class NotificationScrimNestedScrollConnectionTest : SysuiTestCase() { + private var isStarted = false + private var scrimOffset = 0f + private var contentHeight = 0f + private var isCurrentGestureOverscroll = false + + private val scrollConnection = + NotificationScrimNestedScrollConnection( + scrimOffset = { scrimOffset }, + snapScrimOffset = { _ -> }, + animateScrimOffset = { _ -> }, + minScrimOffset = { MIN_SCRIM_OFFSET }, + maxScrimOffset = MAX_SCRIM_OFFSET, + contentHeight = { contentHeight }, + minVisibleScrimHeight = { MIN_VISIBLE_SCRIM_HEIGHT }, + isCurrentGestureOverscroll = { isCurrentGestureOverscroll }, + onStart = { isStarted = true }, + onStop = { isStarted = false }, + ) + + @Test + fun onScrollUp_canStartPreScroll_contentNotExpanded_ignoreScroll() = runTest { + contentHeight = COLLAPSED_CONTENT_HEIGHT + + val offsetConsumed = + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = -1f), + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(false) + } + + @Test + fun onScrollUp_canStartPreScroll_contentExpandedAtMinOffset_ignoreScroll() = runTest { + contentHeight = EXPANDED_CONTENT_HEIGHT + scrimOffset = MIN_SCRIM_OFFSET + + val offsetConsumed = + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = -1f), + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(false) + } + + @Test + fun onScrollUp_canStartPreScroll_contentExpanded_consumeScroll() = runTest { + contentHeight = EXPANDED_CONTENT_HEIGHT + + val availableOffset = Offset(x = 0f, y = -1f) + val offsetConsumed = + scrollConnection.onPreScroll( + available = availableOffset, + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(availableOffset) + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun onScrollUp_canStartPreScroll_contentExpanded_consumeScrollWithRemainder() = runTest { + contentHeight = EXPANDED_CONTENT_HEIGHT + scrimOffset = MIN_SCRIM_OFFSET + 1 + + val availableOffset = Offset(x = 0f, y = -2f) + val consumableOffset = Offset(x = 0f, y = -1f) + val offsetConsumed = + scrollConnection.onPreScroll( + available = availableOffset, + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(consumableOffset) + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun onScrollUp_canStartPostScroll_ignoreScroll() = runTest { + val offsetConsumed = + scrollConnection.onPostScroll( + consumed = Offset.Zero, + available = Offset(x = 0f, y = -1f), + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(false) + } + + @Test + fun onScrollDown_canStartPreScroll_ignoreScroll() = runTest { + val offsetConsumed = + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = 1f), + source = NestedScrollSource.Drag, + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(false) + } + + @Test + fun onScrollDown_canStartPostScroll_consumeScroll() = runTest { + scrimOffset = MIN_SCRIM_OFFSET + + val availableOffset = Offset(x = 0f, y = 1f) + val offsetConsumed = + scrollConnection.onPostScroll( + consumed = Offset.Zero, + available = availableOffset, + source = NestedScrollSource.Drag + ) + + assertThat(offsetConsumed).isEqualTo(availableOffset) + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun onScrollDown_canStartPostScroll_consumeScrollWithRemainder() = runTest { + scrimOffset = MAX_SCRIM_OFFSET - 1 + + val availableOffset = Offset(x = 0f, y = 2f) + val consumableOffset = Offset(x = 0f, y = 1f) + val offsetConsumed = + scrollConnection.onPostScroll( + consumed = Offset.Zero, + available = availableOffset, + source = NestedScrollSource.Drag + ) + + assertThat(offsetConsumed).isEqualTo(consumableOffset) + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun canStartPostScroll_atMaxOffset_ignoreScroll() = runTest { + scrimOffset = MAX_SCRIM_OFFSET + + val offsetConsumed = + scrollConnection.onPostScroll( + consumed = Offset.Zero, + available = Offset(x = 0f, y = 1f), + source = NestedScrollSource.Drag + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(false) + } + + @Test + fun canStartPostScroll_externalOverscrollGesture_startButIgnoreScroll() = runTest { + scrimOffset = MAX_SCRIM_OFFSET + isCurrentGestureOverscroll = true + + val offsetConsumed = + scrollConnection.onPostScroll( + consumed = Offset.Zero, + available = Offset(x = 0f, y = 1f), + source = NestedScrollSource.Drag + ) + + assertThat(offsetConsumed).isEqualTo(Offset.Zero) + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun canContinueScroll_inBetweenMinMaxOffset_true() = runTest { + scrimOffset = (MIN_SCRIM_OFFSET + MAX_SCRIM_OFFSET) / 2f + contentHeight = EXPANDED_CONTENT_HEIGHT + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = -1f), + source = NestedScrollSource.Drag + ) + + assertThat(isStarted).isEqualTo(true) + + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = 1f), + source = NestedScrollSource.Drag + ) + + assertThat(isStarted).isEqualTo(true) + } + + @Test + fun canContinueScroll_atMaxOffset_false() = runTest { + scrimOffset = MAX_SCRIM_OFFSET + contentHeight = EXPANDED_CONTENT_HEIGHT + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = -1f), + source = NestedScrollSource.Drag + ) + + assertThat(isStarted).isEqualTo(true) + + scrollConnection.onPreScroll( + available = Offset(x = 0f, y = 1f), + source = NestedScrollSource.Drag + ) + + assertThat(isStarted).isEqualTo(false) + } + + companion object { + const val MIN_SCRIM_OFFSET = -100f + const val MAX_SCRIM_OFFSET = 0f + + const val EXPANDED_CONTENT_HEIGHT = 200f + const val COLLAPSED_CONTENT_HEIGHT = 40f + + const val MIN_VISIBLE_SCRIM_HEIGHT = 50f + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt index 632196ccf66d..2af2602c6f52 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractorTest.kt @@ -21,6 +21,7 @@ import android.graphics.drawable.TestStubDrawable import android.media.AudioDeviceInfo import android.media.AudioDevicePort import android.media.AudioManager +import android.testing.TestableLooper import androidx.test.ext.junit.runners.AndroidJUnit4 import androidx.test.filters.SmallTest import com.android.settingslib.R @@ -54,6 +55,7 @@ import org.junit.runner.RunWith @OptIn(ExperimentalCoroutinesApi::class) @RunWith(AndroidJUnit4::class) @SmallTest +@TestableLooper.RunWithLooper(setAsMainLooper = true) class AudioOutputInteractorTest : SysuiTestCase() { private val kosmos = testKosmos() diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt new file mode 100644 index 000000000000..9e86cedb6732 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractorTest.kt @@ -0,0 +1,231 @@ +/* + * 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.volume.panel.component.mediaoutput.domain.interactor + +import android.media.AudioAttributes +import android.media.VolumeProvider +import android.media.session.MediaController +import android.media.session.PlaybackState +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +import com.android.systemui.volume.data.repository.FakeLocalMediaRepository +import com.android.systemui.volume.localMediaController +import com.android.systemui.volume.localMediaRepositoryFactory +import com.android.systemui.volume.localPlaybackInfo +import com.android.systemui.volume.localPlaybackStateBuilder +import com.android.systemui.volume.mediaControllerRepository +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.panel.component.mediaoutput.shared.model.MediaDeviceSession +import com.android.systemui.volume.panel.shared.model.Result +import com.android.systemui.volume.remoteMediaController +import com.android.systemui.volume.remotePlaybackInfo +import com.android.systemui.volume.remotePlaybackStateBuilder +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@OptIn(ExperimentalCoroutinesApi::class) +@SmallTest +@RunWith(AndroidJUnit4::class) +@TestableLooper.RunWithLooper(setAsMainLooper = true) +class MediaOutputInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + + private lateinit var underTest: MediaOutputInteractor + + @Before + fun setUp() = + with(kosmos) { + localMediaRepositoryFactory.setLocalMediaRepository( + "local.test.pkg", + FakeLocalMediaRepository().apply { + updateCurrentConnectedDevice( + mock { whenever(name).thenReturn("local_media_device") } + ) + }, + ) + localMediaRepositoryFactory.setLocalMediaRepository( + "remote.test.pkg", + FakeLocalMediaRepository().apply { + updateCurrentConnectedDevice( + mock { whenever(name).thenReturn("remote_media_device") } + ) + }, + ) + + underTest = kosmos.mediaOutputInteractor + } + + @Test + fun noActiveMediaDeviceSessions_nulls() = + with(kosmos) { + testScope.runTest { + mediaControllerRepository.setActiveSessions(emptyList()) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + assertThat(activeMediaDeviceSessions!!.local).isNull() + assertThat(activeMediaDeviceSessions!!.remote).isNull() + } + } + + @Test + fun activeMediaDeviceSessions_areParsed() = + with(kosmos) { + testScope.runTest { + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + with(activeMediaDeviceSessions!!.local!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + with(activeMediaDeviceSessions!!.remote!!) { + assertThat(packageName).isEqualTo("remote.test.pkg") + assertThat(appLabel).isEqualTo("remote_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + } + } + + @Test + fun activeMediaDeviceSessions_volumeControlFixed_cantAdjustVolume() = + with(kosmos) { + testScope.runTest { + localPlaybackInfo = + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + VolumeProvider.VOLUME_CONTROL_FIXED, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + remotePlaybackInfo = + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + VolumeProvider.VOLUME_CONTROL_FIXED, + 0, + 0, + AudioAttributes.Builder().build(), + "", + ) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val activeMediaDeviceSessions by + collectLastValue(underTest.activeMediaDeviceSessions) + runCurrent() + + assertThat(activeMediaDeviceSessions!!.local!!.canAdjustVolume).isFalse() + assertThat(activeMediaDeviceSessions!!.remote!!.canAdjustVolume).isFalse() + } + } + + @Test + fun activeLocalAndRemoteSession_defaultSession_local() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("local_media_device") + } + } + + @Test + fun activeRemoteSession_defaultSession_remote() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PLAYING, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("remote.test.pkg") + assertThat(appLabel).isEqualTo("remote_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("remote_media_device") + } + } + + @Test + fun inactiveLocalAndRemoteSession_defaultSession_local() = + with(kosmos) { + testScope.runTest { + localPlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + remotePlaybackStateBuilder.setState(PlaybackState.STATE_PAUSED, 0, 0f) + mediaControllerRepository.setActiveSessions( + listOf(localMediaController, remoteMediaController) + ) + + val defaultActiveMediaSession by + collectLastValue(underTest.defaultActiveMediaSession) + val currentDevice by collectLastValue(underTest.currentConnectedDevice) + runCurrent() + + with((defaultActiveMediaSession as Result.Data<MediaDeviceSession?>).data!!) { + assertThat(packageName).isEqualTo("local.test.pkg") + assertThat(appLabel).isEqualTo("local_media_controller_label") + assertThat(canAdjustVolume).isTrue() + } + assertThat(currentDevice!!.name).isEqualTo("local_media_device") + } + } +} diff --git a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml index a5b44e564157..0a1f2a8f5048 100644 --- a/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml +++ b/packages/SystemUI/res/drawable/shelf_action_chip_divider.xml @@ -16,6 +16,6 @@ <shape xmlns:android = "http://schemas.android.com/apk/res/android"> <size - android:width = "@dimen/overlay_action_chip_margin_start" + android:width = "@dimen/shelf_action_chip_margin_start" android:height = "0dp"/> </shape> diff --git a/packages/SystemUI/res/layout/clipboard_overlay2.xml b/packages/SystemUI/res/layout/clipboard_overlay2.xml new file mode 100644 index 000000000000..33ad2cd3a30f --- /dev/null +++ b/packages/SystemUI/res/layout/clipboard_overlay2.xml @@ -0,0 +1,191 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> +<com.android.systemui.clipboardoverlay.ClipboardOverlayView + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:androidprv="http://schemas.android.com/apk/prv/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:id="@+id/clipboard_ui" + android:theme="@style/FloatingOverlay" + android:alpha="0" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:contentDescription="@string/clipboard_overlay_window_name"> + <FrameLayout + android:id="@+id/actions_container_background" + android:visibility="gone" + android:layout_height="0dp" + android:layout_width="0dp" + android:elevation="4dp" + android:background="@drawable/shelf_action_chip_container_background" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="@+id/actions_container" + app:layout_constraintEnd_toEndOf="@+id/actions_container" + app:layout_constraintBottom_toBottomOf="parent"/> + <HorizontalScrollView + android:id="@+id/actions_container" + android:layout_width="0dp" + android:layout_height="wrap_content" + android:layout_marginEnd="@dimen/overlay_action_container_margin_horizontal" + android:paddingEnd="@dimen/overlay_action_container_padding_end" + android:paddingVertical="@dimen/overlay_action_container_padding_vertical" + android:elevation="4dp" + android:scrollbars="none" + app:layout_constraintHorizontal_bias="0" + app:layout_constraintWidth_percent="1.0" + app:layout_constraintWidth_max="wrap" + app:layout_constraintStart_toEndOf="@+id/preview_border" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"> + <LinearLayout + android:id="@+id/actions" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:paddingStart="@dimen/shelf_action_chip_margin_start" + android:showDividers="middle" + android:divider="@drawable/shelf_action_chip_divider" + android:animateLayoutChanges="true"> + <include layout="@layout/shelf_action_chip" + android:id="@+id/share_chip"/> + <include layout="@layout/shelf_action_chip" + android:id="@+id/remote_copy_chip"/> + </LinearLayout> + </HorizontalScrollView> + <View + android:id="@+id/preview_border" + android:layout_width="0dp" + android:layout_height="0dp" + android:layout_marginStart="@dimen/overlay_preview_container_margin" + android:layout_marginTop="@dimen/overlay_border_width_neg" + android:layout_marginEnd="@dimen/overlay_border_width_neg" + android:layout_marginBottom="@dimen/overlay_preview_container_margin" + android:elevation="7dp" + android:background="@drawable/overlay_border" + app:layout_constraintStart_toStartOf="@id/actions_container_background" + app:layout_constraintTop_toTopOf="@id/clipboard_preview" + app:layout_constraintEnd_toEndOf="@id/clipboard_preview" + app:layout_constraintBottom_toBottomOf="@id/actions_container_background"/> + <FrameLayout + android:id="@+id/clipboard_preview" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="wrap_content" + android:layout_marginStart="@dimen/overlay_border_width" + android:layout_marginBottom="@dimen/overlay_border_width" + android:layout_gravity="center" + android:elevation="7dp" + android:background="@drawable/overlay_preview_background" + android:clipChildren="true" + android:clipToOutline="true" + android:clipToPadding="true" + app:layout_constraintStart_toStartOf="@id/preview_border" + app:layout_constraintBottom_toBottomOf="@id/preview_border"> + <TextView android:id="@+id/text_preview" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center|start" + android:ellipsize="end" + android:autoSizeTextType="uniform" + android:autoSizeMinTextSize="@dimen/clipboard_overlay_min_font" + android:autoSizeMaxTextSize="@dimen/clipboard_overlay_max_font" + android:textColor="?attr/overlayButtonTextColor" + android:textColorLink="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + <ImageView + android:id="@+id/image_preview" + android:scaleType="fitCenter" + android:adjustViewBounds="true" + android:contentDescription="@string/clipboard_image_preview" + android:layout_width="match_parent" + android:layout_height="wrap_content"/> + <TextView + android:id="@+id/hidden_preview" + android:visibility="gone" + android:textFontWeight="500" + android:padding="8dp" + android:gravity="center" + android:textSize="14sp" + android:textColor="?attr/overlayButtonTextColor" + android:background="?androidprv:attr/colorAccentSecondary" + android:layout_width="@dimen/clipboard_preview_size" + android:layout_height="@dimen/clipboard_preview_size"/> + </FrameLayout> + <LinearLayout + android:id="@+id/minimized_preview" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:visibility="gone" + android:elevation="7dp" + android:padding="8dp" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintStart_toStartOf="parent" + android:layout_marginStart="@dimen/overlay_action_container_margin_horizontal" + android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom" + android:background="@drawable/clipboard_minimized_background"> + <ImageView + android:src="@drawable/ic_content_paste" + android:tint="?attr/overlayButtonTextColor" + android:layout_width="24dp" + android:layout_height="24dp"/> + <ImageView + android:src="@*android:drawable/ic_chevron_end" + android:tint="?attr/overlayButtonTextColor" + android:layout_width="24dp" + android:layout_height="24dp" + android:paddingEnd="-8dp" + android:paddingStart="-4dp"/> + </LinearLayout> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_content_top" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="horizontal" + app:barrierDirection="top" + app:constraint_referenced_ids="clipboard_preview,minimized_preview"/> + <androidx.constraintlayout.widget.Barrier + android:id="@+id/clipboard_content_end" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:orientation="vertical" + app:barrierDirection="end" + app:constraint_referenced_ids="clipboard_preview,minimized_preview"/> + <FrameLayout + android:id="@+id/dismiss_button" + android:layout_width="@dimen/overlay_dismiss_button_tappable_size" + android:layout_height="@dimen/overlay_dismiss_button_tappable_size" + android:elevation="10dp" + android:visibility="gone" + android:alpha="0" + app:layout_constraintStart_toEndOf="@id/clipboard_content_end" + app:layout_constraintEnd_toEndOf="@id/clipboard_content_end" + app:layout_constraintTop_toTopOf="@id/clipboard_content_top" + app:layout_constraintBottom_toTopOf="@id/clipboard_content_top" + android:contentDescription="@string/clipboard_dismiss_description"> + <ImageView + android:id="@+id/dismiss_image" + android:layout_width="match_parent" + android:layout_height="match_parent" + android:layout_margin="@dimen/overlay_dismiss_button_margin" + android:background="@drawable/circular_background" + android:backgroundTint="?androidprv:attr/materialColorPrimaryFixedDim" + android:tint="?androidprv:attr/materialColorOnPrimaryFixed" + android:padding="4dp" + android:src="@drawable/ic_close"/> + </FrameLayout> +</com.android.systemui.clipboardoverlay.ClipboardOverlayView>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/config.xml b/packages/SystemUI/res/values/config.xml index 6bfd0887c404..2ba72e386cd1 100644 --- a/packages/SystemUI/res/values/config.xml +++ b/packages/SystemUI/res/values/config.xml @@ -635,9 +635,13 @@ 58.0001 29.2229,56.9551 26.8945,55.195 </string> - <!-- The time (in ms) needed to trigger the lock icon view's long-press affordance --> + <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance --> <integer name="config_lockIconLongPress" translatable="false">200</integer> + <!-- The time (in ms) needed to trigger the device entry icon view's long-press affordance + when the device supports an under-display fingerprint sensor --> + <integer name="config_udfpsDeviceEntryIconLongPress" translatable="false">100</integer> + <!-- package name of a built-in camera app to use to restrict implicit intent resolution when the double-press power gesture is used. Ignored if empty. --> <string translatable="false" name="config_cameraGesturePackage"></string> diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index a7a6d5b2f305..a1daebd7513e 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -430,6 +430,7 @@ <dimen name="overlay_button_corner_radius">16dp</dimen> <!-- Margin between successive chips --> <dimen name="overlay_action_chip_margin_start">8dp</dimen> + <dimen name="shelf_action_chip_margin_start">12dp</dimen> <dimen name="overlay_action_chip_padding_vertical">12dp</dimen> <dimen name="overlay_action_chip_icon_size">24sp</dimen> <!-- Padding on each side of the icon for icon-only chips --> diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java index c613afbda5b8..473719fa76df 100644 --- a/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java +++ b/packages/SystemUI/shared/src/com/android/systemui/shared/system/TaskStackChangeListeners.java @@ -141,6 +141,7 @@ public class TaskStackChangeListeners { private static final int ON_TASK_DESCRIPTION_CHANGED = 21; private static final int ON_ACTIVITY_ROTATION = 22; private static final int ON_LOCK_TASK_MODE_CHANGED = 23; + private static final int ON_TASK_SNAPSHOT_INVALIDATED = 24; /** * List of {@link TaskStackChangeListener} registered from {@link #addListener}. @@ -272,6 +273,12 @@ public class TaskStackChangeListeners { } @Override + public void onTaskSnapshotInvalidated(int taskId) { + mHandler.obtainMessage(ON_TASK_SNAPSHOT_INVALIDATED, taskId, 0 /* unused */) + .sendToTarget(); + } + + @Override public void onTaskCreated(int taskId, ComponentName componentName) { mHandler.obtainMessage(ON_TASK_CREATED, taskId, 0, componentName).sendToTarget(); } @@ -496,6 +503,15 @@ public class TaskStackChangeListeners { } break; } + case ON_TASK_SNAPSHOT_INVALIDATED: { + Trace.beginSection("onTaskSnapshotInvalidated"); + final ThumbnailData thumbnail = new ThumbnailData(); + for (int i = mTaskStackListeners.size() - 1; i >= 0; i--) { + mTaskStackListeners.get(i).onTaskSnapshotChanged(msg.arg1, thumbnail); + } + Trace.endSection(); + break; + } } } if (msg.obj instanceof SomeArgs) { diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java index 91fb6888bf06..87a90b526b73 100644 --- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java +++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java @@ -35,12 +35,14 @@ import static com.android.systemui.flags.Flags.LOCKSCREEN_ENABLE_LANDSCAPE; import android.app.ActivityManager; import android.app.admin.DevicePolicyManager; +import android.app.admin.flags.Flags; import android.content.Intent; import android.content.res.ColorStateList; import android.content.res.Configuration; import android.content.res.Resources; import android.media.AudioManager; import android.metrics.LogMaker; +import android.os.Looper; import android.os.SystemClock; import android.os.UserHandle; import android.telephony.TelephonyManager; @@ -96,12 +98,15 @@ import com.android.systemui.util.ViewController; import com.android.systemui.util.kotlin.JavaAdapter; import com.android.systemui.util.settings.GlobalSettings; +import com.google.common.util.concurrent.ListenableFuture; + import dagger.Lazy; import kotlinx.coroutines.Job; import java.io.File; import java.util.Arrays; +import java.util.concurrent.ExecutionException; import javax.inject.Inject; import javax.inject.Provider; @@ -134,6 +139,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard private final BouncerMessageInteractor mBouncerMessageInteractor; private int mTranslationY; private final KeyguardTransitionInteractor mKeyguardTransitionInteractor; + private final DevicePolicyManager mDevicePolicyManager; // Whether the volume keys should be handled by keyguard. If true, then // they will be handled here for specific media types such as music, otherwise // the audio service will bring up the volume dialog. @@ -460,6 +466,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard SelectedUserInteractor selectedUserInteractor, DeviceProvisionedController deviceProvisionedController, FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate, + DevicePolicyManager devicePolicyManager, KeyguardTransitionInteractor keyguardTransitionInteractor, Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor, Provider<DeviceEntryInteractor> deviceEntryInteractor @@ -495,6 +502,7 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard mKeyguardTransitionInteractor = keyguardTransitionInteractor; mDeviceProvisionedController = deviceProvisionedController; mPrimaryBouncerInteractor = primaryBouncerInteractor; + mDevicePolicyManager = devicePolicyManager; } @Override @@ -1105,35 +1113,36 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard if (DEBUG) Log.d(TAG, "reportFailedPatternAttempt: #" + failedAttempts); - final DevicePolicyManager dpm = mLockPatternUtils.getDevicePolicyManager(); final int failedAttemptsBeforeWipe = - dpm.getMaximumFailedPasswordsForWipe(null, userId); + mDevicePolicyManager.getMaximumFailedPasswordsForWipe(null, userId); final int remainingBeforeWipe = failedAttemptsBeforeWipe > 0 ? (failedAttemptsBeforeWipe - failedAttempts) : Integer.MAX_VALUE; // because DPM returns 0 if no restriction if (remainingBeforeWipe < LockPatternUtils.FAILED_ATTEMPTS_BEFORE_WIPE_GRACE) { - // The user has installed a DevicePolicyManager that requests a user/profile to be wiped - // N attempts. Once we get below the grace period, we post this dialog every time as a - // clear warning until the deletion fires. - // Check which profile has the strictest policy for failed password attempts - final int expiringUser = dpm.getProfileWithMinimumFailedPasswordsForWipe(userId); - int userType = USER_TYPE_PRIMARY; - if (expiringUser == userId) { - // TODO: http://b/23522538 - if (expiringUser != UserHandle.USER_SYSTEM) { - userType = USER_TYPE_SECONDARY_USER; + // The user has installed a DevicePolicyManager that requests a + // user/profile to be wiped N attempts. Once we get below the grace period, + // we post this dialog every time as a clear warning until the deletion + // fires. Check which profile has the strictest policy for failed password + // attempts. + final int expiringUser = + mDevicePolicyManager.getProfileWithMinimumFailedPasswordsForWipe(userId); + ListenableFuture<Integer> getMainUserIdFuture = + mSelectedUserInteractor.getMainUserIdAsync(); + getMainUserIdFuture.addListener(() -> { + Looper.prepare(); + Integer mainUser; + try { + mainUser = getMainUserIdFuture.get(); + } catch (InterruptedException | ExecutionException e) { + // Nothing we can, keep using the system user as the primary + // user. + mainUser = null; } - } else if (expiringUser != UserHandle.USER_NULL) { - userType = USER_TYPE_WORK_PROFILE; - } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY - if (remainingBeforeWipe > 0) { - mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, userType); - } else { - // Too many attempts. The device will be wiped shortly. - Slog.i(TAG, "Too many unlock attempts; user " + expiringUser + " will be wiped!"); - mView.showWipeDialog(failedAttempts, userType); - } + showMessageForFailedUnlockAttempt( + userId, expiringUser, mainUser, remainingBeforeWipe, failedAttempts); + Looper.loop(); + }, ThreadUtils.getBackgroundExecutor()); } mLockPatternUtils.reportFailedPasswordAttempt(userId); if (timeoutMs > 0) { @@ -1145,6 +1154,35 @@ public class KeyguardSecurityContainerController extends ViewController<Keyguard } } + @VisibleForTesting + void showMessageForFailedUnlockAttempt(int userId, int expiringUserId, Integer mainUserId, + int remainingBeforeWipe, int failedAttempts) { + int userType = USER_TYPE_PRIMARY; + if (expiringUserId == userId) { + int primaryUser = UserHandle.USER_SYSTEM; + if (Flags.headlessSingleUserFixes()) { + if (mainUserId != null) { + primaryUser = mainUserId; + } + } + // TODO: http://b/23522538 + if (expiringUserId != primaryUser) { + userType = USER_TYPE_SECONDARY_USER; + } + } else if (expiringUserId != UserHandle.USER_NULL) { + userType = USER_TYPE_WORK_PROFILE; + } // If USER_NULL, which shouldn't happen, leave it as USER_TYPE_PRIMARY + if (remainingBeforeWipe > 0) { + mView.showAlmostAtWipeDialog(failedAttempts, remainingBeforeWipe, + userType); + } else { + // Too many attempts. The device will be wiped shortly. + Slog.i(TAG, "Too many unlock attempts; user " + expiringUserId + + " will be wiped!"); + mView.showWipeDialog(failedAttempts, userType); + } + } + private void getCurrentSecurityController( KeyguardSecurityViewFlipperController.OnViewInflatedCallback onViewInflatedCallback) { mSecurityViewFlipperController diff --git a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java index 57c1fd091ae9..42896a419658 100644 --- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java +++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java @@ -569,6 +569,11 @@ public class ExpandHelper implements Gefingerpoken { return true; } + /** Finish the current expand motion without accounting for velocity. */ + public void finishExpanding() { + finishExpanding(false, 0); + } + /** * Finish the current expand motion * @param forceAbort whether the expansion should be forcefully aborted and returned to the old diff --git a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt index 5df7fc9865ff..fcba425f0956 100644 --- a/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/authentication/domain/interactor/AuthenticationInteractor.kt @@ -16,6 +16,7 @@ package com.android.systemui.authentication.domain.interactor +import android.app.admin.flags.Flags import android.os.UserHandle import com.android.internal.widget.LockPatternUtils import com.android.internal.widget.LockPatternView @@ -288,9 +289,15 @@ constructor( private suspend fun getWipeTarget(): WipeTarget { // Check which profile has the strictest policy for failed authentication attempts. val userToBeWiped = repository.getProfileWithMinFailedUnlockAttemptsForWipe() + val primaryUser = + if (Flags.headlessSingleUserFixes()) { + selectedUserInteractor.getMainUserId() ?: UserHandle.USER_SYSTEM + } else { + UserHandle.USER_SYSTEM + } return when (userToBeWiped) { selectedUserInteractor.getSelectedUserId() -> - if (userToBeWiped == UserHandle.USER_SYSTEM) { + if (userToBeWiped == primaryUser) { WipeTarget.WholeDevice } else { WipeTarget.User diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java index b2699673f7ea..8efc66de24cd 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/ClipboardOverlayView.java @@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay; import static android.content.res.Configuration.ORIENTATION_PORTRAIT; +import static com.android.systemui.Flags.screenshotShelfUi2; + import android.animation.Animator; import android.animation.AnimatorListenerAdapter; import android.animation.AnimatorSet; @@ -25,6 +27,7 @@ import android.animation.ObjectAnimator; import android.animation.TimeInterpolator; import android.animation.ValueAnimator; import android.annotation.Nullable; +import android.app.PendingIntent; import android.app.RemoteAction; import android.content.Context; import android.content.res.Resources; @@ -36,6 +39,7 @@ import android.graphics.Region; import android.graphics.drawable.Icon; import android.util.AttributeSet; import android.util.DisplayMetrics; +import android.util.Log; import android.util.MathUtils; import android.util.TypedValue; import android.view.DisplayCutout; @@ -58,9 +62,15 @@ import com.android.systemui.res.R; import com.android.systemui.screenshot.DraggableConstraintLayout; import com.android.systemui.screenshot.FloatingWindowUtil; import com.android.systemui.screenshot.OverlayActionChip; +import com.android.systemui.screenshot.ui.binder.ActionButtonViewBinder; +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonAppearance; +import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel; import java.util.ArrayList; +import kotlin.Unit; +import kotlin.jvm.functions.Function0; + /** * Handles the visual elements and animations for the clipboard overlay. */ @@ -85,7 +95,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { private final DisplayMetrics mDisplayMetrics; private final AccessibilityManager mAccessibilityManager; - private final ArrayList<OverlayActionChip> mActionChips = new ArrayList<>(); + private final ArrayList<View> mActionChips = new ArrayList<>(); private View mClipboardPreview; private ImageView mImagePreview; @@ -93,11 +103,12 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { private TextView mHiddenPreview; private LinearLayout mMinimizedPreview; private View mPreviewBorder; - private OverlayActionChip mShareChip; - private OverlayActionChip mRemoteCopyChip; + private View mShareChip; + private View mRemoteCopyChip; private View mActionContainerBackground; private View mDismissButton; private LinearLayout mActionContainer; + private ClipboardOverlayCallbacks mClipboardCallbacks; public ClipboardOverlayView(Context context) { this(context, null); @@ -128,17 +139,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { mRemoteCopyChip = requireViewById(R.id.remote_copy_chip); mDismissButton = requireViewById(R.id.dismiss_button); - mShareChip.setAlpha(1); - mRemoteCopyChip.setAlpha(1); - mShareChip.setContentDescription(mContext.getString(com.android.internal.R.string.share)); - - mRemoteCopyChip.setIcon( - Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24), true); - mShareChip.setIcon( - Icon.createWithResource(mContext, R.drawable.ic_screenshot_share), true); - - mRemoteCopyChip.setContentDescription( - mContext.getString(R.string.clipboard_send_nearby_description)); + bindDefaultActionChips(); mTextPreview.getViewTreeObserver().addOnPreDrawListener(() -> { int availableHeight = mTextPreview.getHeight() @@ -149,15 +150,68 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { super.onFinishInflate(); } + private void bindDefaultActionChips() { + if (screenshotShelfUi2()) { + ActionButtonViewBinder.INSTANCE.bind(mRemoteCopyChip, + ActionButtonViewModel.Companion.withNextId( + new ActionButtonAppearance( + Icon.createWithResource(mContext, + R.drawable.ic_baseline_devices_24).loadDrawable( + mContext), + null, + mContext.getString(R.string.clipboard_send_nearby_description)), + new Function0<>() { + @Override + public Unit invoke() { + if (mClipboardCallbacks != null) { + mClipboardCallbacks.onRemoteCopyButtonTapped(); + } + return null; + } + })); + ActionButtonViewBinder.INSTANCE.bind(mShareChip, + ActionButtonViewModel.Companion.withNextId( + new ActionButtonAppearance( + Icon.createWithResource(mContext, + R.drawable.ic_screenshot_share).loadDrawable(mContext), + null, mContext.getString(com.android.internal.R.string.share)), + new Function0<>() { + @Override + public Unit invoke() { + if (mClipboardCallbacks != null) { + mClipboardCallbacks.onShareButtonTapped(); + } + return null; + } + })); + } else { + mShareChip.setAlpha(1); + mRemoteCopyChip.setAlpha(1); + + ((ImageView) mRemoteCopyChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon( + Icon.createWithResource(mContext, R.drawable.ic_baseline_devices_24)); + ((ImageView) mShareChip.findViewById(R.id.overlay_action_chip_icon)).setImageIcon( + Icon.createWithResource(mContext, R.drawable.ic_screenshot_share)); + + mShareChip.setContentDescription( + mContext.getString(com.android.internal.R.string.share)); + mRemoteCopyChip.setContentDescription( + mContext.getString(R.string.clipboard_send_nearby_description)); + } + } + @Override public void setCallbacks(SwipeDismissCallbacks callbacks) { super.setCallbacks(callbacks); ClipboardOverlayCallbacks clipboardCallbacks = (ClipboardOverlayCallbacks) callbacks; - mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); + if (!screenshotShelfUi2()) { + mShareChip.setOnClickListener(v -> clipboardCallbacks.onShareButtonTapped()); + mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); + } mDismissButton.setOnClickListener(v -> clipboardCallbacks.onDismissButtonTapped()); - mRemoteCopyChip.setOnClickListener(v -> clipboardCallbacks.onRemoteCopyButtonTapped()); mClipboardPreview.setOnClickListener(v -> clipboardCallbacks.onPreviewTapped()); mMinimizedPreview.setOnClickListener(v -> clipboardCallbacks.onMinimizedViewTapped()); + mClipboardCallbacks = clipboardCallbacks; } void setEditAccessibilityAction(boolean editable) { @@ -285,7 +339,7 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { } void resetActionChips() { - for (OverlayActionChip chip : mActionChips) { + for (View chip : mActionChips) { mActionContainer.removeView(chip); } mActionChips.clear(); @@ -437,7 +491,12 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { void setActionChip(RemoteAction action, Runnable onFinish) { mActionContainerBackground.setVisibility(View.VISIBLE); - OverlayActionChip chip = constructActionChip(action, onFinish); + View chip; + if (screenshotShelfUi2()) { + chip = constructShelfActionChip(action, onFinish); + } else { + chip = constructActionChip(action, onFinish); + } mActionContainer.addView(chip); mActionChips.add(chip); } @@ -450,6 +509,27 @@ public class ClipboardOverlayView extends DraggableConstraintLayout { v.setVisibility(View.VISIBLE); } + private View constructShelfActionChip(RemoteAction action, Runnable onFinish) { + View chip = LayoutInflater.from(mContext).inflate( + R.layout.shelf_action_chip, mActionContainer, false); + ActionButtonViewBinder.INSTANCE.bind(chip, ActionButtonViewModel.Companion.withNextId( + new ActionButtonAppearance(action.getIcon().loadDrawable(mContext), + action.getTitle(), action.getTitle()), new Function0<>() { + @Override + public Unit invoke() { + try { + action.getActionIntent().send(); + onFinish.run(); + } catch (PendingIntent.CanceledException e) { + Log.e(TAG, "Failed to send intent"); + } + return null; + } + })); + + return chip; + } + private OverlayActionChip constructActionChip(RemoteAction action, Runnable onFinish) { OverlayActionChip chip = (OverlayActionChip) LayoutInflater.from(mContext).inflate( R.layout.overlay_action_chip, mActionContainer, false); diff --git a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java index ff9fba4c03f1..740a93eb081c 100644 --- a/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java +++ b/packages/SystemUI/src/com/android/systemui/clipboardoverlay/dagger/ClipboardOverlayModule.java @@ -18,6 +18,8 @@ package com.android.systemui.clipboardoverlay.dagger; import static android.view.WindowManager.LayoutParams.TYPE_SCREENSHOT; +import static com.android.systemui.Flags.screenshotShelfUi2; + import static java.lang.annotation.RetentionPolicy.RUNTIME; import android.content.Context; @@ -57,8 +59,13 @@ public interface ClipboardOverlayModule { */ @Provides static ClipboardOverlayView provideClipboardOverlayView(@OverlayWindowContext Context context) { - return (ClipboardOverlayView) LayoutInflater.from(context).inflate( - R.layout.clipboard_overlay, null); + if (screenshotShelfUi2()) { + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay2, null); + } else { + return (ClipboardOverlayView) LayoutInflater.from(context).inflate( + R.layout.clipboard_overlay, null); + } } @Qualifier diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt index 07814512b4b8..85e2bdb43ba5 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingView.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.DisposableHandle class LongPressHandlingView( context: Context, attrs: AttributeSet?, - private val longPressDuration: () -> Long, + longPressDuration: () -> Long, ) : View( context, @@ -89,6 +89,12 @@ class LongPressHandlingView( ) } + var longPressDuration: () -> Long + get() = interactionHandler.longPressDuration + set(longPressDuration) { + interactionHandler.longPressDuration = longPressDuration + } + fun setLongPressHandlingEnabled(isEnabled: Boolean) { interactionHandler.isLongPressHandlingEnabled = isEnabled } diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt index a742e8d614b1..d3fc610bc52e 100644 --- a/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt +++ b/packages/SystemUI/src/com/android/systemui/common/ui/view/LongPressHandlingViewInteractionHandler.kt @@ -34,7 +34,7 @@ class LongPressHandlingViewInteractionHandler( /** Callback reporting the a single tap gesture was detected at the given coordinates. */ private val onSingleTapDetected: () -> Unit, /** Time for the touch to be considered a long-press in ms */ - private val longPressDuration: () -> Long, + var longPressDuration: () -> Long, ) { sealed class MotionEventModel { object Other : MotionEventModel() diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt index ba45a51ad9a3..30a56a21e322 100644 --- a/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/deviceentry/data/repository/DeviceEntryFaceAuthRepository.kt @@ -46,7 +46,6 @@ import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthR import com.android.systemui.keyguard.data.repository.FaceAuthTableLog import com.android.systemui.keyguard.data.repository.FaceDetectTableLog import com.android.systemui.keyguard.data.repository.KeyguardRepository -import com.android.systemui.keyguard.data.repository.TrustRepository import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor import com.android.systemui.keyguard.shared.model.KeyguardState @@ -64,6 +63,7 @@ import com.android.systemui.user.data.repository.UserRepository import com.google.errorprone.annotations.CompileTimeConstant import java.io.PrintWriter import java.util.Arrays +import java.util.concurrent.Executor import java.util.stream.Collectors import javax.inject.Inject import kotlinx.coroutines.CoroutineDispatcher @@ -150,12 +150,12 @@ constructor( @Application private val applicationScope: CoroutineScope, @Main private val mainDispatcher: CoroutineDispatcher, @Background private val backgroundDispatcher: CoroutineDispatcher, + @Background private val backgroundExecutor: Executor, private val sessionTracker: SessionTracker, private val uiEventsLogger: UiEventLogger, private val faceAuthLogger: FaceAuthenticationLogger, private val biometricSettingsRepository: BiometricSettingsRepository, private val deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository, - trustRepository: TrustRepository, private val keyguardRepository: KeyguardRepository, private val powerInteractor: PowerInteractor, private val keyguardInteractor: KeyguardInteractor, @@ -235,7 +235,10 @@ constructor( } init { - faceManager?.addLockoutResetCallback(faceLockoutResetCallback) + backgroundExecutor.execute { + faceManager?.addLockoutResetCallback(faceLockoutResetCallback) + faceAuthLogger.addLockoutResetCallbackDone() + } faceAcquiredInfoIgnoreList = Arrays.stream( context.resources.getIntArray( diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt index 4a726ae3aafc..a49b3ae7b7e3 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepository.kt @@ -65,23 +65,43 @@ constructor( * @param blueprintId * @return whether the transition has succeeded. */ + fun applyBlueprint(index: Int): Boolean { + ArrayList(blueprintIdMap.values)[index]?.let { + applyBlueprint(it) + return true + } + return false + } + + /** + * Emits the blueprint value to the collectors. + * + * @param blueprintId + * @return whether the transition has succeeded. + */ fun applyBlueprint(blueprintId: String?): Boolean { val blueprint = blueprintIdMap[blueprintId] - if (blueprint == null) { + return if (blueprint != null) { + applyBlueprint(blueprint) + true + } else { Log.e( TAG, "Could not find blueprint with id: $blueprintId. " + "Perhaps it was not added to KeyguardBlueprintModule?" ) - return false + false } + } + /** Emits the blueprint value to the collectors. */ + fun applyBlueprint(blueprint: KeyguardBlueprint?) { if (blueprint == this.blueprint.value) { - return true + refreshBlueprint() + return } - this.blueprint.value = blueprint - return true + blueprint?.let { this.blueprint.value = it } } /** diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt index cf995faea77d..da4f85e0dd2f 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardBlueprintInteractor.kt @@ -82,17 +82,12 @@ constructor( } /** - * Transitions to a blueprint, or refreshes it if already applied. + * Transitions to a blueprint. * * @param blueprintId * @return whether the transition has succeeded. */ - fun transitionOrRefreshBlueprint(blueprintId: String): Boolean { - if (blueprintId == blueprint.value.id) { - refreshBlueprint() - return true - } - + fun transitionToBlueprint(blueprintId: String): Boolean { return keyguardBlueprintRepository.applyBlueprint(blueprintId) } @@ -102,7 +97,7 @@ constructor( * @param blueprintId * @return whether the transition has succeeded. */ - fun transitionToBlueprint(blueprintId: String): Boolean { + fun transitionToBlueprint(blueprintId: Int): Boolean { return keyguardBlueprintRepository.applyBlueprint(blueprintId) } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt index 20b7b2a91ade..82255a0c0d54 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractor.kt @@ -31,6 +31,7 @@ import kotlinx.coroutines.flow.Flow import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart /** * Distance over which the surface behind the keyguard is animated in during a Y-translation @@ -102,8 +103,11 @@ constructor( */ private val isNotificationLaunchAnimationRunningOnKeyguard = notificationLaunchInteractor.isLaunchAnimationRunning - .sample(transitionInteractor.finishedKeyguardState) - .map { it != KeyguardState.GONE } + .sample(transitionInteractor.finishedKeyguardState, ::Pair) + .map { (animationRunning, finishedState) -> + animationRunning && finishedState != KeyguardState.GONE + } + .onStart { emit(false) } /** * Whether we're animating the surface, or a notification launch animation is running (which diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt index db47bfbd4cd4..4f00495819e8 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/DeviceEntryIconViewBinder.kt @@ -34,6 +34,7 @@ import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryForegroundViewModel import com.android.systemui.keyguard.ui.viewmodel.DeviceEntryIconViewModel import com.android.systemui.lifecycle.repeatWhenAttached import com.android.systemui.plugins.FalsingManager +import com.android.systemui.res.R import com.android.systemui.statusbar.VibratorHelper import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi @@ -94,6 +95,24 @@ object DeviceEntryIconViewBinder { longPressHandlingView.setLongPressHandlingEnabled(isEnabled) } } + launch("$TAG#viewModel.isUdfpsSupported") { + viewModel.isUdfpsSupported.collect { udfpsSupported -> + longPressHandlingView.longPressDuration = + if (udfpsSupported) { + { + view.resources + .getInteger(R.integer.config_udfpsDeviceEntryIconLongPress) + .toLong() + } + } else { + { + view.resources + .getInteger(R.integer.config_lockIconLongPress) + .toLong() + } + } + } + } launch("$TAG#viewModel.accessibilityDelegateHint") { viewModel.accessibilityDelegateHint.collect { hint -> view.accessibilityHintType = hint diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt index abd79ab793d5..b9a79dccf76b 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt @@ -118,6 +118,7 @@ object KeyguardQuickAffordanceViewBinder { } override fun destroy() { + view.setOnApplyWindowInsetsListener(null) disposableHandle.dispose() } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt index 5713a158fde4..35b259849b78 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/DeviceEntryIconView.kt @@ -40,10 +40,7 @@ constructor( attrs: AttributeSet?, defStyleAttrs: Int = 0, ) : FrameLayout(context, attrs, defStyleAttrs) { - val longPressHandlingView: LongPressHandlingView = - LongPressHandlingView(context, attrs) { - context.resources.getInteger(R.integer.config_lockIconLongPress).toLong() - } + val longPressHandlingView: LongPressHandlingView = LongPressHandlingView(context, attrs) val iconView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_fg } val bgView: ImageView = ImageView(context, attrs).apply { id = R.id.device_entry_icon_bg } val aodFpDrawable: LottieDrawable = LottieDrawable() diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt index 962cdf10cf86..ce7ec0e22f1c 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListener.kt @@ -46,14 +46,15 @@ constructor( return } - when { - arg.isDigitsOnly() -> pw.println("Invalid argument! Use string ids.") - keyguardBlueprintInteractor.transitionOrRefreshBlueprint(arg) -> - pw.println("Transition succeeded!") - else -> { - pw.println("Invalid argument! To see available blueprint ids, run:") - pw.println("$ adb shell cmd statusbar blueprint help") - } + if ( + arg.isDigitsOnly() && keyguardBlueprintInteractor.transitionToBlueprint(arg.toInt()) + ) { + pw.println("Transition succeeded!") + } else if (keyguardBlueprintInteractor.transitionToBlueprint(arg)) { + pw.println("Transition succeeded!") + } else { + pw.println("Invalid argument! To see available blueprint ids, run:") + pw.println("$ adb shell cmd statusbar blueprint help") } } diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt index 45b82576c6c4..9146c605ab63 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt @@ -18,6 +18,7 @@ package com.android.systemui.keyguard.ui.view.layout.sections import android.content.res.Resources +import android.view.WindowInsets import androidx.constraintlayout.widget.ConstraintLayout import androidx.constraintlayout.widget.ConstraintSet import androidx.constraintlayout.widget.ConstraintSet.BOTTOM @@ -25,15 +26,19 @@ import androidx.constraintlayout.widget.ConstraintSet.LEFT import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID import androidx.constraintlayout.widget.ConstraintSet.RIGHT import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE +import com.android.systemui.animation.view.LaunchableImageView import com.android.systemui.dagger.qualifiers.Main import com.android.systemui.keyguard.KeyguardBottomAreaRefactor +import com.android.systemui.keyguard.domain.interactor.KeyguardBlueprintInteractor import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder +import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel import com.android.systemui.plugins.FalsingManager import com.android.systemui.res.R import com.android.systemui.statusbar.KeyguardIndicationController import com.android.systemui.statusbar.VibratorHelper +import dagger.Lazy import javax.inject.Inject class DefaultShortcutsSection @@ -46,11 +51,29 @@ constructor( private val falsingManager: FalsingManager, private val indicationController: KeyguardIndicationController, private val vibratorHelper: VibratorHelper, + private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>, ) : BaseShortcutSection() { + + // Amount to increase the bottom margin by to avoid colliding with inset + private var safeInsetBottom = 0 + override fun addViews(constraintLayout: ConstraintLayout) { if (KeyguardBottomAreaRefactor.isEnabled) { addLeftShortcut(constraintLayout) addRightShortcut(constraintLayout) + + constraintLayout + .requireViewById<LaunchableImageView>(R.id.start_button) + .setOnApplyWindowInsetsListener { _, windowInsets -> + val tempSafeInset = windowInsets?.displayCutout?.safeInsetBottom ?: 0 + if (safeInsetBottom != tempSafeInset) { + safeInsetBottom = tempSafeInset + keyguardBlueprintInteractor + .get() + .refreshBlueprint(IntraBlueprintTransition.Type.DefaultTransition) + } + WindowInsets.CONSUMED + } } } @@ -91,12 +114,24 @@ constructor( constrainWidth(R.id.start_button, width) constrainHeight(R.id.start_button, height) connect(R.id.start_button, LEFT, PARENT_ID, LEFT, horizontalOffsetMargin) - connect(R.id.start_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin) + connect( + R.id.start_button, + BOTTOM, + PARENT_ID, + BOTTOM, + verticalOffsetMargin + safeInsetBottom + ) constrainWidth(R.id.end_button, width) constrainHeight(R.id.end_button, height) connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT, horizontalOffsetMargin) - connect(R.id.end_button, BOTTOM, PARENT_ID, BOTTOM, verticalOffsetMargin) + connect( + R.id.end_button, + BOTTOM, + PARENT_ID, + BOTTOM, + verticalOffsetMargin + safeInsetBottom + ) // The constraint set visibility for start and end button are default visible, set to // ignore so the view's own initial visibility (invisible) is used diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt index 2c1e75e2263b..d8b50133949d 100644 --- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/LockscreenSceneViewModel.kt @@ -100,7 +100,13 @@ constructor( // Swiping down from the top edge goes to QS (or shade if in split shade mode). swipeDownFromTop(pointerCount = 1) to quickSettingsIfSingleShade, - swipeDownFromTop(pointerCount = 2) to quickSettingsIfSingleShade, + swipeDownFromTop(pointerCount = 2) to + // TODO(b/338577208): Remove 'Dual' once we add Dual Shade invocation zones. + if (shadeMode is ShadeMode.Dual) { + Scenes.QuickSettingsShade + } else { + quickSettingsIfSingleShade + }, // Swiping down, not from the edge, always navigates to the shade scene. swipeDown(pointerCount = 1) to shadeSceneKey, diff --git a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt index 9e6c5520d1b7..b276f532e874 100644 --- a/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt +++ b/packages/SystemUI/src/com/android/systemui/log/FaceAuthenticationLogger.kt @@ -201,6 +201,10 @@ constructor( ) } + fun addLockoutResetCallbackDone() { + logBuffer.log(TAG, DEBUG, {}, { "addlockoutResetCallback done" }) + } + fun authRequested(uiEvent: FaceAuthUiEvent) { logBuffer.log( TAG, diff --git a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt index 0c07c0543246..89e4760615f0 100644 --- a/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt +++ b/packages/SystemUI/src/com/android/systemui/model/SceneContainerPlugin.kt @@ -85,7 +85,10 @@ constructor( { it.scene == Scenes.NotificationsShade || it.scene == Scenes.Shade }, - SYSUI_STATE_QUICK_SETTINGS_EXPANDED to { it.scene == Scenes.QuickSettings }, + SYSUI_STATE_QUICK_SETTINGS_EXPANDED to + { + it.scene == Scenes.QuickSettingsShade || it.scene == Scenes.QuickSettings + }, SYSUI_STATE_BOUNCER_SHOWING to { it.scene == Scenes.Bouncer }, SYSUI_STATE_STATUS_BAR_KEYGUARD_SHOWING to { diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt index ba01776aaf8f..f677ec1b31bb 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt @@ -14,7 +14,7 @@ * limitations under the License. */ -package com.android.systemui.shade.ui.viewmodel +package com.android.systemui.notifications.ui.viewmodel import com.android.compose.animation.scene.Back import com.android.compose.animation.scene.SceneKey @@ -23,6 +23,7 @@ import com.android.compose.animation.scene.UserAction import com.android.compose.animation.scene.UserActionResult import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.SharingStarted diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt new file mode 100644 index 000000000000..d48d55dd9918 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt @@ -0,0 +1,57 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.qs.ui.viewmodel + +import com.android.compose.animation.scene.Back +import com.android.compose.animation.scene.SceneKey +import com.android.compose.animation.scene.Swipe +import com.android.compose.animation.scene.UserAction +import com.android.compose.animation.scene.UserActionResult +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application +import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.stateIn + +/** Models UI state and handles user input for the Quick Settings Shade scene. */ +@SysUISingleton +class QuickSettingsShadeSceneViewModel +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + overlayShadeViewModel: OverlayShadeViewModel, +) { + val destinationScenes: StateFlow<Map<UserAction, UserActionResult>> = + overlayShadeViewModel.backgroundScene + .map(::destinationScenes) + .stateIn( + scope = applicationScope, + started = SharingStarted.WhileSubscribed(), + initialValue = destinationScenes(overlayShadeViewModel.backgroundScene.value), + ) + + private fun destinationScenes(backgroundScene: SceneKey): Map<UserAction, UserActionResult> { + return mapOf( + Swipe.Up to backgroundScene, + Back to backgroundScene, + ) + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt index aa8ecfc41fc1..28569d817279 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt @@ -63,7 +63,8 @@ interface KeyguardlessSceneContainerFrameworkModule { sceneKeys = listOfNotNull( Scenes.Gone, - Scenes.QuickSettings, + Scenes.QuickSettings.takeUnless { DualShade.isEnabled }, + Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled }, Scenes.NotificationsShade.takeIf { DualShade.isEnabled }, Scenes.Shade.takeUnless { DualShade.isEnabled }, ), @@ -73,7 +74,8 @@ interface KeyguardlessSceneContainerFrameworkModule { Scenes.Gone to 0, Scenes.NotificationsShade to 1.takeIf { DualShade.isEnabled }, Scenes.Shade to 1.takeUnless { DualShade.isEnabled }, - Scenes.QuickSettings to 2, + Scenes.QuickSettingsShade to 2.takeIf { DualShade.isEnabled }, + Scenes.QuickSettings to 2.takeUnless { DualShade.isEnabled }, ) .filterValues { it != null } .mapValues { checkNotNull(it.value) } diff --git a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt index 551aa124727d..dbe0342a5319 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/SceneContainerFrameworkModule.kt @@ -41,6 +41,7 @@ import dagger.multibindings.IntoMap LockscreenSceneModule::class, QuickSettingsSceneModule::class, ShadeSceneModule::class, + QuickSettingsShadeSceneModule::class, NotificationsShadeSceneModule::class, ], ) @@ -71,7 +72,8 @@ interface SceneContainerFrameworkModule { Scenes.Communal, Scenes.Lockscreen, Scenes.Bouncer, - Scenes.QuickSettings, + Scenes.QuickSettings.takeUnless { DualShade.isEnabled }, + Scenes.QuickSettingsShade.takeIf { DualShade.isEnabled }, Scenes.NotificationsShade.takeIf { DualShade.isEnabled }, Scenes.Shade.takeUnless { DualShade.isEnabled }, ), @@ -83,7 +85,8 @@ interface SceneContainerFrameworkModule { Scenes.Communal to 1, Scenes.NotificationsShade to 2.takeIf { DualShade.isEnabled }, Scenes.Shade to 2.takeUnless { DualShade.isEnabled }, - Scenes.QuickSettings to 3, + Scenes.QuickSettingsShade to 3.takeIf { DualShade.isEnabled }, + Scenes.QuickSettings to 3.takeUnless { DualShade.isEnabled }, Scenes.Bouncer to 4, ) .filterValues { it != null } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt index ace449136fad..6bcd92316106 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/SceneContainerOcclusionInteractor.kt @@ -127,6 +127,7 @@ constructor( Scenes.Lockscreen -> true Scenes.NotificationsShade -> false Scenes.QuickSettings -> false + Scenes.QuickSettingsShade -> false Scenes.Shade -> false else -> error("SceneKey \"$this\" doesn't have a mapping for canBeOccluded!") } diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt index de3b87aebcd3..9c2b992c0de6 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/WindowRootViewVisibilityInteractor.kt @@ -78,13 +78,16 @@ constructor( is ObservableTransitionState.Idle -> state.currentScene == Scenes.Shade || state.currentScene == Scenes.NotificationsShade || + state.currentScene == Scenes.QuickSettingsShade || state.currentScene == Scenes.Lockscreen is ObservableTransitionState.Transition -> state.toScene == Scenes.Shade || state.toScene == Scenes.NotificationsShade || + state.toScene == Scenes.QuickSettingsShade || state.toScene == Scenes.Lockscreen || state.fromScene == Scenes.Shade || state.fromScene == Scenes.NotificationsShade || + state.fromScene == Scenes.QuickSettingsShade || state.fromScene == Scenes.Lockscreen } } diff --git a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt index 08f1be908afd..6d139da99345 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/shared/model/Scenes.kt @@ -47,19 +47,45 @@ object Scenes { * overlay UI. * * It's used only in the dual shade configuration, where there are two separate shades: one for - * notifications (this scene) and another for quick settings (where a separate scene is used). + * notifications (this scene) and another for [QuickSettingsShade]. * * It's not used in the single/accordion configuration (swipe down once to reveal the shade, - * swipe down again the to expand quick settings) and for the "split" shade configuration (on + * swipe down again the to expand quick settings) or in the "split" shade configuration (on * large screens or unfolded foldables, where notifications and quick settings are shown * side-by-side in their own columns). */ @JvmField val NotificationsShade = SceneKey("notifications_shade") - /** The quick settings scene shows the quick setting tiles. */ + /** + * The quick settings scene shows the quick setting tiles. + * + * This scene is used for single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings). + * + * For the "split" shade configuration (on large screens or unfolded foldables, where + * notifications and quick settings are shown side-by-side in their own columns), the [Shade] + * scene is used]. + * + * For the dual shade configuration, where there are two separate shades: one for notifications + * and one for quick settings, [NotificationsShade] and [QuickSettingsShade] scenes are used + * respectively. + */ @JvmField val QuickSettings = SceneKey("quick_settings") /** + * The quick settings shade scene shows the quick setting tiles as an overlay UI. + * + * It's used only in the dual shade configuration, where there are two separate shades: one for + * quick settings (this scene) and another for [NotificationsShade]. + * + * It's not used in the single/accordion configuration (swipe down once to reveal the shade, + * swipe down again the to expand quick settings) or in the "split" shade configuration (on + * large screens or unfolded foldables, where notifications and quick settings are shown + * side-by-side in their own columns). + */ + @JvmField val QuickSettingsShade = SceneKey("quick_settings_shade") + + /** * The shade is the scene that shows a scrollable list of notifications and the minimized * version of quick settings (AKA "quick quick settings" or "QQS"). * diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt index e4435ccd205c..b0af7f9ce072 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/GoneSceneViewModel.kt @@ -60,6 +60,16 @@ constructor( )] = UserActionResult(Scenes.QuickSettings) } + // TODO(b/338577208): Remove this once we add Dual Shade invocation zones. + if (shadeMode is ShadeMode.Dual) { + this[ + Swipe( + pointerCount = 2, + fromSource = Edge.Top, + direction = SwipeDirection.Down, + )] = UserActionResult(Scenes.QuickSettingsShade) + } + this[Swipe(direction = SwipeDirection.Down)] = UserActionResult( if (shadeMode is ShadeMode.Dual) Scenes.NotificationsShade else Scenes.Shade diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt index 7fbede475e69..09c80b09a388 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/viewmodel/SceneContainerViewModel.kt @@ -127,6 +127,7 @@ constructor( Scenes.NotificationsShade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.Shade -> Classifier.NOTIFICATION_DRAG_DOWN Scenes.QuickSettings -> Classifier.QUICK_SETTINGS + Scenes.QuickSettingsShade -> Classifier.QUICK_SETTINGS else -> null } diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt new file mode 100644 index 000000000000..0bc280c6c1e5 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/TransitioningIconDrawable.kt @@ -0,0 +1,134 @@ +/* + * 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.screenshot.ui + +import android.animation.ValueAnimator +import android.content.res.ColorStateList +import android.graphics.Canvas +import android.graphics.ColorFilter +import android.graphics.drawable.Drawable +import androidx.core.animation.doOnEnd +import java.util.Objects + +/** */ +class TransitioningIconDrawable : Drawable() { + // The drawable for the current icon of this view. During icon transitions, this is the one + // being animated out. + private var drawable: Drawable? = null + + // The incoming new icon. Only populated during transition animations (when drawable is also + // non-null). + private var enteringDrawable: Drawable? = null + private var colorFilter: ColorFilter? = null + private var tint: ColorStateList? = null + private var alpha = 255 + + private var transitionAnimator = + ValueAnimator.ofFloat(0f, 1f).also { it.doOnEnd { onTransitionComplete() } } + + /** + * Set the drawable to be displayed, potentially animating the transition from one icon to the + * next. + */ + fun setIcon(incomingDrawable: Drawable?) { + if (Objects.equals(drawable, incomingDrawable) && !transitionAnimator.isRunning) { + return + } + + incomingDrawable?.colorFilter = colorFilter + incomingDrawable?.setTintList(tint) + + if (drawable == null) { + // No existing icon drawn, just show the new one without a transition + drawable = incomingDrawable + invalidateSelf() + return + } + + if (enteringDrawable != null) { + // There's already an entrance animation happening, just update the entering icon, not + // maintaining a queue or anything. + enteringDrawable = incomingDrawable + return + } + + // There was already an icon, need to animate between icons. + enteringDrawable = incomingDrawable + transitionAnimator.setCurrentFraction(0f) + transitionAnimator.start() + invalidateSelf() + } + + override fun draw(canvas: Canvas) { + // Scale the old one down, scale the new one up. + drawable?.let { + val scale = + if (transitionAnimator.isRunning) { + 1f - transitionAnimator.animatedFraction + } else { + 1f + } + drawScaledDrawable(it, canvas, scale) + } + enteringDrawable?.let { + val scale = transitionAnimator.animatedFraction + drawScaledDrawable(it, canvas, scale) + } + + if (transitionAnimator.isRunning) { + invalidateSelf() + } + } + + private fun drawScaledDrawable(drawable: Drawable, canvas: Canvas, scale: Float) { + drawable.bounds = getBounds() + canvas.save() + canvas.scale( + scale, + scale, + (drawable.intrinsicWidth / 2).toFloat(), + (drawable.intrinsicHeight / 2).toFloat() + ) + drawable.draw(canvas) + canvas.restore() + } + + private fun onTransitionComplete() { + drawable = enteringDrawable + enteringDrawable = null + invalidateSelf() + } + + override fun setTintList(tint: ColorStateList?) { + super.setTintList(tint) + drawable?.setTintList(tint) + enteringDrawable?.setTintList(tint) + this.tint = tint + } + + override fun setAlpha(alpha: Int) { + this.alpha = alpha + } + + override fun setColorFilter(colorFilter: ColorFilter?) { + this.colorFilter = colorFilter + drawable?.colorFilter = colorFilter + enteringDrawable?.colorFilter = colorFilter + } + + override fun getOpacity(): Int = alpha +} diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt index 3c5a0ec107f8..750bd530d9b2 100644 --- a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt @@ -21,6 +21,7 @@ import android.widget.ImageView import android.widget.LinearLayout import android.widget.TextView import com.android.systemui.res.R +import com.android.systemui.screenshot.ui.TransitioningIconDrawable import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel object ActionButtonViewBinder { @@ -28,7 +29,13 @@ object ActionButtonViewBinder { fun bind(view: View, viewModel: ActionButtonViewModel) { val iconView = view.requireViewById<ImageView>(R.id.overlay_action_chip_icon) val textView = view.requireViewById<TextView>(R.id.overlay_action_chip_text) - iconView.setImageDrawable(viewModel.appearance.icon) + if (iconView.drawable == null) { + iconView.setImageDrawable(TransitioningIconDrawable()) + } + val drawable = iconView.drawable as? TransitioningIconDrawable + // Note we never re-bind a view to a different ActionButtonViewModel, different view + // models would remove/create separate views. + drawable?.setIcon(viewModel.appearance.icon) textView.text = viewModel.appearance.label setMargins(iconView, textView, viewModel.appearance.label?.isNotEmpty() ?: false) if (viewModel.onClicked != null) { diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java index 4a636d28aa88..3eb43895c7ae 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationShadeWindowControllerImpl.java @@ -412,9 +412,9 @@ public class NotificationShadeWindowControllerImpl implements NotificationShadeW } if (state.bouncerShowing) { - mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + mLpChanged.inputFeatures |= LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } else { - mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; + mLpChanged.inputFeatures &= ~LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt index 5cc30bd8c27f..43e782c44206 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt @@ -203,7 +203,11 @@ constructor( } override fun expandToQs() { - sceneInteractor.changeScene(Scenes.QuickSettings, "ShadeController.animateExpandQs") + val shadeMode = shadeInteractor.shadeMode.value + sceneInteractor.changeScene( + if (shadeMode is ShadeMode.Dual) Scenes.QuickSettingsShade else Scenes.QuickSettings, + "ShadeController.animateExpandQs" + ) } override fun setVisibilityListener(listener: ShadeVisibilityListener) { diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java index 96a50f7686b9..70632d5aa27a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java @@ -679,6 +679,7 @@ public class StatusBarStateControllerImpl implements Scenes.Shade, StatusBarState.SHADE_LOCKED, Scenes.NotificationsShade, StatusBarState.SHADE_LOCKED, Scenes.QuickSettings, StatusBarState.SHADE_LOCKED, + Scenes.QuickSettingsShade, StatusBarState.SHADE_LOCKED, Scenes.Gone, StatusBarState.SHADE ); diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java index 3bd873580fbb..d669369103ab 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayout.java @@ -1185,6 +1185,11 @@ public class NotificationStackScrollLayout } @Override + public void setCurrentGestureOverscrollConsumer(@Nullable Consumer<Boolean> consumer) { + mScrollViewFields.setCurrentGestureOverscrollConsumer(consumer); + } + + @Override public void setStackHeightConsumer(@Nullable Consumer<Float> consumer) { mScrollViewFields.setStackHeightConsumer(consumer); } @@ -3403,6 +3408,8 @@ public class NotificationStackScrollLayout boolean isUpOrCancel = action == ACTION_UP || action == ACTION_CANCEL; if (mSendingTouchesToSceneFramework) { mController.sendTouchToSceneFramework(ev); + mScrollViewFields.sendCurrentGestureOverscroll( + getExpandedInThisMotion() && !isUpOrCancel); } else if (!isUpOrCancel) { // if this is the first touch being sent to the scene framework, // convert it into a synthetic DOWN event. @@ -3410,6 +3417,7 @@ public class NotificationStackScrollLayout MotionEvent downEvent = MotionEvent.obtain(ev); downEvent.setAction(MotionEvent.ACTION_DOWN); mController.sendTouchToSceneFramework(downEvent); + mScrollViewFields.sendCurrentGestureOverscroll(getExpandedInThisMotion()); downEvent.recycle(); } @@ -3428,6 +3436,14 @@ public class NotificationStackScrollLayout downEvent.recycle(); } + // Only when scene container is enabled, mark that we are being dragged so that we start + // dispatching the rest of the gesture to scene container. + void startOverscrollAfterExpanding() { + SceneContainerFlag.isUnexpectedlyInLegacyMode(); + getExpandHelper().finishExpanding(); + setIsBeingDragged(true); + } + @Override public boolean onGenericMotionEvent(MotionEvent event) { if (!isScrollingEnabled() @@ -5545,6 +5561,11 @@ public class NotificationStackScrollLayout return mExpandingNotification; } + @VisibleForTesting + void setExpandingNotification(boolean isExpanding) { + mExpandingNotification = isExpanding; + } + boolean getDisallowScrollingInThisMotion() { return mDisallowScrollingInThisMotion; } @@ -5557,6 +5578,11 @@ public class NotificationStackScrollLayout return mExpandedInThisMotion; } + @VisibleForTesting + void setExpandedInThisMotion(boolean expandedInThisMotion) { + mExpandedInThisMotion = expandedInThisMotion; + } + boolean getDisallowDismissInThisMotion() { return mDisallowDismissInThisMotion; } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java index 5bb3f4273c52..3011bc284961 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutController.java @@ -206,6 +206,7 @@ public class NotificationStackScrollLayoutController implements Dumpable { private final SeenNotificationsInteractor mSeenNotificationsInteractor; private final KeyguardTransitionRepository mKeyguardTransitionRepo; private NotificationStackScrollLayout mView; + private TouchHandler mTouchHandler; private NotificationSwipeHelper mSwipeHelper; @Nullable private Boolean mHistoryEnabled; @@ -807,7 +808,8 @@ public class NotificationStackScrollLayoutController implements Dumpable { mView.setStackStateLogger(mStackStateLogger); mView.setController(this); mView.setLogger(mLogger); - mView.setTouchHandler(new TouchHandler()); + mTouchHandler = new TouchHandler(); + mView.setTouchHandler(mTouchHandler); mView.setResetUserExpandedStatesRunnable(mNotificationsController::resetUserExpandedStates); mView.setActivityStarter(mActivityStarter); mView.setClearAllAnimationListener(this::onAnimationEnd); @@ -1793,6 +1795,11 @@ public class NotificationStackScrollLayoutController implements Dumpable { } } + @VisibleForTesting + TouchHandler getTouchHandler() { + return mTouchHandler; + } + @Override public void dump(@NonNull PrintWriter pw, @NonNull String[] args) { pw.println("mMaxAlphaFromView=" + mMaxAlphaFromView); @@ -2043,7 +2050,14 @@ public class NotificationStackScrollLayoutController implements Dumpable { expandingNotification = mView.isExpandingNotification(); if (mView.getExpandedInThisMotion() && !expandingNotification && wasExpandingBefore && !mView.getDisallowScrollingInThisMotion()) { - mView.dispatchDownEventToScroller(ev); + // We need to dispatch the overscroll differently when Scene Container is on, + // since NSSL no longer controls its own scroll. + if (SceneContainerFlag.isEnabled() && !isCancelOrUp) { + mView.startOverscrollAfterExpanding(); + return true; + } else { + mView.dispatchDownEventToScroller(ev); + } } } boolean horizontalSwipeWantsIt = false; diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt index edac5ede1e91..a3827c140214 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ScrollViewFields.kt @@ -51,6 +51,11 @@ class ScrollViewFields { */ var syntheticScrollConsumer: Consumer<Float>? = null /** + * When a gesture is consumed internally by NSSL but needs to be handled by other elements (such + * as the notif scrim) as overscroll, we can notify the placeholder through here. + */ + var currentGestureOverscrollConsumer: Consumer<Boolean>? = null + /** * Any time the stack height is recalculated, it should be updated here to be used by the * placeholder */ @@ -64,6 +69,9 @@ class ScrollViewFields { /** send the [syntheticScroll] to the [syntheticScrollConsumer], if present. */ fun sendSyntheticScroll(syntheticScroll: Float) = syntheticScrollConsumer?.accept(syntheticScroll) + /** send [isCurrentGestureOverscroll] to the [currentGestureOverscrollConsumer], if present. */ + fun sendCurrentGestureOverscroll(isCurrentGestureOverscroll: Boolean) = + currentGestureOverscrollConsumer?.accept(isCurrentGestureOverscroll) /** send the [stackHeight] to the [stackHeightConsumer], if present. */ fun sendStackHeight(stackHeight: Float) = stackHeightConsumer?.accept(stackHeight) /** send the [headsUpHeight] to the [headsUpHeightConsumer], if present. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt index 8a9da69079d4..920c9c213060 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationViewHeightRepository.kt @@ -43,4 +43,10 @@ class NotificationViewHeightRepository @Inject constructor() { * necessary to scroll up to keep expanding the notification. */ val syntheticScroll = MutableStateFlow(0f) + + /** + * Whether the current touch gesture is overscroll. If true, it means the NSSL has already + * consumed part of the gesture. + */ + val isCurrentGestureOverscroll = MutableStateFlow(false) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt index b8660bad78d9..b94da388cef4 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractor.kt @@ -105,6 +105,13 @@ constructor( */ val syntheticScroll: Flow<Float> = viewHeightRepository.syntheticScroll.asStateFlow() + /** + * Whether the current touch gesture is overscroll. If true, it means the NSSL has already + * consumed part of the gesture. + */ + val isCurrentGestureOverscroll: Flow<Boolean> = + viewHeightRepository.isCurrentGestureOverscroll.asStateFlow() + /** Sets the alpha to apply to the NSSL for the brightness mirror */ fun setAlphaForBrightnessMirror(alpha: Float) { placeholderRepository.alphaForBrightnessMirror.value = alpha @@ -146,6 +153,11 @@ constructor( viewHeightRepository.syntheticScroll.value = delta } + /** Sets whether the current touch gesture is overscroll. */ + fun setCurrentGestureOverscroll(isOverscroll: Boolean) { + viewHeightRepository.isCurrentGestureOverscroll.value = isOverscroll + } + fun setConstrainedAvailableSpace(height: Int) { placeholderRepository.constrainedAvailableSpace.value = height } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt index a56384dc47dd..2c8884504c32 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/view/NotificationScrollView.kt @@ -51,6 +51,8 @@ interface NotificationScrollView { /** Set a consumer for synthetic scroll events */ fun setSyntheticScrollConsumer(consumer: Consumer<Float>?) + /** Set a consumer for current gesture overscroll events */ + fun setCurrentGestureOverscrollConsumer(consumer: Consumer<Boolean>?) /** Set a consumer for stack height changed events */ fun setStackHeightConsumer(consumer: Consumer<Float>?) /** Set a consumer for heads up height changed events */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt index 4476d87fbcf6..26f7ad775f1d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt @@ -89,10 +89,12 @@ constructor( launchAndDispose { view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer) + view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer) view.setStackHeightConsumer(viewModel.stackHeightConsumer) view.setHeadsUpHeightConsumer(viewModel.headsUpHeightConsumer) DisposableHandle { view.setSyntheticScrollConsumer(null) + view.setCurrentGestureOverscrollConsumer(null) view.setStackHeightConsumer(null) view.setHeadsUpHeightConsumer(null) } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt index 8b1b93bfb0e5..b2184db0879d 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt @@ -145,6 +145,12 @@ constructor( /** Receives the amount (px) that the stack should scroll due to internal expansion. */ val syntheticScrollConsumer: (Float) -> Unit = stackAppearanceInteractor::setSyntheticScroll + /** + * Receives whether the current touch gesture is overscroll as it has already been consumed by + * the stack. + */ + val currentGestureOverscrollConsumer: (Boolean) -> Unit = + stackAppearanceInteractor::setCurrentGestureOverscroll /** Receives the height of the contents of the notification stack. */ val stackHeightConsumer: (Float) -> Unit = stackAppearanceInteractor::setStackHeight /** Receives the height of the heads up notification. */ diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt index 486e305af05f..11eaf54efe47 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt @@ -111,6 +111,13 @@ constructor( val syntheticScroll: Flow<Float> = interactor.syntheticScroll.dumpWhileCollecting("syntheticScroll") + /** + * Whether the current touch gesture is overscroll. If true, it means the NSSL has already + * consumed part of the gesture. + */ + val isCurrentGestureOverscroll: Flow<Boolean> = + interactor.isCurrentGestureOverscroll.dumpWhileCollecting("isCurrentGestureOverScroll") + /** Sets whether the notification stack is scrolled to the top. */ fun setScrolledToTop(scrolledToTop: Boolean) { interactor.setScrolledToTop(scrolledToTop) diff --git a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt index 37be1c6aa73d..a817b31070a1 100644 --- a/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt +++ b/packages/SystemUI/src/com/android/systemui/user/data/repository/UserRepository.kt @@ -18,6 +18,7 @@ package com.android.systemui.user.data.repository import android.annotation.SuppressLint +import android.annotation.UserIdInt import android.content.Context import android.content.pm.UserInfo import android.os.UserHandle @@ -107,6 +108,22 @@ interface UserRepository { fun isSimpleUserSwitcher(): Boolean fun isUserSwitcherEnabled(): Boolean + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt suspend fun getMainUserId(): Int? } @SysUISingleton @@ -239,6 +256,10 @@ constructor( return _userSwitcherSettings.value.isUserSwitcherEnabled } + override suspend fun getMainUserId(): Int? { + return withContext(backgroundDispatcher) { manager.mainUser?.identifier } + } + private suspend fun getSettings(): UserSwitcherSettingsModel { return withContext(backgroundDispatcher) { val isSimpleUserSwitcher = diff --git a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt index 38b381ac543e..a5728d061d48 100644 --- a/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/user/domain/interactor/SelectedUserInteractor.kt @@ -2,17 +2,27 @@ package com.android.systemui.user.domain.interactor import android.annotation.UserIdInt import android.content.pm.UserInfo +import android.os.UserManager import com.android.keyguard.KeyguardUpdateMonitor import com.android.systemui.Flags.refactorGetCurrentUser import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.user.data.repository.UserRepository +import com.google.common.util.concurrent.ListenableFuture import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.map +import kotlinx.coroutines.guava.future /** Encapsulates business logic to interact the selected user */ @SysUISingleton -class SelectedUserInteractor @Inject constructor(private val repository: UserRepository) { +class SelectedUserInteractor +@Inject +constructor( + @Application private val applicationScope: CoroutineScope, + private val repository: UserRepository +) { /** Flow providing the ID of the currently selected user. */ val selectedUser = repository.selectedUserInfo.map { it.id }.distinctUntilChanged() @@ -38,4 +48,41 @@ class SelectedUserInteractor @Inject constructor(private val repository: UserRep KeyguardUpdateMonitor.getCurrentUser() } } + + /** + * Returns the user ID of the "main user" of the device. This user may have access to certain + * features which are limited to at most one user. There will never be more than one main user + * on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + @UserIdInt + suspend fun getMainUserId(): Int? { + return repository.getMainUserId() + } + + /** + * Returns a [ListenableFuture] for the user ID of the "main user" of the device. This user may + * have access to certain features which are limited to at most one user. There will never be + * more than one main user on a device. + * + * <p>Currently, on most form factors the first human user on the device will be the main user; + * in the future, the concept may be transferable, so a different user (or even no user at all) + * may be designated the main user instead. On other form factors there might not be a main + * user. + * + * <p> When the device doesn't have a main user, this will return {@code null}. + * + * @see [UserManager.getMainUser] + */ + fun getMainUserIdAsync(): ListenableFuture<Int?> { + return applicationScope.future { getMainUserId() } + } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt index 19d9c3f125b7..3eec3d91c809 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/domain/interactor/AudioOutputInteractor.kt @@ -32,7 +32,6 @@ import com.android.systemui.volume.domain.model.AudioOutputDevice import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope -import com.android.systemui.volume.panel.shared.model.filterData import javax.inject.Inject import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope @@ -69,14 +68,9 @@ constructor( communicationDevice?.toAudioOutputDevice() } } else { - mediaOutputInteractor.defaultActiveMediaSession - .filterData() - .flatMapLatest { - localMediaRepositoryFactory - .create(it?.packageName) - .currentConnectedDevice - } - .map { mediaDevice -> mediaDevice?.toAudioOutputDevice() } + mediaOutputInteractor.currentConnectedDevice.map { mediaDevice -> + mediaDevice?.toAudioOutputDevice() + } } } .map { it ?: AudioOutputDevice.Unknown } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt index 22c053099ac5..199bc3b78dd2 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputActionsInteractor.kt @@ -33,15 +33,15 @@ constructor( private val mediaOutputDialogManager: MediaOutputDialogManager, ) { - fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable) { + fun onBarClick(sessionWithPlaybackState: SessionWithPlaybackState?, expandable: Expandable?) { if (sessionWithPlaybackState?.isPlaybackActive == true) { mediaOutputDialogManager.createAndShowWithController( sessionWithPlaybackState.session.packageName, false, - expandable.dialogController() + expandable?.dialogController() ) } else { - mediaOutputDialogManager.createAndShowForSystemRouting(expandable.dialogController()) + mediaOutputDialogManager.createAndShowForSystemRouting(expandable?.dialogController()) } } diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt index b974f90191e9..b00829e48404 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaOutputInteractor.kt @@ -19,10 +19,12 @@ package com.android.systemui.volume.panel.component.mediaoutput.domain.interacto import android.content.pm.PackageManager import android.media.VolumeProvider import android.media.session.MediaController +import android.os.Handler import android.util.Log import com.android.settingslib.media.MediaDevice import com.android.settingslib.volume.data.repository.LocalMediaRepository import com.android.settingslib.volume.data.repository.MediaControllerRepository +import com.android.settingslib.volume.data.repository.stateChanges import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions @@ -36,14 +38,15 @@ import kotlin.coroutines.CoroutineContext import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.SharedFlow import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.flow.distinctUntilChanged import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf import kotlinx.coroutines.flow.flowOn import kotlinx.coroutines.flow.map -import kotlinx.coroutines.flow.shareIn +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.onStart import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.withContext @@ -58,21 +61,31 @@ constructor( @VolumePanelScope private val coroutineScope: CoroutineScope, @Background private val backgroundCoroutineContext: CoroutineContext, mediaControllerRepository: MediaControllerRepository, + @Background private val backgroundHandler: Handler, ) { private val activeMediaControllers: Flow<MediaControllers> = mediaControllerRepository.activeSessions + .flatMapLatest { activeSessions -> + activeSessions + .map { activeSession -> activeSession.stateChanges() } + .merge() + .map { activeSessions } + .onStart { emit(activeSessions) } + } .map { getMediaControllers(it) } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn(coroutineScope, SharingStarted.Eagerly, MediaControllers(null, null)) /** [MediaDeviceSessions] that contains currently active sessions. */ val activeMediaDeviceSessions: Flow<MediaDeviceSessions> = - activeMediaControllers.map { - MediaDeviceSessions( - local = it.local?.mediaDeviceSession(), - remote = it.remote?.mediaDeviceSession() - ) - } + activeMediaControllers + .map { + MediaDeviceSessions( + local = it.local?.mediaDeviceSession(), + remote = it.remote?.mediaDeviceSession() + ) + } + .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSessions(null, null)) /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */ val defaultActiveMediaSession: StateFlow<Result<MediaDeviceSession?>> = @@ -89,13 +102,17 @@ constructor( .flowOn(backgroundCoroutineContext) .stateIn(coroutineScope, SharingStarted.Eagerly, Result.Loading()) - private val localMediaRepository: SharedFlow<LocalMediaRepository> = + private val localMediaRepository: Flow<LocalMediaRepository> = defaultActiveMediaSession .filterData() .map { it?.packageName } .distinctUntilChanged() .map { localMediaRepositoryFactory.create(it) } - .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1) + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + localMediaRepositoryFactory.create(null) + ) /** Currently connected [MediaDevice]. */ val currentConnectedDevice: Flow<MediaDevice?> = @@ -134,21 +151,33 @@ constructor( } if (!remoteMediaSessions.contains(controller.packageName)) { remoteMediaSessions.add(controller.packageName) - if (remoteController == null) { - remoteController = controller - } + remoteController = chooseController(remoteController, controller) } } MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> { if (controller.packageName in remoteMediaSessions) continue - if (localController != null) continue - localController = controller + localController = chooseController(localController, controller) } } } return MediaControllers(local = localController, remote = remoteController) } + private fun chooseController( + currentController: MediaController?, + newController: MediaController, + ): MediaController { + if (currentController == null) { + return newController + } + val isNewControllerActive = newController.playbackState?.isActive == true + val isCurrentControllerActive = currentController.playbackState?.isActive == true + if (isNewControllerActive && !isCurrentControllerActive) { + return newController + } + return currentController + } + private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? { return MediaDeviceSession( packageName = packageName, @@ -160,6 +189,14 @@ constructor( ) } + private fun MediaController?.stateChanges(): Flow<MediaController?> { + if (this == null) { + return flowOf(null) + } + + return stateChanges(backgroundHandler).map { this }.onStart { emit(this@stateChanges) } + } + private data class MediaControllers( val local: MediaController?, val remote: MediaController?, diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt index 192e0ec76132..be3a529d9a75 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModel.kt @@ -143,7 +143,7 @@ constructor( null, ) - fun onBarClick(expandable: Expandable) { + fun onBarClick(expandable: Expandable?) { uiEventLogger.log(VolumePanelUiEvent.VOLUME_PANEL_MEDIA_OUTPUT_CLICKED) val result = sessionWithPlaybackState.value actionsInteractor.onBarClick((result as? Result.Data)?.data, expandable) diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt index f5b5261de139..bcaad01f1a24 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardBlueprintRepositoryTest.kt @@ -19,20 +19,24 @@ package com.android.systemui.keyguard.data.repository +import android.os.fakeExecutorHandler import androidx.test.filters.SmallTest import com.android.systemui.SysuiTestCase import com.android.systemui.common.ui.data.repository.ConfigurationRepository +import com.android.systemui.concurrency.fakeExecutor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint -import com.android.systemui.keyguard.ui.view.layout.blueprints.SplitShadeKeyguardBlueprint +import com.android.systemui.keyguard.ui.view.layout.blueprints.DefaultKeyguardBlueprint.Companion.DEFAULT import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.testScope import com.android.systemui.testKosmos import com.android.systemui.util.ThreadAssert +import com.android.systemui.util.mockito.whenever import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.ExperimentalCoroutinesApi import kotlinx.coroutines.test.StandardTestDispatcher import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test @@ -46,32 +50,31 @@ import org.mockito.MockitoAnnotations class KeyguardBlueprintRepositoryTest : SysuiTestCase() { private lateinit var underTest: KeyguardBlueprintRepository @Mock lateinit var configurationRepository: ConfigurationRepository + @Mock lateinit var defaultLockscreenBlueprint: DefaultKeyguardBlueprint @Mock lateinit var threadAssert: ThreadAssert - private val testScope = TestScope(StandardTestDispatcher()) private val kosmos: Kosmos = testKosmos() @Before fun setup() { MockitoAnnotations.initMocks(this) - underTest = kosmos.keyguardBlueprintRepository - } - - @Test - fun testApplyBlueprint_DefaultLayout() { - testScope.runTest { - val blueprint by collectLastValue(underTest.blueprint) - underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT) - assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint) + with(kosmos) { + whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT) + underTest = + KeyguardBlueprintRepository( + setOf(defaultLockscreenBlueprint), + fakeExecutorHandler, + threadAssert, + ) } } @Test - fun testApplyBlueprint_SplitShadeLayout() { + fun testApplyBlueprint_DefaultLayout() { testScope.runTest { val blueprint by collectLastValue(underTest.blueprint) - underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID) - assertThat(blueprint).isEqualTo(kosmos.splitShadeBlueprint) + underTest.applyBlueprint(defaultLockscreenBlueprint) + assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint) } } @@ -80,22 +83,33 @@ class KeyguardBlueprintRepositoryTest : SysuiTestCase() { testScope.runTest { val blueprint by collectLastValue(underTest.blueprint) underTest.refreshBlueprint() - assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint) + assertThat(blueprint).isEqualTo(defaultLockscreenBlueprint) } } @Test - fun testTransitionToDefaultLayout_validId() { - assertThat(underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)).isTrue() - } - - @Test - fun testTransitionToSplitShadeLayout_validId() { - assertThat(underTest.applyBlueprint(SplitShadeKeyguardBlueprint.ID)).isTrue() + fun testTransitionToLayout_validId() { + assertThat(underTest.applyBlueprint(DEFAULT)).isTrue() } @Test fun testTransitionToLayout_invalidId() { assertThat(underTest.applyBlueprint("abc")).isFalse() } + + @Test + fun testTransitionToSameBlueprint_refreshesBlueprint() = + with(kosmos) { + testScope.runTest { + val transition by collectLastValue(underTest.refreshTransition) + fakeExecutor.runAllReady() + runCurrent() + + underTest.applyBlueprint(defaultLockscreenBlueprint) + fakeExecutor.runAllReady() + runCurrent() + + assertThat(transition).isNotNull() + } + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt index 9ccf2121b8d2..f32e7757328f 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/KeyguardSurfaceBehindInteractorTest.kt @@ -274,4 +274,27 @@ class KeyguardSurfaceBehindInteractorTest : SysuiTestCase() { runCurrent() assertThat(isAnimatingSurface).isFalse() } + + @Test + fun notificationLaunchFalse_isAnimatingSurfaceFalse() = + testScope.runTest { + val isAnimatingSurface by collectLastValue(underTest.isAnimatingSurface) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.STARTED, + ) + ) + transitionRepository.sendTransitionStep( + TransitionStep( + from = KeyguardState.AOD, + to = KeyguardState.LOCKSCREEN, + transitionState = TransitionState.FINISHED, + ) + ) + kosmos.notificationLaunchAnimationInteractor.setIsLaunchAnimationRunning(false) + runCurrent() + assertThat(isAnimatingSurface).isFalse() + } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt index 8a0613f9b010..dbf6a29073a6 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/view/layout/KeyguardBlueprintCommandListenerTest.kt @@ -66,19 +66,25 @@ class KeyguardBlueprintCommandListenerTest : SysuiTestCase() { fun testHelp() { command().execute(pw, listOf("help")) verify(pw, atLeastOnce()).println(anyString()) - verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString()) + verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString()) } @Test fun testBlank() { command().execute(pw, listOf()) verify(pw, atLeastOnce()).println(anyString()) - verify(keyguardBlueprintInteractor, never()).transitionOrRefreshBlueprint(anyString()) + verify(keyguardBlueprintInteractor, never()).transitionToBlueprint(anyString()) } @Test fun testValidArg() { command().execute(pw, listOf("fake")) - verify(keyguardBlueprintInteractor).transitionOrRefreshBlueprint("fake") + verify(keyguardBlueprintInteractor).transitionToBlueprint("fake") + } + + @Test + fun testValidArg_Int() { + command().execute(pw, listOf("1")) + verify(keyguardBlueprintInteractor).transitionToBlueprint(1) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java index a66a1360f8f8..f262df1d875a 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutControllerTest.java @@ -24,6 +24,7 @@ import static com.android.systemui.statusbar.notification.stack.NotificationStac import static kotlinx.coroutines.flow.FlowKt.emptyFlow; +import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyBoolean; import static org.mockito.ArgumentMatchers.anyInt; @@ -43,6 +44,7 @@ import android.platform.test.annotations.DisableFlags; import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import android.testing.TestableLooper; +import android.view.MotionEvent; import android.view.View; import android.view.ViewTreeObserver; @@ -51,11 +53,14 @@ import androidx.test.filters.SmallTest; import com.android.internal.logging.MetricsLogger; import com.android.internal.logging.UiEventLogger; import com.android.internal.logging.nano.MetricsProto; +import com.android.systemui.ExpandHelper; import com.android.systemui.SysuiTestCase; import com.android.systemui.bouncer.domain.interactor.PrimaryBouncerInteractor; import com.android.systemui.classifier.FalsingCollectorFake; import com.android.systemui.classifier.FalsingManagerFake; import com.android.systemui.dump.DumpManager; +import com.android.systemui.flags.DisableSceneContainer; +import com.android.systemui.flags.EnableSceneContainer; import com.android.systemui.keyguard.data.repository.KeyguardTransitionRepository; import com.android.systemui.keyguard.shared.model.KeyguardState; import com.android.systemui.keyguard.shared.model.TransitionStep; @@ -171,6 +176,7 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { @Mock private NotificationListViewBinder mViewBinder; @Mock private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController; + @Mock private ExpandHelper mExpandHelper; @Captor private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor; @@ -895,6 +901,50 @@ public class NotificationStackScrollLayoutControllerTest extends SysuiTestCase { verify(mSensitiveNotificationProtectionController).registerSensitiveStateListener(any()); } + @Test + @EnableSceneContainer + public void onTouchEvent_stopExpandingNotification_sceneContainerEnabled() { + boolean touchHandled = stopExpandingNotification(); + + verify(mNotificationStackScrollLayout).startOverscrollAfterExpanding(); + verify(mNotificationStackScrollLayout, never()).dispatchDownEventToScroller(any()); + assertTrue(touchHandled); + } + + @Test + @DisableSceneContainer + public void onTouchEvent_stopExpandingNotification_sceneContainerDisabled() { + stopExpandingNotification(); + + verify(mNotificationStackScrollLayout, never()).startOverscrollAfterExpanding(); + verify(mNotificationStackScrollLayout).dispatchDownEventToScroller(any()); + } + + private boolean stopExpandingNotification() { + when(mNotificationStackScrollLayout.getExpandHelper()).thenReturn(mExpandHelper); + when(mNotificationStackScrollLayout.getIsExpanded()).thenReturn(true); + when(mNotificationStackScrollLayout.getExpandedInThisMotion()).thenReturn(true); + when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(true); + + when(mExpandHelper.onTouchEvent(any())).thenAnswer(i -> { + when(mNotificationStackScrollLayout.isExpandingNotification()).thenReturn(false); + return false; + }); + + initController(/* viewIsAttached= */ true); + NotificationStackScrollLayoutController.TouchHandler touchHandler = + mController.getTouchHandler(); + + return touchHandler.onTouchEvent(MotionEvent.obtain( + /* downTime= */ 0, + /* eventTime= */ 0, + MotionEvent.ACTION_DOWN, + 0, + 0, + /* metaState= */ 0 + )); + } + private LogMaker logMatcher(int category, int type) { return argThat(new LogMatcher(category, type)); } diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java index 939d0556e347..0c0a2a59d9bc 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java @@ -207,6 +207,7 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { .thenReturn(mNotificationRoundnessManager); mStackScroller.setController(mStackScrollLayoutController); mStackScroller.setShelf(mNotificationShelf); + when(mStackScroller.getExpandHelper()).thenReturn(mExpandHelper); doNothing().when(mGroupExpansionManager).collapseGroups(); doNothing().when(mExpandHelper).cancelImmediately(); @@ -1139,6 +1140,14 @@ public class NotificationStackScrollLayoutTest extends SysuiTestCase { assertFalse(mStackScroller.mHeadsUpAnimatingAway); } + @Test + @EnableSceneContainer + public void finishExpanding_sceneContainerEnabled() { + mStackScroller.startOverscrollAfterExpanding(); + verify(mStackScroller.getExpandHelper()).finishExpanding(); + assertTrue(mStackScroller.getIsBeingDragged()); + } + private MotionEvent captureTouchSentToSceneFramework() { ArgumentCaptor<MotionEvent> captor = ArgumentCaptor.forClass(MotionEvent.class); verify(mStackScrollLayoutController).sendTouchToSceneFramework(captor.capture()); diff --git a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt index 140e919d613f..78d4f02e74a9 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorTest.kt @@ -7,6 +7,7 @@ import com.android.systemui.SysuiTestCase import com.android.systemui.user.data.repository.FakeUserRepository import com.google.common.truth.Truth.assertThat import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.test.TestCoroutineScope import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -23,7 +24,7 @@ class SelectedUserInteractorTest : SysuiTestCase() { @Before fun setUp() { userRepository.setUserInfos(USER_INFOS) - underTest = SelectedUserInteractor(userRepository) + underTest = SelectedUserInteractor(TestCoroutineScope(), userRepository) } @Test diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt index 7eef704c1622..c7b06b6bcd8f 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardDismissInteractorFactory.kt @@ -58,7 +58,7 @@ object KeyguardDismissInteractorFactory { bouncerRepository: FakeKeyguardBouncerRepository = FakeKeyguardBouncerRepository(), keyguardUpdateMonitor: KeyguardUpdateMonitor = mock(KeyguardUpdateMonitor::class.java), powerRepository: FakePowerRepository = FakePowerRepository(), - userRepository: FakeUserRepository = FakeUserRepository(), + userRepository: FakeUserRepository = FakeUserRepository() ): WithDependencies { val primaryBouncerInteractor = PrimaryBouncerInteractor( @@ -95,7 +95,11 @@ object KeyguardDismissInteractorFactory { PowerInteractorFactory.create( repository = powerRepository, ) - val selectedUserInteractor = SelectedUserInteractor(repository = userRepository) + val selectedUserInteractor = + SelectedUserInteractor( + applicationScope = testScope.backgroundScope, + repository = userRepository + ) return WithDependencies( trustRepository = trustRepository, keyguardRepository = keyguardRepository, diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt index 5e71227ad49a..872eba06961e 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt @@ -18,6 +18,7 @@ package com.android.systemui.shade.ui.viewmodel import com.android.systemui.kosmos.Kosmos import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by Kosmos.Fixture { diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt new file mode 100644 index 000000000000..8c5ff1d5d216 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt @@ -0,0 +1,29 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.shade.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel + +val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by + Kosmos.Fixture { + QuickSettingsShadeSceneViewModel( + applicationScope = applicationCoroutineScope, + overlayShadeViewModel = overlayShadeViewModel, + ) + } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt index 3e9ae4d2e354..1f2ecb7d172d 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/data/repository/FakeUserRepository.kt @@ -37,7 +37,7 @@ import kotlinx.coroutines.yield class FakeUserRepository @Inject constructor() : UserRepository { companion object { // User id to represent a non system (human) user id. We presume this is the main user. - private const val MAIN_USER_ID = 10 + const val MAIN_USER_ID = 10 private const val DEFAULT_SELECTED_USER = 0 private val DEFAULT_SELECTED_USER_INFO = @@ -84,6 +84,10 @@ class FakeUserRepository @Inject constructor() : UserRepository { override var isRefreshUsersPaused: Boolean = false + override suspend fun getMainUserId(): Int? { + return MAIN_USER_ID + } + var refreshUsersCallCount: Int = 0 private set diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt index 89672f109657..9dddfcdddc8c 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/user/domain/interactor/SelectedUserInteractorKosmos.kt @@ -17,6 +17,8 @@ package com.android.systemui.user.domain.interactor import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.applicationCoroutineScope import com.android.systemui.user.data.repository.userRepository -val Kosmos.selectedUserInteractor by Kosmos.Fixture { SelectedUserInteractor(userRepository) } +val Kosmos.selectedUserInteractor by + Kosmos.Fixture { SelectedUserInteractor(applicationCoroutineScope, userRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt index 5db17243c4e3..546a797482a5 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt @@ -19,8 +19,10 @@ package com.android.systemui.volume import android.content.packageManager import android.content.pm.ApplicationInfo import android.media.AudioAttributes +import android.media.VolumeProvider import android.media.session.MediaController import android.media.session.MediaSession +import android.media.session.PlaybackState import com.android.systemui.kosmos.Kosmos import com.android.systemui.util.mockito.any import com.android.systemui.util.mockito.eq @@ -28,6 +30,18 @@ import com.android.systemui.util.mockito.mock import com.android.systemui.util.mockito.whenever private const val LOCAL_PACKAGE = "local.test.pkg" +var Kosmos.localPlaybackInfo by + Kosmos.Fixture { + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, + VolumeProvider.VOLUME_CONTROL_ABSOLUTE, + 10, + 3, + AudioAttributes.Builder().build(), + "", + ) + } +var Kosmos.localPlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() } var Kosmos.localMediaController: MediaController by Kosmos.Fixture { val appInfo: ApplicationInfo = mock { @@ -39,22 +53,25 @@ var Kosmos.localMediaController: MediaController by val localSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) mock { whenever(packageName).thenReturn(LOCAL_PACKAGE) - whenever(playbackInfo) - .thenReturn( - MediaController.PlaybackInfo( - MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL, - 0, - 0, - 0, - AudioAttributes.Builder().build(), - "", - ) - ) + whenever(playbackInfo).thenReturn(localPlaybackInfo) + whenever(playbackState).thenReturn(localPlaybackStateBuilder.build()) whenever(sessionToken).thenReturn(localSessionToken) } } private const val REMOTE_PACKAGE = "remote.test.pkg" +var Kosmos.remotePlaybackInfo by + Kosmos.Fixture { + MediaController.PlaybackInfo( + MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, + VolumeProvider.VOLUME_CONTROL_ABSOLUTE, + 10, + 7, + AudioAttributes.Builder().build(), + "", + ) + } +var Kosmos.remotePlaybackStateBuilder by Kosmos.Fixture { PlaybackState.Builder() } var Kosmos.remoteMediaController: MediaController by Kosmos.Fixture { val appInfo: ApplicationInfo = mock { @@ -66,17 +83,8 @@ var Kosmos.remoteMediaController: MediaController by val remoteSessionToken: MediaSession.Token = MediaSession.Token(0, mock {}) mock { whenever(packageName).thenReturn(REMOTE_PACKAGE) - whenever(playbackInfo) - .thenReturn( - MediaController.PlaybackInfo( - MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE, - 0, - 0, - 0, - AudioAttributes.Builder().build(), - "", - ) - ) + whenever(playbackInfo).thenReturn(remotePlaybackInfo) + whenever(playbackState).thenReturn(remotePlaybackStateBuilder.build()) whenever(sessionToken).thenReturn(remoteSessionToken) } } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt index fa3a19bae655..d74355894581 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaOutputKosmos.kt @@ -30,13 +30,12 @@ import com.android.systemui.util.mockito.whenever import com.android.systemui.volume.data.repository.FakeLocalMediaRepository import com.android.systemui.volume.data.repository.FakeMediaControllerRepository import com.android.systemui.volume.panel.component.mediaoutput.data.repository.FakeLocalMediaRepositoryFactory -import com.android.systemui.volume.panel.component.mediaoutput.data.repository.LocalMediaRepositoryFactory import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputActionsInteractor import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() } -val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by +val Kosmos.localMediaRepositoryFactory by Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } } val Kosmos.mediaOutputActionsInteractor by @@ -55,6 +54,7 @@ val Kosmos.mediaOutputInteractor by testScope.backgroundScope, testScope.testScheduler, mediaControllerRepository, + Handler(TestableLooper.get(testCase).looper), ) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt index 1b3480c423e4..9c902cf57fde 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/FakeLocalMediaRepositoryFactory.kt @@ -18,9 +18,15 @@ package com.android.systemui.volume.panel.component.mediaoutput.data.repository import com.android.settingslib.volume.data.repository.LocalMediaRepository -class FakeLocalMediaRepositoryFactory( - val provider: (packageName: String?) -> LocalMediaRepository -) : LocalMediaRepositoryFactory { +class FakeLocalMediaRepositoryFactory(private val defaultProvider: () -> LocalMediaRepository) : + LocalMediaRepositoryFactory { - override fun create(packageName: String?): LocalMediaRepository = provider(packageName) + private val repositories = mutableMapOf<String, LocalMediaRepository>() + + fun setLocalMediaRepository(packageName: String, localMediaRepository: LocalMediaRepository) { + repositories[packageName] = localMediaRepository + } + + override fun create(packageName: String?): LocalMediaRepository = + repositories[packageName] ?: defaultProvider() } diff --git a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java index 9b83ede09da4..95206212c99d 100644 --- a/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java +++ b/services/core/java/com/android/server/am/SettingsToPropertiesMapper.java @@ -128,6 +128,7 @@ public class SettingsToPropertiesMapper { "aoc", "app_widgets", "arc_next", + "art_mainline", "avic", "biometrics", "biometrics_framework", diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java index 3c702b4125b7..b1976cd0d13b 100644 --- a/services/core/java/com/android/server/pm/UserManagerService.java +++ b/services/core/java/com/android/server/pm/UserManagerService.java @@ -7155,6 +7155,7 @@ public class UserManagerService extends IUserManager.Stub { synchronized (mUsersLock) { pw.println(" Boot user: " + mBootUser); } + pw.println("Can add private profile: "+ canAddPrivateProfile(currentUserId)); pw.println(); pw.println("Number of listeners for"); diff --git a/services/core/java/com/android/server/power/batterysaver/flags.aconfig b/services/core/java/com/android/server/power/batterysaver/flags.aconfig index fa29dc19b555..059c4a491e31 100644 --- a/services/core/java/com/android/server/power/batterysaver/flags.aconfig +++ b/services/core/java/com/android/server/power/batterysaver/flags.aconfig @@ -3,7 +3,7 @@ container: "system" flag { name: "update_auto_turn_on_notification_string_and_action" - namespace: "battery_saver" + namespace: "power_optimization" description: "Improve the string and hightligh settings item for battery saver auto-turn-on notification" bug: "336960905" metadata { diff --git a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java index d20b3b22d778..f8eb78914857 100644 --- a/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java +++ b/services/core/java/com/android/server/wallpaper/WallpaperManagerService.java @@ -3646,7 +3646,8 @@ public class WallpaperManagerService extends IWallpaperManager.Stub } // System wallpaper does not support multiple displays, attach this display to // the fallback wallpaper. - if (mFallbackWallpaper != null) { + if (mFallbackWallpaper != null && mFallbackWallpaper + .connection != null) { final DisplayConnector connector = mFallbackWallpaper .connection.getDisplayConnectorOrCreate(displayId); if (connector == null) return; diff --git a/services/core/java/com/android/server/wm/ActivityClientController.java b/services/core/java/com/android/server/wm/ActivityClientController.java index c5683f31f3e7..fe4522acc148 100644 --- a/services/core/java/com/android/server/wm/ActivityClientController.java +++ b/services/core/java/com/android/server/wm/ActivityClientController.java @@ -99,7 +99,6 @@ import android.view.RemoteAnimationDefinition; import android.window.SizeConfigurationBuckets; import android.window.TransitionInfo; -import com.android.internal.annotations.VisibleForTesting; import com.android.internal.app.AssistUtils; import com.android.internal.policy.IKeyguardDismissCallback; import com.android.internal.protolog.common.ProtoLog; @@ -109,9 +108,6 @@ import com.android.server.pm.KnownPackages; import com.android.server.pm.pkg.AndroidPackage; import com.android.server.uri.GrantUri; import com.android.server.uri.NeededUriGrants; -import com.android.server.utils.quota.Categorizer; -import com.android.server.utils.quota.Category; -import com.android.server.utils.quota.CountQuotaTracker; import com.android.server.vr.VrManagerInternal; /** @@ -127,13 +123,6 @@ class ActivityClientController extends IActivityClientController.Stub { private final ActivityTaskSupervisor mTaskSupervisor; private final Context mContext; - // Prevent malicious app abusing the Activity#setPictureInPictureParams API - @VisibleForTesting CountQuotaTracker mSetPipAspectRatioQuotaTracker; - // Limit to 60 times / minute - private static final int SET_PIP_ASPECT_RATIO_LIMIT = 60; - // The timeWindowMs here can not be smaller than QuotaTracker#MIN_WINDOW_SIZE_MS - private static final long SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS = 60_000; - /** Wrapper around VoiceInteractionServiceManager. */ private AssistUtils mAssistUtils; @@ -1046,25 +1035,6 @@ class ActivityClientController extends IActivityClientController.Stub { + ": Current activity does not support picture-in-picture."); } - // Rate limit how frequent an app can request aspect ratio change via - // Activity#setPictureInPictureParams - final int userId = UserHandle.getCallingUserId(); - if (mSetPipAspectRatioQuotaTracker == null) { - mSetPipAspectRatioQuotaTracker = new CountQuotaTracker(mContext, - Categorizer.SINGLE_CATEGORIZER); - mSetPipAspectRatioQuotaTracker.setCountLimit(Category.SINGLE_CATEGORY, - SET_PIP_ASPECT_RATIO_LIMIT, SET_PIP_ASPECT_RATIO_TIME_WINDOW_MS); - } - if (r.pictureInPictureArgs.hasSetAspectRatio() - && params.hasSetAspectRatio() - && !r.pictureInPictureArgs.getAspectRatio().equals( - params.getAspectRatio()) - && !mSetPipAspectRatioQuotaTracker.noteEvent( - userId, r.packageName, "setPipAspectRatio")) { - throw new IllegalStateException(caller - + ": Too many PiP aspect ratio change requests from " + r.packageName); - } - final float minAspectRatio = mContext.getResources().getFloat( com.android.internal.R.dimen.config_pictureInPictureMinAspectRatio); final float maxAspectRatio = mContext.getResources().getFloat( diff --git a/services/core/java/com/android/server/wm/InputConfigAdapter.java b/services/core/java/com/android/server/wm/InputConfigAdapter.java index ef1b02d8accc..119fafde6f77 100644 --- a/services/core/java/com/android/server/wm/InputConfigAdapter.java +++ b/services/core/java/com/android/server/wm/InputConfigAdapter.java @@ -58,8 +58,8 @@ class InputConfigAdapter { LayoutParams.INPUT_FEATURE_SPY, InputConfig.SPY, false /* inverted */), new FlagMapping( - LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING, - InputConfig.SENSITIVE_FOR_TRACING, false /* inverted */)); + LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, + InputConfig.SENSITIVE_FOR_PRIVACY, false /* inverted */)); @InputConfigFlags private static final int INPUT_FEATURE_TO_CONFIG_MASK = diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java index 56e5d76ac4e0..5f136727008b 100644 --- a/services/core/java/com/android/server/wm/Task.java +++ b/services/core/java/com/android/server/wm/Task.java @@ -3176,6 +3176,11 @@ class Task extends TaskFragment { mTaskId, snapshot); } + void onSnapshotInvalidated() { + mAtmService.getTaskChangeNotificationController().notifyTaskSnapshotInvalidated(mTaskId); + } + + TaskDescription getTaskDescription() { return mTaskDescription; } diff --git a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java index 9324e29daafb..21e7a8d63773 100644 --- a/services/core/java/com/android/server/wm/TaskChangeNotificationController.java +++ b/services/core/java/com/android/server/wm/TaskChangeNotificationController.java @@ -61,6 +61,7 @@ class TaskChangeNotificationController { private static final int NOTIFY_ACTIVITY_ROTATED_MSG = 26; private static final int NOTIFY_TASK_MOVED_TO_BACK_LISTENERS_MSG = 27; private static final int NOTIFY_LOCK_TASK_MODE_CHANGED_MSG = 28; + private static final int NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG = 29; // Delay in notifying task stack change listeners (in millis) private static final int NOTIFY_TASK_STACK_CHANGE_LISTENERS_DELAY = 100; @@ -150,6 +151,9 @@ class TaskChangeNotificationController { private final TaskStackConsumer mNotifyTaskSnapshotChanged = (l, m) -> { l.onTaskSnapshotChanged(m.arg1, (TaskSnapshot) m.obj); }; + private final TaskStackConsumer mNotifyTaskSnapshotInvalidated = (l, m) -> { + l.onTaskSnapshotInvalidated(m.arg1); + }; private final TaskStackConsumer mNotifyTaskDisplayChanged = (l, m) -> { l.onTaskDisplayChanged(m.arg1, m.arg2); @@ -271,6 +275,9 @@ class TaskChangeNotificationController { case NOTIFY_LOCK_TASK_MODE_CHANGED_MSG: forAllRemoteListeners(mNotifyLockTaskModeChanged, msg); break; + case NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG: + forAllRemoteListeners(mNotifyTaskSnapshotInvalidated, msg); + break; } if (msg.obj instanceof SomeArgs) { ((SomeArgs) msg.obj).recycle(); @@ -485,6 +492,16 @@ class TaskChangeNotificationController { } /** + * Notify listeners that the snapshot of a task is invalidated. + */ + void notifyTaskSnapshotInvalidated(int taskId) { + final Message msg = mHandler.obtainMessage(NOTIFY_TASK_SNAPSHOT_INVALIDATED_LISTENERS_MSG, + taskId, 0 /* unused */); + forAllLocalListeners(mNotifyTaskSnapshotInvalidated, msg); + msg.sendToTarget(); + } + + /** * Notify listeners that an activity received a back press when there are no other activities * in the back stack. */ diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index d0a50d5c5c23..dbe3d369db7d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -44,6 +44,7 @@ import static android.os.Process.myUid; import static android.os.Trace.TRACE_TAG_WINDOW_MANAGER; import static android.permission.flags.Flags.sensitiveContentImprovements; import static android.permission.flags.Flags.sensitiveContentMetricsBugfix; +import static android.permission.flags.Flags.sensitiveContentRecentsScreenshotBugfix; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_FREEFORM_WINDOWS_SUPPORT; import static android.provider.Settings.Global.DEVELOPMENT_ENABLE_NON_RESIZABLE_MULTI_WINDOW; import static android.provider.Settings.Global.DEVELOPMENT_FORCE_DESKTOP_MODE_ON_EXTERNAL_DISPLAYS; @@ -68,7 +69,7 @@ import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER; import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WHEN_LOCKED; import static android.view.WindowManager.LayoutParams.FLAG_SLIPPERY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_NO_INPUT_CHANNEL; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.LAST_APPLICATION_WINDOW; @@ -8868,6 +8869,14 @@ public class WindowManagerService extends IWindowManager.Stub mRoot.forAllWindows((w) -> { if (w.isVisible()) { WindowManagerService.this.showToastIfBlockingScreenCapture(w); + } else if (sensitiveContentRecentsScreenshotBugfix() + && shouldInvalidateSnapshot(w)) { + final Task task = w.getTask(); + // preventing from showing up in starting window. + mTaskSnapshotController.removeAndDeleteSnapshot( + task.mTaskId, task.mUserId); + // Refresh TaskThumbnailCache + task.onSnapshotInvalidated(); } }, /* traverseTopToBottom= */ true); } @@ -8875,6 +8884,12 @@ public class WindowManagerService extends IWindowManager.Stub } } + private boolean shouldInvalidateSnapshot(WindowState w) { + return w.getTask() != null + && mSensitiveContentPackages.shouldBlockScreenCaptureForApp( + w.getOwningPackage(), w.getOwningUid(), w.getWindowToken()); + } + @Override public void removeBlockScreenCaptureForApps(ArraySet<PackageInfo> packageInfos) { synchronized (mGlobalLock) { @@ -9296,11 +9311,11 @@ public class WindowManagerService extends IWindowManager.Stub } } - // You can only use INPUT_FEATURE_SENSITIVE_FOR_TRACING on a trusted overlay. - if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_TRACING) != 0 && !isTrustedOverlay) { - Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_TRACING from '" + windowName + // You can only use INPUT_FEATURE_SENSITIVE_FOR_PRIVACY on a trusted overlay. + if ((inputFeatures & INPUT_FEATURE_SENSITIVE_FOR_PRIVACY) != 0 && !isTrustedOverlay) { + Slog.w(TAG, "Removing INPUT_FEATURE_SENSITIVE_FOR_PRIVACY from '" + windowName + "' because it isn't a trusted overlay"); - return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_TRACING; + return inputFeatures & ~INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; } return inputFeatures; } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java index 950ec77f5ba8..502607be7310 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BooleanPolicySerializer.java @@ -19,7 +19,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.BooleanPolicyValue; -import android.app.admin.PolicyKey; import android.util.Log; import com.android.modules.utils.TypedXmlPullParser; @@ -37,8 +36,7 @@ final class BooleanPolicySerializer extends PolicySerializer<Boolean> { private static final String TAG = "BooleanPolicySerializer"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull Boolean value) - throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull Boolean value) throws IOException { Objects.requireNonNull(value); serializer.attributeBoolean(/* namespace= */ null, ATTR_VALUE, value); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java index d24afabe95a4..a65c7e1870f3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/BundlePolicySerializer.java @@ -18,8 +18,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.app.admin.BundlePolicyValue; -import android.app.admin.PackagePolicyKey; -import android.app.admin.PolicyKey; import android.os.Bundle; import android.os.Parcelable; import android.util.Log; @@ -53,14 +51,8 @@ final class BundlePolicySerializer extends PolicySerializer<Bundle> { private static final String ATTR_TYPE_BUNDLE_ARRAY = "BA"; @Override - void saveToXml(@NonNull PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull Bundle value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull Bundle value) throws IOException { Objects.requireNonNull(value); - Objects.requireNonNull(policyKey); - if (!(policyKey instanceof PackagePolicyKey)) { - throw new IllegalArgumentException("policyKey is not of type " - + "PackagePolicyKey"); - } writeBundle(value, serializer); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java index 6303a1a8b860..01f56e07e157 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/ComponentNamePolicySerializer.java @@ -19,7 +19,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.ComponentNamePolicyValue; -import android.app.admin.PolicyKey; import android.content.ComponentName; import android.util.Log; @@ -37,8 +36,7 @@ final class ComponentNamePolicySerializer extends PolicySerializer<ComponentName private static final String ATTR_CLASS_NAME = "class-name"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull ComponentName value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull ComponentName value) throws IOException { Objects.requireNonNull(value); serializer.attribute( /* namespace= */ null, ATTR_PACKAGE_NAME, value.getPackageName()); diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java index f553a5ad1db2..dd173af9a93e 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyManagerService.java @@ -1985,11 +1985,6 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { CryptoTestHelper.runAndLogSelfTest(); } - public String[] getPersonalAppsForSuspension(@UserIdInt int userId) { - return PersonalAppsSuspensionHelper.forUser(mContext, userId) - .getPersonalAppsForSuspension(); - } - public long systemCurrentTimeMillis() { return System.currentTimeMillis(); } @@ -21610,9 +21605,12 @@ public class DevicePolicyManagerService extends IDevicePolicyManager.Stub { == HEADLESS_DEVICE_OWNER_MODE_SINGLE_USER; } - if (Flags.headlessSingleUserFixes() && mInjector.userManagerIsHeadlessSystemUserMode() - && isSingleUserMode && !mInjector.isChangeEnabled( - PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), caller.getUserId())) { + if (Flags.headlessSingleMinTargetSdk() + && mInjector.userManagerIsHeadlessSystemUserMode() + && isSingleUserMode + && !mInjector.isChangeEnabled( + PROVISION_SINGLE_USER_MODE, deviceAdmin.getPackageName(), + caller.getUserId())) { throw new IllegalStateException("Device admin is not targeting Android V."); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java index 45a2d2a7bda1..ebbf22cfef69 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/IntegerPolicySerializer.java @@ -19,7 +19,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.IntegerPolicyValue; -import android.app.admin.PolicyKey; import android.util.Log; import com.android.modules.utils.TypedXmlPullParser; @@ -37,8 +36,7 @@ final class IntegerPolicySerializer extends PolicySerializer<Integer> { private static final String ATTR_VALUE = "value"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull Integer value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull Integer value) throws IOException { Objects.requireNonNull(value); serializer.attributeInt(/* namespace= */ null, ATTR_VALUE, value); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java index 20bd2d75f846..13412d05aba3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/LockTaskPolicySerializer.java @@ -18,7 +18,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.app.admin.LockTaskPolicy; -import android.app.admin.PolicyKey; import android.util.Log; import com.android.modules.utils.TypedXmlPullParser; @@ -39,8 +38,8 @@ final class LockTaskPolicySerializer extends PolicySerializer<LockTaskPolicy> { private static final String ATTR_FLAGS = "flags"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull LockTaskPolicy value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull LockTaskPolicy value) + throws IOException { Objects.requireNonNull(value); serializer.attribute( /* namespace= */ null, diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java index 522c4b5e84be..c363e6626de3 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/LongPolicySerializer.java @@ -19,7 +19,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; import android.app.admin.LongPolicyValue; -import android.app.admin.PolicyKey; import android.util.Log; import com.android.modules.utils.TypedXmlPullParser; @@ -37,8 +36,7 @@ final class LongPolicySerializer extends PolicySerializer<Long> { private static final String ATTR_VALUE = "value"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull Long value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull Long value) throws IOException { Objects.requireNonNull(value); serializer.attributeLong(/* namespace= */ null, ATTR_VALUE, value); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java index 8cb511e8727c..7483b43baf13 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PersonalAppsSuspensionHelper.java @@ -37,7 +37,6 @@ import android.provider.Telephony; import android.text.TextUtils; import android.util.ArraySet; import android.util.IndentingPrintWriter; -import android.util.Log; import android.view.accessibility.AccessibilityManager; import android.view.accessibility.IAccessibilityManager; import android.view.inputmethod.InputMethodInfo; @@ -107,10 +106,6 @@ public final class PersonalAppsSuspensionHelper { for (final String pkg : unsuspendablePackages) { result.remove(pkg); } - - if (Log.isLoggable(LOG_TAG, Log.INFO)) { - Slogf.i(LOG_TAG, "Packages subject to suspension: %s", String.join(",", result)); - } return result.toArray(new String[0]); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java index 7a9fa0fb5658..9a73d5e76082 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java @@ -684,7 +684,7 @@ final class PolicyDefinition<V> { void savePolicyValueToXml(TypedXmlSerializer serializer, V value) throws IOException { - mPolicySerializer.saveToXml(mPolicyKey, serializer, value); + mPolicySerializer.saveToXml(serializer, value); } @Nullable diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java index eeb49765cc9d..4bf3ff4265d4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyEnforcerCallbacks.java @@ -375,6 +375,7 @@ final class PolicyEnforcerCallbacks { private static void suspendPersonalAppsInPackageManager(Context context, int userId) { final String[] appsToSuspend = PersonalAppsSuspensionHelper.forUser(context, userId) .getPersonalAppsForSuspension(); + Slogf.i(LOG_TAG, "Suspending personal apps: %s", String.join(",", appsToSuspend)); final String[] failedApps = LocalServices.getService(PackageManagerInternal.class) .setPackagesSuspendedByAdmin(userId, appsToSuspend, true); if (!ArrayUtils.isEmpty(failedApps)) { diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java index 5af2fa285483..e83b031fd3c0 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicySerializer.java @@ -17,7 +17,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; -import android.app.admin.PolicyKey; import android.app.admin.PolicyValue; import com.android.modules.utils.TypedXmlPullParser; @@ -26,7 +25,6 @@ import com.android.modules.utils.TypedXmlSerializer; import java.io.IOException; abstract class PolicySerializer<V> { - abstract void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, @NonNull V value) - throws IOException; + abstract void saveToXml(TypedXmlSerializer serializer, @NonNull V value) throws IOException; abstract PolicyValue<V> readFromXml(TypedXmlPullParser parser); } diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java b/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java index 0265453eecc7..a9d65ac5c7d4 100644 --- a/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java +++ b/services/devicepolicy/java/com/android/server/devicepolicy/StringSetPolicySerializer.java @@ -18,7 +18,6 @@ package com.android.server.devicepolicy; import android.annotation.NonNull; import android.annotation.Nullable; -import android.app.admin.PolicyKey; import android.app.admin.PolicyValue; import android.app.admin.StringSetPolicyValue; import android.util.Log; @@ -35,8 +34,7 @@ final class StringSetPolicySerializer extends PolicySerializer<Set<String>> { private static final String ATTR_VALUES = "strings"; private static final String ATTR_VALUES_SEPARATOR = ";"; @Override - void saveToXml(PolicyKey policyKey, TypedXmlSerializer serializer, - @NonNull Set<String> value) throws IOException { + void saveToXml(TypedXmlSerializer serializer, @NonNull Set<String> value) throws IOException { Objects.requireNonNull(value); serializer.attribute( /* namespace= */ null, ATTR_VALUES, String.join(ATTR_VALUES_SEPARATOR, value)); diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java index 855c6582dfec..b4cc3434e013 100644 --- a/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java +++ b/services/tests/servicestests/src/com/android/server/devicepolicy/DevicePolicyManagerServiceTestable.java @@ -441,11 +441,6 @@ public class DevicePolicyManagerServiceTestable extends DevicePolicyManagerServi @Override public void runCryptoSelfTest() {} - @Override - public String[] getPersonalAppsForSuspension(int userId) { - return new String[]{}; - } - public void setSystemCurrentTimeMillis(long value) { mCurrentTimeMillis = value; } diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java index a78fc10bd893..7e6301fda872 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowManagerServiceTests.java @@ -21,13 +21,15 @@ import static android.app.WindowConfiguration.ACTIVITY_TYPE_HOME; import static android.app.WindowConfiguration.ACTIVITY_TYPE_STANDARD; import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM; import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN; +import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_IMPROVEMENTS; +import static android.permission.flags.Flags.FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX; import static android.permission.flags.Flags.FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION; import static android.view.Display.DEFAULT_DISPLAY; import static android.view.Display.FLAG_OWN_FOCUS; import static android.view.Display.INVALID_DISPLAY; import static android.view.WindowManager.LayoutParams.FLAG_NOT_FOCUSABLE; import static android.view.WindowManager.LayoutParams.FLAG_SECURE; -import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_TRACING; +import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SENSITIVE_FOR_PRIVACY; import static android.view.WindowManager.LayoutParams.INPUT_FEATURE_SPY; import static android.view.WindowManager.LayoutParams.INVALID_WINDOW_TYPE; import static android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY; @@ -1020,6 +1022,35 @@ public class WindowManagerServiceTests extends WindowTestsBase { } @Test + @RequiresFlagsEnabled( + {FLAG_SENSITIVE_NOTIFICATION_APP_PROTECTION, FLAG_SENSITIVE_CONTENT_IMPROVEMENTS, + FLAG_SENSITIVE_CONTENT_RECENTS_SCREENSHOT_BUGFIX}) + public void addBlockScreenCaptureForApps_appNotInForeground_invalidateSnapshot() { + spyOn(mWm.mTaskSnapshotController); + + // createAppWindow uses package name of "test" and uid of "0" + String testPackage = "test"; + int ownerId1 = 0; + + final Task task = createTask(mDisplayContent); + final WindowState win = createAppWindow(task, ACTIVITY_TYPE_STANDARD, "appWindow"); + mWm.mWindowMap.put(win.mClient.asBinder(), win); + final ActivityRecord activity = win.mActivityRecord; + activity.setVisibleRequested(false); + activity.setVisible(false); + win.setHasSurface(false); + + PackageInfo blockedPackage = new PackageInfo(testPackage, ownerId1); + ArraySet<PackageInfo> blockedPackages = new ArraySet(); + blockedPackages.add(blockedPackage); + + WindowManagerInternal wmInternal = LocalServices.getService(WindowManagerInternal.class); + wmInternal.addBlockScreenCaptureForApps(blockedPackages); + + verify(mWm.mTaskSnapshotController).removeAndDeleteSnapshot(anyInt(), eq(ownerId1)); + } + + @Test public void clearBlockedApps_clearsCache() { String testPackage = "test"; int ownerId1 = 20; @@ -1192,20 +1223,20 @@ public class WindowManagerServiceTests extends WindowTestsBase { final InputChannel inputChannel = new InputChannel(); mWm.grantInputChannel(session, callingUid, callingPid, DEFAULT_DISPLAY, surfaceControl, window, null /* hostInputToken */, FLAG_NOT_FOCUSABLE, 0 /* privateFlags */, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, TYPE_APPLICATION, null /* windowToken */, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, TYPE_APPLICATION, null /* windowToken */, inputTransferToken, "TestInputChannel", inputChannel); verify(mTransaction).setInputWindowInfo( eq(surfaceControl), - argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) == 0)); + argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) == 0)); mWm.updateInputChannel(inputChannel.getToken(), DEFAULT_DISPLAY, surfaceControl, FLAG_NOT_FOCUSABLE, PRIVATE_FLAG_TRUSTED_OVERLAY, - INPUT_FEATURE_SENSITIVE_FOR_TRACING, + INPUT_FEATURE_SENSITIVE_FOR_PRIVACY, null /* region */); verify(mTransaction).setInputWindowInfo( eq(surfaceControl), - argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_TRACING) != 0)); + argThat(h -> (h.inputConfig & InputConfig.SENSITIVE_FOR_PRIVACY) != 0)); } @RequiresFlagsDisabled(Flags.FLAG_ALWAYS_DRAW_MAGNIFICATION_FULLSCREEN_BORDER) diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java index fb854c5bda68..43b424fab907 100644 --- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java +++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java @@ -1235,12 +1235,6 @@ public class WindowOrganizerTests extends WindowTestsBase { assertNotNull(o.mInfo); assertNotNull(o.mInfo.pictureInPictureParams); - // Bypass the quota check, which causes NPE in current test setup. - if (mWm.mAtmService.mActivityClientController.mSetPipAspectRatioQuotaTracker != null) { - mWm.mAtmService.mActivityClientController.mSetPipAspectRatioQuotaTracker - .setEnabled(false); - } - final PictureInPictureParams p2 = new PictureInPictureParams.Builder() .setAspectRatio(new Rational(3, 4)).build(); mWm.mAtmService.mActivityClientController.setPictureInPictureParams(record.token, p2); |