Merge "Add KeyguardTransitionBootInteractor." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index ab5d503..0ccdf37 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -1043,12 +1043,20 @@
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 3c6ff28..f2228f9 100644
--- a/core/java/android/app/ITaskStackListener.aidl
+++ b/core/java/android/app/ITaskStackListener.aidl
@@ -145,6 +145,11 @@
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 0290cee..36f61fd 100644
--- a/core/java/android/app/TaskStackListener.java
+++ b/core/java/android/app/TaskStackListener.java
@@ -178,6 +178,9 @@
}
@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 9ef8b38..46c9e78 100644
--- a/core/java/android/app/admin/DeviceAdminInfo.java
+++ b/core/java/android/app/admin/DeviceAdminInfo.java
@@ -21,6 +21,7 @@
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 @@
* 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 @@
*
* <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 @@
}
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 18914e1..83daa45 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -303,3 +303,24 @@
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 205f1e9..45591d7 100644
--- a/core/java/android/content/pm/flags.aconfig
+++ b/core/java/android/content/pm/flags.aconfig
@@ -248,3 +248,11 @@
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 de5fc7f..58ef5ef 100644
--- a/core/java/android/view/InputWindowHandle.java
+++ b/core/java/android/view/InputWindowHandle.java
@@ -67,7 +67,7 @@
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 1d84375..ca8eaf9 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -12696,9 +12696,11 @@
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 @@
} 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 0bc2430..f22e8f5 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -4365,7 +4365,8 @@
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 @@
*
* @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 @@
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 7ac553c..3af1dd7 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.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 @@
@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 96860b0..2c35c9b 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 5e900f7..27b756d 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 e38038e..59092d4 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 @@
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 8bc3a30..a525877 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 @@
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 @@
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 12dce5b..8b2d0dd 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 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 @@
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 @@
private WindowContainerToken mPipTaskToken;
@Nullable
private SurfaceControl mPipLeash;
+ @Nullable
+ private Transitions.TransitionFinishCallback mFinishCallback;
public PipTransition(
Context context,
@@ -223,7 +227,6 @@
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 @@
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 @@
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 @@
.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 @@
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 @@
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 f7bc622..9a9c59e2 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 @@
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 b52faba..4c76fb0 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -44,8 +44,15 @@
/**
* @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 0db01e8..0000000
--- 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 d14b960..d9f82e8 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.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 @@
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 979cf3b..70d353d 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 @@
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 d08d97e..0546719 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 @@
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 706bd0a..36cd136 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.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 @@
@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 6f2c38c..60814bf 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 @@
.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 @@
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 6aac5bf3..48cd145 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 @@
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 930d0a1..99b2524 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.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 @@
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 d423d9f..6e5f32e 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 @@
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 f3f89e0..0000000
--- 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 68da143..bededf0 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.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.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 @@
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 5a6c0a1..dd7c036 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.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 @@
DisposableBroadcastReceiverAsUser(INTENT_FILTER, USER_HANDLE) {}
}
}
- composeTestRule.delay()
- assertThat(registeredBroadcastReceiver).isNotNull()
+ composeTestRule.waitUntil { registeredBroadcastReceiver != null }
}
@Test
@@ -91,9 +88,8 @@
}
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 70b38fe..cd747cc1 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 @@
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 29a89be..ecc92f8 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 @@
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 4e52c77..cb6a930 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 @@
}
/**
+ * 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 1069b71..9bf42f9 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 @@
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 @@
}
}
+ 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 @@
}
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 4055986..8dfeb55 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 @@
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 68f471d..d198136 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 @@
.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 e4ac9fe..195ccfc 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 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 @@
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 aa5a298..1f0da90 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 @@
@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 @@
}
@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 cef0835..6ff90ba 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 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 @@
@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 @@
* 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 @@
}
/**
+ * 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 65c5708..ed62ce7 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -362,6 +362,7 @@
"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 @@
"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 @@
"androidx.activity_activity-compose",
"androidx.compose.animation_animation-graphics",
"TraceurCommon",
+ "kotlinx_coroutines_guava",
],
}
@@ -622,6 +625,7 @@
"//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 796e391..d2e5a13 100644
--- a/packages/SystemUI/OWNERS
+++ b/packages/SystemUI/OWNERS
@@ -4,13 +4,13 @@
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 @@
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 @@
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 @@
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 0000000..3d7401d
--- /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 2ba78cf..fdf82ca 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 @@
*/
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 @@
// 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 @@
minScrimOffset() < currentHeight && currentHeight < maxScrimOffset
},
canScrollOnFling = true,
- onStart = { /* do nothing */},
+ onStart = { offsetAvailable -> onStart(offsetAvailable) },
onScroll = { offsetAvailable ->
val currentHeight = scrimOffset()
val amountConsumed =
@@ -68,10 +72,16 @@
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 6e987bd..16ae5b1 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.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.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 @@
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 @@
// 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 @@
// 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 @@
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 @@
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 1f03408..1c675e3 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.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 0000000..636c6c3
--- /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 6f2ed81..ded63a1 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 @@
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 9f5ab3c..a46f4e5 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 @@
// 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 33a59c2..dac37ee 100644
--- a/packages/SystemUI/compose/scene/OWNERS
+++ b/packages/SystemUI/compose/scene/OWNERS
@@ -2,12 +2,13 @@
# 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 6b289f3..b5e9313 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 @@
}
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 @@
} 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 @@
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 @@
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 @@
// 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 @@
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 @@
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 f78ed2f..6758990 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 @@
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 @@
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 @@
return PriorityNestedScrollConnection(
orientation = orientation,
canStartPreScroll = { offsetAvailable, offsetBeforeStart ->
- canChangeScene = offsetBeforeStart == 0f
+ canChangeScene =
+ if (isExternalOverscrollGesture()) false else offsetBeforeStart == 0f
val canInterceptSwipeTransition =
canChangeScene &&
@@ -950,7 +964,8 @@
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 ca64323..20742ee 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 @@
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 @@
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 @@
// 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 0000000..54c64fd
--- /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 5a2f85a..1fa6b3f7 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 @@
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) =
this then
NestedScrollToSceneElement(
@@ -82,6 +83,7 @@
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
private data class NestedScrollToSceneElement(
@@ -89,6 +91,7 @@
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 @@
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
override fun update(node: NestedScrollToSceneNode) {
@@ -104,6 +108,7 @@
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
}
@@ -121,6 +126,7 @@
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) : DelegatingNode() {
private var priorityNestedScrollConnection: PriorityNestedScrollConnection =
scenePriorityNestedScrollConnection(
@@ -128,6 +134,7 @@
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
private var nestedScrollNode: DelegatableNode =
@@ -150,6 +157,7 @@
orientation: Orientation,
topOrLeftBehavior: NestedScrollBehavior,
bottomOrRightBehavior: NestedScrollBehavior,
+ isExternalOverscrollGesture: () -> Boolean,
) {
// Clean up the old nested scroll connection
priorityNestedScrollConnection.reset()
@@ -162,6 +170,7 @@
orientation = orientation,
topOrLeftBehavior = topOrLeftBehavior,
bottomOrRightBehavior = bottomOrRightBehavior,
+ isExternalOverscrollGesture = isExternalOverscrollGesture,
)
nestedScrollNode =
nestedScrollModifierNode(
@@ -177,11 +186,13 @@
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 339868c..6fef33c 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 @@
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 c7c874c..11e711a 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 @@
fun Modifier.horizontalNestedScrollToScene(
leftBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
rightBehavior: NestedScrollBehavior = NestedScrollBehavior.EdgeNoPreview,
+ isExternalOverscrollGesture: () -> Boolean = { false },
): Modifier
/**
@@ -262,6 +263,7 @@
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 5fda77a..7f94f0d 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 @@
*/
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 @@
}
/**
- * 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 @@
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 @@
}
}
+ 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 b466143..0f6a1d2 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 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 @@
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 6bc397e..a4682ff 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 @@
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 1c9080f..802ab1f 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 @@
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 73393a1..79f126d 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 @@
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 1fd1bf4..8625482 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.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 @@
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 @@
}
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 @@
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 @@
}
@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 92e1b2c..e19dc96 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.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.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 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 @@
@get:Rule val rule = createComposeRule()
@Composable
- @OptIn(ExperimentalComposeUiApi::class)
private fun SceneScope.Element(
key: ElementKey,
size: Dp,
@@ -496,7 +494,6 @@
}
@Test
- @OptIn(ExperimentalFoundationApi::class)
fun elementModifierNodeIsRecycledInLazyLayouts() = runTest {
val nPages = 2
val pagerState = PagerState(currentPage = 0) { nPages }
@@ -654,8 +651,7 @@
}
}
- 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 @@
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 @@
}
// 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 @@
}
// 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 @@
}
}
- 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 @@
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 @@
}
// 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 @@
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 @@
}
// 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 @@
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 @@
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 @@
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 @@
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 @@
// 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 @@
)
rule.waitForIdle()
rule
- .onNode(isElement(TestElements.Foo, SceneC))
+ .onNode(isElement(TestElements.Foo, SceneB))
.assertPositionInRootIsEqualTo(
offsetInBToCWithInterruption.x,
offsetInBToCWithInterruption.y,
@@ -1140,7 +1137,24 @@
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 0000000..85d4165
--- /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 224ffe2..9523896 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.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 @@
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 93e94f8..f29d0a7 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.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 @@
// 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 @@
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 @@
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 @@
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 7836581..692c18b 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 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 @@
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 @@
// 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 @@
// 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 @@
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 @@
// 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 @@
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 @@
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 @@
// 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 @@
// 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 f034c18..1dd9322 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.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 @@
@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 @@
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 @@
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 @@
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 @@
// 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 @@
}
// 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 @@
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 @@
// 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 @@
}
// 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 @@
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 @@
}
// 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 @@
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 @@
}
// 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 @@
}
// 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 @@
layoutState(
transitions =
transitions {
- from(TestScenes.SceneA, to = TestScenes.SceneB) {
+ from(SceneA, to = SceneB) {
distance = FixedDistance(verticalSwipeDistance)
}
}
@@ -395,12 +378,12 @@
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 @@
}
// 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 @@
}
// 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 @@
}
// 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 @@
}
// 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 @@
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 @@
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 @@
// 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 @@
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 @@
}
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 c49a5b8..a609be4 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 @@
to: SceneKey,
current: () -> SceneKey = { from },
progress: () -> Float = { 0f },
+ progressVelocity: () -> Float = { 0f },
interruptionProgress: () -> Float = { 100f },
isInitiatedByUserInput: Boolean = false,
isUserInputOngoing: Boolean = false,
@@ -42,6 +43,8 @@
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 0000000..3489892
--- /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 447c280..c8717d8 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 @@
@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 @@
mSelectedUserInteractor,
deviceProvisionedController,
faceAuthAccessibilityDelegate,
+ devicePolicyManager,
keyguardTransitionInteractor,
{ primaryBouncerInteractor },
) {
@@ -934,6 +938,45 @@
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 81878aa..0c5e726 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.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 @@
}
@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 20beabb..2546f27 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.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 @@
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 @@
testScope.backgroundScope,
testDispatcher,
testDispatcher,
+ fakeExecutor,
sessionTracker,
uiEventLogger,
FaceAuthenticationLogger(logcatLogBuffer("DeviceEntryFaceAuthRepositoryLog")),
biometricSettingsRepository,
deviceEntryFingerprintAuthRepository,
- trustRepository,
keyguardRepository,
powerInteractor,
keyguardInteractor,
@@ -292,6 +294,7 @@
fun faceLockoutStatusIsPropagated() =
testScope.runTest {
initCollectors()
+ fakeExecutor.runAllReady()
verify(faceManager).addLockoutResetCallback(faceLockoutResetCallback.capture())
allPreconditionsToRunFaceAuthAreTrue()
@@ -1177,6 +1180,7 @@
}
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
similarity index 97%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
index abc684c..5661bd3 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.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/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
similarity index 94%
copy from packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
copy to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
index abc684c..034c2e9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.qs.ui.viewmodel
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
@@ -32,6 +32,7 @@
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
@@ -46,14 +47,14 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
-class NotificationsShadeSceneViewModelTest : SysuiTestCase() {
+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.notificationsShadeSceneViewModel
+ private val underTest = kosmos.quickSettingsShadeSceneViewModel
@Test
fun upTransitionSceneKey_deviceLocked_lockscreen() =
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 66f7416..d6e3879 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_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.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 @@
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 @@
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 0000000..35e4047
--- /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 632196c..2af2602 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.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 @@
@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 0000000..9e86ced
--- /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 a5b44e5..0a1f2a8 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 0000000..33ad2cd
--- /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 6bfd088..2ba72e3 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 a7a6d5b..a1daebd 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 c613afb..473719fa 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 @@
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 @@
}
@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 @@
}
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 91fb688..87a90b5 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardSecurityContainerController.java
@@ -35,12 +35,14 @@
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.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 @@
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 @@
SelectedUserInteractor selectedUserInteractor,
DeviceProvisionedController deviceProvisionedController,
FaceAuthAccessibilityDelegate faceAuthAccessibilityDelegate,
+ DevicePolicyManager devicePolicyManager,
KeyguardTransitionInteractor keyguardTransitionInteractor,
Lazy<PrimaryBouncerInteractor> primaryBouncerInteractor,
Provider<DeviceEntryInteractor> deviceEntryInteractor
@@ -495,6 +502,7 @@
mKeyguardTransitionInteractor = keyguardTransitionInteractor;
mDeviceProvisionedController = deviceProvisionedController;
mPrimaryBouncerInteractor = primaryBouncerInteractor;
+ mDevicePolicyManager = devicePolicyManager;
}
@Override
@@ -1105,35 +1113,36 @@
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 @@
}
}
+ @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 57c1fd0..42896a4 100644
--- a/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
+++ b/packages/SystemUI/src/com/android/systemui/ExpandHelper.java
@@ -569,6 +569,11 @@
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 5df7fc9..fcba425 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 @@
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 b269967..8efc66d 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 @@
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.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.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.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 @@
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 @@
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 @@
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 @@
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 @@
}
void resetActionChips() {
- for (OverlayActionChip chip : mActionChips) {
+ for (View chip : mActionChips) {
mActionContainer.removeView(chip);
}
mActionChips.clear();
@@ -437,7 +491,12 @@
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 @@
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 ff9fba4..740a93e 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 @@
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 @@
*/
@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 0781451..85e2bdb 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 @@
class LongPressHandlingView(
context: Context,
attrs: AttributeSet?,
- private val longPressDuration: () -> Long,
+ longPressDuration: () -> Long,
) :
View(
context,
@@ -89,6 +89,12 @@
)
}
+ 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 a742e8d..d3fc610 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 @@
/** 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 ba45a51..30a56a2 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.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.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 @@
@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 @@
}
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 4a726ae..a49b3ae 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 @@
* @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 cf995fa..da4f85e 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 @@
}
/**
- * 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 @@
* @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 20b7b2a..82255a0 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.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 @@
*/
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 db47bfb..4f00495 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.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 @@
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 abd79ab..b9a79dc 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 @@
}
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 5713a15..35b2598 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 @@
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 962cdf1..ce7ec0e 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 @@
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 45b8257..9146c60 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.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 @@
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 @@
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 2c1e75e..d8b5013 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 @@
// 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 9e6c552..b276f53 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 @@
)
}
+ 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 0c07c05..89e4760 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 @@
{
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
similarity index 94%
rename from packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
index ba01776..f677ec1b 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.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/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
similarity index 89%
copy from packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
copy to packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
index ba01776..d48d55d 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
@@ -14,7 +14,7 @@
* limitations under the License.
*/
-package com.android.systemui.shade.ui.viewmodel
+package com.android.systemui.qs.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.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
@@ -30,9 +31,9 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
-/** Models UI state and handles user input for the Notifications Shade scene. */
+/** Models UI state and handles user input for the Quick Settings Shade scene. */
@SysUISingleton
-class NotificationsShadeSceneViewModel
+class QuickSettingsShadeSceneViewModel
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt b/packages/SystemUI/src/com/android/systemui/scene/KeyguardlessSceneContainerFrameworkModule.kt
index aa8ecfc..28569d8 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 @@
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 @@
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 551aa12..dbe0342 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 @@
LockscreenSceneModule::class,
QuickSettingsSceneModule::class,
ShadeSceneModule::class,
+ QuickSettingsShadeSceneModule::class,
NotificationsShadeSceneModule::class,
],
)
@@ -71,7 +72,8 @@
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 @@
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 ace4491..6bcd923 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 @@
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 de3b87a..9c2b992 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 @@
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 08f1be9..6d139da 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 @@
* 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 e4435cc..b0af7f9 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 @@
)] = 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 7fbede47..09c80b0 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 @@
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 0000000..0bc280c
--- /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 3c5a0ec..750bd53 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.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 @@
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 4a636d2..3eb4389 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 @@
}
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 5cc30bd..43e782c 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 @@
}
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 96a50f7..70632d5 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 @@
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 3bd8735..d669369 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 @@
}
@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 @@
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 @@
MotionEvent downEvent = MotionEvent.obtain(ev);
downEvent.setAction(MotionEvent.ACTION_DOWN);
mController.sendTouchToSceneFramework(downEvent);
+ mScrollViewFields.sendCurrentGestureOverscroll(getExpandedInThisMotion());
downEvent.recycle();
}
@@ -3428,6 +3436,14 @@
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 @@
return mExpandingNotification;
}
+ @VisibleForTesting
+ void setExpandingNotification(boolean isExpanding) {
+ mExpandingNotification = isExpanding;
+ }
+
boolean getDisallowScrollingInThisMotion() {
return mDisallowScrollingInThisMotion;
}
@@ -5557,6 +5578,11 @@
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 5bb3f42..3011bc2 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 @@
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 @@
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 @@
}
}
+ @VisibleForTesting
+ TouchHandler getTouchHandler() {
+ return mTouchHandler;
+ }
+
@Override
public void dump(@NonNull PrintWriter pw, @NonNull String[] args) {
pw.println("mMaxAlphaFromView=" + mMaxAlphaFromView);
@@ -2043,7 +2050,14 @@
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 edac5ed..a3827c1 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 @@
*/
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 @@
/** 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 8a9da69..920c9c2 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 @@
* 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 b8660ba..b94da38 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 @@
*/
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 @@
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 a56384d..2c88845 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 @@
/** 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 4476d87..26f7ad7 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 @@
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 8b1b93bf..b2184db 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 @@
/** 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 486e305..11eaf54 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 @@
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 37be1c6..a817b31 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 @@
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 @@
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 38b381a..a5728d0 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 @@
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 @@
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 19d9c3f..3eec3d9 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.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 @@
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 22c0530..199bc3b 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 @@
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 b974f90..b00829e 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 @@
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 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 @@
@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 @@
.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 @@
}
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 @@
)
}
+ 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 192e0ec..be3a529 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 @@
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 f5b5261..bcaad01 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 @@
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
+ with(kosmos) {
+ whenever(defaultLockscreenBlueprint.id).thenReturn(DEFAULT)
+ underTest =
+ KeyguardBlueprintRepository(
+ setOf(defaultLockscreenBlueprint),
+ fakeExecutorHandler,
+ threadAssert,
+ )
+ }
}
@Test
fun testApplyBlueprint_DefaultLayout() {
testScope.runTest {
val blueprint by collectLastValue(underTest.blueprint)
- underTest.applyBlueprint(DefaultKeyguardBlueprint.DEFAULT)
- assertThat(blueprint).isEqualTo(kosmos.defaultKeyguardBlueprint)
- }
- }
-
- @Test
- fun testApplyBlueprint_SplitShadeLayout() {
- 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 @@
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 9ccf212..f32e775 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 @@
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 8a0613f..dbf6a29 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 @@
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 a66a136..f262df1 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 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.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 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 @@
@Mock private NotificationListViewBinder mViewBinder;
@Mock
private SensitiveNotificationProtectionController mSensitiveNotificationProtectionController;
+ @Mock private ExpandHelper mExpandHelper;
@Captor
private ArgumentCaptor<Runnable> mSensitiveStateListenerArgumentCaptor;
@@ -895,6 +901,50 @@
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 939d055..0c0a2a5 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 @@
.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 @@
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 140e919..78d4f02 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.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 @@
@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 7eef704..c7b06b6 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 @@
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 @@
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 5e71227..872eba06 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 @@
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 0000000..8c5ff1d
--- /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 3e9ae4d..1f2ecb7 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 @@
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 @@
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 89672f1..9dddfcd 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 5db1724..546a797 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 @@
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.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 @@
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 @@
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 fa3a19b..d743558 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.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 @@
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 1b3480c..9c902cf 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 @@
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 9b83ede..9520621 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 @@
"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 3c702b4..b1976cd 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 @@
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 fa29dc1..059c4a4 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 @@
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 d20b3b2..f8eb789 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 @@
}
// 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 c5683f3..fe4522a 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.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.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 @@
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 @@
+ ": 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 ef1b02d..119fafd 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 @@
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 56e5d76..5f13672 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 @@
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 9324e29..21e7a8d 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 @@
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 @@
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 @@
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 @@
}
/**
+ * 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 d0a50d5..dbe3d36 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.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_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 @@
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 @@
}
}
+ 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 @@
}
}
- // 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 950ec77..502607b 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 @@
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 @@
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 d24afabe..a65c7e1 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 @@
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 @@
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 6303a1a..01f56e0 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 @@
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 @@
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 f553a5a..dd173af 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 @@
CryptoTestHelper.runAndLogSelfTest();
}
- public String[] getPersonalAppsForSuspension(@UserIdInt int userId) {
- return PersonalAppsSuspensionHelper.forUser(mContext, userId)
- .getPersonalAppsForSuspension();
- }
-
public long systemCurrentTimeMillis() {
return System.currentTimeMillis();
}
@@ -21610,9 +21605,12 @@
== 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 45a2d2a..ebbf22c 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 @@
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 @@
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 20bd2d7..13412d0 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 @@
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 @@
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 522c4b5..c363e66 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 @@
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 @@
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 8cb511e..7483b43 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.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 @@
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 7a9fa0f..9a73d5e 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 @@
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 eeb4976..4bf3ff4 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 @@
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 5af2fa2..e83b031 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 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 0265453..a9d65ac 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 @@
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 @@
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 855c658..b4cc343 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 @@
@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 a78fc10..7e6301f 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_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 @@
}
@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 @@
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 fb854c5..43b424f 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 @@
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);