Merge "Update DeviceState intdef to have flag = false." into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 6ecd38f..3391698 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -335,6 +335,11 @@
aconfig_declarations: "android.os.flags-aconfig",
defaults: ["framework-minus-apex-aconfig-java-defaults"],
mode: "exported",
+ min_sdk_version: "30",
+ apex_available: [
+ "//apex_available:platform",
+ "com.android.mediaprovider",
+ ],
}
cc_aconfig_library {
@@ -716,6 +721,7 @@
name: "android.credentials.flags-aconfig",
package: "android.credentials.flags",
srcs: ["core/java/android/credentials/flags.aconfig"],
+ exportable: true,
}
java_aconfig_library {
@@ -724,6 +730,13 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+java_aconfig_library {
+ name: "android.credentials.flags-aconfig-java-export",
+ aconfig_declarations: "android.credentials.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+ mode: "exported",
+}
+
// Content Protection
aconfig_declarations {
name: "android.view.contentprotection.flags-aconfig",
diff --git a/core/java/android/app/SystemServiceRegistry.java b/core/java/android/app/SystemServiceRegistry.java
index 1cbec31..66ec865 100644
--- a/core/java/android/app/SystemServiceRegistry.java
+++ b/core/java/android/app/SystemServiceRegistry.java
@@ -450,6 +450,11 @@
new CachedServiceFetcher<VcnManager>() {
@Override
public VcnManager createService(ContextImpl ctx) throws ServiceNotFoundException {
+ if (!ctx.getPackageManager().hasSystemFeature(
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
+ return null;
+ }
+
IBinder b = ServiceManager.getService(Context.VCN_MANAGEMENT_SERVICE);
IVcnManagementService service = IVcnManagementService.Stub.asInterface(b);
return new VcnManager(ctx, service);
@@ -1736,6 +1741,13 @@
return fetcher;
}
+ private static boolean hasSystemFeatureOpportunistic(@NonNull ContextImpl ctx,
+ @NonNull String featureName) {
+ PackageManager manager = ctx.getPackageManager();
+ if (manager == null) return true;
+ return manager.hasSystemFeature(featureName);
+ }
+
/**
* Gets a system service from a given context.
* @hide
@@ -1758,12 +1770,18 @@
case Context.VIRTUALIZATION_SERVICE:
case Context.VIRTUAL_DEVICE_SERVICE:
return null;
- case Context.SEARCH_SERVICE:
- // Wear device does not support SEARCH_SERVICE so we do not print WTF here
- PackageManager manager = ctx.getPackageManager();
- if (manager != null && manager.hasSystemFeature(PackageManager.FEATURE_WATCH)) {
+ case Context.VCN_MANAGEMENT_SERVICE:
+ if (!hasSystemFeatureOpportunistic(ctx,
+ PackageManager.FEATURE_TELEPHONY_SUBSCRIPTION)) {
return null;
}
+ break;
+ case Context.SEARCH_SERVICE:
+ // Wear device does not support SEARCH_SERVICE so we do not print WTF here
+ if (hasSystemFeatureOpportunistic(ctx, PackageManager.FEATURE_WATCH)) {
+ return null;
+ }
+ break;
}
Slog.wtf(TAG, "Manager wrapper not available: " + name);
return null;
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index 441d521..3ec6fe7 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -1,3 +1,6 @@
+# proto-file: build/make/tools/aconfig/aconfig_protos/protos/aconfig.proto
+# proto-message: flag_declarations
+
package: "android.app.admin.flags"
flag {
@@ -180,3 +183,10 @@
description: "Allow COPE admin to control screen brightness and timeout."
bug: "323894620"
}
+
+flag {
+ name: "is_recursive_required_app_merging_enabled"
+ namespace: "enterprise"
+ description: "Guards a new flow for recursive required enterprise app list merging"
+ bug: "319084618"
+}
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 3304475..ec59cf6 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -972,6 +972,7 @@
*
* @param config camera configuration.
* @return newly created camera.
+ * @throws UnsupportedOperationException if virtual camera isn't supported on this device.
* @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
diff --git a/core/java/android/companion/virtual/flags/flags.aconfig b/core/java/android/companion/virtual/flags/flags.aconfig
index 24d6a5c..2904e7c 100644
--- a/core/java/android/companion/virtual/flags/flags.aconfig
+++ b/core/java/android/companion/virtual/flags/flags.aconfig
@@ -44,3 +44,11 @@
description: "Enable device awareness in camera service"
bug: "305170199"
}
+
+flag {
+ namespace: "virtual_devices"
+ name: "device_aware_drm"
+ description: "Makes MediaDrm APIs device-aware"
+ bug: "303535376"
+ is_fixed_read_only: true
+}
\ No newline at end of file
diff --git a/core/java/android/credentials/flags.aconfig b/core/java/android/credentials/flags.aconfig
index 47edba6..16ca31f 100644
--- a/core/java/android/credentials/flags.aconfig
+++ b/core/java/android/credentials/flags.aconfig
@@ -47,6 +47,7 @@
name: "configurable_selector_ui_enabled"
description: "Enables OEM configurable Credential Selector UI"
bug: "319448437"
+ is_exported: true
}
flag {
diff --git a/core/java/android/hardware/camera2/CaptureRequest.java b/core/java/android/hardware/camera2/CaptureRequest.java
index 13d5c7e..6f901d7 100644
--- a/core/java/android/hardware/camera2/CaptureRequest.java
+++ b/core/java/android/hardware/camera2/CaptureRequest.java
@@ -2800,7 +2800,9 @@
* upright.</p>
* <p>Camera devices may either encode this value into the JPEG EXIF header, or
* rotate the image data to match this orientation. When the image data is rotated,
- * the thumbnail data will also be rotated.</p>
+ * the thumbnail data will also be rotated. Additionally, in the case where the image data
+ * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+ * will not be updated to reflect the height and width of the rotated image.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
* <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/hardware/camera2/CaptureResult.java b/core/java/android/hardware/camera2/CaptureResult.java
index 7145501..69b1c34 100644
--- a/core/java/android/hardware/camera2/CaptureResult.java
+++ b/core/java/android/hardware/camera2/CaptureResult.java
@@ -3091,7 +3091,9 @@
* upright.</p>
* <p>Camera devices may either encode this value into the JPEG EXIF header, or
* rotate the image data to match this orientation. When the image data is rotated,
- * the thumbnail data will also be rotated.</p>
+ * the thumbnail data will also be rotated. Additionally, in the case where the image data
+ * is rotated, {@link android.media.Image#getWidth } and {@link android.media.Image#getHeight }
+ * will not be updated to reflect the height and width of the rotated image.</p>
* <p>Note that this orientation is relative to the orientation of the camera sensor, given
* by {@link CameraCharacteristics#SENSOR_ORIENTATION android.sensor.orientation}.</p>
* <p>To translate from the device orientation given by the Android sensor APIs for camera
diff --git a/core/java/android/service/wallpaper/WallpaperService.java b/core/java/android/service/wallpaper/WallpaperService.java
index bbda068..cd486d0 100644
--- a/core/java/android/service/wallpaper/WallpaperService.java
+++ b/core/java/android/service/wallpaper/WallpaperService.java
@@ -211,7 +211,7 @@
* @hide
*/
@ChangeId
- @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE)
+ @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.VANILLA_ICE_CREAM)
public static final long WEAROS_WALLPAPER_HANDLES_SCALING = 272527315L;
static final class WallpaperCommand {
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index aff1d4a..5366a4d 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -126,3 +126,10 @@
description: "When handwriting is initiated in an unfocused TextView, cursor is placed at the end of the closest paragraph."
bug: "323376217"
}
+
+flag {
+ name: "handwriting_unsupported_message"
+ namespace: "text"
+ description: "Feature flag for showing error message when user tries stylus handwriting on a text field which doesn't support it"
+ bug: "297962571"
+}
diff --git a/core/java/android/view/HandwritingInitiator.java b/core/java/android/view/HandwritingInitiator.java
index 29c8350..192b2ec 100644
--- a/core/java/android/view/HandwritingInitiator.java
+++ b/core/java/android/view/HandwritingInitiator.java
@@ -34,7 +34,9 @@
import android.widget.EditText;
import android.widget.Editor;
import android.widget.TextView;
+import android.widget.Toast;
+import com.android.internal.R;
import com.android.internal.annotations.VisibleForTesting;
import java.lang.ref.WeakReference;
@@ -223,7 +225,24 @@
View candidateView = findBestCandidateView(mState.mStylusDownX,
mState.mStylusDownY, /* isHover */ false);
if (candidateView != null && candidateView.isEnabled()) {
- if (candidateView == getConnectedOrFocusedView()) {
+ if (shouldShowHandwritingUnavailableMessageForView(candidateView)) {
+ int messagesResId = (candidateView instanceof TextView tv
+ && tv.isAnyPasswordInputType())
+ ? R.string.error_handwriting_unsupported_password
+ : R.string.error_handwriting_unsupported;
+ Toast.makeText(candidateView.getContext(), messagesResId,
+ Toast.LENGTH_SHORT).show();
+ if (!candidateView.hasFocus()) {
+ requestFocusWithoutReveal(candidateView);
+ }
+ mImm.showSoftInput(candidateView, 0);
+ mState.mHandled = true;
+ mState.mShouldInitHandwriting = false;
+ motionEvent.setAction((motionEvent.getAction()
+ & MotionEvent.ACTION_POINTER_INDEX_MASK)
+ | MotionEvent.ACTION_CANCEL);
+ candidateView.getRootView().dispatchTouchEvent(motionEvent);
+ } else if (candidateView == getConnectedOrFocusedView()) {
if (!mInitiateWithoutConnection && !candidateView.hasFocus()) {
requestFocusWithoutReveal(candidateView);
}
@@ -484,6 +503,15 @@
return view.isStylusHandwritingAvailable();
}
+ private static boolean shouldShowHandwritingUnavailableMessageForView(@NonNull View view) {
+ return (view instanceof TextView) && !shouldTriggerStylusHandwritingForView(view);
+ }
+
+ private static boolean shouldTriggerHandwritingOrShowUnavailableMessageForView(
+ @NonNull View view) {
+ return (view instanceof TextView) || shouldTriggerStylusHandwritingForView(view);
+ }
+
/**
* Returns the pointer icon for the motion event, or null if it doesn't specify the icon.
* This gives HandwritingInitiator a chance to show the stylus handwriting icon over a
@@ -491,7 +519,7 @@
*/
public PointerIcon onResolvePointerIcon(Context context, MotionEvent event) {
final View hoverView = findHoverView(event);
- if (hoverView == null) {
+ if (hoverView == null || !shouldTriggerStylusHandwritingForView(hoverView)) {
return null;
}
@@ -594,7 +622,7 @@
/**
* Given the location of the stylus event, return the best candidate view to initialize
- * handwriting mode.
+ * handwriting mode or show the handwriting unavailable error message.
*
* @param x the x coordinates of the stylus event, in the coordinates of the window.
* @param y the y coordinates of the stylus event, in the coordinates of the window.
@@ -610,7 +638,8 @@
Rect handwritingArea = mTempRect;
if (getViewHandwritingArea(connectedOrFocusedView, handwritingArea)
&& isInHandwritingArea(handwritingArea, x, y, connectedOrFocusedView, isHover)
- && shouldTriggerStylusHandwritingForView(connectedOrFocusedView)) {
+ && shouldTriggerHandwritingOrShowUnavailableMessageForView(
+ connectedOrFocusedView)) {
if (!isHover && mState != null) {
mState.mStylusDownWithinEditorBounds =
contains(handwritingArea, x, y, 0f, 0f, 0f, 0f);
@@ -628,7 +657,7 @@
final View view = viewInfo.getView();
final Rect handwritingArea = viewInfo.getHandwritingArea();
if (!isInHandwritingArea(handwritingArea, x, y, view, isHover)
- || !shouldTriggerStylusHandwritingForView(view)) {
+ || !shouldTriggerHandwritingOrShowUnavailableMessageForView(view)) {
continue;
}
@@ -856,7 +885,7 @@
/** The helper method to check if the given view is still active for handwriting. */
private static boolean isViewActive(@Nullable View view) {
return view != null && view.isAttachedToWindow() && view.isAggregatedVisible()
- && view.shouldInitiateHandwriting();
+ && view.shouldTrackHandwritingArea();
}
private CursorAnchorInfo getCursorAnchorInfoForConnectionless(View view) {
diff --git a/core/java/android/view/OWNERS b/core/java/android/view/OWNERS
index a2f767d..07d05a4 100644
--- a/core/java/android/view/OWNERS
+++ b/core/java/android/view/OWNERS
@@ -75,12 +75,14 @@
per-file View.java = file:/services/core/java/com/android/server/input/OWNERS
per-file View.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file View.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file View.java = file:/core/java/android/text/OWNERS
per-file ViewRootImpl.java = file:/services/accessibility/OWNERS
per-file ViewRootImpl.java = file:/core/java/android/service/autofill/OWNERS
per-file ViewRootImpl.java = file:/graphics/java/android/graphics/OWNERS
per-file ViewRootImpl.java = file:/services/core/java/com/android/server/input/OWNERS
per-file ViewRootImpl.java = file:/services/core/java/com/android/server/wm/OWNERS
per-file ViewRootImpl.java = file:/core/java/android/view/inputmethod/OWNERS
+per-file ViewRootImpl.java = file:/core/java/android/text/OWNERS
per-file AccessibilityInteractionController.java = file:/services/accessibility/OWNERS
per-file OnReceiveContentListener.java = file:/core/java/android/service/autofill/OWNERS
per-file OnReceiveContentListener.java = file:/core/java/android/widget/OWNERS
diff --git a/core/java/android/view/View.java b/core/java/android/view/View.java
index 0a75f4e..41bfb24 100644
--- a/core/java/android/view/View.java
+++ b/core/java/android/view/View.java
@@ -12695,7 +12695,7 @@
if (getSystemGestureExclusionRects().isEmpty()
&& collectPreferKeepClearRects().isEmpty()
&& collectUnrestrictedPreferKeepClearRects().isEmpty()
- && (info.mHandwritingArea == null || !shouldInitiateHandwriting())) {
+ && (info.mHandwritingArea == null || !shouldTrackHandwritingArea())) {
if (info.mPositionUpdateListener != null) {
mRenderNode.removePositionUpdateListener(info.mPositionUpdateListener);
info.mPositionUpdateListener = null;
@@ -13062,7 +13062,7 @@
void updateHandwritingArea() {
// If autoHandwritingArea is not enabled, do nothing.
- if (!shouldInitiateHandwriting()) return;
+ if (!shouldTrackHandwritingArea()) return;
final AttachInfo ai = mAttachInfo;
if (ai != null) {
ai.mViewRootImpl.getHandwritingInitiator().updateHandwritingAreasForView(this);
@@ -13080,6 +13080,16 @@
}
/**
+ * Returns whether the handwriting initiator should track the handwriting area for this view,
+ * either to initiate handwriting mode, or to prepare handwriting delegation, or to show the
+ * handwriting unsupported message.
+ * @hide
+ */
+ public boolean shouldTrackHandwritingArea() {
+ return shouldInitiateHandwriting();
+ }
+
+ /**
* Sets a callback which should be called when a stylus {@link MotionEvent} occurs within this
* view's bounds. The callback will be called from the UI thread.
*
diff --git a/core/java/android/view/WindowManager.java b/core/java/android/view/WindowManager.java
index 86fc6f4..5666739 100644
--- a/core/java/android/view/WindowManager.java
+++ b/core/java/android/view/WindowManager.java
@@ -1476,15 +1476,26 @@
*/
@TestApi
static boolean hasWindowExtensionsEnabled() {
- return HAS_WINDOW_EXTENSIONS_ON_DEVICE
- && ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication())
- // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
- // on all devices by default as a build file property.
- // Until finishing flag ramp up, only return true when
- // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
- // OEMs.
- && (Flags.enableWmExtensionsForAllFlag()
- || !ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15);
+ if (!Flags.enableWmExtensionsForAllFlag() && ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15) {
+ // Since enableWmExtensionsForAllFlag, HAS_WINDOW_EXTENSIONS_ON_DEVICE is now true
+ // on all devices by default as a build file property.
+ // Until finishing flag ramp up, only return true when
+ // ACTIVITY_EMBEDDING_GUARD_WITH_ANDROID_15 is false, which is set per device by
+ // OEMs.
+ return false;
+ }
+
+ if (!HAS_WINDOW_EXTENSIONS_ON_DEVICE) {
+ return false;
+ }
+
+ try {
+ return ActivityTaskManager.supportsMultiWindow(ActivityThread.currentApplication());
+ } catch (Exception e) {
+ // In case the PackageManager is not set up correctly in test.
+ Log.e("WindowManager", "Unable to read if the device supports multi window", e);
+ return false;
+ }
}
/**
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index 0373539..5260470 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -13565,6 +13565,15 @@
/** @hide */
@Override
+ public boolean shouldTrackHandwritingArea() {
+ // The handwriting initiator tracks all editable TextViews regardless of whether handwriting
+ // is supported, so that it can show an error message for unsupported editable TextViews.
+ return super.shouldTrackHandwritingArea()
+ || (Flags.handwritingUnsupportedMessage() && onCheckIsTextEditor());
+ }
+
+ /** @hide */
+ @Override
public boolean isStylusHandwritingAvailable() {
if (mTextOperationUser == null) {
return super.isStylusHandwritingAvailable();
diff --git a/core/java/android/window/InputTransferToken.java b/core/java/android/window/InputTransferToken.java
index 5fab48f..d2cefa8 100644
--- a/core/java/android/window/InputTransferToken.java
+++ b/core/java/android/window/InputTransferToken.java
@@ -57,6 +57,7 @@
private static native void nativeWriteToParcel(long nativeObject, Parcel out);
private static native long nativeReadFromParcel(Parcel in);
private static native IBinder nativeGetBinderToken(long nativeObject);
+ private static native long nativeGetBinderTokenRef(long nativeObject);
private static native long nativeGetNativeInputTransferTokenFinalizer();
private static native boolean nativeEquals(long nativeObject1, long nativeObject2);
@@ -130,7 +131,7 @@
*/
@Override
public int hashCode() {
- return Objects.hash(getToken());
+ return Objects.hash(nativeGetBinderTokenRef(mNativeObject));
}
/**
diff --git a/core/java/android/window/flags/accessibility.aconfig b/core/java/android/window/flags/accessibility.aconfig
index 814c620..90b54bd 100644
--- a/core/java/android/window/flags/accessibility.aconfig
+++ b/core/java/android/window/flags/accessibility.aconfig
@@ -12,4 +12,7 @@
namespace: "accessibility"
description: "Always draw fullscreen orange border in fullscreen magnification"
bug: "291891390"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/com/android/internal/app/ResolverActivity.java b/core/java/com/android/internal/app/ResolverActivity.java
index 78f06b6..84715aa 100644
--- a/core/java/com/android/internal/app/ResolverActivity.java
+++ b/core/java/com/android/internal/app/ResolverActivity.java
@@ -217,6 +217,12 @@
public static final String EXTRA_IS_AUDIO_CAPTURE_DEVICE = "is_audio_capture_device";
/**
+ * Boolean extra to indicate if Resolver Sheet needs to be started in single user mode.
+ */
+ protected static final String EXTRA_RESTRICT_TO_SINGLE_USER =
+ "com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER";
+
+ /**
* Integer extra to indicate which profile should be automatically selected.
* <p>Can only be used if there is a work profile.
* <p>Possible values can be either {@link #PROFILE_PERSONAL} or {@link #PROFILE_WORK}.
@@ -750,8 +756,10 @@
}
protected UserHandle getPersonalProfileUserHandle() {
- if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()){
- return mPrivateProfileUserHandle;
+ // When launched in single user mode, only personal tab is populated, so we use
+ // tabOwnerUserHandleForLaunch as personal tab's user handle.
+ if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
+ return getTabOwnerUserHandleForLaunch();
}
return mPersonalProfileUserHandle;
}
@@ -822,11 +830,11 @@
// If we are in work or private profile's process, return WorkProfile/PrivateProfile user
// as owner, otherwise we always return PersonalProfile user as owner
if (UserHandle.of(UserHandle.myUserId()).equals(getWorkProfileUserHandle())) {
- return getWorkProfileUserHandle();
+ return mWorkProfileUserHandle;
} else if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
- return getPrivateProfileUserHandle();
+ return mPrivateProfileUserHandle;
}
- return getPersonalProfileUserHandle();
+ return mPersonalProfileUserHandle;
}
private boolean hasWorkProfile() {
@@ -847,8 +855,18 @@
&& (UserHandle.myUserId() == getPrivateProfileUserHandle().getIdentifier());
}
+ protected final boolean isLaunchedInSingleUserMode() {
+ // When launched from Private Profile, return true
+ if (isLaunchedAsPrivateProfile()) {
+ return true;
+ }
+ return getIntent()
+ .getBooleanExtra(EXTRA_RESTRICT_TO_SINGLE_USER, /* defaultValue = */ false);
+ }
+
protected boolean shouldShowTabs() {
- if (privateSpaceEnabled() && isLaunchedAsPrivateProfile()) {
+ // No Tabs are shown when launched in single user mode.
+ if (privateSpaceEnabled() && isLaunchedInSingleUserMode()) {
return false;
}
return hasWorkProfile() && ENABLE_TABBED_VIEW;
diff --git a/core/java/com/android/internal/policy/DecorView.java b/core/java/com/android/internal/policy/DecorView.java
index 0f1f7e9..a65a1bb 100644
--- a/core/java/com/android/internal/policy/DecorView.java
+++ b/core/java/com/android/internal/policy/DecorView.java
@@ -137,7 +137,7 @@
private static final int SCRIM_LIGHT = 0xe6ffffff; // 90% white
- private static final int SCRIM_ALPHA = 0xcc0000; // 80% alpha
+ private static final int SCRIM_ALPHA = 0xcc000000; // 80% alpha
public static final ColorViewAttributes STATUS_BAR_COLOR_VIEW_ATTRIBUTES =
new ColorViewAttributes(FLAG_TRANSLUCENT_STATUS,
diff --git a/core/java/com/android/internal/widget/ImageFloatingTextView.java b/core/java/com/android/internal/widget/ImageFloatingTextView.java
index 5da6435..352e6d8 100644
--- a/core/java/com/android/internal/widget/ImageFloatingTextView.java
+++ b/core/java/com/android/internal/widget/ImageFloatingTextView.java
@@ -31,6 +31,8 @@
import android.widget.RemoteViews;
import android.widget.TextView;
+import com.android.internal.R;
+
/**
* A TextView that can float around an image on the end.
*
@@ -49,6 +51,7 @@
private int mMaxLinesForHeight = -1;
private int mLayoutMaxLines = -1;
private int mImageEndMargin;
+ private final int mMaxLineUpperLimit;
private int mStaticLayoutCreationCountInOnMeasure = 0;
@@ -71,6 +74,8 @@
super(context, attrs, defStyleAttr, defStyleRes);
setHyphenationFrequency(Layout.HYPHENATION_FREQUENCY_FULL_FAST);
setBreakStrategy(Layout.BREAK_STRATEGY_HIGH_QUALITY);
+ mMaxLineUpperLimit =
+ getResources().getInteger(R.integer.config_notificationLongTextMaxLineCount);
}
@Override
@@ -102,6 +107,11 @@
} else {
maxLines = getMaxLines() >= 0 ? getMaxLines() : Integer.MAX_VALUE;
}
+
+ if (mMaxLineUpperLimit > 0) {
+ maxLines = Math.min(maxLines, mMaxLineUpperLimit);
+ }
+
builder.setMaxLines(maxLines);
mLayoutMaxLines = maxLines;
if (shouldEllipsize) {
diff --git a/core/jni/OWNERS b/core/jni/OWNERS
index 3aca751..2a4f062 100644
--- a/core/jni/OWNERS
+++ b/core/jni/OWNERS
@@ -27,6 +27,7 @@
# WindowManager
per-file android_graphics_BLASTBufferQueue.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file android_view_Surface* = file:/services/core/java/com/android/server/wm/OWNERS
+per-file android_view_WindowManagerGlobal.cpp = file:/services/core/java/com/android/server/wm/OWNERS
per-file android_window_* = file:/services/core/java/com/android/server/wm/OWNERS
# Resources
diff --git a/core/jni/android_view_WindowManagerGlobal.cpp b/core/jni/android_view_WindowManagerGlobal.cpp
index b03ac88..abc621d 100644
--- a/core/jni/android_view_WindowManagerGlobal.cpp
+++ b/core/jni/android_view_WindowManagerGlobal.cpp
@@ -48,7 +48,7 @@
surfaceControlObj(env,
android_view_SurfaceControl_getJavaSurfaceControl(env,
surfaceControl));
- jobject clientTokenObj = javaObjectForIBinder(env, clientToken);
+ ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
ScopedLocalRef<jobject> clientInputTransferTokenObj(
env,
android_window_InputTransferToken_getJavaInputTransferToken(env,
@@ -57,7 +57,7 @@
inputChannelObj(env,
env->CallStaticObjectMethod(gWindowManagerGlobal.clazz,
gWindowManagerGlobal.createInputChannel,
- clientTokenObj,
+ clientTokenObj.get(),
hostInputTransferTokenObj.get(),
surfaceControlObj.get(),
clientInputTransferTokenObj.get()));
@@ -68,9 +68,9 @@
void removeInputChannel(const sp<IBinder>& clientToken) {
JNIEnv* env = AndroidRuntime::getJNIEnv();
- jobject clientTokenObj(javaObjectForIBinder(env, clientToken));
+ ScopedLocalRef<jobject> clientTokenObj(env, javaObjectForIBinder(env, clientToken));
env->CallStaticObjectMethod(gWindowManagerGlobal.clazz, gWindowManagerGlobal.removeInputChannel,
- clientTokenObj);
+ clientTokenObj.get());
}
int register_android_view_WindowManagerGlobal(JNIEnv* env) {
diff --git a/core/jni/android_window_InputTransferToken.cpp b/core/jni/android_window_InputTransferToken.cpp
index 8fb668d..5bcea9b 100644
--- a/core/jni/android_window_InputTransferToken.cpp
+++ b/core/jni/android_window_InputTransferToken.cpp
@@ -70,6 +70,11 @@
return javaObjectForIBinder(env, inputTransferToken->mToken);
}
+static jlong nativeGetBinderTokenRef(JNIEnv*, jclass, jlong nativeObj) {
+ sp<InputTransferToken> inputTransferToken = reinterpret_cast<InputTransferToken*>(nativeObj);
+ return reinterpret_cast<jlong>(inputTransferToken->mToken.get());
+}
+
InputTransferToken* android_window_InputTransferToken_getNativeInputTransferToken(
JNIEnv* env, jobject inputTransferTokenObj) {
if (inputTransferTokenObj != nullptr &&
@@ -114,6 +119,7 @@
{"nativeWriteToParcel", "(JLandroid/os/Parcel;)V", (void*)nativeWriteToParcel},
{"nativeReadFromParcel", "(Landroid/os/Parcel;)J", (void*)nativeReadFromParcel},
{"nativeGetBinderToken", "(J)Landroid/os/IBinder;", (void*)nativeGetBinderToken},
+ {"nativeGetBinderTokenRef", "(J)J", (void*)nativeGetBinderTokenRef},
{"nativeGetNativeInputTransferTokenFinalizer", "()J", (void*)nativeGetNativeInputTransferTokenFinalizer},
{"nativeEquals", "(JJ)Z", (void*) nativeEquals},
// clang-format on
diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml
index e3f1cb6..efba709 100644
--- a/core/res/res/values/config.xml
+++ b/core/res/res/values/config.xml
@@ -1483,6 +1483,11 @@
<!-- Number of notifications to keep in the notification service historical archive -->
<integer name="config_notificationServiceArchiveSize">100</integer>
+ <!-- Upper limit imposed for long text content for BigTextStyle, MessagingStyle and
+ ConversationStyle notifications for performance reasons, and that line count is also
+ capped by vertical space available. It is only enabled when the value is positive int.-->
+ <integer name="config_notificationLongTextMaxLineCount">10</integer>
+
<!-- Allow the menu hard key to be disabled in LockScreen on some devices -->
<bool name="config_disableMenuKeyInLockScreen">false</bool>
diff --git a/core/res/res/values/strings.xml b/core/res/res/values/strings.xml
index f915f03..a3dba48 100644
--- a/core/res/res/values/strings.xml
+++ b/core/res/res/values/strings.xml
@@ -231,8 +231,10 @@
<string name="NetworkPreferenceSwitchSummary">Try changing preferred network. Tap to change.</string>
<!-- Displayed to tell the user that emergency calls might not be available. -->
<string name="EmergencyCallWarningTitle">Emergency calling unavailable</string>
- <!-- Displayed to tell the user that emergency calls might not be available. -->
- <string name="EmergencyCallWarningSummary">Can\u2019t make emergency calls over Wi\u2011Fi</string>
+ <!-- Displayed to tell the user that emergency calls might not be available; this is shown to
+ the user when only WiFi calling is available and the carrier does not support emergency
+ calls over WiFi calling. -->
+ <string name="EmergencyCallWarningSummary">Emergency calls require a mobile network</string>
<!-- Telephony notification channel name for a channel containing network alert notifications. -->
<string name="notification_channel_network_alert">Alerts</string>
@@ -3247,6 +3249,12 @@
<!-- Title for EditText context menu [CHAR LIMIT=20] -->
<string name="editTextMenuTitle">Text actions</string>
+ <!-- Error shown when a user uses a stylus to try handwriting on a text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+ <string name="error_handwriting_unsupported">Handwriting is not supported in this field</string>
+
+ <!-- Error shown when a user uses a stylus to try handwriting on a password text field which doesn't support stylus handwriting. [CHAR LIMIT=TOAST] -->
+ <string name="error_handwriting_unsupported_password">Handwriting is not supported in password fields</string>
+
<!-- Content description of the back button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
<string name="input_method_nav_back_button_desc">Back</string>
<!-- Content description of the switch input method button for accessibility (not shown on the screen). [CHAR LIMIT=NONE] -->
diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml
index f4b42f6..c2fa297 100644
--- a/core/res/res/values/symbols.xml
+++ b/core/res/res/values/symbols.xml
@@ -2078,6 +2078,7 @@
<java-symbol type="integer" name="config_notificationsBatteryMediumARGB" />
<java-symbol type="integer" name="config_notificationsBatteryNearlyFullLevel" />
<java-symbol type="integer" name="config_notificationServiceArchiveSize" />
+ <java-symbol type="integer" name="config_notificationLongTextMaxLineCount" />
<java-symbol type="dimen" name="config_rotaryEncoderAxisScrollTickInterval" />
<java-symbol type="integer" name="config_recentVibrationsDumpSizeLimit" />
<java-symbol type="integer" name="config_previousVibrationsDumpSizeLimit" />
@@ -3121,6 +3122,8 @@
<!-- TextView -->
<java-symbol type="bool" name="config_textShareSupported" />
<java-symbol type="string" name="failed_to_copy_to_clipboard" />
+ <java-symbol type="string" name="error_handwriting_unsupported" />
+ <java-symbol type="string" name="error_handwriting_unsupported_password" />
<java-symbol type="id" name="notification_material_reply_container" />
<java-symbol type="id" name="notification_material_reply_text_1" />
diff --git a/core/tests/bugreports/OWNERS b/core/tests/bugreports/OWNERS
new file mode 100644
index 0000000..dbd767c
--- /dev/null
+++ b/core/tests/bugreports/OWNERS
@@ -0,0 +1,2 @@
+# Bug component: 153446
+file:/platform/frameworks/native:/cmds/dumpstate/OWNERS
diff --git a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
index a5c9624..6c00fd8 100644
--- a/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
+++ b/core/tests/coretests/src/android/view/stylus/HandwritingInitiatorTest.java
@@ -55,6 +55,7 @@
import android.view.inputmethod.InputMethodManager;
import android.widget.EditText;
+import androidx.test.annotation.UiThreadTest;
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import androidx.test.platform.app.InstrumentationRegistry;
@@ -72,6 +73,7 @@
*/
@Presubmit
@SmallTest
+@UiThreadTest
@RunWith(AndroidJUnit4.class)
public class HandwritingInitiatorTest {
private static final long TIMEOUT = ViewConfiguration.getLongPressTimeout();
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
index cb8754a..488f017 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverActivityTest.java
@@ -27,6 +27,7 @@
import static androidx.test.espresso.matcher.ViewMatchers.withText;
import static com.android.internal.app.MatcherUtils.first;
+import static com.android.internal.app.ResolverActivity.EXTRA_RESTRICT_TO_SINGLE_USER;
import static com.android.internal.app.ResolverDataProvider.createPackageManagerMockedInfo;
import static com.android.internal.app.ResolverWrapperActivity.sOverrides;
@@ -1254,6 +1255,51 @@
}
}
+ @Test
+ public void testTriggerFromMainProfile_inSingleUserMode_withWorkProfilePresent() {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markWorkProfileUserAvailable();
+ setTabOwnerUserHandleForLaunch(PERSONAL_USER_HANDLE);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTestWithOtherProfile(3, PERSONAL_USER_HANDLE);
+ List<ResolvedComponentInfo> workResolvedComponentInfos = createResolvedComponentsForTest(4,
+ sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos, workResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getPersonalListAdapter().getCount(), is(2));
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle, PERSONAL_USER_HANDLE);
+ }
+ }
+
+ @Test
+ public void testTriggerFromWorkProfile_inSingleUserMode() {
+ mSetFlagsRule.enableFlags(android.os.Flags.FLAG_ALLOW_PRIVATE_PROFILE,
+ android.multiuser.Flags.FLAG_ALLOW_RESOLVER_SHEET_FOR_PRIVATE_SPACE);
+ markWorkProfileUserAvailable();
+ setTabOwnerUserHandleForLaunch(sOverrides.workProfileUserHandle);
+ Intent sendIntent = createSendImageIntent();
+ sendIntent.putExtra(EXTRA_RESTRICT_TO_SINGLE_USER, true);
+ List<ResolvedComponentInfo> personalResolvedComponentInfos =
+ createResolvedComponentsForTest(3, sOverrides.workProfileUserHandle);
+ setupResolverControllers(personalResolvedComponentInfos);
+ final ResolverWrapperActivity activity = mActivityRule.launchActivity(sendIntent);
+ waitForIdle();
+ assertThat(activity.getPersonalListAdapter().getCount(), is(3));
+ onView(withId(R.id.tabs)).check(matches(not(isDisplayed())));
+ assertEquals(activity.getMultiProfilePagerAdapterCount(), 1);
+ for (ResolvedComponentInfo resolvedInfo : personalResolvedComponentInfos) {
+ assertEquals(resolvedInfo.getResolveInfoAt(0).userHandle,
+ sOverrides.workProfileUserHandle);
+ }
+ }
+
private Intent createSendImageIntent() {
Intent sendIntent = new Intent();
sendIntent.setAction(Intent.ACTION_SEND);
@@ -1339,6 +1385,10 @@
ResolverWrapperActivity.sOverrides.privateProfileUserHandle = UserHandle.of(12);
}
+ private void setTabOwnerUserHandleForLaunch(UserHandle tabOwnerUserHandleForLaunch) {
+ sOverrides.tabOwnerUserHandleForLaunch = tabOwnerUserHandleForLaunch;
+ }
+
private void setupResolverControllers(
List<ResolvedComponentInfo> personalResolvedComponentInfos,
List<ResolvedComponentInfo> workResolvedComponentInfos) {
diff --git a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
index 862cbd5..4604b01 100644
--- a/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
+++ b/core/tests/coretests/src/com/android/internal/app/ResolverWrapperActivity.java
@@ -116,6 +116,10 @@
when(sOverrides.resolverListController.getUserHandle()).thenReturn(UserHandle.SYSTEM);
return sOverrides.resolverListController;
}
+ if (isLaunchedInSingleUserMode()) {
+ when(sOverrides.resolverListController.getUserHandle()).thenReturn(userHandle);
+ return sOverrides.resolverListController;
+ }
when(sOverrides.workResolverListController.getUserHandle()).thenReturn(userHandle);
return sOverrides.workResolverListController;
}
diff --git a/keystore/java/android/security/AndroidKeyStoreMaintenance.java b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
index 2430e8d..efbbfc2 100644
--- a/keystore/java/android/security/AndroidKeyStoreMaintenance.java
+++ b/keystore/java/android/security/AndroidKeyStoreMaintenance.java
@@ -175,20 +175,6 @@
}
/**
- * Informs Keystore 2.0 that an off body event was detected.
- */
- public static void onDeviceOffBody() {
- StrictMode.noteDiskWrite();
- try {
- getService().onDeviceOffBody();
- } catch (Exception e) {
- // TODO This fails open. This is not a regression with respect to keystore1 but it
- // should get fixed.
- Log.e(TAG, "Error while reporting device off body event.", e);
- }
- }
-
- /**
* Migrates a key given by the source descriptor to the location designated by the destination
* descriptor.
*
diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java
index bd9abec..f105072 100644
--- a/keystore/java/android/security/KeyStore.java
+++ b/keystore/java/android/security/KeyStore.java
@@ -56,11 +56,4 @@
return Authorization.addAuthToken(authToken);
}
-
- /**
- * Notify keystore that the device went off-body.
- */
- public void onDeviceOffBody() {
- AndroidKeyStoreMaintenance.onDeviceOffBody();
- }
}
diff --git a/keystore/java/android/security/keystore/KeyGenParameterSpec.java b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
index 9ba5a81..d359a90 100644
--- a/keystore/java/android/security/keystore/KeyGenParameterSpec.java
+++ b/keystore/java/android/security/keystore/KeyGenParameterSpec.java
@@ -1670,16 +1670,16 @@
* {@link #setUserAuthenticationValidityDurationSeconds} and
* {@link #setUserAuthenticationRequired}). Once the device has been removed from the
* user's body, the key will be considered unauthorized and the user will need to
- * re-authenticate to use it. For keys without an authentication validity period this
- * parameter has no effect.
+ * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+ * not have an authentication validity period, this parameter has no effect.
+ * <p>
+ * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+ * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+ * Meanwhile, it is recommended to not use it.
*
- * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
- * effect; the device will always be considered to be "on-body" and the key will therefore
- * remain authorized until the validity period ends.
- *
- * @param remainsValid if {@code true}, and if the device supports on-body detection, key
- * will be invalidated when the device is removed from the user's body or when the
- * authentication validity expires, whichever occurs first.
+ * @param remainsValid if {@code true}, and if the device supports enforcement of this
+ * parameter, the key will be invalidated when the device is removed from the user's body or
+ * when the authentication validity expires, whichever occurs first.
*/
@NonNull
public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
diff --git a/keystore/java/android/security/keystore/KeyProtection.java b/keystore/java/android/security/keystore/KeyProtection.java
index 9b455f0..8e5ac45 100644
--- a/keystore/java/android/security/keystore/KeyProtection.java
+++ b/keystore/java/android/security/keystore/KeyProtection.java
@@ -1037,16 +1037,16 @@
* {@link #setUserAuthenticationValidityDurationSeconds} and
* {@link #setUserAuthenticationRequired}). Once the device has been removed from the
* user's body, the key will be considered unauthorized and the user will need to
- * re-authenticate to use it. For keys without an authentication validity period this
- * parameter has no effect.
+ * re-authenticate to use it. If the device does not have an on-body sensor or the key does
+ * not have an authentication validity period, this parameter has no effect.
+ * <p>
+ * Since Android 12 (API level 31), this parameter has no effect even on devices that have
+ * an on-body sensor. A future version of Android may restore enforcement of this parameter.
+ * Meanwhile, it is recommended to not use it.
*
- * <p>Similarly, on devices that do not have an on-body sensor, this parameter will have no
- * effect; the device will always be considered to be "on-body" and the key will therefore
- * remain authorized until the validity period ends.
- *
- * @param remainsValid if {@code true}, and if the device supports on-body detection, key
- * will be invalidated when the device is removed from the user's body or when the
- * authentication validity expires, whichever occurs first.
+ * @param remainsValid if {@code true}, and if the device supports enforcement of this
+ * parameter, the key will be invalidated when the device is removed from the user's body or
+ * when the authentication validity expires, whichever occurs first.
*/
@NonNull
public Builder setUserAuthenticationValidWhileOnBody(boolean remainsValid) {
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
index e422198..e73d880 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubblePositionerTest.kt
@@ -26,6 +26,7 @@
import androidx.test.core.app.ApplicationProvider
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.internal.protolog.common.ProtoLog
import com.android.wm.shell.R
import com.android.wm.shell.bubbles.BubblePositioner.MAX_HEIGHT
import com.android.wm.shell.common.bubbles.BubbleBarLocation
@@ -54,6 +55,7 @@
@Before
fun setUp() {
+ ProtoLog.REQUIRE_PROTOLOGTOOL = false
val windowManager = context.getSystemService(WindowManager::class.java)
positioner = BubblePositioner(context, windowManager)
}
@@ -167,8 +169,9 @@
@Test
fun testGetRestingPosition_afterBoundsChange() {
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = true,
- windowBounds = Rect(0, 0, 2000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = true, windowBounds = Rect(0, 0, 2000, 1600))
+ )
// Set the resting position to the right side
var allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -176,8 +179,9 @@
positioner.restingPosition = restingPosition
// Now make the device smaller
- positioner.update(defaultDeviceConfig.copy(isLargeScreen = false,
- windowBounds = Rect(0, 0, 1000, 1600)))
+ positioner.update(
+ defaultDeviceConfig.copy(isLargeScreen = false, windowBounds = Rect(0, 0, 1000, 1600))
+ )
// Check the resting position is on the correct side
allowableStackRegion = positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
@@ -236,7 +240,8 @@
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
assertThat(positioner.getExpandedViewHeight(bubble))
@@ -263,7 +268,8 @@
0 /* taskId */,
null /* locus */,
true /* isDismissable */,
- directExecutor()) {}
+ directExecutor()
+ ) {}
// Ensure the height is the same as the desired value
val minHeight =
@@ -471,20 +477,20 @@
fun testGetTaskViewContentWidth_onLeft() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(true /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(true /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(true /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
fun testGetTaskViewContentWidth_onRight() {
positioner.update(defaultDeviceConfig.copy(insets = Insets.of(100, 0, 200, 0)))
val taskViewWidth = positioner.getTaskViewContentWidth(false /* onLeft */)
- val paddings = positioner.getExpandedViewContainerPadding(false /* onLeft */,
- false /* isOverflow */)
- assertThat(taskViewWidth).isEqualTo(
- positioner.screenRect.width() - paddings[0] - paddings[2])
+ val paddings =
+ positioner.getExpandedViewContainerPadding(false /* onLeft */, false /* isOverflow */)
+ assertThat(taskViewWidth)
+ .isEqualTo(positioner.screenRect.width() - paddings[0] - paddings[2])
}
@Test
@@ -513,6 +519,66 @@
assertThat(positioner.isBubbleBarOnLeft).isFalse()
}
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = false)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onLeft() {
+ testGetBubbleBarExpandedViewBounds(onLeft = true, isOverflow = true)
+ }
+
+ @Test
+ fun testGetBubbleBarExpandedViewBounds_isOverflow_onRight() {
+ testGetBubbleBarExpandedViewBounds(onLeft = false, isOverflow = true)
+ }
+
+ private fun testGetBubbleBarExpandedViewBounds(onLeft: Boolean, isOverflow: Boolean) {
+ positioner.setShowingInBubbleBar(true)
+ val deviceConfig =
+ defaultDeviceConfig.copy(
+ isLargeScreen = true,
+ isLandscape = true,
+ insets = Insets.of(10, 20, 5, 15),
+ windowBounds = Rect(0, 0, 2000, 2600)
+ )
+ positioner.update(deviceConfig)
+
+ positioner.bubbleBarBounds = getBubbleBarBounds(onLeft, deviceConfig)
+
+ val expandedViewPadding =
+ context.resources.getDimensionPixelSize(R.dimen.bubble_expanded_view_padding)
+
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ // Pin to the left, calculate right
+ left = deviceConfig.insets.left + expandedViewPadding
+ right = left + positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ } else {
+ // Pin to the right, calculate left
+ right =
+ deviceConfig.windowBounds.right - deviceConfig.insets.right - expandedViewPadding
+ left = right - positioner.getExpandedViewWidthForBubbleBar(isOverflow)
+ }
+ // Above the bubble bar
+ val bottom = positioner.bubbleBarBounds.top - expandedViewPadding
+ // Calculate right and top based on size
+ val top = bottom - positioner.getExpandedViewHeightForBubbleBar(isOverflow)
+ val expectedBounds = Rect(left, top, right, bottom)
+
+ val bounds = Rect()
+ positioner.getBubbleBarExpandedViewBounds(onLeft, isOverflow, bounds)
+
+ assertThat(bounds).isEqualTo(expectedBounds)
+ }
+
private val defaultYPosition: Float
/**
* Calculates the Y position bubbles should be placed based on the config. Based on the
@@ -544,4 +610,21 @@
positioner.getAllowableStackPositionRegion(1 /* bubbleCount */)
return allowableStackRegion.top + allowableStackRegion.height() * offsetPercent
}
+
+ private fun getBubbleBarBounds(onLeft: Boolean, deviceConfig: DeviceConfig): Rect {
+ val width = 200
+ val height = 100
+ val bottom = deviceConfig.windowBounds.bottom - deviceConfig.insets.bottom
+ val top = bottom - height
+ val left: Int
+ val right: Int
+ if (onLeft) {
+ left = deviceConfig.insets.left
+ right = left + width
+ } else {
+ right = deviceConfig.windowBounds.right - deviceConfig.insets.right
+ left = right - width
+ }
+ return Rect(left, top, right, bottom)
+ }
}
diff --git a/libs/WindowManager/Shell/res/values/dimen.xml b/libs/WindowManager/Shell/res/values/dimen.xml
index 00fb298..43ce166 100644
--- a/libs/WindowManager/Shell/res/values/dimen.xml
+++ b/libs/WindowManager/Shell/res/values/dimen.xml
@@ -535,5 +535,7 @@
<!-- The vertical margin that needs to be preserved between the scaled window bounds and the
original window bounds (once the surface is scaled enough to do so) -->
<dimen name="cross_task_back_vertical_margin">8dp</dimen>
+ <!-- The offset from the left edge of the entering page for the cross-activity animation -->
+ <dimen name="cross_activity_back_entering_start_offset">96dp</dimen>
</resources>
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
deleted file mode 100644
index d6f7c36..0000000
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.java
+++ /dev/null
@@ -1,455 +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.wm.shell.back;
-
-import static android.view.RemoteAnimationTarget.MODE_CLOSING;
-import static android.view.RemoteAnimationTarget.MODE_OPENING;
-import static android.window.BackEvent.EDGE_RIGHT;
-
-import static com.android.internal.jank.InteractionJankMonitor.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY;
-import static com.android.wm.shell.back.BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD;
-import static com.android.wm.shell.protolog.ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW;
-
-import android.animation.Animator;
-import android.animation.AnimatorListenerAdapter;
-import android.animation.ValueAnimator;
-import android.annotation.NonNull;
-import android.content.Context;
-import android.graphics.Matrix;
-import android.graphics.PointF;
-import android.graphics.Rect;
-import android.graphics.RectF;
-import android.os.RemoteException;
-import android.util.FloatProperty;
-import android.util.TypedValue;
-import android.view.IRemoteAnimationFinishedCallback;
-import android.view.IRemoteAnimationRunner;
-import android.view.RemoteAnimationTarget;
-import android.view.SurfaceControl;
-import android.view.animation.Interpolator;
-import android.window.BackEvent;
-import android.window.BackMotionEvent;
-import android.window.BackProgressAnimator;
-import android.window.IOnBackInvokedCallback;
-
-import com.android.internal.dynamicanimation.animation.SpringAnimation;
-import com.android.internal.dynamicanimation.animation.SpringForce;
-import com.android.internal.policy.ScreenDecorationsUtils;
-import com.android.internal.protolog.common.ProtoLog;
-import com.android.wm.shell.animation.Interpolators;
-import com.android.wm.shell.common.annotations.ShellMainThread;
-
-import javax.inject.Inject;
-
-/** Class that defines cross-activity animation. */
-@ShellMainThread
-public class CrossActivityBackAnimation extends ShellBackAnimation {
- /**
- * Minimum scale of the entering/closing window.
- */
- private static final float MIN_WINDOW_SCALE = 0.9f;
-
- /** Duration of post animation after gesture committed. */
- private static final int POST_ANIMATION_DURATION = 350;
- private static final Interpolator INTERPOLATOR = Interpolators.STANDARD_DECELERATE;
- private static final FloatProperty<CrossActivityBackAnimation> ENTER_PROGRESS_PROP =
- new FloatProperty<>("enter-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setEnteringProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getEnteringProgress();
- }
- };
- private static final FloatProperty<CrossActivityBackAnimation> LEAVE_PROGRESS_PROP =
- new FloatProperty<>("leave-alpha") {
- @Override
- public void setValue(CrossActivityBackAnimation anim, float value) {
- anim.setLeavingProgress(value);
- }
-
- @Override
- public Float get(CrossActivityBackAnimation object) {
- return object.getLeavingProgress();
- }
- };
- private static final float MIN_WINDOW_ALPHA = 0.01f;
- private static final float WINDOW_X_SHIFT_DP = 48;
- private static final int SCALE_FACTOR = 100;
- // TODO(b/264710590): Use the progress commit threshold from ViewConfiguration once it exists.
- private static final float TARGET_COMMIT_PROGRESS = 0.5f;
- private static final float ENTER_ALPHA_THRESHOLD = 0.22f;
-
- private final Rect mStartTaskRect = new Rect();
- private final float mCornerRadius;
-
- // The closing window properties.
- private final RectF mClosingRect = new RectF();
-
- // The entering window properties.
- private final Rect mEnteringStartRect = new Rect();
- private final RectF mEnteringRect = new RectF();
- private final SpringAnimation mEnteringProgressSpring;
- private final SpringAnimation mLeavingProgressSpring;
- // Max window x-shift in pixels.
- private final float mWindowXShift;
- private final BackAnimationRunner mBackAnimationRunner;
-
- private float mEnteringProgress = 0f;
- private float mLeavingProgress = 0f;
-
- private final PointF mInitialTouchPos = new PointF();
-
- private final Matrix mTransformMatrix = new Matrix();
-
- private final float[] mTmpFloat9 = new float[9];
-
- private RemoteAnimationTarget mEnteringTarget;
- private RemoteAnimationTarget mClosingTarget;
- private SurfaceControl.Transaction mTransaction = new SurfaceControl.Transaction();
-
- private boolean mBackInProgress = false;
- private boolean mIsRightEdge;
- private boolean mTriggerBack = false;
-
- private PointF mTouchPos = new PointF();
- private IRemoteAnimationFinishedCallback mFinishCallback;
-
- private final BackProgressAnimator mProgressAnimator = new BackProgressAnimator();
-
- private final BackAnimationBackground mBackground;
-
- @Inject
- public CrossActivityBackAnimation(Context context, BackAnimationBackground background) {
- mCornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context);
- mBackAnimationRunner = new BackAnimationRunner(
- new Callback(), new Runner(), context, CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY);
- mBackground = background;
- mEnteringProgressSpring = new SpringAnimation(this, ENTER_PROGRESS_PROP);
- mEnteringProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mLeavingProgressSpring = new SpringAnimation(this, LEAVE_PROGRESS_PROP);
- mLeavingProgressSpring.setSpring(new SpringForce()
- .setStiffness(SpringForce.STIFFNESS_MEDIUM)
- .setDampingRatio(SpringForce.DAMPING_RATIO_NO_BOUNCY));
- mWindowXShift = TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, WINDOW_X_SHIFT_DP,
- context.getResources().getDisplayMetrics());
- }
-
- /**
- * Returns 1 if x >= edge1, 0 if x <= edge0, and a smoothed value between the two.
- * From https://en.wikipedia.org/wiki/Smoothstep
- */
- private static float smoothstep(float edge0, float edge1, float x) {
- if (x < edge0) return 0;
- if (x >= edge1) return 1;
-
- x = (x - edge0) / (edge1 - edge0);
- return x * x * (3 - 2 * x);
- }
-
- /**
- * Linearly map x from range (a1, a2) to range (b1, b2).
- */
- private static float mapLinear(float x, float a1, float a2, float b1, float b2) {
- return b1 + (x - a1) * (b2 - b1) / (a2 - a1);
- }
-
- /**
- * Linearly map a normalized value from (0, 1) to (min, max).
- */
- private static float mapRange(float value, float min, float max) {
- return min + (value * (max - min));
- }
-
- private void startBackAnimation() {
- if (mEnteringTarget == null || mClosingTarget == null) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Entering target or closing target is null.");
- return;
- }
- mTransaction.setAnimationTransaction();
-
- // Offset start rectangle to align task bounds.
- mStartTaskRect.set(mClosingTarget.windowConfiguration.getBounds());
- mStartTaskRect.offsetTo(0, 0);
-
- // Draw background with task background color.
- mBackground.ensureBackground(mClosingTarget.windowConfiguration.getBounds(),
- mEnteringTarget.taskInfo.taskDescription.getBackgroundColor(), mTransaction);
- setEnteringProgress(0);
- setLeavingProgress(0);
- }
-
- private void applyTransform(SurfaceControl leash, RectF targetRect, float targetAlpha) {
- if (leash == null || !leash.isValid()) {
- return;
- }
-
- final float scale = targetRect.width() / mStartTaskRect.width();
- mTransformMatrix.reset();
- mTransformMatrix.setScale(scale, scale);
- mTransformMatrix.postTranslate(targetRect.left, targetRect.top);
- mTransaction.setAlpha(leash, targetAlpha)
- .setMatrix(leash, mTransformMatrix, mTmpFloat9)
- .setWindowCrop(leash, mStartTaskRect)
- .setCornerRadius(leash, mCornerRadius);
- }
-
- private void finishAnimation() {
- if (mEnteringTarget != null) {
- if (mEnteringTarget.leash != null && mEnteringTarget.leash.isValid()) {
- mTransaction.setCornerRadius(mEnteringTarget.leash, 0);
- mEnteringTarget.leash.release();
- }
- mEnteringTarget = null;
- }
- if (mClosingTarget != null) {
- if (mClosingTarget.leash != null) {
- mClosingTarget.leash.release();
- }
- mClosingTarget = null;
- }
- if (mBackground != null) {
- mBackground.removeBackground(mTransaction);
- }
-
- mTransaction.apply();
- mBackInProgress = false;
- mTransformMatrix.reset();
- mInitialTouchPos.set(0, 0);
-
- if (mFinishCallback != null) {
- try {
- mFinishCallback.onAnimationFinished();
- } catch (RemoteException e) {
- e.printStackTrace();
- }
- mFinishCallback = null;
- }
- mEnteringProgressSpring.animateToFinalPosition(0);
- mEnteringProgressSpring.skipToEnd();
- mLeavingProgressSpring.animateToFinalPosition(0);
- mLeavingProgressSpring.skipToEnd();
- }
-
- private void onGestureProgress(@NonNull BackEvent backEvent) {
- if (!mBackInProgress) {
- mIsRightEdge = backEvent.getSwipeEdge() == EDGE_RIGHT;
- mInitialTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
- mBackInProgress = true;
- }
- mTouchPos.set(backEvent.getTouchX(), backEvent.getTouchY());
-
- float progress = backEvent.getProgress();
- float springProgress = (mTriggerBack
- ? mapLinear(progress, 0f, 1, TARGET_COMMIT_PROGRESS, 1)
- : mapLinear(progress, 0, 1f, 0, TARGET_COMMIT_PROGRESS)) * SCALE_FACTOR;
- mLeavingProgressSpring.animateToFinalPosition(springProgress);
- mEnteringProgressSpring.animateToFinalPosition(springProgress);
- mBackground.onBackProgressed(progress);
- }
-
- private void onGestureCommitted() {
- if (mEnteringTarget == null || mClosingTarget == null || mClosingTarget.leash == null
- || mEnteringTarget.leash == null || !mEnteringTarget.leash.isValid()
- || !mClosingTarget.leash.isValid()) {
- finishAnimation();
- return;
- }
- // End the fade animations
- mLeavingProgressSpring.cancel();
- mEnteringProgressSpring.cancel();
-
- // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
- // coordinate of the gesture driven phase.
- mEnteringRect.round(mEnteringStartRect);
- mTransaction.hide(mClosingTarget.leash);
-
- ValueAnimator valueAnimator =
- ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION);
- valueAnimator.setInterpolator(INTERPOLATOR);
- valueAnimator.addUpdateListener(animation -> {
- float progress = animation.getAnimatedFraction();
- updatePostCommitEnteringAnimation(progress);
- if (progress > 1 - UPDATE_SYSUI_FLAGS_THRESHOLD) {
- mBackground.resetStatusBarCustomization();
- }
- mTransaction.apply();
- });
-
- valueAnimator.addListener(new AnimatorListenerAdapter() {
- @Override
- public void onAnimationEnd(Animator animation) {
- mBackground.resetStatusBarCustomization();
- finishAnimation();
- }
- });
- valueAnimator.start();
- }
-
- private void updatePostCommitEnteringAnimation(float progress) {
- float left = mapRange(progress, mEnteringStartRect.left, mStartTaskRect.left);
- float top = mapRange(progress, mEnteringStartRect.top, mStartTaskRect.top);
- float width = mapRange(progress, mEnteringStartRect.width(), mStartTaskRect.width());
- float height = mapRange(progress, mEnteringStartRect.height(), mStartTaskRect.height());
- float alpha = mapRange(progress, getPreCommitEnteringAlpha(), 1.0f);
- mEnteringRect.set(left, top, left + width, top + height);
- applyTransform(mEnteringTarget.leash, mEnteringRect, alpha);
- }
-
- private float getPreCommitEnteringAlpha() {
- return Math.max(smoothstep(ENTER_ALPHA_THRESHOLD, 0.7f, mEnteringProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getEnteringProgress() {
- return mEnteringProgress * SCALE_FACTOR;
- }
-
- private void setEnteringProgress(float value) {
- mEnteringProgress = value / SCALE_FACTOR;
- if (mEnteringTarget != null && mEnteringTarget.leash != null) {
- transformWithProgress(
- mEnteringProgress,
- getPreCommitEnteringAlpha(),
- mEnteringTarget.leash,
- mEnteringRect,
- -mWindowXShift,
- 0
- );
- }
- }
-
- private float getPreCommitLeavingAlpha() {
- return Math.max(1 - smoothstep(0, ENTER_ALPHA_THRESHOLD, mLeavingProgress),
- MIN_WINDOW_ALPHA);
- }
-
- private float getLeavingProgress() {
- return mLeavingProgress * SCALE_FACTOR;
- }
-
- private void setLeavingProgress(float value) {
- mLeavingProgress = value / SCALE_FACTOR;
- if (mClosingTarget != null && mClosingTarget.leash != null) {
- transformWithProgress(
- mLeavingProgress,
- getPreCommitLeavingAlpha(),
- mClosingTarget.leash,
- mClosingRect,
- 0,
- mIsRightEdge ? 0 : mWindowXShift
- );
- }
- }
-
- private void transformWithProgress(float progress, float alpha, SurfaceControl surface,
- RectF targetRect, float deltaXMin, float deltaXMax) {
-
- final int width = mStartTaskRect.width();
- final int height = mStartTaskRect.height();
-
- final float interpolatedProgress = INTERPOLATOR.getInterpolation(progress);
- final float closingScale = MIN_WINDOW_SCALE
- + (1 - interpolatedProgress) * (1 - MIN_WINDOW_SCALE);
- final float closingWidth = closingScale * width;
- final float closingHeight = (float) height / width * closingWidth;
-
- // Move the window along the X axis.
- float closingLeft = mStartTaskRect.left + (width - closingWidth) / 2;
- closingLeft += mapRange(interpolatedProgress, deltaXMin, deltaXMax);
-
- // Move the window along the Y axis.
- final float closingTop = (height - closingHeight) * 0.5f;
- targetRect.set(
- closingLeft, closingTop, closingLeft + closingWidth, closingTop + closingHeight);
-
- applyTransform(surface, targetRect, Math.max(alpha, MIN_WINDOW_ALPHA));
- mTransaction.apply();
- }
-
- @Override
- public BackAnimationRunner getRunner() {
- return mBackAnimationRunner;
- }
-
- private final class Callback extends IOnBackInvokedCallback.Default {
- @Override
- public void onBackStarted(BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackStarted(backEvent,
- CrossActivityBackAnimation.this::onGestureProgress);
- }
-
- @Override
- public void onBackProgressed(@NonNull BackMotionEvent backEvent) {
- mTriggerBack = backEvent.getTriggerBack();
- mProgressAnimator.onBackProgressed(backEvent);
- }
-
- @Override
- public void onBackCancelled() {
- mProgressAnimator.onBackCancelled(() -> {
- // mProgressAnimator can reach finish stage earlier than mLeavingProgressSpring,
- // and if we release all animation leash first, the leavingProgressSpring won't
- // able to update the animation anymore, which cause flicker.
- // Here should force update the closing animation target to the final stage before
- // release it.
- setLeavingProgress(0);
- finishAnimation();
- });
- }
-
- @Override
- public void onBackInvoked() {
- mProgressAnimator.reset();
- onGestureCommitted();
- }
- }
-
- private final class Runner extends IRemoteAnimationRunner.Default {
- @Override
- public void onAnimationStart(
- int transit,
- RemoteAnimationTarget[] apps,
- RemoteAnimationTarget[] wallpapers,
- RemoteAnimationTarget[] nonApps,
- IRemoteAnimationFinishedCallback finishedCallback) {
- ProtoLog.d(WM_SHELL_BACK_PREVIEW, "Start back to activity animation.");
- for (RemoteAnimationTarget a : apps) {
- if (a.mode == MODE_CLOSING) {
- mClosingTarget = a;
- }
- if (a.mode == MODE_OPENING) {
- mEnteringTarget = a;
- }
- }
-
- startBackAnimation();
- mFinishCallback = finishedCallback;
- }
-
- @Override
- public void onAnimationCancelled() {
- finishAnimation();
- }
- }
-}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
new file mode 100644
index 0000000..edf29dd
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/back/CrossActivityBackAnimation.kt
@@ -0,0 +1,367 @@
+/*
+ * 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.wm.shell.back
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.content.Context
+import android.content.res.Configuration
+import android.graphics.Matrix
+import android.graphics.PointF
+import android.graphics.Rect
+import android.graphics.RectF
+import android.os.RemoteException
+import android.view.Display
+import android.view.IRemoteAnimationFinishedCallback
+import android.view.IRemoteAnimationRunner
+import android.view.RemoteAnimationTarget
+import android.view.SurfaceControl
+import android.view.animation.DecelerateInterpolator
+import android.view.animation.Interpolator
+import android.window.BackEvent
+import android.window.BackMotionEvent
+import android.window.BackProgressAnimator
+import android.window.IOnBackInvokedCallback
+import com.android.internal.jank.Cuj
+import com.android.internal.policy.ScreenDecorationsUtils
+import com.android.internal.protolog.common.ProtoLog
+import com.android.wm.shell.R
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer
+import com.android.wm.shell.animation.Interpolators
+import com.android.wm.shell.common.annotations.ShellMainThread
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+import javax.inject.Inject
+import kotlin.math.abs
+import kotlin.math.max
+import kotlin.math.min
+
+/** Class that defines cross-activity animation. */
+@ShellMainThread
+class CrossActivityBackAnimation @Inject constructor(
+ private val context: Context,
+ private val background: BackAnimationBackground,
+ private val rootTaskDisplayAreaOrganizer: RootTaskDisplayAreaOrganizer
+) : ShellBackAnimation() {
+
+ private val startClosingRect = RectF()
+ private val targetClosingRect = RectF()
+ private val currentClosingRect = RectF()
+
+ private val startEnteringRect = RectF()
+ private val targetEnteringRect = RectF()
+ private val currentEnteringRect = RectF()
+
+ private val taskBoundsRect = Rect()
+
+ private val cornerRadius = ScreenDecorationsUtils.getWindowCornerRadius(context)
+
+ private val backAnimationRunner = BackAnimationRunner(
+ Callback(), Runner(), context, Cuj.CUJ_PREDICTIVE_BACK_CROSS_ACTIVITY
+ )
+ private val initialTouchPos = PointF()
+ private val transformMatrix = Matrix()
+ private val tmpFloat9 = FloatArray(9)
+ private var enteringTarget: RemoteAnimationTarget? = null
+ private var closingTarget: RemoteAnimationTarget? = null
+ private val transaction = SurfaceControl.Transaction()
+ private var triggerBack = false
+ private var finishCallback: IRemoteAnimationFinishedCallback? = null
+ private val progressAnimator = BackProgressAnimator()
+ private val displayBoundsMargin =
+ context.resources.getDimension(R.dimen.cross_task_back_vertical_margin)
+ private val enteringStartOffset =
+ context.resources.getDimension(R.dimen.cross_activity_back_entering_start_offset)
+
+ private val gestureInterpolator = Interpolators.STANDARD_DECELERATE
+ private val postCommitInterpolator = Interpolators.FAST_OUT_SLOW_IN
+ private val verticalMoveInterpolator: Interpolator = DecelerateInterpolator()
+
+ private var scrimLayer: SurfaceControl? = null
+ private var maxScrimAlpha: Float = 0f
+
+ override fun getRunner() = backAnimationRunner
+
+ private fun startBackAnimation(backMotionEvent: BackMotionEvent) {
+ if (enteringTarget == null || closingTarget == null) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW,
+ "Entering target or closing target is null."
+ )
+ return
+ }
+ triggerBack = backMotionEvent.triggerBack
+ initialTouchPos.set(backMotionEvent.touchX, backMotionEvent.touchY)
+
+ transaction.setAnimationTransaction()
+
+ // Offset start rectangle to align task bounds.
+ taskBoundsRect.set(closingTarget!!.windowConfiguration.bounds)
+ taskBoundsRect.offsetTo(0, 0)
+
+ startClosingRect.set(taskBoundsRect)
+
+ // scale closing target into the middle for rhs and to the right for lhs
+ targetClosingRect.set(startClosingRect)
+ targetClosingRect.scaleCentered(MAX_SCALE)
+ if (backMotionEvent.swipeEdge != BackEvent.EDGE_RIGHT) {
+ targetClosingRect.offset(
+ startClosingRect.right - targetClosingRect.right - displayBoundsMargin, 0f
+ )
+ }
+
+ // the entering target starts 96dp to the left of the screen edge...
+ startEnteringRect.set(startClosingRect)
+ startEnteringRect.offset(-enteringStartOffset, 0f)
+
+ // ...and gets scaled in sync with the closing target
+ targetEnteringRect.set(startEnteringRect)
+ targetEnteringRect.scaleCentered(MAX_SCALE)
+
+ // Draw background with task background color.
+ background.ensureBackground(
+ closingTarget!!.windowConfiguration.bounds,
+ enteringTarget!!.taskInfo.taskDescription!!.backgroundColor, transaction
+ )
+ ensureScrimLayer()
+ transaction.apply()
+ }
+
+ private fun onGestureProgress(backEvent: BackEvent) {
+ val progress = gestureInterpolator.getInterpolation(backEvent.progress)
+ background.onBackProgressed(progress)
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ val yOffset = getYOffset(currentClosingRect, backEvent.touchY)
+ currentClosingRect.offset(0f, yOffset)
+ applyTransform(closingTarget?.leash, currentClosingRect, 1f)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ currentEnteringRect.offset(0f, yOffset)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun getYOffset(centeredRect: RectF, touchY: Float): Float {
+ val screenHeight = taskBoundsRect.height()
+ // Base the window movement in the Y axis on the touch movement in the Y axis.
+ val rawYDelta = touchY - initialTouchPos.y
+ val yDirection = (if (rawYDelta < 0) -1 else 1)
+ // limit yDelta interpretation to 1/2 of screen height in either direction
+ val deltaYRatio = min(screenHeight / 2f, abs(rawYDelta)) / (screenHeight / 2f)
+ val interpolatedYRatio: Float = verticalMoveInterpolator.getInterpolation(deltaYRatio)
+ // limit y-shift so surface never passes 8dp screen margin
+ val deltaY = yDirection * interpolatedYRatio * max(
+ 0f, (screenHeight - centeredRect.height()) / 2f - displayBoundsMargin
+ )
+ return deltaY
+ }
+
+ private fun onGestureCommitted() {
+ if (closingTarget?.leash == null || enteringTarget?.leash == null ||
+ !enteringTarget!!.leash.isValid || !closingTarget!!.leash.isValid
+ ) {
+ finishAnimation()
+ return
+ }
+
+ // We enter phase 2 of the animation, the starting coordinates for phase 2 are the current
+ // coordinate of the gesture driven phase. Let's update the start and target rects and kick
+ // off the animator
+ startClosingRect.set(currentClosingRect)
+ startEnteringRect.set(currentEnteringRect)
+ targetEnteringRect.set(taskBoundsRect)
+ targetClosingRect.set(taskBoundsRect)
+ targetClosingRect.offset(currentClosingRect.left + enteringStartOffset, 0f)
+
+ val valueAnimator = ValueAnimator.ofFloat(1f, 0f).setDuration(POST_ANIMATION_DURATION)
+ valueAnimator.addUpdateListener { animation: ValueAnimator ->
+ val progress = animation.animatedFraction
+ onPostCommitProgress(progress)
+ if (progress > 1 - BackAnimationConstants.UPDATE_SYSUI_FLAGS_THRESHOLD) {
+ background.resetStatusBarCustomization()
+ }
+ }
+ valueAnimator.addListener(object : AnimatorListenerAdapter() {
+ override fun onAnimationEnd(animation: Animator) {
+ background.resetStatusBarCustomization()
+ finishAnimation()
+ }
+ })
+ valueAnimator.start()
+ }
+
+ private fun onPostCommitProgress(linearProgress: Float) {
+ val closingAlpha = max(1f - linearProgress * 2, 0f)
+ val progress = postCommitInterpolator.getInterpolation(linearProgress)
+ scrimLayer?.let { transaction.setAlpha(it, maxScrimAlpha * (1f - linearProgress)) }
+ currentClosingRect.setInterpolatedRectF(startClosingRect, targetClosingRect, progress)
+ applyTransform(closingTarget?.leash, currentClosingRect, closingAlpha)
+ currentEnteringRect.setInterpolatedRectF(startEnteringRect, targetEnteringRect, progress)
+ applyTransform(enteringTarget?.leash, currentEnteringRect, 1f)
+ transaction.apply()
+ }
+
+ private fun finishAnimation() {
+ enteringTarget?.let {
+ if (it.leash != null && it.leash.isValid) {
+ transaction.setCornerRadius(it.leash, 0f)
+ it.leash.release()
+ }
+ enteringTarget = null
+ }
+
+ closingTarget?.leash?.release()
+ closingTarget = null
+
+ background.removeBackground(transaction)
+ transaction.apply()
+ transformMatrix.reset()
+ initialTouchPos.set(0f, 0f)
+ try {
+ finishCallback?.onAnimationFinished()
+ } catch (e: RemoteException) {
+ e.printStackTrace()
+ }
+ finishCallback = null
+ removeScrimLayer()
+ }
+
+ private fun applyTransform(leash: SurfaceControl?, rect: RectF, alpha: Float) {
+ if (leash == null || !leash.isValid) return
+ val scale = rect.width() / taskBoundsRect.width()
+ transformMatrix.reset()
+ transformMatrix.setScale(scale, scale)
+ transformMatrix.postTranslate(rect.left, rect.top)
+ transaction.setAlpha(leash, alpha)
+ .setMatrix(leash, transformMatrix, tmpFloat9)
+ .setCrop(leash, taskBoundsRect)
+ .setCornerRadius(leash, cornerRadius)
+ }
+
+ private fun ensureScrimLayer() {
+ if (scrimLayer != null) return
+ val isDarkTheme: Boolean = isDarkMode(context)
+ val scrimBuilder = SurfaceControl.Builder()
+ .setName("Cross-Activity back animation scrim")
+ .setCallsite("CrossActivityBackAnimation")
+ .setColorLayer()
+ .setOpaque(false)
+ .setHidden(false)
+
+ rootTaskDisplayAreaOrganizer.attachToDisplayArea(Display.DEFAULT_DISPLAY, scrimBuilder)
+ scrimLayer = scrimBuilder.build()
+ val colorComponents = floatArrayOf(0f, 0f, 0f)
+ maxScrimAlpha = if (isDarkTheme) MAX_SCRIM_ALPHA_DARK else MAX_SCRIM_ALPHA_LIGHT
+ transaction
+ .setColor(scrimLayer, colorComponents)
+ .setAlpha(scrimLayer!!, maxScrimAlpha)
+ .setRelativeLayer(scrimLayer!!, closingTarget!!.leash, -1)
+ .show(scrimLayer)
+ }
+
+ private fun removeScrimLayer() {
+ scrimLayer?.let {
+ if (it.isValid) {
+ transaction.remove(it).apply()
+ }
+ }
+ scrimLayer = null
+ }
+
+
+ private inner class Callback : IOnBackInvokedCallback.Default() {
+ override fun onBackStarted(backMotionEvent: BackMotionEvent) {
+ startBackAnimation(backMotionEvent)
+ progressAnimator.onBackStarted(backMotionEvent) { backEvent: BackEvent ->
+ onGestureProgress(backEvent)
+ }
+ }
+
+ override fun onBackProgressed(backEvent: BackMotionEvent) {
+ triggerBack = backEvent.triggerBack
+ progressAnimator.onBackProgressed(backEvent)
+ }
+
+ override fun onBackCancelled() {
+ progressAnimator.onBackCancelled {
+ finishAnimation()
+ }
+ }
+
+ override fun onBackInvoked() {
+ progressAnimator.reset()
+ onGestureCommitted()
+ }
+ }
+
+ private inner class Runner : IRemoteAnimationRunner.Default() {
+ override fun onAnimationStart(
+ transit: Int,
+ apps: Array<RemoteAnimationTarget>,
+ wallpapers: Array<RemoteAnimationTarget>?,
+ nonApps: Array<RemoteAnimationTarget>?,
+ finishedCallback: IRemoteAnimationFinishedCallback
+ ) {
+ ProtoLog.d(
+ ShellProtoLogGroup.WM_SHELL_BACK_PREVIEW, "Start back to activity animation."
+ )
+ for (a in apps) {
+ when (a.mode) {
+ RemoteAnimationTarget.MODE_CLOSING -> closingTarget = a
+ RemoteAnimationTarget.MODE_OPENING -> enteringTarget = a
+ }
+ }
+ finishCallback = finishedCallback
+ }
+
+ override fun onAnimationCancelled() {
+ finishAnimation()
+ }
+ }
+
+ companion object {
+ /** Max scale of the entering/closing window.*/
+ private const val MAX_SCALE = 0.9f
+
+ /** Duration of post animation after gesture committed. */
+ private const val POST_ANIMATION_DURATION = 300L
+
+ private const val MAX_SCRIM_ALPHA_DARK = 0.8f
+ private const val MAX_SCRIM_ALPHA_LIGHT = 0.2f
+ }
+}
+
+private fun isDarkMode(context: Context): Boolean {
+ return context.resources.configuration.uiMode and Configuration.UI_MODE_NIGHT_MASK ==
+ Configuration.UI_MODE_NIGHT_YES
+}
+
+private fun RectF.setInterpolatedRectF(start: RectF, target: RectF, progress: Float) {
+ require(!(progress < 0 || progress > 1)) { "Progress value must be between 0 and 1" }
+ left = start.left + (target.left - start.left) * progress
+ top = start.top + (target.top - start.top) * progress
+ right = start.right + (target.right - start.right) * progress
+ bottom = start.bottom + (target.bottom - start.bottom) * progress
+}
+
+private fun RectF.scaleCentered(
+ scale: Float,
+ pivotX: Float = left + width() / 2,
+ pivotY: Float = top + height() / 2
+) {
+ offset(-pivotX, -pivotY) // move pivot to origin
+ scale(scale)
+ offset(pivotX, pivotY) // Move back to the original position
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
index f4a401c..4d5e516 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubblePositioner.java
@@ -870,7 +870,7 @@
if (onLeft) {
left = getInsets().left + padding;
} else {
- left = getAvailableRect().width() - width - padding;
+ left = getAvailableRect().right - width - padding;
}
int top = getExpandedViewBottomForBubbleBar() - height;
out.offsetTo(left, top);
diff --git a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
index 1ccc7d8..5f25d70 100644
--- a/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
+++ b/libs/WindowManager/Shell/tests/flicker/pip/src/com/android/wm/shell/flicker/pip/FromSplitScreenEnterPipOnUserLeaveHintTest.kt
@@ -24,6 +24,7 @@
import android.tools.flicker.legacy.LegacyFlickerTestFactory
import android.tools.helpers.WindowUtils
import android.tools.traces.parsers.toFlickerComponent
+import androidx.test.filters.FlakyTest
import androidx.test.filters.RequiresDevice
import com.android.server.wm.flicker.helpers.SimpleAppHelper
import com.android.server.wm.flicker.testapp.ActivityOptions
@@ -181,6 +182,12 @@
}
}
+ /** {@inheritDoc} */
+ @FlakyTest(bugId = 312446524)
+ @Test
+ override fun visibleLayersShownMoreThanOneConsecutiveEntry() =
+ super.visibleLayersShownMoreThanOneConsecutiveEntry()
+
companion object {
@Parameterized.Parameters(name = "{0}")
@JvmStatic
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
index 9ded6ea..703eb19 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/back/BackAnimationControllerTest.java
@@ -61,6 +61,7 @@
import androidx.test.filters.SmallTest;
import com.android.internal.util.test.FakeSettingsProvider;
+import com.android.wm.shell.RootTaskDisplayAreaOrganizer;
import com.android.wm.shell.ShellTestCase;
import com.android.wm.shell.TestShellExecutor;
import com.android.wm.shell.sysui.ShellCommandHandler;
@@ -113,6 +114,8 @@
private InputManager mInputManager;
@Mock
private ShellCommandHandler mShellCommandHandler;
+ @Mock
+ private RootTaskDisplayAreaOrganizer mRootTaskDisplayAreaOrganizer;
private BackAnimationController mController;
private TestableContentResolver mContentResolver;
@@ -133,7 +136,8 @@
mShellInit = spy(new ShellInit(mShellExecutor));
mShellBackAnimationRegistry =
new ShellBackAnimationRegistry(
- new CrossActivityBackAnimation(mContext, mAnimationBackground),
+ new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer),
new CrossTaskBackAnimation(mContext, mAnimationBackground),
/* dialogCloseAnimation= */ null,
new CustomizeActivityAnimation(mContext, mAnimationBackground),
@@ -528,8 +532,8 @@
@Test
public void testBackToActivity() throws RemoteException {
- final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(mContext,
- mAnimationBackground);
+ final CrossActivityBackAnimation animation = new CrossActivityBackAnimation(
+ mContext, mAnimationBackground, mRootTaskDisplayAreaOrganizer);
verifySystemBackBehavior(BackNavigationInfo.TYPE_CROSS_ACTIVITY, animation.getRunner());
}
diff --git a/media/java/android/media/MediaCas.java b/media/java/android/media/MediaCas.java
index ab7c27f..2d7db5e 100644
--- a/media/java/android/media/MediaCas.java
+++ b/media/java/android/media/MediaCas.java
@@ -35,6 +35,7 @@
import android.os.Bundle;
import android.os.Handler;
import android.os.HandlerThread;
+import android.os.IBinder;
import android.os.IHwBinder;
import android.os.Looper;
import android.os.Message;
@@ -43,7 +44,6 @@
import android.os.ServiceManager;
import android.os.ServiceSpecificException;
import android.util.Log;
-import android.util.Singleton;
import com.android.internal.util.FrameworkStatsLog;
@@ -264,71 +264,107 @@
public static final int PLUGIN_STATUS_SESSION_NUMBER_CHANGED =
android.hardware.cas.StatusEvent.PLUGIN_SESSION_NUMBER_CHANGED;
- private static final Singleton<IMediaCasService> sService =
- new Singleton<IMediaCasService>() {
+ private static IMediaCasService sService = null;
+ private static Object sAidlLock = new Object();
+
+ /** DeathListener for AIDL service */
+ private static IBinder.DeathRecipient sDeathListener =
+ new IBinder.DeathRecipient() {
@Override
- protected IMediaCasService create() {
- try {
- Log.d(TAG, "Trying to get AIDL service");
- IMediaCasService serviceAidl =
- IMediaCasService.Stub.asInterface(
- ServiceManager.waitForDeclaredService(
- IMediaCasService.DESCRIPTOR + "/default"));
- if (serviceAidl != null) {
- return serviceAidl;
- }
- } catch (Exception eAidl) {
- Log.d(TAG, "Failed to get cas AIDL service");
+ public void binderDied() {
+ synchronized (sAidlLock) {
+ Log.d(TAG, "The service is dead");
+ sService.asBinder().unlinkToDeath(sDeathListener, 0);
+ sService = null;
}
- return null;
- }
- };
-
- private static final Singleton<android.hardware.cas.V1_0.IMediaCasService> sServiceHidl =
- new Singleton<android.hardware.cas.V1_0.IMediaCasService>() {
- @Override
- protected android.hardware.cas.V1_0.IMediaCasService create() {
- try {
- Log.d(TAG, "Trying to get cas@1.2 service");
- android.hardware.cas.V1_2.IMediaCasService serviceV12 =
- android.hardware.cas.V1_2.IMediaCasService.getService(
- true /*wait*/);
- if (serviceV12 != null) {
- return serviceV12;
- }
- } catch (Exception eV1_2) {
- Log.d(TAG, "Failed to get cas@1.2 service");
- }
-
- try {
- Log.d(TAG, "Trying to get cas@1.1 service");
- android.hardware.cas.V1_1.IMediaCasService serviceV11 =
- android.hardware.cas.V1_1.IMediaCasService.getService(
- true /*wait*/);
- if (serviceV11 != null) {
- return serviceV11;
- }
- } catch (Exception eV1_1) {
- Log.d(TAG, "Failed to get cas@1.1 service");
- }
-
- try {
- Log.d(TAG, "Trying to get cas@1.0 service");
- return android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
- } catch (Exception eV1_0) {
- Log.d(TAG, "Failed to get cas@1.0 service");
- }
-
- return null;
}
};
static IMediaCasService getService() {
- return sService.get();
+ synchronized (sAidlLock) {
+ if (sService == null || !sService.asBinder().isBinderAlive()) {
+ try {
+ Log.d(TAG, "Trying to get AIDL service");
+ sService =
+ IMediaCasService.Stub.asInterface(
+ ServiceManager.waitForDeclaredService(
+ IMediaCasService.DESCRIPTOR + "/default"));
+ if (sService != null) {
+ sService.asBinder().linkToDeath(sDeathListener, 0);
+ }
+ } catch (Exception eAidl) {
+ Log.d(TAG, "Failed to get cas AIDL service");
+ }
+ }
+ return sService;
+ }
}
+ private static android.hardware.cas.V1_0.IMediaCasService sServiceHidl = null;
+ private static Object sHidlLock = new Object();
+
+ /** Used to indicate the right end-point to handle the serviceDied method */
+ private static final long MEDIA_CAS_HIDL_COOKIE = 394;
+
+ /** DeathListener for HIDL service */
+ private static IHwBinder.DeathRecipient sDeathListenerHidl =
+ new IHwBinder.DeathRecipient() {
+ @Override
+ public void serviceDied(long cookie) {
+ if (cookie == MEDIA_CAS_HIDL_COOKIE) {
+ synchronized (sHidlLock) {
+ sServiceHidl = null;
+ }
+ }
+ }
+ };
+
static android.hardware.cas.V1_0.IMediaCasService getServiceHidl() {
- return sServiceHidl.get();
+ synchronized (sHidlLock) {
+ if (sServiceHidl != null) {
+ return sServiceHidl;
+ } else {
+ try {
+ Log.d(TAG, "Trying to get cas@1.2 service");
+ android.hardware.cas.V1_2.IMediaCasService serviceV12 =
+ android.hardware.cas.V1_2.IMediaCasService.getService(true /*wait*/);
+ if (serviceV12 != null) {
+ sServiceHidl = serviceV12;
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ return sServiceHidl;
+ }
+ } catch (Exception eV1_2) {
+ Log.d(TAG, "Failed to get cas@1.2 service");
+ }
+
+ try {
+ Log.d(TAG, "Trying to get cas@1.1 service");
+ android.hardware.cas.V1_1.IMediaCasService serviceV11 =
+ android.hardware.cas.V1_1.IMediaCasService.getService(true /*wait*/);
+ if (serviceV11 != null) {
+ sServiceHidl = serviceV11;
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ return sServiceHidl;
+ }
+ } catch (Exception eV1_1) {
+ Log.d(TAG, "Failed to get cas@1.1 service");
+ }
+
+ try {
+ Log.d(TAG, "Trying to get cas@1.0 service");
+ sServiceHidl =
+ android.hardware.cas.V1_0.IMediaCasService.getService(true /*wait*/);
+ if (sServiceHidl != null) {
+ sServiceHidl.linkToDeath(sDeathListenerHidl, MEDIA_CAS_HIDL_COOKIE);
+ }
+ return sServiceHidl;
+ } catch (Exception eV1_0) {
+ Log.d(TAG, "Failed to get cas@1.0 service");
+ }
+ }
+ }
+ // Couldn't find an HIDL service, returning null.
+ return null;
}
private void validateInternalStates() {
@@ -756,7 +792,7 @@
* @return Whether the specified CA system is supported on this device.
*/
public static boolean isSystemIdSupported(int CA_system_id) {
- IMediaCasService service = sService.get();
+ IMediaCasService service = getService();
if (service != null) {
try {
return service.isSystemIdSupported(CA_system_id);
@@ -765,7 +801,7 @@
}
}
- android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+ android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
if (serviceHidl != null) {
try {
return serviceHidl.isSystemIdSupported(CA_system_id);
@@ -781,7 +817,7 @@
* @return an array of descriptors for the available CA plugins.
*/
public static PluginDescriptor[] enumeratePlugins() {
- IMediaCasService service = sService.get();
+ IMediaCasService service = getService();
if (service != null) {
try {
AidlCasPluginDescriptor[] descriptors = service.enumeratePlugins();
@@ -794,10 +830,11 @@
}
return results;
} catch (RemoteException e) {
+ Log.e(TAG, "Some exception while enumerating plugins");
}
}
- android.hardware.cas.V1_0.IMediaCasService serviceHidl = sServiceHidl.get();
+ android.hardware.cas.V1_0.IMediaCasService serviceHidl = getServiceHidl();
if (serviceHidl != null) {
try {
ArrayList<HidlCasPluginDescriptor> descriptors = serviceHidl.enumeratePlugins();
diff --git a/native/android/surface_control_input_receiver.cpp b/native/android/surface_control_input_receiver.cpp
index da0defd..d178abc 100644
--- a/native/android/surface_control_input_receiver.cpp
+++ b/native/android/surface_control_input_receiver.cpp
@@ -45,6 +45,8 @@
mClientToken(clientToken),
mInputTransferToken(inputTransferToken) {}
+ // The InputConsumer does not keep the InputReceiver alive so the receiver is cleared once the
+ // owner releases it.
~InputReceiver() {
remove();
}
diff --git a/nfc/api/current.txt b/nfc/api/current.txt
index da292a81..80b2be2 100644
--- a/nfc/api/current.txt
+++ b/nfc/api/current.txt
@@ -268,10 +268,9 @@
}
@FlaggedApi("android.nfc.nfc_read_polling_loop") public final class PollingFrame implements android.os.Parcelable {
- ctor public PollingFrame(int, @Nullable byte[], int, int, boolean);
method public int describeContents();
method @NonNull public byte[] getData();
- method public int getTimestamp();
+ method public long getTimestamp();
method public boolean getTriggeredAutoTransact();
method public int getType();
method public int getVendorSpecificGain();
diff --git a/nfc/java/android/nfc/cardemulation/PollingFrame.java b/nfc/java/android/nfc/cardemulation/PollingFrame.java
index af63a6e..654e8cc 100644
--- a/nfc/java/android/nfc/cardemulation/PollingFrame.java
+++ b/nfc/java/android/nfc/cardemulation/PollingFrame.java
@@ -16,6 +16,7 @@
package android.nfc.cardemulation;
+import android.annotation.DurationMillisLong;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
import android.annotation.NonNull;
@@ -148,7 +149,8 @@
private final int mType;
private final byte[] mData;
private final int mGain;
- private final int mTimestamp;
+ @DurationMillisLong
+ private final long mTimestamp;
private final boolean mTriggeredAutoTransact;
public static final @NonNull Parcelable.Creator<PollingFrame> CREATOR =
@@ -180,16 +182,18 @@
* @param type the type of the frame
* @param data a byte array of the data contained in the frame
* @param gain the vendor-specific gain of the field
- * @param timestamp the timestamp in millisecones
+ * @param timestampMillis the timestamp in millisecones
* @param triggeredAutoTransact whether or not this frame triggered the device to start a
* transaction automatically
+ *
+ * @hide
*/
public PollingFrame(@PollingFrameType int type, @Nullable byte[] data,
- int gain, int timestamp, boolean triggeredAutoTransact) {
+ int gain, @DurationMillisLong long timestampMillis, boolean triggeredAutoTransact) {
mType = type;
mData = data == null ? new byte[0] : data;
mGain = gain;
- mTimestamp = timestamp;
+ mTimestamp = timestampMillis;
mTriggeredAutoTransact = triggeredAutoTransact;
}
@@ -230,7 +234,7 @@
* frames relative to each other.
* @return the timestamp in milliseconds
*/
- public int getTimestamp() {
+ public @DurationMillisLong long getTimestamp() {
return mTimestamp;
}
@@ -264,7 +268,7 @@
frame.putInt(KEY_POLLING_LOOP_GAIN, (byte) getVendorSpecificGain());
}
frame.putByteArray(KEY_POLLING_LOOP_DATA, getData());
- frame.putInt(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
+ frame.putLong(KEY_POLLING_LOOP_TIMESTAMP, getTimestamp());
frame.putBoolean(KEY_POLLING_LOOP_TRIGGERED_AUTOTRANSACT, getTriggeredAutoTransact());
return frame;
}
@@ -273,7 +277,7 @@
public String toString() {
return "PollingFrame { Type: " + (char) getType()
+ ", gain: " + getVendorSpecificGain()
- + ", timestamp: " + Integer.toUnsignedString(getTimestamp())
+ + ", timestamp: " + Long.toUnsignedString(getTimestamp())
+ ", data: [" + HexFormat.ofDelimiter(" ").formatHex(getData()) + "] }";
}
}
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
index 7f09dd5..914987a 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_bottom_sheet.xml
@@ -33,10 +33,9 @@
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_gravity="center"
- android:paddingLeft="@dimen/autofill_view_left_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
android:src="@drawable/more_horiz_24px"
android:tint="?androidprv:attr/materialColorOnSurface"
- android:layout_alignParentStart="true"
android:contentDescription="@string/more_options_content_description"
android:background="@null"/>
@@ -44,8 +43,8 @@
android:id="@+id/text_container"
android:layout_width="@dimen/autofill_dropdown_textview_max_width"
android:layout_height="wrap_content"
- android:paddingLeft="@dimen/autofill_view_left_padding"
- android:paddingRight="@dimen/autofill_view_right_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
+ android:paddingEnd="@dimen/autofill_view_right_padding"
android:paddingTop="@dimen/more_options_item_vertical_padding"
android:paddingBottom="@dimen/more_options_item_vertical_padding"
android:orientation="vertical">
@@ -54,9 +53,7 @@
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
android:textColor="?androidprv:attr/materialColorOnSurface"
- android:layout_toEndOf="@android:id/icon1"
style="@style/autofill.TextTitle"/>
</LinearLayout>
diff --git a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
index 08948d7..e998fe8 100644
--- a/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
+++ b/packages/CredentialManager/res/layout/credman_dropdown_presentation_layout.xml
@@ -42,8 +42,8 @@
android:id="@+id/text_container"
android:layout_width="@dimen/autofill_dropdown_textview_max_width"
android:layout_height="wrap_content"
- android:paddingLeft="@dimen/autofill_view_left_padding"
- android:paddingRight="@dimen/autofill_view_right_padding"
+ android:paddingStart="@dimen/autofill_view_left_padding"
+ android:paddingEnd="@dimen/autofill_view_right_padding"
android:paddingTop="@dimen/autofill_view_top_padding"
android:paddingBottom="@dimen/autofill_view_bottom_padding"
android:orientation="vertical">
@@ -52,8 +52,6 @@
android:id="@android:id/text1"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_alignParentTop="true"
- android:layout_toEndOf="@android:id/icon1"
android:textColor="?androidprv:attr/materialColorOnSurface"
style="@style/autofill.TextTitle"/>
@@ -61,8 +59,6 @@
android:id="@android:id/text2"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@android:id/text1"
- android:layout_toEndOf="@android:id/icon1"
android:textColor="?androidprv:attr/materialColorOnSurfaceVariant"
style="@style/autofill.TextSubtitle"/>
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
new file mode 100644
index 0000000..b52586c
--- /dev/null
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/KeyedObserverTest.kt
@@ -0,0 +1,215 @@
+/*
+ * 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.settingslib.datastore
+
+import com.google.common.truth.Truth.assertThat
+import com.google.common.util.concurrent.MoreExecutors.directExecutor
+import java.util.concurrent.Executor
+import java.util.concurrent.atomic.AtomicInteger
+import org.junit.Assert
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.reset
+import org.mockito.kotlin.verify
+import org.robolectric.RobolectricTestRunner
+
+@RunWith(RobolectricTestRunner::class)
+class KeyedObserverTest {
+ @get:Rule
+ val mockitoRule: MockitoRule = MockitoJUnit.rule()
+
+ @Mock
+ private lateinit var observer1: KeyedObserver<Any?>
+
+ @Mock
+ private lateinit var observer2: KeyedObserver<Any?>
+
+ @Mock
+ private lateinit var keyedObserver1: KeyedObserver<Any>
+
+ @Mock
+ private lateinit var keyedObserver2: KeyedObserver<Any>
+
+ @Mock
+ private lateinit var key1: Any
+
+ @Mock
+ private lateinit var key2: Any
+
+ @Mock
+ private lateinit var executor: Executor
+
+ private val keyedObservable = KeyedDataObservable<Any>()
+
+ @Test
+ fun addObserver_sameExecutor() {
+ keyedObservable.addObserver(observer1, executor)
+ keyedObservable.addObserver(observer1, executor)
+ }
+
+ @Test
+ fun addObserver_keyedObserver_sameExecutor() {
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ }
+
+ @Test
+ fun addObserver_differentExecutor() {
+ keyedObservable.addObserver(observer1, executor)
+ Assert.assertThrows(IllegalStateException::class.java) {
+ keyedObservable.addObserver(observer1, directExecutor())
+ }
+ }
+
+ @Test
+ fun addObserver_keyedObserver_differentExecutor() {
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ Assert.assertThrows(IllegalStateException::class.java) {
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ }
+ }
+
+ @Test
+ fun addObserver_weaklyReferenced() {
+ val counter = AtomicInteger()
+ var observer: KeyedObserver<Any?>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+ keyedObservable.addObserver(observer!!, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+
+ // trigger GC, the observer callback should not be invoked
+ null.also { observer = it }
+ System.gc()
+ System.runFinalization()
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+ }
+
+ @Test
+ fun addObserver_keyedObserver_weaklyReferenced() {
+ val counter = AtomicInteger()
+ var keyObserver: KeyedObserver<Any>? = KeyedObserver { _, _ -> counter.incrementAndGet() }
+ keyedObservable.addObserver(key1, keyObserver!!, directExecutor())
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+
+ // trigger GC, the observer callback should not be invoked
+ null.also { keyObserver = it }
+ System.gc()
+ System.runFinalization()
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ assertThat(counter.get()).isEqualTo(1)
+ }
+
+ @Test
+ fun addObserver_notifyObservers_removeObserver() {
+ keyedObservable.addObserver(observer1, directExecutor())
+ keyedObservable.addObserver(observer2, executor)
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+ verify(observer2, never()).onKeyChanged(any(), any())
+ verify(executor).execute(any())
+
+ reset(observer1, executor)
+ keyedObservable.removeObserver(observer2)
+
+ keyedObservable.notifyChange(ChangeReason.DELETE)
+ verify(observer1).onKeyChanged(null, ChangeReason.DELETE)
+ verify(executor, never()).execute(any())
+ }
+
+ @Test
+ fun addObserver_keyedObserver_notifyObservers_removeObserver() {
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ keyedObservable.addObserver(key2, keyedObserver2, executor)
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2, never()).onKeyChanged(any(), any())
+ verify(executor, never()).execute(any())
+
+ reset(keyedObserver1, executor)
+ keyedObservable.removeObserver(key2, keyedObserver2)
+
+ keyedObservable.notifyChange(key1, ChangeReason.DELETE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.DELETE)
+ verify(executor, never()).execute(any())
+ }
+
+ @Test
+ fun notifyChange_addMoreTypeObservers_checkOnKeyChanged() {
+ keyedObservable.addObserver(observer1, directExecutor())
+ keyedObservable.addObserver(key1, keyedObserver1, directExecutor())
+ keyedObservable.addObserver(key2, keyedObserver2, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ verify(observer1).onKeyChanged(null, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+
+ reset(observer1, keyedObserver1, keyedObserver2)
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+
+ verify(observer1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver1).onKeyChanged(key1, ChangeReason.UPDATE)
+ verify(keyedObserver2, never()).onKeyChanged(key1, ChangeReason.UPDATE)
+
+ reset(observer1, keyedObserver1, keyedObserver2)
+ keyedObservable.notifyChange(key2, ChangeReason.UPDATE)
+
+ verify(observer1).onKeyChanged(key2, ChangeReason.UPDATE)
+ verify(keyedObserver1, never()).onKeyChanged(key2, ChangeReason.UPDATE)
+ verify(keyedObserver2).onKeyChanged(key2, ChangeReason.UPDATE)
+ }
+
+ @Test
+ fun notifyChange_addObserverWithinCallback() {
+ // ConcurrentModificationException is raised if it is not implemented correctly
+ val observer: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+ keyedObservable.addObserver(observer1, executor)
+ }
+
+ keyedObservable.addObserver(observer, directExecutor())
+
+ keyedObservable.notifyChange(ChangeReason.UPDATE)
+ keyedObservable.removeObserver(observer)
+ }
+
+ @Test
+ fun notifyChange_KeyedObserver_addObserverWithinCallback() {
+ // ConcurrentModificationException is raised if it is not implemented correctly
+ val keyObserver: KeyedObserver<Any?> = KeyedObserver { _, _ ->
+ keyedObservable.addObserver(key1, keyedObserver1, executor)
+ }
+
+ keyedObservable.addObserver(key1, keyObserver, directExecutor())
+
+ keyedObservable.notifyChange(key1, ChangeReason.UPDATE)
+ keyedObservable.removeObserver(key1, keyObserver)
+ }
+}
\ No newline at end of file
diff --git a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
index bb791dc..f065829 100644
--- a/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
+++ b/packages/SettingsLib/DataStore/tests/src/com/android/settingslib/datastore/ObserverTest.kt
@@ -69,8 +69,7 @@
assertThat(counter.get()).isEqualTo(1)
// trigger GC, the observer callback should not be invoked
- @Suppress("unused")
- observer = null
+ null.also { observer = it }
System.gc()
System.runFinalization()
@@ -100,10 +99,12 @@
@Test
fun notifyChange_addObserverWithinCallback() {
// ConcurrentModificationException is raised if it is not implemented correctly
+ val observer = Observer { observable.addObserver(observer1, executor) }
observable.addObserver(
- { observable.addObserver(observer1, executor) },
+ observer,
MoreExecutors.directExecutor()
)
observable.notifyChange(ChangeReason.UPDATE)
+ observable.removeObserver(observer)
}
}
diff --git a/packages/SettingsLib/ProfileSelector/Android.bp b/packages/SettingsLib/ProfileSelector/Android.bp
index 6dc07b2..4aa67c1 100644
--- a/packages/SettingsLib/ProfileSelector/Android.bp
+++ b/packages/SettingsLib/ProfileSelector/Android.bp
@@ -20,6 +20,7 @@
static_libs: [
"com.google.android.material_material",
"SettingsLibSettingsTheme",
+ "android.os.flags-aconfig-java-export",
],
sdk_version: "system_current",
diff --git a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
index 80f6b76..303e20c 100644
--- a/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
+++ b/packages/SettingsLib/ProfileSelector/AndroidManifest.xml
@@ -18,5 +18,5 @@
<manifest xmlns:android="http://schemas.android.com/apk/res/android"
package="com.android.settingslib.widget.profileselector">
- <uses-sdk android:minSdkVersion="23" />
+ <uses-sdk android:minSdkVersion="29" />
</manifest>
diff --git a/packages/SettingsLib/ProfileSelector/res/values/strings.xml b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
index 68d4047..76ccb65 100644
--- a/packages/SettingsLib/ProfileSelector/res/values/strings.xml
+++ b/packages/SettingsLib/ProfileSelector/res/values/strings.xml
@@ -21,4 +21,6 @@
<string name="settingslib_category_personal">Personal</string>
<!-- Header for items under the work user [CHAR LIMIT=30] -->
<string name="settingslib_category_work">Work</string>
+ <!-- Header for items under the private profile user [CHAR LIMIT=30] -->
+ <string name="settingslib_category_private">Private</string>
</resources>
\ No newline at end of file
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
index be5753be..c52386b 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileSelectFragment.java
@@ -16,31 +16,77 @@
package com.android.settingslib.widget;
+import android.annotation.TargetApi;
import android.app.Activity;
+import android.content.Context;
+import android.content.pm.UserProperties;
+import android.os.Build;
import android.os.Bundle;
+import android.os.UserHandle;
+import android.os.UserManager;
+import android.util.ArrayMap;
+import android.util.Log;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
+import androidx.core.os.BuildCompat;
import androidx.fragment.app.Fragment;
import androidx.viewpager2.widget.ViewPager2;
+import com.android.settingslib.widget.profileselector.R;
+
import com.google.android.material.tabs.TabLayout;
import com.google.android.material.tabs.TabLayoutMediator;
-import com.android.settingslib.widget.profileselector.R;
+
+import java.util.ArrayList;
+import java.util.List;
/**
* Base fragment class for profile settings.
*/
public abstract class ProfileSelectFragment extends Fragment {
+ private static final String TAG = "ProfileSelectFragment";
+ // UserHandle#USER_NULL is a @TestApi so is not accessible.
+ private static final int USER_NULL = -10000;
+ private static final int DEFAULT_POSITION = 0;
/**
- * Personal or Work profile tab of {@link ProfileSelectFragment}
- * <p>0: Personal tab.
- * <p>1: Work profile tab.
+ * The type of profile tab of {@link ProfileSelectFragment} to show
+ * <ul>
+ * <li>0: Personal tab.
+ * <li>1: Work profile tab.
+ * </ul>
+ *
+ * <p> Please note that this is supported for legacy reasons. Please use
+ * {@link #EXTRA_SHOW_FRAGMENT_USER_ID} instead.
*/
- public static final String EXTRA_SHOW_FRAGMENT_TAB =
- ":settings:show_fragment_tab";
+ public static final String EXTRA_SHOW_FRAGMENT_TAB = ":settings:show_fragment_tab";
+
+ /**
+ * An {@link ArrayList} of users to show. The supported users are: System user, the managed
+ * profile user, and the private profile user. A client should pass all the user ids that need
+ * to be shown in this list. Note that if this list is not provided then, for legacy reasons
+ * see {@link #EXTRA_SHOW_FRAGMENT_TAB}, an attempt will be made to show two tabs: one for the
+ * System user and one for the managed profile user.
+ *
+ * <p>Please note that this MUST be used in conjunction with
+ * {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
+ */
+ public static final String EXTRA_LIST_OF_USER_IDS = ":settings:list_user_ids";
+
+ /**
+ * The user id of the user to be show in {@link ProfileSelectFragment}. Only the below user
+ * types are supported:
+ * <ul>
+ * <li> System user.
+ * <li> Managed profile user.
+ * <li> Private profile user.
+ * </ul>
+ *
+ * <p>Please note that this MUST be used in conjunction with {@link #EXTRA_LIST_OF_USER_IDS}.
+ */
+ public static final String EXTRA_SHOW_FRAGMENT_USER_ID = ":settings:show_fragment_user_id";
/**
* Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
@@ -48,13 +94,23 @@
public static final int PERSONAL_TAB = 0;
/**
- * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB
+ * Used in fragment argument with Extra key EXTRA_SHOW_FRAGMENT_TAB for the managed profile
*/
public static final int WORK_TAB = 1;
+ /**
+ * Please note that private profile is available from API LEVEL
+ * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} only, therefore PRIVATE_TAB MUST be
+ * passed in {@link #EXTRA_SHOW_FRAGMENT_TAB} and {@link #EXTRA_LIST_OF_PROFILE_TABS} for
+ * {@link Build.VERSION_CODES.VANILLA_ICE_CREAM} or higher API Levels only.
+ */
+ private static final int PRIVATE_TAB = 2;
+
private ViewGroup mContentView;
private ViewPager2 mViewPager;
+ private final ArrayMap<UserHandle, Integer> mProfileTabsByUsers = new ArrayMap<>();
+ private boolean mUsingUserIds = false;
@Override
public View onCreateView(LayoutInflater inflater, ViewGroup container,
@@ -67,7 +123,7 @@
if (titleResId > 0) {
activity.setTitle(titleResId);
}
- final int selectedTab = getTabId(activity, getArguments());
+ initProfileTabsToShow();
final View tabContainer = mContentView.findViewById(R.id.tab_container);
mViewPager = tabContainer.findViewById(R.id.view_pager);
@@ -78,16 +134,14 @@
).attach();
tabContainer.setVisibility(View.VISIBLE);
- final TabLayout.Tab tab = tabs.getTabAt(selectedTab);
+ final TabLayout.Tab tab = tabs.getTabAt(getSelectedTabPosition(activity, getArguments()));
tab.select();
return mContentView;
}
/**
- * create Personal or Work profile fragment
- * <p>0: Personal profile.
- * <p>1: Work profile.
+ * Create Personal or Work or Private profile fragment. See {@link #EXTRA_SHOW_FRAGMENT_USER_ID}
*/
public abstract Fragment createFragment(int position);
@@ -99,21 +153,90 @@
return 0;
}
- int getTabId(Activity activity, Bundle bundle) {
+ int getSelectedTabPosition(Activity activity, Bundle bundle) {
if (bundle != null) {
+ final int extraUserId = bundle.getInt(EXTRA_SHOW_FRAGMENT_USER_ID, USER_NULL);
+ if (extraUserId != USER_NULL) {
+ return mProfileTabsByUsers.indexOfKey(UserHandle.of(extraUserId));
+ }
final int extraTab = bundle.getInt(EXTRA_SHOW_FRAGMENT_TAB, -1);
if (extraTab != -1) {
return extraTab;
}
}
- return PERSONAL_TAB;
+ return DEFAULT_POSITION;
+ }
+
+ int getTabCount() {
+ return mUsingUserIds ? mProfileTabsByUsers.size() : 2;
+ }
+
+ void initProfileTabsToShow() {
+ Bundle bundle = getArguments();
+ if (bundle != null) {
+ ArrayList<Integer> userIdsToShow =
+ bundle.getIntegerArrayList(EXTRA_LIST_OF_USER_IDS);
+ if (userIdsToShow != null && !userIdsToShow.isEmpty()) {
+ mUsingUserIds = true;
+ UserManager userManager = getContext().getSystemService(UserManager.class);
+ List<UserHandle> userHandles = userManager.getUserProfiles();
+ for (UserHandle userHandle : userHandles) {
+ if (!userIdsToShow.contains(userHandle.getIdentifier())) {
+ continue;
+ }
+ if (userHandle.isSystem()) {
+ mProfileTabsByUsers.put(userHandle, PERSONAL_TAB);
+ } else if (userManager.isManagedProfile(userHandle.getIdentifier())) {
+ mProfileTabsByUsers.put(userHandle, WORK_TAB);
+ } else if (shouldShowPrivateProfileIfItsOne(userHandle)) {
+ mProfileTabsByUsers.put(userHandle, PRIVATE_TAB);
+ }
+ }
+ }
+ }
+ }
+
+ private int getProfileTabForPosition(int position) {
+ return mUsingUserIds ? mProfileTabsByUsers.valueAt(position) : position;
+ }
+
+ int getUserIdForPosition(int position) {
+ return mUsingUserIds ? mProfileTabsByUsers.keyAt(position).getIdentifier() : position;
}
private CharSequence getPageTitle(int position) {
- if (position == WORK_TAB) {
+ int tab = getProfileTabForPosition(position);
+ if (tab == WORK_TAB) {
return getContext().getString(R.string.settingslib_category_work);
+ } else if (tab == PRIVATE_TAB) {
+ return getContext().getString(R.string.settingslib_category_private);
}
return getString(R.string.settingslib_category_personal);
}
+
+ @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private boolean shouldShowUserInQuietMode(UserHandle userHandle, UserManager userManager) {
+ UserProperties userProperties = userManager.getUserProperties(userHandle);
+ return !userManager.isQuietModeEnabled(userHandle)
+ || userProperties.getShowInQuietMode() != UserProperties.SHOW_IN_QUIET_MODE_HIDDEN;
+ }
+
+ // It's sufficient to have this method marked with the appropriate API level because we expect
+ // to be here only for this API level - when then private profile was introduced.
+ @TargetApi(Build.VERSION_CODES.VANILLA_ICE_CREAM)
+ private boolean shouldShowPrivateProfileIfItsOne(UserHandle userHandle) {
+ if (!BuildCompat.isAtLeastV() || !android.os.Flags.allowPrivateProfile()) {
+ return false;
+ }
+ try {
+ Context userContext = getContext().createContextAsUser(userHandle, /* flags= */ 0);
+ UserManager userManager = userContext.getSystemService(UserManager.class);
+ return userManager.isPrivateProfile()
+ && shouldShowUserInQuietMode(userHandle, userManager);
+ } catch (IllegalStateException exception) {
+ Log.i(TAG, "Ignoring this user as the calling package not available in this user.");
+ }
+ return false;
+ }
}
diff --git a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
index f5ab647..37f4f27 100644
--- a/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
+++ b/packages/SettingsLib/ProfileSelector/src/com/android/settingslib/widget/ProfileViewPagerAdapter.java
@@ -18,7 +18,6 @@
import androidx.fragment.app.Fragment;
import androidx.viewpager2.adapter.FragmentStateAdapter;
-import com.android.settingslib.widget.profileselector.R;
/**
* ViewPager Adapter to handle between TabLayout and ViewPager2
@@ -34,11 +33,11 @@
@Override
public Fragment createFragment(int position) {
- return mParentFragments.createFragment(position);
+ return mParentFragments.createFragment(mParentFragments.getUserIdForPosition(position));
}
@Override
public int getItemCount() {
- return 2;
+ return mParentFragments.getTabCount();
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
index b8624fd..4777b0d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CachedBluetoothDevice.java
@@ -1315,8 +1315,7 @@
boolean isActiveAshaHearingAid = mIsActiveDeviceHearingAid;
boolean isActiveLeAudioHearingAid = mIsActiveDeviceLeAudio
&& isConnectedHapClientDevice();
- if ((isActiveAshaHearingAid || isActiveLeAudioHearingAid)
- && stringRes == R.string.bluetooth_active_no_battery_level) {
+ if (isActiveAshaHearingAid || isActiveLeAudioHearingAid) {
final Set<CachedBluetoothDevice> memberDevices = getMemberDevice();
final CachedBluetoothDevice subDevice = getSubDevice();
if (memberDevices.stream().anyMatch(m -> m.isConnected())) {
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 cda6b8b..68f471d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/media/session/MediaSessionManagerExt.kt
@@ -17,6 +17,7 @@
package com.android.settingslib.media.session
import android.media.session.MediaController
+import android.media.session.MediaSession
import android.media.session.MediaSessionManager
import android.os.UserHandle
import androidx.concurrent.futures.DirectExecutor
@@ -28,7 +29,7 @@
import kotlinx.coroutines.launch
/** [Flow] for [MediaSessionManager.OnActiveSessionsChangedListener]. */
-val MediaSessionManager.activeMediaChanges: Flow<Collection<MediaController>?>
+val MediaSessionManager.activeMediaChanges: Flow<List<MediaController>?>
get() =
callbackFlow {
val listener =
@@ -42,3 +43,24 @@
awaitClose { removeOnActiveSessionsChangedListener(listener) }
}
.buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [MediaSessionManager.RemoteSessionCallback]. */
+val MediaSessionManager.remoteSessionChanges: Flow<MediaSession.Token?>
+ get() =
+ callbackFlow {
+ val callback =
+ object : MediaSessionManager.RemoteSessionCallback {
+ override fun onVolumeChanged(sessionToken: MediaSession.Token, flags: Int) {
+ launch { send(sessionToken) }
+ }
+
+ override fun onDefaultRemoteSessionChanged(
+ sessionToken: MediaSession.Token?
+ ) {
+ launch { send(sessionToken) }
+ }
+ }
+ registerRemoteSessionCallback(DirectExecutor.INSTANCE, callback)
+ awaitClose { unregisterRemoteSessionCallback(callback) }
+ }
+ .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
index 298dd71e..724dd51 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/LocalMediaRepository.kt
@@ -15,14 +15,10 @@
*/
package com.android.settingslib.volume.data.repository
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.settingslib.volume.shared.model.AudioManagerEvent
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.flow.Flow
@@ -30,35 +26,23 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.callbackFlow
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.withContext
/** Repository providing data about connected media devices. */
interface LocalMediaRepository {
- /** Available devices list */
- val mediaDevices: StateFlow<Collection<MediaDevice>>
-
/** Currently connected media device */
val currentConnectedDevice: StateFlow<MediaDevice?>
-
- val remoteRoutingSessions: StateFlow<Collection<RoutingSession>>
-
- suspend fun adjustSessionVolume(sessionId: String?, volume: Int)
}
class LocalMediaRepositoryImpl(
audioManagerEventsReceiver: AudioManagerEventsReceiver,
private val localMediaManager: LocalMediaManager,
- private val mediaRouter2Manager: MediaRouter2Manager,
coroutineScope: CoroutineScope,
- private val backgroundContext: CoroutineContext,
) : LocalMediaRepository {
private val devicesChanges =
@@ -94,18 +78,6 @@
}
.shareIn(coroutineScope, SharingStarted.WhileSubscribed(), replay = 0)
- override val mediaDevices: StateFlow<Collection<MediaDevice>> =
- mediaDevicesUpdates
- .mapNotNull {
- if (it is DevicesUpdate.DeviceListUpdate) {
- it.newDevices ?: emptyList()
- } else {
- null
- }
- }
- .flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
override val currentConnectedDevice: StateFlow<MediaDevice?> =
merge(devicesChanges, mediaDevicesUpdates)
.map { localMediaManager.currentConnectedDevice }
@@ -116,30 +88,6 @@
localMediaManager.currentConnectedDevice
)
- override val remoteRoutingSessions: StateFlow<Collection<RoutingSession>> =
- merge(devicesChanges, mediaDevicesUpdates)
- .onStart { emit(Unit) }
- .map { localMediaManager.remoteRoutingSessions.map(::toRoutingSession) }
- .flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
- override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
- withContext(backgroundContext) {
- if (sessionId == null) {
- localMediaManager.adjustSessionVolume(volume)
- } else {
- localMediaManager.adjustSessionVolume(sessionId, volume)
- }
- }
- }
-
- private fun toRoutingSession(info: RoutingSessionInfo): RoutingSession =
- RoutingSession(
- info,
- isMediaOutputDisabled = mediaRouter2Manager.getTransferableRoutes(info).isEmpty(),
- isVolumeSeekBarEnabled = localMediaManager.shouldEnableVolumeSeekBar(info)
- )
-
private sealed interface DevicesUpdate {
data class DeviceListUpdate(val newDevices: List<MediaDevice>?) : DevicesUpdate
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 7c231d1..e4ac9fe 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
@@ -27,18 +27,26 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterIsInstance
-import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
/** Provides controllers for currently active device media sessions. */
interface MediaControllerRepository {
- /** Current [MediaController]. Null is emitted when there is no active [MediaController]. */
- val activeLocalMediaController: StateFlow<MediaController?>
+ /**
+ * Get a list of controllers for all ongoing sessions. The controllers will be provided in
+ * priority order with the most important controller at index 0.
+ *
+ * This requires the [android.Manifest.permission.MEDIA_CONTENT_CONTROL] permission be held by
+ * the calling app.
+ */
+ val activeSessions: StateFlow<List<MediaController>>
}
class MediaControllerRepositoryImpl(
@@ -49,51 +57,17 @@
backgroundContext: CoroutineContext,
) : MediaControllerRepository {
- private val devicesChanges =
- audioManagerEventsReceiver.events.filterIsInstance(
- AudioManagerEvent.StreamDevicesChanged::class
- )
-
- override val activeLocalMediaController: StateFlow<MediaController?> =
- combine(
- mediaSessionManager.activeMediaChanges.onStart {
- emit(mediaSessionManager.getActiveSessions(null))
- },
- localBluetoothManager?.headsetAudioModeChanges?.onStart { emit(Unit) }
- ?: flowOf(null),
- devicesChanges.onStart { emit(AudioManagerEvent.StreamDevicesChanged) },
- ) { controllers, _, _ ->
- controllers?.let(::findLocalMediaController)
- }
+ override val activeSessions: StateFlow<List<MediaController>> =
+ merge(
+ mediaSessionManager.activeMediaChanges.filterNotNull(),
+ localBluetoothManager?.headsetAudioModeChanges?.map {
+ mediaSessionManager.getActiveSessions(null)
+ } ?: emptyFlow(),
+ audioManagerEventsReceiver.events
+ .filterIsInstance(AudioManagerEvent.StreamDevicesChanged::class)
+ .map { mediaSessionManager.getActiveSessions(null) },
+ )
+ .onStart { emit(mediaSessionManager.getActiveSessions(null)) }
.flowOn(backgroundContext)
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null)
-
- private fun findLocalMediaController(
- controllers: Collection<MediaController>,
- ): MediaController? {
- var localController: MediaController? = null
- val remoteMediaSessionLists: MutableList<String> = ArrayList()
- for (controller in controllers) {
- val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
- when (playbackInfo.playbackType) {
- MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
- if (localController?.packageName.equals(controller.packageName)) {
- localController = null
- }
- if (!remoteMediaSessionLists.contains(controller.packageName)) {
- remoteMediaSessionLists.add(controller.packageName)
- }
- }
- MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
- if (
- localController == null &&
- !remoteMediaSessionLists.contains(controller.packageName)
- ) {
- localController = controller
- }
- }
- }
- }
- return localController
- }
+ .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
deleted file mode 100644
index f621335..0000000
--- a/packages/SettingsLib/src/com/android/settingslib/volume/domain/interactor/LocalMediaInteractor.kt
+++ /dev/null
@@ -1,57 +0,0 @@
-/*
- * 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.settingslib.volume.domain.interactor
-
-import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.repository.LocalMediaRepository
-import com.android.settingslib.volume.domain.model.RoutingSession
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-class LocalMediaInteractor(
- private val repository: LocalMediaRepository,
- coroutineScope: CoroutineScope,
-) {
-
- /** Available devices list */
- val mediaDevices: StateFlow<Collection<MediaDevice>>
- get() = repository.mediaDevices
-
- /** Currently connected media device */
- val currentConnectedDevice: StateFlow<MediaDevice?>
- get() = repository.currentConnectedDevice
-
- val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
- repository.remoteRoutingSessions
- .map { sessions ->
- sessions.map {
- RoutingSession(
- routingSessionInfo = it.routingSessionInfo,
- isMediaOutputDisabled = it.isMediaOutputDisabled,
- isVolumeSeekBarEnabled =
- it.isVolumeSeekBarEnabled && it.routingSessionInfo.volumeMax > 0
- )
- }
- }
- .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyList())
-
- suspend fun adjustSessionVolume(sessionId: String?, volume: Int) =
- repository.adjustSessionVolume(sessionId, volume)
-}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
index 2d12dae..caf41f2 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/LocalMediaRepositoryImplTest.kt
@@ -15,17 +15,12 @@
*/
package com.android.settingslib.volume.data.repository
-import android.media.MediaRoute2Info
-import android.media.MediaRouter2Manager
-import android.media.RoutingSessionInfo
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.settingslib.media.LocalMediaManager
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -37,15 +32,10 @@
import org.mockito.ArgumentCaptor
import org.mockito.Captor
import org.mockito.Mock
-import org.mockito.Mockito.any
-import org.mockito.Mockito.anyInt
-import org.mockito.Mockito.anyString
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class LocalMediaRepositoryImplTest {
@@ -53,7 +43,6 @@
@Mock private lateinit var localMediaManager: LocalMediaManager
@Mock private lateinit var mediaDevice1: MediaDevice
@Mock private lateinit var mediaDevice2: MediaDevice
- @Mock private lateinit var mediaRouter2Manager: MediaRouter2Manager
@Captor
private lateinit var deviceCallbackCaptor: ArgumentCaptor<LocalMediaManager.DeviceCallback>
@@ -71,29 +60,11 @@
LocalMediaRepositoryImpl(
eventsReceiver,
localMediaManager,
- mediaRouter2Manager,
testScope.backgroundScope,
- testScope.testScheduler,
)
}
@Test
- fun mediaDevices_areUpdated() {
- testScope.runTest {
- var mediaDevices: Collection<MediaDevice>? = null
- underTest.mediaDevices.onEach { mediaDevices = it }.launchIn(backgroundScope)
- runCurrent()
- verify(localMediaManager).registerCallback(deviceCallbackCaptor.capture())
- deviceCallbackCaptor.value.onDeviceListUpdate(listOf(mediaDevice1, mediaDevice2))
- runCurrent()
-
- assertThat(mediaDevices).hasSize(2)
- assertThat(mediaDevices).contains(mediaDevice1)
- assertThat(mediaDevices).contains(mediaDevice2)
- }
- }
-
- @Test
fun deviceListUpdated_currentConnectedDeviceUpdated() {
testScope.runTest {
var currentConnectedDevice: MediaDevice? = null
@@ -110,78 +81,4 @@
assertThat(currentConnectedDevice).isEqualTo(mediaDevice1)
}
}
-
- @Test
- fun kek() {
- testScope.runTest {
- `when`(localMediaManager.remoteRoutingSessions)
- .thenReturn(
- listOf(
- testRoutingSessionInfo1,
- testRoutingSessionInfo2,
- testRoutingSessionInfo3,
- )
- )
- `when`(localMediaManager.shouldEnableVolumeSeekBar(any())).then {
- (it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo1
- }
- `when`(mediaRouter2Manager.getTransferableRoutes(any<RoutingSessionInfo>())).then {
- if ((it.arguments[0] as RoutingSessionInfo) == testRoutingSessionInfo2) {
- return@then listOf(mock(MediaRoute2Info::class.java))
- }
- emptyList<MediaRoute2Info>()
- }
- var remoteRoutingSessions: Collection<RoutingSession>? = null
- underTest.remoteRoutingSessions
- .onEach { remoteRoutingSessions = it }
- .launchIn(backgroundScope)
-
- runCurrent()
-
- assertThat(remoteRoutingSessions)
- .containsExactlyElementsIn(
- listOf(
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo1,
- isVolumeSeekBarEnabled = true,
- isMediaOutputDisabled = true,
- ),
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo2,
- isVolumeSeekBarEnabled = false,
- isMediaOutputDisabled = false,
- ),
- RoutingSession(
- routingSessionInfo = testRoutingSessionInfo3,
- isVolumeSeekBarEnabled = false,
- isMediaOutputDisabled = true,
- )
- )
- )
- }
- }
-
- @Test
- fun adjustSessionVolume_adjusts() {
- testScope.runTest {
- var volume = 0
- `when`(localMediaManager.adjustSessionVolume(anyString(), anyInt())).then {
- volume = it.arguments[1] as Int
- Unit
- }
-
- underTest.adjustSessionVolume("test_session", 10)
-
- assertThat(volume).isEqualTo(10)
- }
- }
-
- private companion object {
- val testRoutingSessionInfo1 =
- RoutingSessionInfo.Builder("id_1", "test.pkg.1").addSelectedRoute("route_1").build()
- val testRoutingSessionInfo2 =
- RoutingSessionInfo.Builder("id_2", "test.pkg.2").addSelectedRoute("route_2").build()
- val testRoutingSessionInfo3 =
- RoutingSessionInfo.Builder("id_3", "test.pkg.3").addSelectedRoute("route_3").build()
- }
}
diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
index f3d1714..964c3f7 100644
--- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
+++ b/packages/SettingsLib/tests/integ/src/com/android/settingslib/volume/data/repository/MediaControllerRepositoryImplTest.kt
@@ -22,13 +22,10 @@
import android.media.session.PlaybackState
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.settingslib.bluetooth.BluetoothCallback
import com.android.settingslib.bluetooth.BluetoothEventManager
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.volume.shared.FakeAudioManagerEventsReceiver
-import com.android.settingslib.volume.shared.model.AudioManagerEvent
import com.google.common.truth.Truth.assertThat
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.test.TestScope
@@ -37,21 +34,15 @@
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentCaptor
-import org.mockito.Captor
import org.mockito.Mock
import org.mockito.Mockito.any
-import org.mockito.Mockito.verify
import org.mockito.Mockito.`when`
import org.mockito.MockitoAnnotations
-@OptIn(ExperimentalCoroutinesApi::class)
@RunWith(AndroidJUnit4::class)
@SmallTest
class MediaControllerRepositoryImplTest {
- @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothCallback>
-
@Mock private lateinit var mediaSessionManager: MediaSessionManager
@Mock private lateinit var localBluetoothManager: LocalBluetoothManager
@Mock private lateinit var eventManager: BluetoothEventManager
@@ -103,7 +94,7 @@
}
@Test
- fun playingMediaDevicesAvailable_sessionIsActive() {
+ fun mediaDevicesAvailable_returnsAllActiveOnes() {
testScope.runTest {
`when`(mediaSessionManager.getActiveSessions(any()))
.thenReturn(
@@ -112,53 +103,25 @@
statelessMediaController,
errorMediaController,
remoteMediaController,
- localMediaController
+ localMediaController,
)
)
- var mediaController: MediaController? = null
- underTest.activeLocalMediaController
- .onEach { mediaController = it }
- .launchIn(backgroundScope)
+
+ var mediaControllers: Collection<MediaController>? = null
+ underTest.activeSessions.onEach { mediaControllers = it }.launchIn(backgroundScope)
runCurrent()
- eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
- triggerOnAudioModeChanged()
- runCurrent()
-
- assertThat(mediaController).isSameInstanceAs(localMediaController)
- }
- }
-
- @Test
- fun noPlayingMediaDevicesAvailable_sessionIsInactive() {
- testScope.runTest {
- `when`(mediaSessionManager.getActiveSessions(any()))
- .thenReturn(
- listOf(
- stoppedMediaController,
- statelessMediaController,
- errorMediaController,
- )
+ assertThat(mediaControllers)
+ .containsExactly(
+ stoppedMediaController,
+ statelessMediaController,
+ errorMediaController,
+ remoteMediaController,
+ localMediaController,
)
- var mediaController: MediaController? = null
- underTest.activeLocalMediaController
- .onEach { mediaController = it }
- .launchIn(backgroundScope)
- runCurrent()
-
- eventsReceiver.triggerEvent(AudioManagerEvent.StreamDevicesChanged)
- triggerOnAudioModeChanged()
- runCurrent()
-
- assertThat(mediaController).isNull()
}
}
- private fun triggerOnAudioModeChanged() {
- verify(eventManager).registerCallback(callbackCaptor.capture())
- callbackCaptor.value.onAudioModeChanged()
- }
-
private companion object {
val statePlaying: PlaybackState =
PlaybackState.Builder().setState(PlaybackState.STATE_PLAYING, 0, 0f).build()
diff --git a/packages/SettingsProvider/Android.bp b/packages/SettingsProvider/Android.bp
index 7ec3d24..bf4f60d 100644
--- a/packages/SettingsProvider/Android.bp
+++ b/packages/SettingsProvider/Android.bp
@@ -60,6 +60,7 @@
// because this test is not an instrumentation test. (because the target runs in the system process.)
"SettingsProviderLib",
"androidx.test.rules",
+ "frameworks-base-testutils",
"device_config_service_flags_java",
"flag-junit",
"junit",
diff --git a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
index 3e0d05c..1eb04ac 100644
--- a/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
+++ b/packages/SettingsProvider/src/com/android/providers/settings/SettingsHelper.java
@@ -98,6 +98,7 @@
sBroadcastOnRestore.add(Settings.Secure.DARK_THEME_CUSTOM_END_TIME);
sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_DISPLAY_MAGNIFICATION_NAVBAR_ENABLED);
sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_BUTTON_TARGETS);
+ sBroadcastOnRestore.add(Settings.Secure.ACCESSIBILITY_QS_TARGETS);
sBroadcastOnRestoreSystemUI = new ArraySet<String>(2);
sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_TILES);
sBroadcastOnRestoreSystemUI.add(Settings.Secure.QS_AUTO_ADDED_TILES);
@@ -229,6 +230,10 @@
} else if (Settings.System.ACCELEROMETER_ROTATION.equals(name)
&& shouldSkipAutoRotateRestore()) {
return;
+ } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(name)) {
+ // Don't write it to setting. Let the broadcast receiver in
+ // AccessibilityManagerService handle restore/merging logic.
+ return;
}
// Default case: write the restored value to settings
diff --git a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
index 197788e..2f8cf4b 100644
--- a/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
+++ b/packages/SettingsProvider/test/src/com/android/providers/settings/SettingsHelperRestoreTest.java
@@ -16,23 +16,31 @@
package com.android.providers.settings;
+import static com.google.common.truth.Truth.assertThat;
+
import static junit.framework.Assert.assertEquals;
import android.content.ContentResolver;
import android.content.ContentValues;
import android.content.Context;
+import android.content.Intent;
import android.net.Uri;
import android.os.Build;
import android.provider.Settings;
+import android.provider.SettingsStringUtil;
import androidx.test.InstrumentationRegistry;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.test.BroadcastInterceptingContext;
+
import org.junit.Before;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
+import java.util.concurrent.ExecutionException;
+
/**
* Tests for {@link SettingsHelper#restoreValue(Context, ContentResolver, ContentValues, Uri,
* String, String, int)}. Specifically verifies that we restore critical accessibility settings only
@@ -165,4 +173,33 @@
assertEquals(restoreSettingValue, Settings.Secure.getInt(mContentResolver, settingName));
}
+
+ @Test
+ public void restoreAccessibilityQsTargets_broadcastSent()
+ throws ExecutionException, InterruptedException {
+ BroadcastInterceptingContext interceptingContext = new BroadcastInterceptingContext(
+ mContext);
+ final String settingName = Settings.Secure.ACCESSIBILITY_QS_TARGETS;
+ final String restoreSettingValue = "com.android.server.accessibility/ColorInversion"
+ + SettingsStringUtil.DELIMITER
+ + "com.android.server.accessibility/ColorCorrectionTile";
+ BroadcastInterceptingContext.FutureIntent futureIntent =
+ interceptingContext.nextBroadcastIntent(Intent.ACTION_SETTING_RESTORED);
+
+ mSettingsHelper.restoreValue(
+ interceptingContext,
+ mContentResolver,
+ new ContentValues(2),
+ Settings.Secure.getUriFor(settingName),
+ settingName,
+ restoreSettingValue,
+ Build.VERSION.SDK_INT);
+
+ Intent intentReceived = futureIntent.get();
+ assertThat(intentReceived.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE))
+ .isEqualTo(restoreSettingValue);
+ assertThat(intentReceived.getIntExtra(
+ Intent.EXTRA_SETTING_RESTORED_FROM_SDK_INT, /* defaultValue= */ 0))
+ .isEqualTo(Build.VERSION.SDK_INT);
+ }
}
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index 6546b87..f70ad9e 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -23,10 +23,10 @@
import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_RECENTS;
import static android.accessibilityservice.AccessibilityService.GLOBAL_ACTION_TAKE_SCREENSHOT;
-import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_GLOBAL_ACTION_EXTRA;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_HIDE_MENU;
+import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_OPEN_BLOCKED;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.INTENT_TOGGLE_MENU;
import static com.android.systemui.accessibility.accessibilitymenu.AccessibilityMenuService.PACKAGE_NAME;
@@ -77,6 +77,8 @@
private static final int TIMEOUT_SERVICE_STATUS_CHANGE_S = 5;
private static final int TIMEOUT_UI_CHANGE_S = 5;
private static final int NO_GLOBAL_ACTION = -1;
+ private static final Intent INTENT_OPEN_MENU = new Intent(INTENT_TOGGLE_MENU)
+ .setPackage(PACKAGE_NAME);
private static Instrumentation sInstrumentation;
private static UiAutomation sUiAutomation;
@@ -152,9 +154,6 @@
@Before
public void setup() throws Throwable {
sOpenBlocked.set(false);
- wakeUpScreen();
- sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
- openMenu();
}
@After
@@ -188,24 +187,17 @@
}
private static void openMenu() throws Throwable {
- openMenu(false);
- }
-
- private static void openMenu(boolean abandonOnBlock) throws Throwable {
- Intent intent = new Intent(INTENT_TOGGLE_MENU);
- intent.setPackage(PACKAGE_NAME);
- sInstrumentation.getContext().sendBroadcast(intent);
+ unlockSignal();
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
TestUtils.waitUntil("Timed out before menu could appear.",
TIMEOUT_UI_CHANGE_S,
() -> {
- if (sOpenBlocked.get() && abandonOnBlock) {
- throw new IllegalStateException();
- }
if (isMenuVisible()) {
return true;
} else {
- sInstrumentation.getContext().sendBroadcast(intent);
+ unlockSignal();
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
return false;
}
});
@@ -249,6 +241,7 @@
@Test
public void testAdjustBrightness() throws Throwable {
+ openMenu();
Context context = sInstrumentation.getTargetContext();
DisplayManager displayManager = context.getSystemService(
DisplayManager.class);
@@ -264,22 +257,28 @@
context.getDisplayId()).getBrightnessInfo();
try {
- displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMinimum);
TestUtils.waitUntil("Could not change to minimum brightness",
TIMEOUT_UI_CHANGE_S,
- () -> displayManager.getBrightness(context.getDisplayId())
- == brightnessInfo.brightnessMinimum);
+ () -> {
+ displayManager.setBrightness(
+ context.getDisplayId(), brightnessInfo.brightnessMinimum);
+ return displayManager.getBrightness(context.getDisplayId())
+ == brightnessInfo.brightnessMinimum;
+ });
brightnessUpButton.performAction(CLICK_ID);
TestUtils.waitUntil("Did not detect an increase in brightness.",
TIMEOUT_UI_CHANGE_S,
() -> displayManager.getBrightness(context.getDisplayId())
> brightnessInfo.brightnessMinimum);
- displayManager.setBrightness(context.getDisplayId(), brightnessInfo.brightnessMaximum);
TestUtils.waitUntil("Could not change to maximum brightness",
TIMEOUT_UI_CHANGE_S,
- () -> displayManager.getBrightness(context.getDisplayId())
- == brightnessInfo.brightnessMaximum);
+ () -> {
+ displayManager.setBrightness(
+ context.getDisplayId(), brightnessInfo.brightnessMaximum);
+ return displayManager.getBrightness(context.getDisplayId())
+ == brightnessInfo.brightnessMaximum;
+ });
brightnessDownButton.performAction(CLICK_ID);
TestUtils.waitUntil("Did not detect a decrease in brightness.",
TIMEOUT_UI_CHANGE_S,
@@ -292,6 +291,7 @@
@Test
public void testAdjustVolume() throws Throwable {
+ openMenu();
Context context = sInstrumentation.getTargetContext();
AudioManager audioManager = context.getSystemService(AudioManager.class);
int resetVolume = audioManager.getStreamVolume(AudioManager.STREAM_MUSIC);
@@ -332,6 +332,7 @@
@Test
public void testAssistantButton_opensVoiceAssistant() throws Throwable {
+ openMenu();
AccessibilityNodeInfo assistantButton = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_ASSISTANT_VALUE.ordinal()));
Intent expectedIntent = new Intent(Intent.ACTION_VOICE_COMMAND);
@@ -349,6 +350,7 @@
@Test
public void testAccessibilitySettingsButton_opensAccessibilitySettings() throws Throwable {
+ openMenu();
AccessibilityNodeInfo settingsButton = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_A11YSETTING_VALUE.ordinal()));
Intent expectedIntent = new Intent(Settings.ACTION_ACCESSIBILITY_SETTINGS);
@@ -364,6 +366,7 @@
@Test
public void testPowerButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_POWER_VALUE.ordinal()));
@@ -376,6 +379,7 @@
@Test
public void testRecentButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_RECENT_VALUE.ordinal()));
@@ -388,6 +392,7 @@
@Test
public void testLockButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_LOCKSCREEN_VALUE.ordinal()));
@@ -400,6 +405,7 @@
@Test
public void testQuickSettingsButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_QUICKSETTING_VALUE.ordinal()));
@@ -412,6 +418,7 @@
@Test
public void testNotificationsButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_NOTIFICATION_VALUE.ordinal()));
@@ -424,6 +431,7 @@
@Test
public void testScreenshotButton_performsGlobalAction() throws Throwable {
+ openMenu();
AccessibilityNodeInfo button = findGridButtonInfo(getGridButtonList(),
String.valueOf(ShortcutId.ID_SCREENSHOT_VALUE.ordinal()));
@@ -436,6 +444,7 @@
@Test
public void testOnScreenLock_closesMenu() throws Throwable {
+ openMenu();
closeScreen();
wakeUpScreen();
@@ -447,13 +456,18 @@
closeScreen();
wakeUpScreen();
- boolean blocked = false;
- try {
- openMenu(true);
- } catch (IllegalStateException e) {
- // Expected
- blocked = true;
- }
- assertThat(blocked).isTrue();
+ TestUtils.waitUntil("Did not receive signal that menu cannot open",
+ TIMEOUT_UI_CHANGE_S,
+ () -> {
+ sInstrumentation.getContext().sendBroadcast(INTENT_OPEN_MENU);
+ return sOpenBlocked.get();
+ });
+ }
+
+ private static void unlockSignal() {
+ // MENU unlocks screen,
+ // BACK closes any menu that may appear if the screen wasn't locked.
+ sUiAutomation.executeShellCommand("input keyevent KEYCODE_MENU");
+ sUiAutomation.executeShellCommand("input keyevent KEYCODE_BACK");
}
}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 8da5021..ad09feb 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -424,6 +424,13 @@
}
flag {
+ name: "screenshot_shelf_ui"
+ namespace: "systemui"
+ description: "Use new shelf UI flow for screenshots"
+ bug: "329659738"
+}
+
+flag {
name: "run_fingerprint_detect_on_dismissible_keyguard"
namespace: "systemui"
description: "Run fingerprint detect instead of authenticate if the keyguard is dismissible."
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
index 621ddf7..1da6c1e 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/BouncerContent.kt
@@ -53,6 +53,7 @@
import androidx.compose.material3.Text
import androidx.compose.material3.windowsizeclass.WindowHeightSizeClass
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
@@ -71,6 +72,7 @@
import androidx.compose.ui.unit.DpOffset
import androidx.compose.ui.unit.LayoutDirection
import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
import androidx.compose.ui.unit.times
import com.android.compose.PlatformButton
import com.android.compose.animation.scene.ElementKey
@@ -84,7 +86,9 @@
import com.android.systemui.bouncer.ui.BouncerDialogFactory
import com.android.systemui.bouncer.ui.helper.BouncerSceneLayout
import com.android.systemui.bouncer.ui.viewmodel.AuthMethodBouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModel
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModel
+import com.android.systemui.bouncer.ui.viewmodel.MessageViewModel
import com.android.systemui.bouncer.ui.viewmodel.PasswordBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PatternBouncerViewModel
import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel
@@ -166,7 +170,7 @@
modifier = Modifier.fillMaxWidth(),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier,
)
@@ -228,7 +232,7 @@
when (authMethod) {
is PinBouncerViewModel -> {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier.align(Alignment.TopCenter),
)
@@ -241,7 +245,7 @@
}
is PatternBouncerViewModel -> {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
modifier = Modifier.align(Alignment.TopCenter),
)
@@ -280,7 +284,7 @@
modifier = Modifier.fillMaxWidth().align(Alignment.Center),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -376,7 +380,7 @@
modifier = Modifier.fillMaxWidth()
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -441,7 +445,7 @@
modifier = Modifier.fillMaxWidth(),
) {
StatusMessage(
- viewModel = viewModel,
+ viewModel = viewModel.message,
)
OutputArea(viewModel = viewModel, modifier = Modifier.padding(top = 24.dp))
@@ -548,26 +552,44 @@
@Composable
private fun StatusMessage(
- viewModel: BouncerViewModel,
+ viewModel: BouncerMessageViewModel,
modifier: Modifier = Modifier,
) {
- val message: BouncerViewModel.MessageViewModel by viewModel.message.collectAsState()
+ val message: MessageViewModel? by viewModel.message.collectAsState()
+
+ DisposableEffect(Unit) {
+ viewModel.onShown()
+ onDispose {}
+ }
Crossfade(
targetState = message,
label = "Bouncer message",
- animationSpec = if (message.isUpdateAnimated) tween() else snap(),
+ animationSpec = if (message?.isUpdateAnimated == true) tween() else snap(),
modifier = modifier.fillMaxWidth(),
- ) {
- Box(
- contentAlignment = Alignment.Center,
+ ) { msg ->
+ Column(
+ horizontalAlignment = Alignment.CenterHorizontally,
modifier = Modifier.fillMaxWidth(),
) {
- Text(
- text = it.text,
- color = MaterialTheme.colorScheme.onSurface,
- style = MaterialTheme.typography.bodyLarge,
- )
+ msg?.let {
+ Text(
+ text = it.text,
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ overflow = TextOverflow.Ellipsis,
+ )
+ Spacer(modifier = Modifier.size(10.dp))
+ Text(
+ text = it.secondaryText ?: "",
+ color = MaterialTheme.colorScheme.onSurface,
+ fontSize = 14.sp,
+ lineHeight = 20.sp,
+ overflow = TextOverflow.Ellipsis,
+ maxLines = 2
+ )
+ }
}
}
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
index 2a13d49..c34f2fd 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PasswordBouncer.kt
@@ -74,10 +74,7 @@
val isImeSwitcherButtonVisible by viewModel.isImeSwitcherButtonVisible.collectAsState()
val selectedUserId by viewModel.selectedUserId.collectAsState()
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
LaunchedEffect(animateFailure) {
if (animateFailure) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
index 0a5f5d2..a78c2c0 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PatternBouncer.kt
@@ -72,10 +72,7 @@
centerDotsVertically: Boolean,
modifier: Modifier = Modifier,
) {
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
val colCount = viewModel.columnCount
val rowCount = viewModel.rowCount
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
index f505b90..5651a46 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/bouncer/ui/composable/PinBouncer.kt
@@ -72,10 +72,7 @@
verticalSpacing: Dp,
modifier: Modifier = Modifier,
) {
- DisposableEffect(Unit) {
- viewModel.onShown()
- onDispose { viewModel.onHidden() }
- }
+ DisposableEffect(Unit) { onDispose { viewModel.onHidden() } }
val isInputEnabled: Boolean by viewModel.isInputEnabled.collectAsState()
val backspaceButtonAppearance by viewModel.backspaceButtonAppearance.collectAsState()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
index 82e19e7c..1d86b15 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/DefaultClockSection.kt
@@ -58,7 +58,6 @@
if (currentClock?.smallClock?.view == null) {
return
}
- viewModel.clock = currentClock
val context = LocalContext.current
MovableElement(key = smallClockElementKey, modifier = modifier) {
@@ -89,7 +88,6 @@
@Composable
fun SceneScope.LargeClock(modifier: Modifier = Modifier) {
val currentClock by viewModel.currentClock.collectAsState()
- viewModel.clock = currentClock
if (currentClock?.largeClock?.view == null) {
return
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
index 5c9b271..525ad16 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/NotificationSection.kt
@@ -16,45 +16,33 @@
package com.android.systemui.keyguard.ui.composable.section
-import android.content.Context
import android.view.ViewGroup
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import com.android.compose.animation.scene.SceneScope
import com.android.systemui.Flags.migrateClocksToBlueprint
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.notifications.ui.composable.NotificationStack
import com.android.systemui.scene.shared.flag.SceneContainerFlags
-import com.android.systemui.statusbar.notification.stack.AmbientState
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
@SysUISingleton
class NotificationSection
@Inject
constructor(
- @Application private val context: Context,
private val viewModel: NotificationsPlaceholderViewModel,
- controller: NotificationStackScrollLayoutController,
sceneContainerFlags: SceneContainerFlags,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
stackScrollLayout: NotificationStackScrollLayout,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+ notificationStackViewBinder: NotificationStackViewBinder,
) {
init {
@@ -73,24 +61,13 @@
sharedNotificationContainer.addNotificationStackScrollLayout(stackScrollLayout)
}
- SharedNotificationContainerBinder.bind(
+ sharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- sceneContainerFlags,
- controller,
- notificationStackSizeCalculator,
- mainImmediateDispatcher = mainImmediateDispatcher,
)
if (sceneContainerFlags.isEnabled()) {
- NotificationStackAppearanceViewBinder.bind(
- context,
- sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- mainImmediateDispatcher = mainImmediateDispatcher,
- )
+ notificationStackViewBinder.bindWhileAttached()
}
}
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 d780978..9ba5e3b 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
@@ -57,6 +57,7 @@
import androidx.compose.ui.layout.positionInWindow
import androidx.compose.ui.platform.LocalConfiguration
import androidx.compose.ui.platform.LocalDensity
+import androidx.compose.ui.res.dimensionResource
import androidx.compose.ui.unit.Dp
import androidx.compose.ui.unit.IntOffset
import androidx.compose.ui.unit.IntSize
@@ -70,9 +71,10 @@
import com.android.systemui.notifications.ui.composable.Notifications.Form
import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_CORNER_RADIUS
import com.android.systemui.notifications.ui.composable.Notifications.TransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
+import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.ui.composable.ShadeHeader
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder.SCRIM_CORNER_RADIUS
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import kotlin.math.roundToInt
@@ -139,6 +141,7 @@
) {
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 expansionFraction by viewModel.expandFraction.collectAsState(0f)
@@ -156,6 +159,8 @@
val contentHeight = viewModel.intrinsicContentHeight.collectAsState()
+ val stackRounding = viewModel.stackRounding.collectAsState(StackRounding())
+
// the offset for the notifications scrim. Its upper bound is 0, and its lower bound is
// calculated in minScrimOffset. The scrim is the same height as the screen minus the
// height of the Shade Header, and at rest (scrimOffset = 0) its top bound is at maxScrimStartY.
@@ -222,16 +227,12 @@
.graphicsLayer {
shape =
calculateCornerRadius(
+ scrimCornerRadius,
screenCornerRadius,
{ expansionFraction },
layoutState.isTransitioningBetween(Scenes.Gone, Scenes.Shade)
)
- .let {
- RoundedCornerShape(
- topStart = it,
- topEnd = it,
- )
- }
+ .let { stackRounding.value.toRoundedCornerShape(it) }
clip = true
}
) {
@@ -359,6 +360,7 @@
}
private fun calculateCornerRadius(
+ scrimCornerRadius: Dp,
screenCornerRadius: Dp,
expansionFraction: () -> Float,
transitioning: Boolean,
@@ -366,12 +368,12 @@
return if (transitioning) {
lerp(
start = screenCornerRadius.value,
- stop = SCRIM_CORNER_RADIUS,
- fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceAtMost(1f),
+ stop = scrimCornerRadius.value,
+ fraction = (expansionFraction() / EXPANSION_FOR_MAX_CORNER_RADIUS).coerceIn(0f, 1f),
)
.dp
} else {
- SCRIM_CORNER_RADIUS.dp
+ scrimCornerRadius
}
}
@@ -394,5 +396,16 @@
this
}
+fun StackRounding.toRoundedCornerShape(radius: Dp): RoundedCornerShape {
+ val topRadius = if (roundTop) radius else 0.dp
+ val bottomRadius = if (roundBottom) radius else 0.dp
+ return RoundedCornerShape(
+ topStart = topRadius,
+ topEnd = topRadius,
+ bottomStart = bottomRadius,
+ bottomEnd = bottomRadius,
+ )
+}
+
private const val TAG = "FlexiNotifs"
private val DEBUG_COLOR = Color(1f, 0f, 0f, 0.2f)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
index bc48dd1..244861c 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettings.kt
@@ -20,6 +20,7 @@
import androidx.compose.foundation.layout.fillMaxHeight
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.DisposableEffect
import androidx.compose.runtime.LaunchedEffect
import androidx.compose.runtime.collectAsState
import androidx.compose.runtime.getValue
@@ -36,7 +37,8 @@
import com.android.systemui.qs.ui.adapter.QSSceneAdapter
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Companion.Collapsing
import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Expanding
-import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.Unsquishing
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQQS
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter.State.UnsquishingQS
import com.android.systemui.scene.shared.model.Scenes
object QuickSettings {
@@ -49,6 +51,8 @@
object Elements {
val Content =
ElementKey("QuickSettingsContent", scenePicker = MovableElementScenePicker(SCENES))
+ val QuickQuickSettings = ElementKey("QuickQuickSettings")
+ val SplitShadeQuickSettings = ElementKey("SplitShadeQuickSettings")
val FooterActions = ElementKey("QuickSettingsFooterActions")
}
@@ -78,12 +82,16 @@
is TransitionState.Transition ->
with(transitionState) {
when {
- isSplitShade -> QSSceneAdapter.State.QS
- fromScene == Scenes.Shade && toScene == Scenes.QuickSettings ->
+ isSplitShade -> UnsquishingQS(squishiness)
+ fromScene == Scenes.Shade && toScene == Scenes.QuickSettings -> {
Expanding(progress)
- fromScene == Scenes.QuickSettings && toScene == Scenes.Shade ->
+ }
+ fromScene == Scenes.QuickSettings && toScene == Scenes.Shade -> {
Collapsing(progress)
- fromScene == Scenes.Shade || toScene == Scenes.Shade -> Unsquishing(squishiness)
+ }
+ fromScene == Scenes.Shade || toScene == Scenes.Shade -> {
+ UnsquishingQQS(squishiness)
+ }
fromScene == Scenes.QuickSettings || toScene == Scenes.QuickSettings -> {
QSSceneAdapter.State.QS
}
@@ -119,6 +127,18 @@
squishiness: Float = QuickSettings.SharedValues.SquishinessValues.Default,
) {
val contentState = stateForQuickSettingsContent(isSplitShade, squishiness)
+ val transitionState = layoutState.transitionState
+ val isClosing =
+ transitionState is TransitionState.Transition &&
+ transitionState.progress >= 0.9f && // almost done closing
+ !(layoutState.isTransitioning(to = Scenes.Shade) ||
+ layoutState.isTransitioning(to = Scenes.QuickSettings))
+
+ if (isClosing) {
+ DisposableEffect(Unit) {
+ onDispose { qsSceneAdapter.setState(QSSceneAdapter.State.CLOSED) }
+ }
+ }
MovableElement(
key = QuickSettings.Elements.Content,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
index 5c6e1c8..9b59708 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/scene/ui/composable/transitions/FromGoneToShadeTransition.kt
@@ -13,11 +13,18 @@
) {
spec = tween(durationMillis = DefaultDuration.times(durationScale).inWholeMilliseconds.toInt())
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.Clock) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentStart) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.CollapsedContentEnd) }
- fractionRange(start = .58f) { fade(ShadeHeader.Elements.PrivacyChip) }
- translate(QuickSettings.Elements.Content, y = -ShadeHeader.Dimensions.CollapsedHeight * .66f)
+ fractionRange(start = .58f) {
+ fade(ShadeHeader.Elements.Clock)
+ fade(ShadeHeader.Elements.CollapsedContentStart)
+ fade(ShadeHeader.Elements.CollapsedContentEnd)
+ fade(ShadeHeader.Elements.PrivacyChip)
+ fade(QuickSettings.Elements.SplitShadeQuickSettings)
+ fade(QuickSettings.Elements.FooterActions)
+ }
+ translate(
+ QuickSettings.Elements.QuickQuickSettings,
+ y = -ShadeHeader.Dimensions.CollapsedHeight * .66f
+ )
translate(Notifications.Elements.NotificationScrim, Edge.Top, false)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 15e7b51..85798ac 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -55,6 +55,7 @@
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.LowestZIndexScenePicker
import com.android.compose.animation.scene.SceneScope
+import com.android.compose.animation.scene.TransitionState
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.compose.animation.scene.animateSceneFloatAsState
@@ -222,15 +223,17 @@
horizontal = Shade.Dimensions.HorizontalPadding
)
)
- QuickSettings(
- viewModel.qsSceneAdapter,
- {
- (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
- .roundToInt()
- },
- isSplitShade = false,
- squishiness = tileSquishiness,
- )
+ Box(Modifier.element(QuickSettings.Elements.QuickQuickSettings)) {
+ QuickSettings(
+ viewModel.qsSceneAdapter,
+ {
+ (viewModel.qsSceneAdapter.qqsHeight * tileSquishiness)
+ .roundToInt()
+ },
+ isSplitShade = false,
+ squishiness = tileSquishiness,
+ )
+ }
MediaIfVisible(
viewModel = viewModel,
@@ -280,6 +283,8 @@
val lifecycleOwner = LocalLifecycleOwner.current
val footerActionsViewModel =
remember(lifecycleOwner, viewModel) { viewModel.getFooterActionsViewModel(lifecycleOwner) }
+ val tileSquishiness by
+ animateSceneFloatAsState(value = 1f, key = QuickSettings.SharedValues.TilesSquishiness)
val navBarBottomHeight = WindowInsets.navigationBars.asPaddingValues().calculateBottomPadding()
val density = LocalDensity.current
@@ -290,6 +295,7 @@
}
val quickSettingsScrollState = rememberScrollState()
+ val isScrollable = layoutState.transitionState is TransitionState.Idle
LaunchedEffect(isCustomizing, quickSettingsScrollState) {
if (isCustomizing) {
quickSettingsScrollState.scrollTo(0)
@@ -318,31 +324,41 @@
Column(
verticalArrangement = Arrangement.Top,
modifier =
- Modifier.weight(1f).fillMaxHeight().thenIf(!isCustomizing) {
- Modifier.verticalNestedScrollToScene()
- .verticalScroll(quickSettingsScrollState)
- .clipScrollableContainer(Orientation.Horizontal)
- .padding(bottom = navBarBottomHeight)
- }
+ Modifier.weight(1f).fillMaxSize().thenIf(!isCustomizing) {
+ Modifier.padding(bottom = navBarBottomHeight)
+ },
) {
- QuickSettings(
- qsSceneAdapter = viewModel.qsSceneAdapter,
- heightProvider = { viewModel.qsSceneAdapter.qsHeight },
- isSplitShade = true,
- modifier = Modifier.fillMaxWidth(),
- )
+ Column(
+ modifier =
+ Modifier.fillMaxSize().weight(1f).thenIf(!isCustomizing) {
+ Modifier.verticalNestedScrollToScene()
+ .verticalScroll(
+ quickSettingsScrollState,
+ enabled = isScrollable
+ )
+ .clipScrollableContainer(Orientation.Horizontal)
+ }
+ ) {
+ Box(
+ modifier =
+ Modifier.element(QuickSettings.Elements.SplitShadeQuickSettings)
+ ) {
+ QuickSettings(
+ qsSceneAdapter = viewModel.qsSceneAdapter,
+ heightProvider = { viewModel.qsSceneAdapter.qsHeight },
+ isSplitShade = true,
+ modifier = Modifier.fillMaxWidth(),
+ squishiness = tileSquishiness,
+ )
+ }
- MediaIfVisible(
- viewModel = viewModel,
- mediaCarouselController = mediaCarouselController,
- mediaHost = mediaHost,
- modifier = Modifier.fillMaxWidth(),
- )
-
- Spacer(
- modifier = Modifier.weight(1f),
- )
-
+ MediaIfVisible(
+ viewModel = viewModel,
+ mediaCarouselController = mediaCarouselController,
+ mediaHost = mediaHost,
+ modifier = Modifier.fillMaxWidth(),
+ )
+ }
FooterActionsWithAnimatedVisibility(
viewModel = footerActionsViewModel,
isCustomizing = isCustomizing,
@@ -354,7 +370,8 @@
NotificationScrollingStack(
viewModel = viewModel.notifications,
maxScrimTop = { 0f },
- modifier = Modifier.weight(1f).fillMaxHeight(),
+ modifier =
+ Modifier.weight(1f).fillMaxHeight().padding(bottom = navBarBottomHeight),
)
}
}
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 af51cee..dc3b612 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
@@ -73,7 +73,7 @@
internal class SceneScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
private val scene: Scene,
-) : SceneScope {
+) : SceneScope, ElementStateScope by layoutImpl.elementStateScope {
override val layoutState: SceneTransitionLayoutState = layoutImpl.state
override fun Modifier.element(key: ElementKey): 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 b7e2dd1..ebc9099 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
@@ -131,9 +131,30 @@
*/
@DslMarker annotation class ElementDsl
+/** A scope that can be used to query the target state of an element or scene. */
+interface ElementStateScope {
+ /**
+ * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
+ * when idle, or `null` if the element is not composed and measured in that scene (yet).
+ */
+ fun ElementKey.targetSize(scene: SceneKey): IntSize?
+
+ /**
+ * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
+ * element when idle, or `null` if the element is not composed and placed in that scene (yet).
+ */
+ fun ElementKey.targetOffset(scene: SceneKey): Offset?
+
+ /**
+ * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
+ * the scene was never composed.
+ */
+ fun SceneKey.targetSize(): IntSize?
+}
+
@Stable
@ElementDsl
-interface BaseSceneScope {
+interface BaseSceneScope : ElementStateScope {
/** The state of the [SceneTransitionLayout] in which this scene is contained. */
val layoutState: SceneTransitionLayoutState
@@ -415,25 +436,7 @@
): Float
}
-interface UserActionDistanceScope : Density {
- /**
- * Return the *target* size of [this] element in the given [scene], i.e. the size of the element
- * when idle, or `null` if the element is not composed and measured in that scene (yet).
- */
- fun ElementKey.targetSize(scene: SceneKey): IntSize?
-
- /**
- * Return the *target* offset of [this] element in the given [scene], i.e. the size of the
- * element when idle, or `null` if the element is not composed and placed in that scene (yet).
- */
- fun ElementKey.targetOffset(scene: SceneKey): Offset?
-
- /**
- * Return the *target* size of [this] scene, i.e. the size of the scene when idle, or `null` if
- * the scene was never composed.
- */
- fun SceneKey.targetSize(): IntSize?
-}
+interface UserActionDistanceScope : Density, ElementStateScope
/** The user action has a fixed [absoluteDistance]. */
class FixedDistance(private val distance: Dp) : UserActionDistance {
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
index 25b0895..b1cfdcf 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/SceneTransitionLayoutImpl.kt
@@ -98,6 +98,7 @@
private val horizontalDraggableHandler: DraggableHandlerImpl
private val verticalDraggableHandler: DraggableHandlerImpl
+ internal val elementStateScope = ElementStateScopeImpl(this)
private var _userActionDistanceScope: UserActionDistanceScope? = null
internal val userActionDistanceScope: UserActionDistanceScope
get() =
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
index 228d19f..b7abb33 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/UserActionDistanceScopeImpl.kt
@@ -19,15 +19,9 @@
import androidx.compose.ui.geometry.Offset
import androidx.compose.ui.unit.IntSize
-internal class UserActionDistanceScopeImpl(
+internal class ElementStateScopeImpl(
private val layoutImpl: SceneTransitionLayoutImpl,
-) : UserActionDistanceScope {
- override val density: Float
- get() = layoutImpl.density.density
-
- override val fontScale: Float
- get() = layoutImpl.density.fontScale
-
+) : ElementStateScope {
override fun ElementKey.targetSize(scene: SceneKey): IntSize? {
return layoutImpl.elements[this]?.sceneStates?.get(scene)?.targetSize.takeIf {
it != Element.SizeUnspecified
@@ -44,3 +38,13 @@
return layoutImpl.scenes[this]?.targetSize.takeIf { it != IntSize.Zero }
}
}
+
+internal class UserActionDistanceScopeImpl(
+ private val layoutImpl: SceneTransitionLayoutImpl,
+) : UserActionDistanceScope, ElementStateScope by layoutImpl.elementStateScope {
+ override val density: Float
+ get() = layoutImpl.density.density
+
+ override val fontScale: Float
+ get() = layoutImpl.density.fontScale
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
index 707777b..b0d03b1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorTest.kt
@@ -71,34 +71,6 @@
}
@Test
- fun pinAuthMethod() =
- testScope.runTest {
- val message by collectLastValue(underTest.message)
-
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- runCurrent()
- underTest.clearMessage()
- assertThat(message).isNull()
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
- // Wrong input.
- assertThat(underTest.authenticate(listOf(9, 8, 7)))
- .isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PIN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PIN)
-
- // Correct input.
- assertThat(underTest.authenticate(FakeAuthenticationRepository.DEFAULT_PIN))
- .isEqualTo(AuthenticationResult.SUCCEEDED)
- }
-
- @Test
fun pinAuthMethod_sim_skipsAuthentication() =
testScope.runTest {
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -146,8 +118,6 @@
@Test
fun pinAuthMethod_tryAutoConfirm_withoutAutoConfirmPin() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
-
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pin
)
@@ -156,7 +126,6 @@
// Incomplete input.
assertThat(underTest.authenticate(listOf(1, 2), tryAutoConfirm = true))
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isNull()
// Correct input.
assertThat(
@@ -166,28 +135,19 @@
)
)
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isNull()
}
@Test
fun passwordAuthMethod() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
)
runCurrent()
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
-
// Wrong input.
assertThat(underTest.authenticate("alohamora".toList()))
.isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PASSWORD)
// Too short input.
assertThat(
@@ -201,7 +161,6 @@
)
)
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PASSWORD)
// Correct input.
assertThat(underTest.authenticate("password".toList()))
@@ -211,13 +170,10 @@
@Test
fun patternAuthMethod() =
testScope.runTest {
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pattern
)
runCurrent()
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Wrong input.
val wrongPattern =
@@ -231,10 +187,6 @@
assertThat(wrongPattern.size)
.isAtLeast(kosmos.fakeAuthenticationRepository.minPatternLength)
assertThat(underTest.authenticate(wrongPattern)).isEqualTo(AuthenticationResult.FAILED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Too short input.
val tooShortPattern =
@@ -244,10 +196,6 @@
)
assertThat(underTest.authenticate(tooShortPattern))
.isEqualTo(AuthenticationResult.SKIPPED)
- assertThat(message).isEqualTo(MESSAGE_WRONG_PATTERN)
-
- underTest.resetMessage()
- assertThat(message).isEqualTo(MESSAGE_ENTER_YOUR_PATTERN)
// Correct input.
assertThat(underTest.authenticate(FakeAuthenticationRepository.PATTERN))
@@ -258,7 +206,6 @@
fun lockoutStarted() =
testScope.runTest {
val lockoutStartedEvents by collectValues(underTest.onLockoutStarted)
- val message by collectLastValue(underTest.message)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Pin
@@ -272,17 +219,14 @@
.isEqualTo(AuthenticationResult.FAILED)
if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
assertThat(lockoutStartedEvents).isEmpty()
- assertThat(message).isNotEmpty()
}
}
assertThat(authenticationInteractor.lockoutEndTimestamp).isNotNull()
assertThat(lockoutStartedEvents.size).isEqualTo(1)
- assertThat(message).isNull()
// Advance the time to finish the lockout:
advanceTimeBy(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS.seconds)
assertThat(authenticationInteractor.lockoutEndTimestamp).isNull()
- assertThat(message).isNull()
assertThat(lockoutStartedEvents.size).isEqualTo(1)
// Trigger lockout again:
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
index 701b703..c878e0b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractorTest.kt
@@ -17,7 +17,6 @@
package com.android.systemui.bouncer.domain.interactor
import android.content.pm.UserInfo
-import android.os.Handler
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
@@ -28,27 +27,25 @@
import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FaceSensorInfo
-import com.android.systemui.biometrics.data.repository.FakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.SensorStrength
import com.android.systemui.bouncer.data.repository.BouncerMessageRepositoryImpl
-import com.android.systemui.bouncer.data.repository.FakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
import com.android.systemui.bouncer.shared.model.BouncerMessageModel
-import com.android.systemui.bouncer.ui.BouncerView
-import com.android.systemui.classifier.FalsingCollector
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
-import com.android.systemui.keyguard.DismissCallbackRegistry
-import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFaceAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.data.repository.FakeTrustRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
import com.android.systemui.res.R.string.kg_trust_agent_disabled
-import com.android.systemui.statusbar.policy.KeyguardStateController
-import com.android.systemui.user.data.repository.FakeUserRepository
-import com.android.systemui.user.domain.interactor.SelectedUserInteractor
+import com.android.systemui.testKosmos
+import com.android.systemui.user.data.repository.fakeUserRepository
import com.android.systemui.util.mockito.KotlinArgumentCaptor
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
@@ -61,7 +58,6 @@
import org.junit.runner.RunWith
import org.mockito.ArgumentMatchers.eq
import org.mockito.Mock
-import org.mockito.Mockito.mock
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -70,34 +66,22 @@
@TestableLooper.RunWithLooper(setAsMainLooper = true)
@RunWith(AndroidJUnit4::class)
class BouncerMessageInteractorTest : SysuiTestCase() {
-
+ private val kosmos = testKosmos()
private val countDownTimerCallback = KotlinArgumentCaptor(CountDownTimerCallback::class.java)
private val repository = BouncerMessageRepositoryImpl()
- private val userRepository = FakeUserRepository()
- private val fakeTrustRepository = FakeTrustRepository()
- private val fakeFacePropertyRepository = FakeFacePropertyRepository()
- private val bouncerRepository = FakeKeyguardBouncerRepository()
- private val fakeDeviceEntryFingerprintAuthRepository =
- FakeDeviceEntryFingerprintAuthRepository()
- private val fakeDeviceEntryFaceAuthRepository = FakeDeviceEntryFaceAuthRepository()
- private val biometricSettingsRepository: FakeBiometricSettingsRepository =
- FakeBiometricSettingsRepository()
+ private val biometricSettingsRepository = kosmos.fakeBiometricSettingsRepository
+ private val testScope = kosmos.testScope
@Mock private lateinit var updateMonitor: KeyguardUpdateMonitor
@Mock private lateinit var securityModel: KeyguardSecurityModel
@Mock private lateinit var countDownTimerUtil: CountDownTimerUtil
@Mock private lateinit var systemPropertiesHelper: SystemPropertiesHelper
- @Mock private lateinit var keyguardUpdateMonitor: KeyguardUpdateMonitor
- @Mock private lateinit var mSelectedUserInteractor: SelectedUserInteractor
- private lateinit var primaryBouncerInteractor: PrimaryBouncerInteractor
- private lateinit var testScope: TestScope
private lateinit var underTest: BouncerMessageInteractor
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
- userRepository.setUserInfos(listOf(PRIMARY_USER))
- testScope = TestScope()
+ kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
allowTestableLooperAsMainThread()
whenever(securityModel.getSecurityMode(PRIMARY_USER_ID)).thenReturn(PIN)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
@@ -105,44 +89,28 @@
}
suspend fun TestScope.init() {
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
mSetFlagsRule.enableFlags(Flags.FLAG_REVAMPED_BOUNCER_MESSAGES)
- primaryBouncerInteractor =
- PrimaryBouncerInteractor(
- bouncerRepository,
- mock(BouncerView::class.java),
- mock(Handler::class.java),
- mock(KeyguardStateController::class.java),
- mock(KeyguardSecurityModel::class.java),
- mock(PrimaryBouncerCallbackInteractor::class.java),
- mock(FalsingCollector::class.java),
- mock(DismissCallbackRegistry::class.java),
- context,
- keyguardUpdateMonitor,
- fakeTrustRepository,
- testScope.backgroundScope,
- mSelectedUserInteractor,
- mock(DeviceEntryFaceAuthInteractor::class.java),
- )
underTest =
BouncerMessageInteractor(
repository = repository,
- userRepository = userRepository,
+ userRepository = kosmos.fakeUserRepository,
countDownTimerUtil = countDownTimerUtil,
updateMonitor = updateMonitor,
biometricSettingsRepository = biometricSettingsRepository,
- applicationScope = this.backgroundScope,
- trustRepository = fakeTrustRepository,
+ applicationScope = testScope.backgroundScope,
+ trustRepository = kosmos.fakeTrustRepository,
systemPropertiesHelper = systemPropertiesHelper,
- primaryBouncerInteractor = primaryBouncerInteractor,
- facePropertyRepository = fakeFacePropertyRepository,
- deviceEntryFingerprintAuthRepository = fakeDeviceEntryFingerprintAuthRepository,
- faceAuthRepository = fakeDeviceEntryFaceAuthRepository,
+ primaryBouncerInteractor = kosmos.primaryBouncerInteractor,
+ facePropertyRepository = kosmos.fakeFacePropertyRepository,
+ deviceEntryFingerprintAuthInteractor = kosmos.deviceEntryFingerprintAuthInteractor,
+ faceAuthRepository = kosmos.fakeDeviceEntryFaceAuthRepository,
securityModel = securityModel
)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
- bouncerRepository.setPrimaryShow(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeKeyguardBouncerRepository.setPrimaryShow(true)
runCurrent()
}
@@ -268,7 +236,7 @@
init()
val lockoutMessage by collectLastValue(underTest.bouncerMessage)
- fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -276,7 +244,7 @@
assertThat(secondaryResMessage(lockoutMessage))
.isEqualTo("Can’t unlock with face. Too many attempts.")
- fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -289,15 +257,17 @@
testScope.runTest {
init()
val lockoutMessage by collectLastValue(underTest.bouncerMessage)
- fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.STRONG))
- fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(
+ FaceSensorInfo(1, SensorStrength.STRONG)
+ )
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockoutMessage)).isEqualTo("Enter PIN")
assertThat(secondaryResMessage(lockoutMessage))
.isEqualTo("PIN is required after too many attempts")
- fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockoutMessage))
@@ -311,14 +281,14 @@
init()
val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
runCurrent()
assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
assertThat(secondaryResMessage(lockedOutMessage))
.isEqualTo("PIN is required after too many attempts")
- fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
runCurrent()
assertThat(primaryResMessage(lockedOutMessage))
@@ -327,6 +297,19 @@
}
@Test
+ fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+ testScope.runTest {
+ init()
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+ val lockedOutMessage by collectLastValue(underTest.bouncerMessage)
+
+ runCurrent()
+
+ assertThat(primaryResMessage(lockedOutMessage)).isEqualTo("Enter PIN")
+ }
+
+ @Test
fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
testScope.runTest {
init()
@@ -344,9 +327,10 @@
fun onAuthFlagsChanged_withTrustNotManagedAndNoBiometrics_isANoop() =
testScope.runTest {
init()
- fakeTrustRepository.setTrustUsuallyManaged(false)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ runCurrent()
val defaultMessage = Pair("Enter PIN", null)
@@ -377,12 +361,13 @@
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ runCurrent()
- fakeTrustRepository.setCurrentUserTrustManaged(true)
- fakeTrustRepository.setTrustUsuallyManaged(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(true)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
val defaultMessage = Pair("Enter PIN", null)
@@ -415,8 +400,8 @@
fun authFlagsChanges_withFaceEnrolled_providesDifferentMessages() =
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
- fakeTrustRepository.setTrustUsuallyManaged(false)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
@@ -453,12 +438,13 @@
fun authFlagsChanges_withFingerprintEnrolled_providesDifferentMessages() =
testScope.runTest {
init()
- userRepository.setSelectedUserInfo(PRIMARY_USER)
- fakeTrustRepository.setCurrentUserTrustManaged(false)
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
biometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
biometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ runCurrent()
verifyMessagesForAuthFlag(
LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
@@ -466,6 +452,7 @@
)
biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(false)
+ runCurrent()
verifyMessagesForAuthFlag(
LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
index d30e333..c9fa671 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModelTest.kt
@@ -48,6 +48,7 @@
isInputEnabled = MutableStateFlow(true),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Pin,
+ onIntentionalUserInput = {},
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
new file mode 100644
index 0000000..16ec9aa
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelTest.kt
@@ -0,0 +1,455 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.pm.UserInfo
+import android.hardware.biometrics.BiometricFaceConstants
+import android.hardware.fingerprint.FingerprintManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.internal.widget.LockPatternUtils
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.authentication.data.repository.FakeAuthenticationRepository
+import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
+import com.android.systemui.biometrics.data.repository.FaceSensorInfo
+import com.android.systemui.biometrics.data.repository.fakeFacePropertyRepository
+import com.android.systemui.biometrics.data.repository.fakeFingerprintPropertyRepository
+import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.shared.flag.fakeComposeBouncerFlags
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.ErrorFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.FailedFaceAuthenticationStatus
+import com.android.systemui.deviceentry.shared.model.HelpFaceAuthenticationStatus
+import com.android.systemui.flags.fakeSystemPropertiesHelper
+import com.android.systemui.keyguard.data.repository.deviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeBiometricSettingsRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFaceAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.data.repository.fakeTrustRepository
+import com.android.systemui.keyguard.shared.model.AuthenticationFlags
+import com.android.systemui.keyguard.shared.model.ErrorFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.FailFingerprintAuthenticationStatus
+import com.android.systemui.keyguard.shared.model.HelpFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+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
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.advanceTimeBy
+import kotlinx.coroutines.test.currentTime
+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)
+class BouncerMessageViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
+ private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
+ private lateinit var underTest: BouncerMessageViewModel
+
+ @Before
+ fun setUp() {
+ kosmos.fakeUserRepository.setUserInfos(listOf(PRIMARY_USER))
+ kosmos.fakeComposeBouncerFlags.composeBouncerEnabled = true
+ underTest = kosmos.bouncerMessageViewModel
+ overrideResource(R.string.kg_trust_agent_disabled, "Trust agent is unavailable")
+ kosmos.fakeSystemPropertiesHelper.set(
+ DeviceEntryInteractor.SYS_BOOT_REASON_PROP,
+ "not mainline reboot"
+ )
+ }
+
+ @Test
+ fun message_defaultMessage_basedOnAuthMethod() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ runCurrent()
+
+ assertThat(message!!.text).isEqualTo("Unlock with PIN or fingerprint")
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pattern)
+ runCurrent()
+ assertThat(message!!.text).isEqualTo("Unlock with pattern or fingerprint")
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ runCurrent()
+ assertThat(message!!.text).isEqualTo("Unlock with password or fingerprint")
+ }
+
+ @Test
+ fun message() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ assertThat(message?.isUpdateAnimated).isTrue()
+
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
+ bouncerInteractor.authenticate(WRONG_PIN)
+ }
+ assertThat(message?.isUpdateAnimated).isFalse()
+
+ val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ advanceTimeBy(lockoutEndMs - testScope.currentTime)
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+
+ @Test
+ fun lockoutMessage() =
+ testScope.runTest {
+ val message by collectLastValue(underTest.message)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
+ assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
+ runCurrent()
+
+ repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
+ bouncerInteractor.authenticate(WRONG_PIN)
+ runCurrent()
+ if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
+ assertThat(message?.text).isEqualTo("Wrong PIN. Try again.")
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+ }
+ val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
+ assertTryAgainMessage(message?.text, lockoutSeconds)
+ assertThat(message?.isUpdateAnimated).isFalse()
+
+ repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
+ advanceTimeBy(1.seconds)
+ val remainingSeconds = lockoutSeconds - time - 1
+ if (remainingSeconds > 0) {
+ assertTryAgainMessage(message?.text, remainingSeconds)
+ }
+ }
+ assertThat(message?.text).isEqualTo("Enter PIN")
+ assertThat(message?.isUpdateAnimated).isTrue()
+ }
+
+ @Test
+ fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenTrustAgentIsEnabled() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(false)
+ kosmos.fakeTrustRepository.setTrustUsuallyManaged(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+ runCurrent()
+
+ val defaultMessage = Pair("Enter PIN", null)
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to defaultMessage,
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "PIN is required after device restarts"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair("Enter PIN", "Added security required. PIN not used for a while."),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair("Enter PIN", "For added security, device was locked by work policy"),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+ Pair("Enter PIN", "Trust agent is unavailable"),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+ Pair("Enter PIN", "Trust agent is unavailable"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair("Enter PIN", "PIN is required after lockdown"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair("Enter PIN", "PIN required for additional security"),
+ LockPatternUtils.StrongAuthTracker
+ .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(
+ "Enter PIN",
+ "Added security required. Device wasn’t unlocked for a while."
+ ),
+ )
+ }
+
+ @Test
+ fun defaultMessage_mapsToDeviceEntryRestrictionReason_whenFingerprintIsAvailable() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeTrustRepository.setCurrentUserTrustManaged(false)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(false)
+ runCurrent()
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_NOT_REQUIRED to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_USER_REQUEST to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.SOME_AUTH_REQUIRED_AFTER_TRUSTAGENT_EXPIRED to
+ Pair("Unlock with PIN or fingerprint", null),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "PIN is required after device restarts"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_TIMEOUT to
+ Pair("Enter PIN", "Added security required. PIN not used for a while."),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_DPM_LOCK_NOW to
+ Pair("Enter PIN", "For added security, device was locked by work policy"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_USER_LOCKDOWN to
+ Pair("Enter PIN", "PIN is required after lockdown"),
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_FOR_UNATTENDED_UPDATE to
+ Pair("Enter PIN", "PIN required for additional security"),
+ LockPatternUtils.StrongAuthTracker
+ .STRONG_AUTH_REQUIRED_AFTER_NON_STRONG_BIOMETRICS_TIMEOUT to
+ Pair(
+ "Unlock with PIN or fingerprint",
+ "Added security required. Device wasn’t unlocked for a while."
+ ),
+ )
+ }
+
+ @Test
+ fun onFingerprintLockout_messageUpdated() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+ val lockedOutMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockedOutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockedOutMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockedOutMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(lockedOutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun onUdfpsFingerprint_DoesNotShowFingerprintMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeFingerprintPropertyRepository.supportsUdfps()
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setLockedOut(false)
+ val message by collectLastValue(underTest.message)
+
+ runCurrent()
+
+ assertThat(message?.text).isEqualTo("Enter PIN")
+ }
+
+ @Test
+ fun onRestartForMainlineUpdate_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeSystemPropertiesHelper.set("sys.boot.reason.last", "reboot,mainline_update")
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ runCurrent()
+
+ verifyMessagesForAuthFlags(
+ LockPatternUtils.StrongAuthTracker.STRONG_AUTH_REQUIRED_AFTER_BOOT to
+ Pair("Enter PIN", "Device updated. Enter PIN to continue.")
+ )
+ }
+
+ @Test
+ fun onFaceLockout_whenItIsClass3_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ val lockoutMessage by collectLastValue(underTest.message)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(
+ FaceSensorInfo(1, SensorStrength.STRONG)
+ )
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun onFaceLockout_whenItIsNotStrong_shouldProvideRelevantMessage() =
+ testScope.runTest {
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ val lockoutMessage by collectLastValue(underTest.message)
+ kosmos.fakeFacePropertyRepository.setSensorInfo(FaceSensorInfo(1, SensorStrength.WEAK))
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(true)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText)
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setLockedOut(false)
+ runCurrent()
+
+ assertThat(lockoutMessage?.text).isEqualTo("Enter PIN")
+ assertThat(lockoutMessage?.secondaryText.isNullOrBlank()).isTrue()
+ }
+
+ @Test
+ fun setFingerprintMessage_propagateValue() =
+ testScope.runTest {
+ val bouncerMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+ kosmos.fakeFingerprintPropertyRepository.supportsSideFps()
+ runCurrent()
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ HelpFingerprintAuthenticationStatus(1, "some helpful message")
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Unlock with PIN or fingerprint")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ FailFingerprintAuthenticationStatus
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Fingerprint not recognized")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+ kosmos.deviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ ErrorFingerprintAuthenticationStatus(
+ FingerprintManager.FINGERPRINT_ERROR_LOCKOUT,
+ "locked out"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText)
+ .isEqualTo("PIN is required after too many attempts")
+ }
+
+ @Test
+ fun setFaceMessage_propagateValue() =
+ testScope.runTest {
+ val bouncerMessage by collectLastValue(underTest.message)
+
+ kosmos.fakeUserRepository.setSelectedUserInfo(PRIMARY_USER)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthEnrolledAndEnabled(true)
+ kosmos.fakeBiometricSettingsRepository.setIsFaceAuthCurrentlyAllowed(true)
+ runCurrent()
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ HelpFaceAuthenticationStatus(1, "some helpful message")
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("some helpful message")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ ErrorFaceAuthenticationStatus(
+ BiometricFaceConstants.FACE_ERROR_TIMEOUT,
+ "Try again"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ FailedFaceAuthenticationStatus()
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Face not recognized")
+ assertThat(bouncerMessage?.secondaryText).isEqualTo("Try again or enter PIN")
+
+ kosmos.fakeDeviceEntryFaceAuthRepository.setAuthenticationStatus(
+ ErrorFaceAuthenticationStatus(
+ BiometricFaceConstants.FACE_ERROR_LOCKOUT,
+ "locked out"
+ )
+ )
+ runCurrent()
+ assertThat(bouncerMessage?.text).isEqualTo("Enter PIN")
+ assertThat(bouncerMessage?.secondaryText)
+ .isEqualTo("Can’t unlock with face. Too many attempts.")
+ }
+
+ private fun TestScope.verifyMessagesForAuthFlags(
+ vararg authFlagToMessagePair: Pair<Int, Pair<String, String?>>
+ ) {
+ val actualMessage by collectLastValue(underTest.message)
+
+ authFlagToMessagePair.forEach { (flag, expectedMessagePair) ->
+ kosmos.fakeBiometricSettingsRepository.setAuthenticationFlags(
+ AuthenticationFlags(userId = PRIMARY_USER_ID, flag = flag)
+ )
+ runCurrent()
+
+ assertThat(actualMessage?.text).isEqualTo(expectedMessagePair.first)
+
+ if (expectedMessagePair.second == null) {
+ assertThat(actualMessage?.secondaryText.isNullOrBlank()).isTrue()
+ } else {
+ assertThat(actualMessage?.secondaryText).isEqualTo(expectedMessagePair.second)
+ }
+ }
+ }
+
+ private fun assertTryAgainMessage(
+ message: String?,
+ time: Int,
+ ) {
+ assertThat(message).contains("Try again in $time second")
+ }
+
+ companion object {
+ private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
+ private const val PRIMARY_USER_ID = 0
+ private val PRIMARY_USER =
+ UserInfo(
+ /* id= */ PRIMARY_USER_ID,
+ /* name= */ "primary user",
+ /* flags= */ UserInfo.FLAG_PRIMARY
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
index 73db175..3afca96 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelTest.kt
@@ -37,7 +37,6 @@
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import com.google.common.truth.Truth.assertWithMessage
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.flatMapLatest
@@ -142,54 +141,6 @@
}
@Test
- fun message() =
- testScope.runTest {
- val message by collectLastValue(underTest.message)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(message?.isUpdateAnimated).isTrue()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- bouncerInteractor.authenticate(WRONG_PIN)
- }
- assertThat(message?.isUpdateAnimated).isFalse()
-
- val lockoutEndMs = authenticationInteractor.lockoutEndTimestamp ?: 0
- advanceTimeBy(lockoutEndMs - testScope.currentTime)
- assertThat(message?.isUpdateAnimated).isTrue()
- }
-
- @Test
- fun lockoutMessage() =
- testScope.runTest {
- val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
- val message by collectLastValue(underTest.message)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(kosmos.fakeAuthenticationRepository.lockoutEndTimestamp).isNull()
- assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) { times ->
- bouncerInteractor.authenticate(WRONG_PIN)
- if (times < FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT - 1) {
- assertThat(message?.text).isEqualTo(bouncerInteractor.message.value)
- assertThat(message?.isUpdateAnimated).isTrue()
- }
- }
- val lockoutSeconds = FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS
- assertTryAgainMessage(message?.text, lockoutSeconds)
- assertThat(message?.isUpdateAnimated).isFalse()
-
- repeat(FakeAuthenticationRepository.LOCKOUT_DURATION_SECONDS) { time ->
- advanceTimeBy(1.seconds)
- val remainingSeconds = lockoutSeconds - time - 1
- if (remainingSeconds > 0) {
- assertTryAgainMessage(message?.text, remainingSeconds)
- }
- }
- assertThat(message?.text).isEmpty()
- assertThat(message?.isUpdateAnimated).isTrue()
- }
-
- @Test
fun isInputEnabled() =
testScope.runTest {
val isInputEnabled by
@@ -212,25 +163,6 @@
}
@Test
- fun dialogViewModel() =
- testScope.runTest {
- val authMethodViewModel by collectLastValue(underTest.authMethodViewModel)
- val dialogViewModel by collectLastValue(underTest.dialogViewModel)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(Pin)
- assertThat(authMethodViewModel?.lockoutMessageId).isNotNull()
-
- repeat(FakeAuthenticationRepository.MAX_FAILED_AUTH_TRIES_BEFORE_LOCKOUT) {
- assertThat(dialogViewModel).isNull()
- bouncerInteractor.authenticate(WRONG_PIN)
- }
- assertThat(dialogViewModel).isNotNull()
- assertThat(dialogViewModel?.text).isNotEmpty()
-
- dialogViewModel?.onDismiss?.invoke()
- assertThat(dialogViewModel).isNull()
- }
-
- @Test
fun isSideBySideSupported() =
testScope.runTest {
val isSideBySideSupported by collectLastValue(underTest.isSideBySideSupported)
@@ -265,13 +197,6 @@
return listOf(None, Pin, Password, Pattern, Sim)
}
- private fun assertTryAgainMessage(
- message: String?,
- time: Int,
- ) {
- assertThat(message).isEqualTo("Try again in $time seconds.")
- }
-
companion object {
private val WRONG_PIN = FakeAuthenticationRepository.DEFAULT_PIN.map { it + 1 }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
index df50eb6..71c5785 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModelTest.kt
@@ -66,7 +66,6 @@
private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
private val selectedUserInteractor by lazy { kosmos.selectedUserInteractor }
private val inputMethodInteractor by lazy { kosmos.inputMethodInteractor }
- private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
private val isInputEnabled = MutableStateFlow(true)
private val underTest =
@@ -76,6 +75,7 @@
interactor = bouncerInteractor,
inputMethodInteractor = inputMethodInteractor,
selectedUserInteractor = selectedUserInteractor,
+ onIntentionalUserInput = {},
)
@Before
@@ -88,11 +88,9 @@
fun onShown() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEmpty()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Password)
@@ -101,16 +99,13 @@
@Test
fun onHidden_resetsPasswordInputAndMessage() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isNotEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isNotEmpty()
underTest.onHidden()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
assertThat(password).isEmpty()
}
@@ -118,13 +113,11 @@
fun onPasswordInputChanged() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isEmpty()
assertThat(password).isEqualTo("password")
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -144,7 +137,6 @@
@Test
fun onAuthenticateKeyPressed_whenWrong() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
@@ -152,13 +144,11 @@
underTest.onAuthenticateKeyPressed()
assertThat(password).isEmpty()
- assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
}
@Test
fun onAuthenticateKeyPressed_whenEmpty() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.Password
@@ -171,14 +161,12 @@
underTest.onAuthenticateKeyPressed()
assertThat(password).isEmpty()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PASSWORD)
}
@Test
fun onAuthenticateKeyPressed_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val password by collectLastValue(underTest.password)
lockDeviceAndOpenPasswordBouncer()
@@ -186,12 +174,10 @@
underTest.onPasswordInputChanged("wrong")
underTest.onAuthenticateKeyPressed()
assertThat(password).isEqualTo("")
- assertThat(message?.text).isEqualTo(WRONG_PASSWORD)
assertThat(authResult).isFalse()
// Enter the correct password:
underTest.onPasswordInputChanged("password")
- assertThat(message?.text).isEmpty()
underTest.onAuthenticateKeyPressed()
@@ -331,10 +317,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
index 91a056d..51b73ee9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModelTest.kt
@@ -63,6 +63,7 @@
viewModelScope = testScope.backgroundScope,
interactor = bouncerInteractor,
isInputEnabled = MutableStateFlow(true).asStateFlow(),
+ onIntentionalUserInput = {},
)
}
@@ -79,12 +80,10 @@
fun onShown() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
- assertThat(message?.text).isEqualTo(ENTER_YOUR_PATTERN)
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -95,14 +94,12 @@
fun onDragStart() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
underTest.onDragStart()
- assertThat(message?.text).isEmpty()
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
@@ -148,7 +145,6 @@
fun onDragEnd_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
@@ -159,7 +155,6 @@
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
- assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -302,7 +297,6 @@
@Test
fun onDragEnd_whenPatternTooShort() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val dialogViewModel by collectLastValue(bouncerViewModel.dialogViewModel)
lockDeviceAndOpenPatternBouncer()
@@ -325,7 +319,6 @@
underTest.onDragEnd()
- assertWithMessage("Attempt #$attempt").that(message?.text).isEqualTo(WRONG_PATTERN)
assertWithMessage("Attempt #$attempt").that(dialogViewModel).isNull()
}
}
@@ -334,7 +327,6 @@
fun onDragEnd_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val selectedDots by collectLastValue(underTest.selectedDots)
val currentDot by collectLastValue(underTest.currentDot)
lockDeviceAndOpenPatternBouncer()
@@ -344,7 +336,6 @@
underTest.onDragEnd()
assertThat(selectedDots).isEmpty()
assertThat(currentDot).isNull()
- assertThat(message?.text).isEqualTo(WRONG_PATTERN)
assertThat(authResult).isFalse()
// Enter the correct pattern:
@@ -370,10 +361,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
index 7b75a37..5647954 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModelTest.kt
@@ -56,7 +56,6 @@
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val authenticationInteractor by lazy { kosmos.authenticationInteractor }
private val bouncerInteractor by lazy { kosmos.bouncerInteractor }
- private val bouncerViewModel by lazy { kosmos.bouncerViewModel }
private lateinit var underTest: PinBouncerViewModel
@Before
@@ -69,6 +68,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Pin,
+ onIntentionalUserInput = {},
)
overrideResource(R.string.keyguard_enter_your_pin, ENTER_YOUR_PIN)
@@ -78,11 +78,9 @@
@Test
fun onShown() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
- assertThat(message?.text).ignoringCase().isEqualTo(ENTER_YOUR_PIN)
assertThat(pin).isEmpty()
assertThat(underTest.authenticationMethod).isEqualTo(AuthenticationMethodModel.Pin)
}
@@ -98,6 +96,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Sim,
+ onIntentionalUserInput = {},
)
assertThat(underTest.isSimAreaVisible).isTrue()
@@ -126,6 +125,7 @@
isInputEnabled = MutableStateFlow(true).asStateFlow(),
simBouncerInteractor = kosmos.simBouncerInteractor,
authenticationMethod = AuthenticationMethodModel.Sim,
+ onIntentionalUserInput = {},
)
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
val hintedPinLength by collectLastValue(underTest.hintedPinLength)
@@ -136,20 +136,17 @@
@Test
fun onPinButtonClicked() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
underTest.onPinButtonClicked(1)
- assertThat(message?.text).isEmpty()
assertThat(pin).containsExactly(1)
}
@Test
fun onBackspaceButtonClicked() =
testScope.runTest {
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -158,7 +155,6 @@
underTest.onBackspaceButtonClicked()
- assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
}
@@ -183,7 +179,6 @@
fun onBackspaceButtonLongPressed() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -195,7 +190,6 @@
underTest.onBackspaceButtonLongPressed()
- assertThat(message?.text).isEmpty()
assertThat(pin).isEmpty()
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -217,7 +211,6 @@
fun onAuthenticateButtonClicked_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -230,7 +223,6 @@
underTest.onAuthenticateButtonClicked()
assertThat(pin).isEmpty()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -238,7 +230,6 @@
fun onAuthenticateButtonClicked_correctAfterWrong() =
testScope.runTest {
val authResult by collectLastValue(authenticationInteractor.onAuthenticationResult)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
lockDeviceAndOpenPinBouncer()
@@ -248,13 +239,11 @@
underTest.onPinButtonClicked(4)
underTest.onPinButtonClicked(5) // PIN is now wrong!
underTest.onAuthenticateButtonClicked()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(pin).isEmpty()
assertThat(authResult).isFalse()
// Enter the correct PIN:
FakeAuthenticationRepository.DEFAULT_PIN.forEach(underTest::onPinButtonClicked)
- assertThat(message?.text).isEmpty()
underTest.onAuthenticateButtonClicked()
@@ -277,7 +266,6 @@
fun onAutoConfirm_whenWrong() =
testScope.runTest {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val message by collectLastValue(bouncerViewModel.message)
val pin by collectLastValue(underTest.pinInput.map { it.getPin() })
kosmos.fakeAuthenticationRepository.setAutoConfirmFeatureEnabled(true)
lockDeviceAndOpenPinBouncer()
@@ -290,7 +278,6 @@
) // PIN is now wrong!
assertThat(pin).isEmpty()
- assertThat(message?.text).ignoringCase().isEqualTo(WRONG_PIN)
assertThat(currentScene).isEqualTo(Scenes.Bouncer)
}
@@ -390,10 +377,8 @@
private fun TestScope.switchToScene(toScene: SceneKey) {
val currentScene by collectLastValue(sceneInteractor.currentScene)
- val bouncerShown = currentScene != Scenes.Bouncer && toScene == Scenes.Bouncer
val bouncerHidden = currentScene == Scenes.Bouncer && toScene != Scenes.Bouncer
sceneInteractor.changeScene(toScene, "reason")
- if (bouncerShown) underTest.onShown()
if (bouncerHidden) underTest.onHidden()
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 8e2e947..a7e98ea 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -18,10 +18,16 @@
import android.app.smartspace.SmartspaceTarget
import android.appwidget.AppWidgetProviderInfo
+import android.content.ActivityNotFoundException
+import android.content.Intent
+import android.content.pm.ActivityInfo
+import android.content.pm.PackageManager
+import android.content.pm.ResolveInfo
import android.content.pm.UserInfo
import android.os.UserHandle
import android.provider.Settings
import android.widget.RemoteViews
+import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
@@ -39,6 +45,7 @@
import com.android.systemui.communal.shared.model.CommunalWidgetContentModel
import com.android.systemui.communal.ui.viewmodel.CommunalEditModeViewModel
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
import com.android.systemui.log.logcatLogBuffer
import com.android.systemui.media.controls.ui.view.MediaHost
@@ -46,15 +53,19 @@
import com.android.systemui.smartspace.data.repository.FakeSmartspaceRepository
import com.android.systemui.smartspace.data.repository.fakeSmartspaceRepository
import com.android.systemui.testKosmos
-import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.whenever
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
+import org.junit.Assert.assertFalse
+import org.junit.Assert.assertTrue
import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyInt
import org.mockito.Mock
import org.mockito.Mockito
+import org.mockito.Mockito.never
import org.mockito.Mockito.verify
import org.mockito.MockitoAnnotations
@@ -64,6 +75,8 @@
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var providerInfo: AppWidgetProviderInfo
+ @Mock private lateinit var packageManager: PackageManager
+ @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
@@ -73,6 +86,8 @@
private lateinit var smartspaceRepository: FakeSmartspaceRepository
private lateinit var mediaRepository: FakeCommunalMediaRepository
+ private val testableResources = context.orCreateTestableResources
+
private lateinit var underTest: CommunalEditModeViewModel
@Before
@@ -96,6 +111,7 @@
mediaHost,
uiEventLogger,
logcatLogBuffer("CommunalEditModeViewModelTest"),
+ kosmos.testDispatcher,
)
}
@@ -217,7 +233,69 @@
verify(uiEventLogger).log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
+ @Test
+ fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).then {
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+ }
+ }
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher
+ )
+
+ verify(activityResultLauncher).launch(any())
+ assertTrue(success)
+ }
+ }
+
+ @Test
+ fun onOpenWidgetPicker_launcherActivityNotResolved_doesNotLaunchWidgetPickerActivity() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).thenReturn(null)
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher
+ )
+
+ verify(activityResultLauncher, never()).launch(any())
+ assertFalse(success)
+ }
+ }
+
+ @Test
+ fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
+ testScope.runTest {
+ whenever(packageManager.resolveActivity(any(), anyInt())).then {
+ ResolveInfo().apply {
+ activityInfo = ActivityInfo().apply { packageName = WIDGET_PICKER_PACKAGE_NAME }
+ }
+ }
+
+ whenever(activityResultLauncher.launch(any()))
+ .thenThrow(ActivityNotFoundException::class.java)
+
+ val success =
+ underTest.onOpenWidgetPicker(
+ testableResources.resources,
+ packageManager,
+ activityResultLauncher,
+ )
+
+ assertFalse(success)
+ }
+ }
+
private companion object {
val MAIN_USER_INFO = UserInfo(0, "primary", UserInfo.FLAG_MAIN)
+ const val WIDGET_PICKER_PACKAGE_NAME = "widget_picker_package_name"
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
index decbdaf..51f9957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractorTest.kt
@@ -26,12 +26,10 @@
import com.android.systemui.kosmos.testScope
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
-import kotlin.test.Test
-import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.runTest
+import org.junit.Test
import org.junit.runner.RunWith
-@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
class DeviceEntryFingerprintAuthInteractorTest : SysuiTestCase() {
@@ -59,17 +57,20 @@
}
@Test
- fun isSensorUnderDisplay_trueForUdfpsSensorTypes() =
+ fun isFingerprintCurrentlyAllowedInBouncer_trueForNonUdfpsSensorTypes() =
testScope.runTest {
- val isSensorUnderDisplay by collectLastValue(underTest.isSensorUnderDisplay)
+ biometricSettingsRepository.setIsFingerprintAuthCurrentlyAllowed(true)
+
+ val isFingerprintCurrentlyAllowedInBouncer by
+ collectLastValue(underTest.isFingerprintCurrentlyAllowedOnBouncer)
fingerprintPropertyRepository.supportsUdfps()
- assertThat(isSensorUnderDisplay).isTrue()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isFalse()
fingerprintPropertyRepository.supportsRearFps()
- assertThat(isSensorUnderDisplay).isFalse()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
fingerprintPropertyRepository.supportsSideFps()
- assertThat(isSensorUnderDisplay).isFalse()
+ assertThat(isFingerprintCurrentlyAllowedInBouncer).isTrue()
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
index 3c0ab24..27c4ec1 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterImplTest.kt
@@ -27,9 +27,17 @@
import com.android.systemui.common.ui.data.repository.FakeConfigurationRepository
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSComponent
import com.android.systemui.qs.dagger.QSSceneComponent
+import com.android.systemui.shade.data.repository.fakeShadeRepository
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.argumentCaptor
import com.android.systemui.util.mockito.capture
@@ -41,8 +49,6 @@
import java.util.Locale
import javax.inject.Provider
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.Test
@@ -57,8 +63,9 @@
@OptIn(ExperimentalCoroutinesApi::class)
class QSSceneAdapterImplTest : SysuiTestCase() {
- private val testDispatcher = StandardTestDispatcher()
- private val testScope = TestScope(testDispatcher)
+ private val kosmos = Kosmos().apply { testCase = this@QSSceneAdapterImplTest }
+ private val testDispatcher = kosmos.testDispatcher
+ private val testScope = kosmos.testScope
private val qsImplProvider =
object : Provider<QSImpl> {
@@ -107,10 +114,15 @@
}
}
+ private val shadeInteractor = kosmos.shadeInteractor
+ private val dumpManager = mock<DumpManager>()
+
private val underTest =
QSSceneAdapterImpl(
qsSceneComponentFactory,
qsImplProvider,
+ shadeInteractor,
+ dumpManager,
testDispatcher,
testScope.backgroundScope,
configurationInteractor,
@@ -158,12 +170,6 @@
)
verify(this).setListening(false)
verify(this).setExpanded(false)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -187,13 +193,7 @@
/* squishinessFraction= */ 1f,
)
verify(this).setListening(true)
- verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
+ verify(this).setExpanded(false)
}
}
@@ -218,12 +218,6 @@
)
verify(this).setListening(true)
verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -249,12 +243,6 @@
)
verify(this).setListening(true)
verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ 1f,
- )
}
}
@@ -268,7 +256,7 @@
runCurrent()
clearInvocations(qsImpl!!)
- underTest.setState(QSSceneAdapter.State.Unsquishing(squishiness))
+ underTest.setState(QSSceneAdapter.State.UnsquishingQQS(squishiness))
with(qsImpl!!) {
verify(this).setQsVisible(true)
verify(this)
@@ -279,13 +267,7 @@
/* squishinessFraction= */ squishiness,
)
verify(this).setListening(true)
- verify(this).setExpanded(true)
- verify(this)
- .setTransitionToFullShadeProgress(
- /* isTransitioningToFullShade= */ false,
- /* qsTransitionFraction= */ 1f,
- /* qsSquishinessFraction = */ squishiness,
- )
+ verify(this).setExpanded(false)
}
}
@@ -497,4 +479,21 @@
verify(qsImpl!!).applyBottomNavBarToCustomizerPadding(navBarHeight)
}
+
+ @Test
+ fun dispatchSplitShade() =
+ testScope.runTest {
+ val shadeRepository = kosmos.fakeShadeRepository
+ shadeRepository.setShadeMode(ShadeMode.Single)
+ val qsImpl by collectLastValue(underTest.qsImpl)
+
+ underTest.inflate(context)
+ runCurrent()
+
+ verify(qsImpl!!).setInSplitShade(false)
+
+ shadeRepository.setShadeMode(ShadeMode.Split)
+ runCurrent()
+ verify(qsImpl!!).setInSplitShade(true)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
index e281383..ebd65fd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/adapter/QSSceneAdapterTest.kt
@@ -49,9 +49,16 @@
}
@Test
- fun unsquishing_expansionSameAsQQS() {
+ fun unsquishingQQS_expansionSameAsQQS() {
val squishiness = 0.6f
- assertThat(QSSceneAdapter.State.Unsquishing(squishiness).expansion)
+ assertThat(QSSceneAdapter.State.UnsquishingQQS(squishiness).expansion)
.isEqualTo(QSSceneAdapter.State.QQS.expansion)
}
+
+ @Test
+ fun unsquishingQS_expansionSameAsQS() {
+ val squishiness = 0.6f
+ assertThat(QSSceneAdapter.State.UnsquishingQS(squishiness).expansion)
+ .isEqualTo(QSSceneAdapter.State.QS.expansion)
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
index 1c54961..d1c4ec3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
@@ -95,7 +95,7 @@
scope = testScope.backgroundScope,
)
- private val qsFlexiglassAdapter = FakeQSSceneAdapter({ mock() })
+ private val qsSceneAdapter = FakeQSSceneAdapter({ mock() })
private lateinit var shadeHeaderViewModel: ShadeHeaderViewModel
@@ -122,7 +122,7 @@
applicationScope = testScope.backgroundScope,
deviceEntryInteractor = deviceEntryInteractor,
shadeHeaderViewModel = shadeHeaderViewModel,
- qsSceneAdapter = qsFlexiglassAdapter,
+ qsSceneAdapter = qsSceneAdapter,
notifications = kosmos.notificationsPlaceholderViewModel,
mediaDataManager = mediaDataManager,
shadeInteractor = kosmos.shadeInteractor,
@@ -279,6 +279,20 @@
}
@Test
+ fun upTransitionSceneKey_customizing_noTransition() =
+ testScope.runTest {
+ val destinationScenes by collectLastValue(underTest.destinationScenes)
+
+ qsSceneAdapter.setCustomizing(true)
+ assertThat(
+ destinationScenes!!
+ .keys
+ .filterIsInstance<Swipe>()
+ .filter { it.direction == SwipeDirection.Up }
+ ).isEmpty()
+ }
+
+ @Test
fun shadeMode() =
testScope.runTest {
val shadeMode by collectLastValue(underTest.shadeMode)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
index 2689fc1..94539a3 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationStackAppearanceIntegrationTest.kt
@@ -22,7 +22,6 @@
import androidx.test.filters.SmallTest
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.flags.Flags
import com.android.systemui.flags.fakeFeatureFlagsClassic
@@ -31,6 +30,7 @@
import com.android.systemui.scene.shared.flag.fakeSceneContainerFlags
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.shared.model.fakeSceneDataSource
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
import com.android.systemui.testKosmos
@@ -64,7 +64,7 @@
@Test
fun updateBounds() =
testScope.runTest {
- val bounds by collectLastValue(appearanceViewModel.stackBounds)
+ val clipping by collectLastValue(appearanceViewModel.stackClipping)
val top = 200f
val left = 0f
@@ -76,15 +76,8 @@
right = right,
bottom = bottom
)
- assertThat(bounds)
- .isEqualTo(
- NotificationContainerBounds(
- left = left,
- top = top,
- right = right,
- bottom = bottom
- )
- )
+ assertThat(clipping?.bounds)
+ .isEqualTo(StackBounds(left = left, top = top, right = right, bottom = bottom))
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
index ffe6e6d..e3fa89c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorTest.kt
@@ -19,10 +19,13 @@
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -30,10 +33,9 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
-@android.platform.test.annotations.EnabledOnRavenwood
class NotificationStackAppearanceInteractorTest : SysuiTestCase() {
- private val kosmos = Kosmos()
+ private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val underTest = kosmos.notificationStackAppearanceInteractor
@@ -43,29 +45,39 @@
val stackBounds by collectLastValue(underTest.stackBounds)
val bounds1 =
- NotificationContainerBounds(
+ StackBounds(
top = 100f,
bottom = 200f,
- isAnimated = true,
)
underTest.setStackBounds(bounds1)
assertThat(stackBounds).isEqualTo(bounds1)
val bounds2 =
- NotificationContainerBounds(
+ StackBounds(
top = 200f,
bottom = 300f,
- isAnimated = false,
)
underTest.setStackBounds(bounds2)
assertThat(stackBounds).isEqualTo(bounds2)
}
+ @Test
+ fun stackRounding() =
+ testScope.runTest {
+ val stackRounding by collectLastValue(underTest.stackRounding)
+
+ kosmos.shadeRepository.setShadeMode(ShadeMode.Single)
+ assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = false))
+
+ kosmos.shadeRepository.setShadeMode(ShadeMode.Split)
+ assertThat(stackRounding).isEqualTo(StackRounding(roundTop = true, roundBottom = true))
+ }
+
@Test(expected = IllegalStateException::class)
fun setStackBounds_withImproperBounds_throwsException() =
testScope.runTest {
underTest.setStackBounds(
- NotificationContainerBounds(
+ StackBounds(
top = 100f,
bottom = 99f,
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
index 693de55..2ccc8b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelTest.kt
@@ -22,6 +22,7 @@
import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import org.junit.Test
@@ -36,9 +37,9 @@
fun onBoundsChanged_setsNotificationContainerBounds() {
underTest.onBoundsChanged(left = 5f, top = 5f, right = 5f, bottom = 5f)
assertThat(kosmos.keyguardInteractor.notificationContainerBounds.value)
- .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ .isEqualTo(NotificationContainerBounds(top = 5f, bottom = 5f))
assertThat(kosmos.notificationStackAppearanceInteractor.stackBounds.value)
- .isEqualTo(NotificationContainerBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
+ .isEqualTo(StackBounds(left = 5f, top = 5f, right = 5f, bottom = 5f))
}
@Test
fun onContentTopChanged_setsContentTop() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
new file mode 100644
index 0000000..a5ad3c3
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/util/kotlin/DisposableHandlesTest.kt
@@ -0,0 +1,68 @@
+/*
+ * 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.util.kotlin
+
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.DisposableHandle
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class DisposableHandlesTest : SysuiTestCase() {
+ @Test
+ fun disposeWorksOnce() {
+ var handleDisposeCount = 0
+ val underTest = DisposableHandles()
+
+ // Add a handle
+ underTest += DisposableHandle { handleDisposeCount++ }
+
+ // dispose() calls dispose() on children
+ underTest.dispose()
+ assertThat(handleDisposeCount).isEqualTo(1)
+
+ // Once disposed, children are not disposed again
+ underTest.dispose()
+ assertThat(handleDisposeCount).isEqualTo(1)
+ }
+
+ @Test
+ fun replaceCallsDispose() {
+ var handleDisposeCount1 = 0
+ var handleDisposeCount2 = 0
+ val underTest = DisposableHandles()
+ val handle1 = DisposableHandle { handleDisposeCount1++ }
+ val handle2 = DisposableHandle { handleDisposeCount2++ }
+
+ // First add handle1
+ underTest += handle1
+
+ // replace() calls dispose() on existing children
+ underTest.replaceAll(handle2)
+ assertThat(handleDisposeCount1).isEqualTo(1)
+ assertThat(handleDisposeCount2).isEqualTo(0)
+
+ // Once disposed, replaced children are not disposed again
+ underTest.dispose()
+ assertThat(handleDisposeCount1).isEqualTo(1)
+ assertThat(handleDisposeCount2).isEqualTo(1)
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
new file mode 100644
index 0000000..b5c5809
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractorTest.kt
@@ -0,0 +1,93 @@
+/*
+ * 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.os.Handler
+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.testCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.localMediaController
+import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaOutputInteractor
+import com.android.systemui.volume.remoteMediaController
+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 MediaDeviceSessionInteractorTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+
+ private lateinit var underTest: MediaDeviceSessionInteractor
+
+ @Before
+ fun setup() {
+ with(kosmos) {
+ mediaControllerRepository.setActiveSessions(
+ listOf(localMediaController, remoteMediaController)
+ )
+
+ underTest =
+ MediaDeviceSessionInteractor(
+ testScope.testScheduler,
+ Handler(TestableLooper.get(kosmos.testCase).looper),
+ mediaControllerRepository,
+ )
+ }
+ }
+
+ @Test
+ fun playbackInfo_returnsPlaybackInfo() {
+ with(kosmos) {
+ testScope.runTest {
+ val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+ runCurrent()
+ val info by collectLastValue(underTest.playbackInfo(session!!))
+ runCurrent()
+
+ assertThat(info).isEqualTo(localMediaController.playbackInfo)
+ }
+ }
+ }
+
+ @Test
+ fun playbackState_returnsPlaybackState() {
+ with(kosmos) {
+ testScope.runTest {
+ val session by collectLastValue(mediaOutputInteractor.defaultActiveMediaSession)
+ runCurrent()
+ val state by collectLastValue(underTest.playbackState(session!!))
+ runCurrent()
+
+ assertThat(state).isEqualTo(localMediaController.playbackState)
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
index dcf635e..6f7f20b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/mediaoutput/ui/viewmodel/MediaOutputViewModelTest.kt
@@ -29,9 +29,10 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
+import com.android.systemui.volume.mediaDeviceSessionInteractor
import com.android.systemui.volume.mediaOutputActionsInteractor
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.volumePanelViewModel
@@ -63,6 +64,7 @@
testScope.backgroundScope,
volumePanelViewModel,
mediaOutputActionsInteractor,
+ mediaDeviceSessionInteractor,
mediaOutputInteractor,
)
@@ -74,11 +76,11 @@
)
}
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).then { playbackStateBuilder.build() }
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).then { playbackStateBuilder.build() }
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
index 1ed7f5d..2f69942 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt
@@ -32,8 +32,8 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.panel.component.spatial.spatialAudioComponentInteractor
import com.google.common.truth.Truth.assertThat
@@ -66,11 +66,11 @@
}
)
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
index 281b03d..e36ae60 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt
@@ -34,8 +34,8 @@
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.mock
import com.android.systemui.util.mockito.whenever
+import com.android.systemui.volume.localMediaController
import com.android.systemui.volume.localMediaRepository
-import com.android.systemui.volume.mediaController
import com.android.systemui.volume.mediaControllerRepository
import com.android.systemui.volume.mediaOutputInteractor
import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel
@@ -70,11 +70,11 @@
}
)
- whenever(mediaController.packageName).thenReturn("test.pkg")
- whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
- whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build())
+ whenever(localMediaController.packageName).thenReturn("test.pkg")
+ whenever(localMediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {}))
+ whenever(localMediaController.playbackState).thenReturn(PlaybackState.Builder().build())
- mediaControllerRepository.setActiveLocalMediaController(mediaController)
+ mediaControllerRepository.setActiveSessions(listOf(localMediaController))
underTest =
SpatialAudioComponentInteractor(
diff --git a/packages/SystemUI/res/layout/screenshot_shelf.xml b/packages/SystemUI/res/layout/screenshot_shelf.xml
new file mode 100644
index 0000000..ef1a21f
--- /dev/null
+++ b/packages/SystemUI/res/layout/screenshot_shelf.xml
@@ -0,0 +1,160 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+<com.android.systemui.screenshot.ui.ScreenshotShelfView
+ 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:layout_width="match_parent"
+ android:layout_height="match_parent">
+ <ImageView
+ android:id="@+id/actions_container_background"
+ android:visibility="gone"
+ android:layout_height="0dp"
+ android:layout_width="0dp"
+ android:elevation="4dp"
+ android:background="@drawable/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_toTopOf="@id/guideline"/>
+ <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/screenshot_preview_border"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintBottom_toBottomOf="@id/actions_container_background">
+ <LinearLayout
+ android:id="@+id/screenshot_actions"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_share_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_edit_chip"/>
+ <include layout="@layout/overlay_action_chip"
+ android:id="@+id/screenshot_scroll_chip"
+ android:visibility="gone" />
+ </LinearLayout>
+ </HorizontalScrollView>
+ <View
+ android:id="@+id/screenshot_preview_border"
+ android:layout_width="0dp"
+ android:layout_height="0dp"
+ android:layout_marginStart="16dp"
+ android:layout_marginTop="@dimen/overlay_border_width_neg"
+ android:layout_marginEnd="@dimen/overlay_border_width_neg"
+ android:layout_marginBottom="14dp"
+ android:elevation="8dp"
+ android:background="@drawable/overlay_border"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+ app:layout_constraintBottom_toBottomOf="parent"/>
+ <ImageView
+ android:id="@+id/screenshot_preview"
+ android:layout_width="@dimen/overlay_x_scale"
+ 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="8dp"
+ android:contentDescription="@string/screenshot_edit_description"
+ android:scaleType="fitEnd"
+ android:background="@drawable/overlay_preview_background"
+ android:adjustViewBounds="true"
+ android:clickable="true"
+ app:layout_constraintStart_toStartOf="@id/screenshot_preview_border"
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"/>
+ <ImageView
+ android:id="@+id/screenshot_badge"
+ android:layout_width="56dp"
+ android:layout_height="56dp"
+ android:visibility="gone"
+ android:elevation="9dp"
+ app:layout_constraintBottom_toBottomOf="@id/screenshot_preview_border"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview_border"/>
+ <FrameLayout
+ android:id="@+id/screenshot_dismiss_button"
+ android:layout_width="@dimen/overlay_dismiss_button_tappable_size"
+ android:layout_height="@dimen/overlay_dismiss_button_tappable_size"
+ android:elevation="11dp"
+ android:visibility="gone"
+ app:layout_constraintStart_toEndOf="@id/screenshot_preview"
+ app:layout_constraintEnd_toEndOf="@id/screenshot_preview"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ app:layout_constraintBottom_toTopOf="@id/screenshot_preview"
+ android:contentDescription="@string/screenshot_dismiss_description">
+ <ImageView
+ android:id="@+id/screenshot_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/materialColorPrimary"
+ android:tint="?androidprv:attr/materialColorOnPrimary"
+ android:padding="4dp"
+ android:src="@drawable/ic_close"/>
+ </FrameLayout>
+ <ImageView
+ android:id="@+id/screenshot_scrollable_preview"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:scaleType="matrix"
+ android:visibility="gone"
+ app:layout_constraintStart_toStartOf="@id/screenshot_preview"
+ app:layout_constraintTop_toTopOf="@id/screenshot_preview"
+ android:elevation="7dp"/>
+
+ <androidx.constraintlayout.widget.Guideline
+ android:id="@+id/guideline"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="horizontal"
+ app:layout_constraintGuide_end="0dp" />
+
+ <FrameLayout
+ android:id="@+id/screenshot_message_container"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:layout_marginHorizontal="@dimen/overlay_action_container_margin_horizontal"
+ android:layout_marginTop="4dp"
+ android:layout_marginBottom="@dimen/overlay_action_container_margin_bottom"
+ android:paddingHorizontal="@dimen/overlay_action_container_padding_end"
+ android:paddingVertical="@dimen/overlay_action_container_padding_vertical"
+ android:elevation="4dp"
+ android:background="@drawable/action_chip_container_background"
+ android:visibility="gone"
+ app:layout_constraintTop_toBottomOf="@id/guideline"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintWidth_max="450dp"
+ app:layout_constraintHorizontal_bias="0">
+ <include layout="@layout/screenshot_work_profile_first_run" />
+ <include layout="@layout/screenshot_detection_notice" />
+ </FrameLayout>
+</com.android.systemui.screenshot.ui.ScreenshotShelfView>
diff --git a/packages/SystemUI/res/layout/window_magnification_settings_view.xml b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
index efdb0a3..704cf0b 100644
--- a/packages/SystemUI/res/layout/window_magnification_settings_view.xml
+++ b/packages/SystemUI/res/layout/window_magnification_settings_view.xml
@@ -29,9 +29,13 @@
android:layout_height="wrap_content"
android:orientation="horizontal">
<TextView
+ android:id="@+id/magnifier_size_title"
android:layout_width="0dp"
android:layout_height="wrap_content"
android:layout_weight="1"
+ android:singleLine="true"
+ android:scrollHorizontally="true"
+ android:ellipsize="marquee"
android:text="@string/accessibility_magnifier_size"
android:textAppearance="@style/TextAppearance.MagnificationSetting.Title"
android:focusable="true"
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 774bbe5..e48959c 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -235,6 +235,8 @@
<string name="screenshot_edit_label">Edit</string>
<!-- Content description indicating that tapping the element will allow editing the screenshot [CHAR LIMIT=NONE] -->
<string name="screenshot_edit_description">Edit screenshot</string>
+ <!-- Label for UI element which allows sharing the screenshot [CHAR LIMIT=30] -->
+ <string name="screenshot_share_label">Share</string>
<!-- Content description indicating that tapping the element will allow sharing the screenshot [CHAR LIMIT=NONE] -->
<string name="screenshot_share_description">Share screenshot</string>
<!-- Label for UI element which allows the user to capture additional off-screen content in a screenshot. [CHAR LIMIT=30] -->
@@ -1437,8 +1439,11 @@
<!-- Indication on the keyguard that appears when a trust agents unlocks the device. [CHAR LIMIT=40] -->
<string name="keyguard_indication_trust_unlocked">Kept unlocked by TrustAgent</string>
- <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
- <string name="kg_prompt_after_adaptive_auth_lock">Theft protection\nDevice locked, too many unlock attempts</string>
+ <!-- Message asking the user to authenticate with primary authentication methods (PIN/pattern/password) or biometrics after the device is locked by adaptive auth. [CHAR LIMIT=70] -->
+ <string name="kg_prompt_after_adaptive_auth_lock">Device was locked, too many authentication attempts</string>
+
+ <!-- Indication on the keyguard that appears after the device is locked by adaptive auth. [CHAR LIMIT=60] -->
+ <string name="keyguard_indication_after_adaptive_auth_lock">Device locked\nFailed authentication</string>
<!-- Accessibility string for current zen mode and selected exit condition. A template that simply concatenates existing mode string and the current condition description. [CHAR LIMIT=20] -->
<string name="zen_mode_and_condition"><xliff:g id="zen_mode" example="Priority interruptions only">%1$s</xliff:g>. <xliff:g id="exit_condition" example="For one hour">%2$s</xliff:g></string>
@@ -1991,8 +1996,6 @@
<string name="group_system_cycle_back">Cycle backward through recent apps</string>
<!-- User visible title for the keyboard shortcut that accesses list of all apps and search. [CHAR LIMIT=70] -->
<string name="group_system_access_all_apps_search">Open apps list</string>
- <!-- User visible title for the keyboard shortcut that hides and (re)showes taskbar. [CHAR LIMIT=70] -->
- <string name="group_system_hide_reshow_taskbar">Show taskbar</string>
<!-- User visible title for the keyboard shortcut that accesses [system] settings. [CHAR LIMIT=70] -->
<string name="group_system_access_system_settings">Open settings</string>
<!-- User visible title for the keyboard shortcut that accesses Assistant app. [CHAR LIMIT=70] -->
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
index e621ffe..86f64f8 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardClockSwitchController.java
@@ -631,7 +631,7 @@
@Nullable
public ClockController getClock() {
if (migrateClocksToBlueprint()) {
- return mKeyguardClockInteractor.getClock();
+ return mKeyguardClockInteractor.getCurrentClock().getValue();
} else {
return mClockEventController.getClock();
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
index a98990a..ca24ccb 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/WindowMagnificationSettings.java
@@ -98,6 +98,7 @@
private ImageButton mMediumButton;
private ImageButton mLargeButton;
private Button mDoneButton;
+ private TextView mSizeTitle;
private Button mEditButton;
private ImageButton mFullScreenButton;
private int mLastSelectedButtonIndex = MagnificationSize.NONE;
@@ -521,6 +522,7 @@
mMediumButton = mSettingView.findViewById(R.id.magnifier_medium_button);
mLargeButton = mSettingView.findViewById(R.id.magnifier_large_button);
mDoneButton = mSettingView.findViewById(R.id.magnifier_done_button);
+ mSizeTitle = mSettingView.findViewById(R.id.magnifier_size_title);
mEditButton = mSettingView.findViewById(R.id.magnifier_edit_button);
mFullScreenButton = mSettingView.findViewById(R.id.magnifier_full_button);
mAllowDiagonalScrollingTitle =
@@ -548,6 +550,7 @@
mDoneButton.setOnClickListener(mButtonClickListener);
mFullScreenButton.setOnClickListener(mButtonClickListener);
mEditButton.setOnClickListener(mButtonClickListener);
+ mSizeTitle.setSelected(true);
mAllowDiagonalScrollingTitle.setSelected(true);
mSettingView.setOnApplyWindowInsetsListener((v, insets) -> {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
index d849b3a..94e0854 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/data/repository/BouncerRepository.kt
@@ -20,7 +20,6 @@
import com.android.systemui.flags.FeatureFlagsClassic
import com.android.systemui.flags.Flags
import javax.inject.Inject
-import kotlinx.coroutines.flow.MutableStateFlow
/** Provides access to bouncer-related application state. */
@SysUISingleton
@@ -29,9 +28,6 @@
constructor(
private val flags: FeatureFlagsClassic,
) {
- /** The user-facing message to show in the bouncer. */
- val message = MutableStateFlow<String?>(null)
-
/** Whether the user switcher should be displayed within the bouncer UI on large screens. */
val isUserSwitcherVisible: Boolean
get() = flags.isEnabled(Flags.FULL_SCREEN_USER_SWITCHER)
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
index d8be1af..aeb564d5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractor.kt
@@ -16,13 +16,8 @@
package com.android.systemui.bouncer.domain.interactor
-import android.content.Context
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.domain.interactor.AuthenticationResult
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Password
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pattern
-import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Pin
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel.Sim
import com.android.systemui.bouncer.data.repository.BouncerRepository
import com.android.systemui.classifier.FalsingClassifier
@@ -31,7 +26,6 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
import com.android.systemui.power.domain.interactor.PowerInteractor
-import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.async
@@ -41,7 +35,6 @@
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
/** Encapsulates business logic and application state accessing use-cases. */
@SysUISingleton
@@ -49,16 +42,14 @@
@Inject
constructor(
@Application private val applicationScope: CoroutineScope,
- @Application private val applicationContext: Context,
private val repository: BouncerRepository,
private val authenticationInteractor: AuthenticationInteractor,
private val deviceEntryFaceAuthInteractor: DeviceEntryFaceAuthInteractor,
private val falsingInteractor: FalsingInteractor,
private val powerInteractor: PowerInteractor,
- private val simBouncerInteractor: SimBouncerInteractor,
) {
- /** The user-facing message to show in the bouncer when lockout is not active. */
- val message: StateFlow<String?> = repository.message
+ private val _onIncorrectBouncerInput = MutableSharedFlow<Unit>()
+ val onIncorrectBouncerInput: SharedFlow<Unit> = _onIncorrectBouncerInput
/** Whether the auto confirm feature is enabled for the currently-selected user. */
val isAutoConfirmEnabled: StateFlow<Boolean> = authenticationInteractor.isAutoConfirmEnabled
@@ -119,25 +110,6 @@
)
}
- fun setMessage(message: String?) {
- repository.message.value = message
- }
-
- /**
- * Resets the user-facing message back to the default according to the current authentication
- * method.
- */
- fun resetMessage() {
- applicationScope.launch {
- setMessage(promptMessage(authenticationInteractor.getAuthenticationMethod()))
- }
- }
-
- /** Removes the user-facing message. */
- fun clearMessage() {
- setMessage(null)
- }
-
/**
* Attempts to authenticate based on the given user input.
*
@@ -176,50 +148,17 @@
.async { authenticationInteractor.authenticate(input, tryAutoConfirm) }
.await()
- if (authenticationInteractor.lockoutEndTimestamp != null) {
- clearMessage()
- } else if (
+ if (
authResult == AuthenticationResult.FAILED ||
(authResult == AuthenticationResult.SKIPPED && !tryAutoConfirm)
) {
- showWrongInputMessage()
+ _onIncorrectBouncerInput.emit(Unit)
}
return authResult
}
- /**
- * Shows the a message notifying the user that their credentials input is wrong.
- *
- * Callers should use this instead of [authenticate] when they know ahead of time that an auth
- * attempt will fail but aren't interested in the other side effects like triggering lockout.
- * For example, if the user entered a pattern that's too short, the system can show the error
- * message without having the attempt trigger lockout.
- */
- private suspend fun showWrongInputMessage() {
- setMessage(wrongInputMessage(authenticationInteractor.getAuthenticationMethod()))
- }
-
/** Notifies that the input method editor (software keyboard) has been hidden by the user. */
suspend fun onImeHiddenByUser() {
_onImeHiddenByUser.emit(Unit)
}
-
- private fun promptMessage(authMethod: AuthenticationMethodModel): String {
- return when (authMethod) {
- is Sim -> simBouncerInteractor.getDefaultMessage()
- is Pin -> applicationContext.getString(R.string.keyguard_enter_your_pin)
- is Password -> applicationContext.getString(R.string.keyguard_enter_your_password)
- is Pattern -> applicationContext.getString(R.string.keyguard_enter_your_pattern)
- else -> ""
- }
- }
-
- private fun wrongInputMessage(authMethod: AuthenticationMethodModel): String {
- return when (authMethod) {
- is Pin -> applicationContext.getString(R.string.kg_wrong_pin)
- is Password -> applicationContext.getString(R.string.kg_wrong_password)
- is Pattern -> applicationContext.getString(R.string.kg_wrong_pattern)
- else -> ""
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
index 7f6fc91..d20c607 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/domain/interactor/BouncerMessageInteractor.kt
@@ -33,15 +33,17 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.deviceentry.data.repository.DeviceEntryFaceAuthRepository
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
import com.android.systemui.flags.SystemPropertiesHelper
import com.android.systemui.keyguard.data.repository.BiometricSettingsRepository
-import com.android.systemui.keyguard.data.repository.DeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.data.repository.TrustRepository
import com.android.systemui.user.data.repository.UserRepository
-import com.android.systemui.util.kotlin.Quint
+import com.android.systemui.util.kotlin.Sextuple
+import com.android.systemui.util.kotlin.combine
import javax.inject.Inject
import kotlin.math.roundToInt
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
@@ -56,6 +58,7 @@
private const val TAG = "BouncerMessageInteractor"
/** Handles business logic for the primary bouncer message area. */
+@OptIn(ExperimentalCoroutinesApi::class)
@SysUISingleton
class BouncerMessageInteractor
@Inject
@@ -63,23 +66,24 @@
private val repository: BouncerMessageRepository,
private val userRepository: UserRepository,
private val countDownTimerUtil: CountDownTimerUtil,
- private val updateMonitor: KeyguardUpdateMonitor,
+ updateMonitor: KeyguardUpdateMonitor,
trustRepository: TrustRepository,
biometricSettingsRepository: BiometricSettingsRepository,
private val systemPropertiesHelper: SystemPropertiesHelper,
primaryBouncerInteractor: PrimaryBouncerInteractor,
@Application private val applicationScope: CoroutineScope,
private val facePropertyRepository: FacePropertyRepository,
- deviceEntryFingerprintAuthRepository: DeviceEntryFingerprintAuthRepository,
+ private val deviceEntryFingerprintAuthInteractor: DeviceEntryFingerprintAuthInteractor,
faceAuthRepository: DeviceEntryFaceAuthRepository,
private val securityModel: KeyguardSecurityModel,
) {
- private val isFingerprintAuthCurrentlyAllowed =
- deviceEntryFingerprintAuthRepository.isLockedOut
- .isFalse()
- .and(biometricSettingsRepository.isFingerprintAuthCurrentlyAllowed)
- .stateIn(applicationScope, SharingStarted.Eagerly, false)
+ private val isFingerprintAuthCurrentlyAllowedOnBouncer =
+ deviceEntryFingerprintAuthInteractor.isFingerprintCurrentlyAllowedOnBouncer.stateIn(
+ applicationScope,
+ SharingStarted.Eagerly,
+ false
+ )
private val currentSecurityMode
get() = securityModel.getSecurityMode(currentUserId)
@@ -99,13 +103,13 @@
BiometricSourceType.FACE ->
BouncerMessageStrings.incorrectFaceInput(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
else ->
BouncerMessageStrings.defaultMessage(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
}
@@ -144,11 +148,12 @@
biometricSettingsRepository.authenticationFlags,
trustRepository.isCurrentUserTrustManaged,
isAnyBiometricsEnabledAndEnrolled,
- deviceEntryFingerprintAuthRepository.isLockedOut,
+ deviceEntryFingerprintAuthInteractor.isLockedOut,
faceAuthRepository.isLockedOut,
- ::Quint
+ isFingerprintAuthCurrentlyAllowedOnBouncer,
+ ::Sextuple
)
- .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut) ->
+ .map { (flags, _, biometricsEnrolledAndEnabled, fpLockedOut, faceLockedOut, _) ->
val isTrustUsuallyManaged = trustRepository.isCurrentUserTrustUsuallyManaged.value
val trustOrBiometricsAvailable =
(isTrustUsuallyManaged || biometricsEnrolledAndEnabled)
@@ -193,14 +198,14 @@
} else {
BouncerMessageStrings.faceLockedOut(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
}
} else if (flags.isSomeAuthRequiredAfterAdaptiveAuthRequest) {
BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (
@@ -209,19 +214,19 @@
) {
BouncerMessageStrings.nonStrongAuthTimeout(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterUserRequest) {
BouncerMessageStrings.trustAgentDisabled(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (isTrustUsuallyManaged && flags.someAuthRequiredAfterTrustAgentExpired) {
BouncerMessageStrings.trustAgentDisabled(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
} else if (trustOrBiometricsAvailable && flags.isInUserLockdown) {
@@ -265,7 +270,7 @@
repository.setMessage(
BouncerMessageStrings.incorrectSecurityInput(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
)
@@ -274,14 +279,22 @@
fun setFingerprintAcquisitionMessage(value: String?) {
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
fun setFaceAcquisitionMessage(value: String?) {
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
@@ -289,7 +302,11 @@
if (!Flags.revampedBouncerMessages()) return
repository.setMessage(
- defaultMessage(currentSecurityMode, value, isFingerprintAuthCurrentlyAllowed.value)
+ defaultMessage(
+ currentSecurityMode,
+ value,
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
+ )
)
}
@@ -297,7 +314,7 @@
get() =
BouncerMessageStrings.defaultMessage(
currentSecurityMode.toAuthModel(),
- isFingerprintAuthCurrentlyAllowed.value
+ isFingerprintAuthCurrentlyAllowedOnBouncer.value
)
.toMessage()
@@ -355,11 +372,6 @@
private fun Flow<Boolean>.or(anotherFlow: Flow<Boolean>) =
this.combine(anotherFlow) { a, b -> a || b }
-private fun Flow<Boolean>.and(anotherFlow: Flow<Boolean>) =
- this.combine(anotherFlow) { a, b -> a && b }
-
-private fun Flow<Boolean>.isFalse() = this.map { !it }
-
private fun defaultMessage(
securityMode: SecurityMode,
secondaryMessage: String?,
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
index f3903de..aebc50f 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/BouncerViewModule.kt
@@ -18,6 +18,7 @@
import android.app.AlertDialog
import android.content.Context
+import com.android.systemui.bouncer.ui.viewmodel.BouncerMessageViewModelModule
import com.android.systemui.bouncer.ui.viewmodel.BouncerViewModelModule
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -30,6 +31,7 @@
includes =
[
BouncerViewModelModule::class,
+ BouncerMessageViewModelModule::class,
],
)
interface BouncerViewModule {
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
index 0d7f6dc..4fbf735 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/AuthMethodBouncerViewModel.kt
@@ -57,17 +57,11 @@
*/
@get:StringRes abstract val lockoutMessageId: Int
- /** Notifies that the UI has been shown to the user. */
- fun onShown() {
- interactor.resetMessage()
- }
-
/**
* Notifies that the UI has been hidden from the user (after any transitions have completed).
*/
open fun onHidden() {
clearInput()
- interactor.resetMessage()
}
/** Notifies that the user has placed down a pointer. */
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
new file mode 100644
index 0000000..6cb9b16
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModel.kt
@@ -0,0 +1,436 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.Context
+import android.util.PluralsMessageFormatter
+import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
+import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.ComposeBouncerFlags
+import com.android.systemui.bouncer.shared.model.BouncerMessagePair
+import com.android.systemui.bouncer.shared.model.BouncerMessageStrings
+import com.android.systemui.bouncer.shared.model.primaryMessage
+import com.android.systemui.bouncer.shared.model.secondaryMessage
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.deviceentry.domain.interactor.BiometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.deviceentry.shared.model.DeviceEntryRestrictionReason
+import com.android.systemui.deviceentry.shared.model.FaceFailureMessage
+import com.android.systemui.deviceentry.shared.model.FaceLockoutMessage
+import com.android.systemui.deviceentry.shared.model.FaceTimeoutMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintFailureMessage
+import com.android.systemui.deviceentry.shared.model.FingerprintLockoutMessage
+import com.android.systemui.res.R.string.kg_too_many_failed_attempts_countdown
+import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
+import com.android.systemui.user.ui.viewmodel.UserViewModel
+import com.android.systemui.util.kotlin.Utils.Companion.sample
+import com.android.systemui.util.time.SystemClock
+import dagger.Module
+import dagger.Provides
+import kotlin.math.ceil
+import kotlin.math.max
+import kotlin.time.Duration.Companion.seconds
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.delay
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.launch
+
+/** Holds UI state for the 2-line status message shown on the bouncer. */
+@OptIn(ExperimentalCoroutinesApi::class)
+class BouncerMessageViewModel(
+ @Application private val applicationContext: Context,
+ @Application private val applicationScope: CoroutineScope,
+ private val bouncerInteractor: BouncerInteractor,
+ private val simBouncerInteractor: SimBouncerInteractor,
+ private val authenticationInteractor: AuthenticationInteractor,
+ selectedUser: Flow<UserViewModel>,
+ private val clock: SystemClock,
+ private val biometricMessageInteractor: BiometricMessageInteractor,
+ private val faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+ flags: ComposeBouncerFlags,
+) {
+ /**
+ * A message shown when the user has attempted the wrong credential too many times and now must
+ * wait a while before attempting to authenticate again.
+ *
+ * This is updated every second (countdown) during the lockout. When lockout is not active, this
+ * is `null` and no lockout message should be shown.
+ */
+ private val lockoutMessage: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+ /** Whether there is a lockout message that is available to be shown in the status message. */
+ val isLockoutMessagePresent: Flow<Boolean> = lockoutMessage.map { it != null }
+
+ /** The user-facing message to show in the bouncer. */
+ val message: MutableStateFlow<MessageViewModel?> = MutableStateFlow(null)
+
+ /** Initializes the bouncer message to default whenever it is shown. */
+ fun onShown() {
+ showDefaultMessage()
+ }
+
+ /** Reset the message shown on the bouncer to the default message. */
+ fun showDefaultMessage() {
+ resetToDefault.tryEmit(Unit)
+ }
+
+ private val resetToDefault = MutableSharedFlow<Unit>(replay = 1)
+
+ private var lockoutCountdownJob: Job? = null
+
+ private fun defaultBouncerMessageInitializer() {
+ applicationScope.launch {
+ resetToDefault.emit(Unit)
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ resetToDefault.map {
+ MessageViewModel(simBouncerInteractor.getDefaultMessage())
+ }
+ } else if (authMethod.isSecure) {
+ combine(
+ deviceEntryInteractor.deviceEntryRestrictionReason,
+ lockoutMessage,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+ resetToDefault,
+ ) { deviceEntryRestrictedReason, lockoutMsg, isFpAllowedInBouncer, _ ->
+ lockoutMsg
+ ?: deviceEntryRestrictedReason.toMessage(
+ authMethod,
+ isFpAllowedInBouncer
+ )
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+ .collectLatest { messageViewModel -> message.value = messageViewModel }
+ }
+ }
+
+ private fun listenForSimBouncerEvents() {
+ // Listen for any events from the SIM bouncer and update the message shown on the bouncer.
+ applicationScope.launch {
+ authenticationInteractor.authenticationMethod
+ .flatMapLatest { authMethod ->
+ if (authMethod == AuthenticationMethodModel.Sim) {
+ simBouncerInteractor.bouncerMessageChanged.map { simMsg ->
+ simMsg?.let { MessageViewModel(it) }
+ }
+ } else {
+ emptyFlow()
+ }
+ }
+ .collectLatest {
+ if (it != null) {
+ message.value = it
+ } else {
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+ }
+
+ private fun listenForFaceMessages() {
+ // Listen for any events from face authentication and update the message shown on the
+ // bouncer.
+ applicationScope.launch {
+ biometricMessageInteractor.faceMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer,
+ )
+ .collectLatest { (faceMessage, authMethod, fingerprintAllowedOnBouncer) ->
+ val isFaceAuthStrong = faceAuthInteractor.isFaceAuthStrong()
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (faceMessage) {
+ is FaceTimeoutMessage ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = true
+ )
+ is FaceLockoutMessage ->
+ if (isFaceAuthStrong)
+ BouncerMessageStrings.class3AuthLockedOut(authMethod)
+ .toMessage()
+ else
+ BouncerMessageStrings.faceLockedOut(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .toMessage()
+ is FaceFailureMessage ->
+ BouncerMessageStrings.incorrectFaceInput(
+ authMethod,
+ fingerprintAllowedOnBouncer
+ )
+ .toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = faceMessage.message,
+ isUpdateAnimated = false
+ )
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun listenForFingerprintMessages() {
+ applicationScope.launch {
+ // Listen for any events from fingerprint authentication and update the message shown
+ // on the bouncer.
+ biometricMessageInteractor.fingerprintMessage
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+ )
+ .collectLatest { (fingerprintMessage, authMethod, isFingerprintAllowed) ->
+ val defaultPrimaryMessage =
+ BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowed)
+ .primaryMessage
+ .toResString()
+ message.value =
+ when (fingerprintMessage) {
+ is FingerprintLockoutMessage ->
+ BouncerMessageStrings.class3AuthLockedOut(authMethod).toMessage()
+ is FingerprintFailureMessage ->
+ BouncerMessageStrings.incorrectFingerprintInput(authMethod)
+ .toMessage()
+ else ->
+ MessageViewModel(
+ text = defaultPrimaryMessage,
+ secondaryText = fingerprintMessage.message,
+ isUpdateAnimated = false
+ )
+ }
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun listenForBouncerEvents() {
+ // Keeps the lockout message up-to-date.
+ applicationScope.launch {
+ bouncerInteractor.onLockoutStarted.collect { startLockoutCountdown() }
+ }
+
+ // Listens to relevant bouncer events
+ applicationScope.launch {
+ bouncerInteractor.onIncorrectBouncerInput
+ .sample(
+ authenticationInteractor.authenticationMethod,
+ fingerprintInteractor.isFingerprintCurrentlyAllowedOnBouncer
+ )
+ .collectLatest { (_, authMethod, isFingerprintAllowed) ->
+ message.emit(
+ BouncerMessageStrings.incorrectSecurityInput(
+ authMethod,
+ isFingerprintAllowed
+ )
+ .toMessage()
+ )
+ delay(MESSAGE_DURATION)
+ resetToDefault.emit(Unit)
+ }
+ }
+ }
+
+ private fun DeviceEntryRestrictionReason?.toMessage(
+ authMethod: AuthenticationMethodModel,
+ isFingerprintAllowedOnBouncer: Boolean,
+ ): MessageViewModel {
+ return when (this) {
+ DeviceEntryRestrictionReason.UserLockdown ->
+ BouncerMessageStrings.authRequiredAfterUserLockdown(authMethod)
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceReboot ->
+ BouncerMessageStrings.authRequiredAfterReboot(authMethod)
+ DeviceEntryRestrictionReason.PolicyLockdown ->
+ BouncerMessageStrings.authRequiredAfterAdminLockdown(authMethod)
+ DeviceEntryRestrictionReason.UnattendedUpdate ->
+ BouncerMessageStrings.authRequiredForUnattendedUpdate(authMethod)
+ DeviceEntryRestrictionReason.DeviceNotUnlockedSinceMainlineUpdate ->
+ BouncerMessageStrings.authRequiredForMainlineUpdate(authMethod)
+ DeviceEntryRestrictionReason.SecurityTimeout ->
+ BouncerMessageStrings.authRequiredAfterPrimaryAuthTimeout(authMethod)
+ DeviceEntryRestrictionReason.StrongBiometricsLockedOut ->
+ BouncerMessageStrings.class3AuthLockedOut(authMethod)
+ DeviceEntryRestrictionReason.NonStrongFaceLockedOut ->
+ BouncerMessageStrings.faceLockedOut(authMethod, isFingerprintAllowedOnBouncer)
+ DeviceEntryRestrictionReason.NonStrongBiometricsSecurityTimeout ->
+ BouncerMessageStrings.nonStrongAuthTimeout(
+ authMethod,
+ isFingerprintAllowedOnBouncer
+ )
+ DeviceEntryRestrictionReason.TrustAgentDisabled ->
+ BouncerMessageStrings.trustAgentDisabled(authMethod, isFingerprintAllowedOnBouncer)
+ DeviceEntryRestrictionReason.AdaptiveAuthRequest ->
+ BouncerMessageStrings.authRequiredAfterAdaptiveAuthRequest(
+ authMethod,
+ isFingerprintAllowedOnBouncer
+ )
+ else -> BouncerMessageStrings.defaultMessage(authMethod, isFingerprintAllowedOnBouncer)
+ }.toMessage()
+ }
+
+ private fun BouncerMessagePair.toMessage(): MessageViewModel {
+ val primaryMsg = this.primaryMessage.toResString()
+ val secondaryMsg =
+ if (this.secondaryMessage == 0) "" else this.secondaryMessage.toResString()
+ return MessageViewModel(primaryMsg, secondaryText = secondaryMsg, isUpdateAnimated = true)
+ }
+
+ /** Shows the countdown message and refreshes it every second. */
+ private fun startLockoutCountdown() {
+ lockoutCountdownJob?.cancel()
+ lockoutCountdownJob =
+ applicationScope.launch {
+ authenticationInteractor.authenticationMethod.collectLatest { authMethod ->
+ do {
+ val remainingSeconds = remainingLockoutSeconds()
+ val authLockedOutMsg =
+ BouncerMessageStrings.primaryAuthLockedOut(authMethod)
+ lockoutMessage.value =
+ if (remainingSeconds > 0) {
+ MessageViewModel(
+ text =
+ kg_too_many_failed_attempts_countdown.toPluralString(
+ mutableMapOf<String, Any>(
+ Pair("count", remainingSeconds)
+ )
+ ),
+ secondaryText = authLockedOutMsg.secondaryMessage.toResString(),
+ isUpdateAnimated = false
+ )
+ } else {
+ null
+ }
+ delay(1.seconds)
+ } while (remainingSeconds > 0)
+ lockoutCountdownJob = null
+ }
+ }
+ }
+
+ private fun remainingLockoutSeconds(): Int {
+ val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
+ val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
+ return ceil(remainingMs / 1000f).toInt()
+ }
+
+ private fun Int.toPluralString(formatterArgs: Map<String, Any>): String =
+ PluralsMessageFormatter.format(applicationContext.resources, formatterArgs, this)
+
+ private fun Int.toResString(): String = applicationContext.getString(this)
+
+ init {
+ if (flags.isComposeBouncerOrSceneContainerEnabled()) {
+ applicationScope.launch {
+ // Update the lockout countdown whenever the selected user is switched.
+ selectedUser.collect { startLockoutCountdown() }
+ }
+
+ defaultBouncerMessageInitializer()
+
+ listenForSimBouncerEvents()
+ listenForBouncerEvents()
+ listenForFaceMessages()
+ listenForFingerprintMessages()
+ }
+ }
+
+ companion object {
+ private const val MESSAGE_DURATION = 2000L
+ }
+}
+
+/** Data class that represents the status message show on the bouncer. */
+data class MessageViewModel(
+ val text: String,
+ val secondaryText: String? = null,
+ /**
+ * Whether updates to the message should be cross-animated from one message to another.
+ *
+ * If `false`, no animation should be applied, the message text should just be replaced
+ * instantly.
+ */
+ val isUpdateAnimated: Boolean = true,
+)
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@Module
+object BouncerMessageViewModelModule {
+
+ @Provides
+ @SysUISingleton
+ fun viewModel(
+ @Application applicationContext: Context,
+ @Application applicationScope: CoroutineScope,
+ bouncerInteractor: BouncerInteractor,
+ simBouncerInteractor: SimBouncerInteractor,
+ authenticationInteractor: AuthenticationInteractor,
+ clock: SystemClock,
+ biometricMessageInteractor: BiometricMessageInteractor,
+ faceAuthInteractor: DeviceEntryFaceAuthInteractor,
+ deviceEntryInteractor: DeviceEntryInteractor,
+ fingerprintInteractor: DeviceEntryFingerprintAuthInteractor,
+ flags: ComposeBouncerFlags,
+ userSwitcherViewModel: UserSwitcherViewModel,
+ ): BouncerMessageViewModel {
+ return BouncerMessageViewModel(
+ applicationContext = applicationContext,
+ applicationScope = applicationScope,
+ bouncerInteractor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ clock = clock,
+ biometricMessageInteractor = biometricMessageInteractor,
+ faceAuthInteractor = faceAuthInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ fingerprintInteractor = fingerprintInteractor,
+ flags = flags,
+ selectedUser = userSwitcherViewModel.selectedUser,
+ )
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
index 6287578..5c07cc5 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModel.kt
@@ -21,7 +21,6 @@
import android.content.Context
import android.graphics.Bitmap
import androidx.core.graphics.drawable.toBitmap
-import com.android.internal.R
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
import com.android.systemui.authentication.shared.model.AuthenticationWipeModel
@@ -40,18 +39,12 @@
import com.android.systemui.user.ui.viewmodel.UserActionViewModel
import com.android.systemui.user.ui.viewmodel.UserSwitcherViewModel
import com.android.systemui.user.ui.viewmodel.UserViewModel
-import com.android.systemui.util.time.SystemClock
import dagger.Module
import dagger.Provides
-import kotlin.math.ceil
-import kotlin.math.max
-import kotlin.time.Duration.Companion.seconds
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
import kotlinx.coroutines.SupervisorJob
import kotlinx.coroutines.cancel
-import kotlinx.coroutines.delay
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
@@ -72,13 +65,13 @@
private val simBouncerInteractor: SimBouncerInteractor,
private val authenticationInteractor: AuthenticationInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
+ private val devicePolicyManager: DevicePolicyManager,
+ bouncerMessageViewModel: BouncerMessageViewModel,
flags: ComposeBouncerFlags,
selectedUser: Flow<UserViewModel>,
users: Flow<List<UserViewModel>>,
userSwitcherMenu: Flow<List<UserActionViewModel>>,
actionButton: Flow<BouncerActionButtonModel?>,
- private val clock: SystemClock,
- private val devicePolicyManager: DevicePolicyManager,
) {
val selectedUserImage: StateFlow<Bitmap?> =
selectedUser
@@ -89,6 +82,8 @@
initialValue = null,
)
+ val message: BouncerMessageViewModel = bouncerMessageViewModel
+
val userSwitcherDropdown: StateFlow<List<UserSwitcherDropdownItemViewModel>> =
combine(
users,
@@ -163,24 +158,6 @@
)
/**
- * A message shown when the user has attempted the wrong credential too many times and now must
- * wait a while before attempting to authenticate again.
- *
- * This is updated every second (countdown) during the lockout duration. When lockout is not
- * active, this is `null` and no lockout message should be shown.
- */
- private val lockoutMessage = MutableStateFlow<String?>(null)
-
- /** The user-facing message to show in the bouncer. */
- val message: StateFlow<MessageViewModel> =
- combine(bouncerInteractor.message, lockoutMessage) { _, _ -> createMessageViewModel() }
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = createMessageViewModel(),
- )
-
- /**
* The bouncer action button (Return to Call / Emergency Call). If `null`, the button should not
* be shown.
*/
@@ -222,31 +199,16 @@
)
private val isInputEnabled: StateFlow<Boolean> =
- lockoutMessage
- .map { it == null }
+ bouncerMessageViewModel.isLockoutMessagePresent
+ .map { lockoutMessagePresent -> !lockoutMessagePresent }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
initialValue = authenticationInteractor.lockoutEndTimestamp == null,
)
- private var lockoutCountdownJob: Job? = null
-
init {
if (flags.isComposeBouncerOrSceneContainerEnabled()) {
- // Keeps the lockout dialog up-to-date.
- applicationScope.launch {
- bouncerInteractor.onLockoutStarted.collect {
- showLockoutDialog()
- startLockoutCountdown()
- }
- }
-
- applicationScope.launch {
- // Update the lockout countdown whenever the selected user is switched.
- selectedUser.collect { startLockoutCountdown() }
- }
-
// Keeps the upcoming wipe dialog up-to-date.
applicationScope.launch {
authenticationInteractor.upcomingWipe.collect { wipeModel ->
@@ -256,48 +218,6 @@
}
}
- private fun showLockoutDialog() {
- applicationScope.launch {
- val failedAttempts = authenticationInteractor.failedAuthenticationAttempts.value
- lockoutDialogMessage.value =
- authMethodViewModel.value?.lockoutMessageId?.let { messageId ->
- applicationContext.getString(
- messageId,
- failedAttempts,
- remainingLockoutSeconds()
- )
- }
- }
- }
-
- /** Shows the countdown message and refreshes it every second. */
- private fun startLockoutCountdown() {
- lockoutCountdownJob?.cancel()
- lockoutCountdownJob =
- applicationScope.launch {
- do {
- val remainingSeconds = remainingLockoutSeconds()
- lockoutMessage.value =
- if (remainingSeconds > 0) {
- applicationContext.getString(
- R.string.lockscreen_too_many_failed_attempts_countdown,
- remainingSeconds,
- )
- } else {
- null
- }
- delay(1.seconds)
- } while (remainingSeconds > 0)
- lockoutCountdownJob = null
- }
- }
-
- private fun remainingLockoutSeconds(): Int {
- val endTimestampMs = authenticationInteractor.lockoutEndTimestamp ?: 0
- val remainingMs = max(0, endTimestampMs - clock.elapsedRealtime())
- return ceil(remainingMs / 1000f).toInt()
- }
-
private fun isSideBySideSupported(authMethod: AuthMethodBouncerViewModel?): Boolean {
return isUserSwitcherVisible || authMethod !is PasswordBouncerViewModel
}
@@ -306,15 +226,6 @@
return authMethod !is PasswordBouncerViewModel
}
- private fun createMessageViewModel(): MessageViewModel {
- val isLockedOut = lockoutMessage.value != null
- return MessageViewModel(
- // A lockout message takes precedence over the non-lockout message.
- text = lockoutMessage.value ?: bouncerInteractor.message.value ?: "",
- isUpdateAnimated = !isLockedOut,
- )
- }
-
private fun getChildViewModel(
authenticationMethod: AuthenticationMethodModel,
): AuthMethodBouncerViewModel? {
@@ -336,7 +247,8 @@
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
simBouncerInteractor = simBouncerInteractor,
- authenticationMethod = authenticationMethod
+ authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Sim ->
PinBouncerViewModel(
@@ -346,6 +258,7 @@
isInputEnabled = isInputEnabled,
simBouncerInteractor = simBouncerInteractor,
authenticationMethod = authenticationMethod,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Password ->
PasswordBouncerViewModel(
@@ -354,6 +267,7 @@
interactor = bouncerInteractor,
inputMethodInteractor = inputMethodInteractor,
selectedUserInteractor = selectedUserInteractor,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
is AuthenticationMethodModel.Pattern ->
PatternBouncerViewModel(
@@ -361,11 +275,17 @@
viewModelScope = newViewModelScope,
interactor = bouncerInteractor,
isInputEnabled = isInputEnabled,
+ onIntentionalUserInput = ::onIntentionalUserInput
)
else -> null
}
}
+ private fun onIntentionalUserInput() {
+ message.showDefaultMessage()
+ bouncerInteractor.onIntentionalUserInput()
+ }
+
private fun createChildCoroutineScope(parentScope: CoroutineScope): CoroutineScope {
return CoroutineScope(
SupervisorJob(parent = parentScope.coroutineContext.job) + mainDispatcher
@@ -437,18 +357,6 @@
}
}
- data class MessageViewModel(
- val text: String,
-
- /**
- * Whether updates to the message should be cross-animated from one message to another.
- *
- * If `false`, no animation should be applied, the message text should just be replaced
- * instantly.
- */
- val isUpdateAnimated: Boolean,
- )
-
data class DialogViewModel(
val text: String,
@@ -480,8 +388,8 @@
selectedUserInteractor: SelectedUserInteractor,
flags: ComposeBouncerFlags,
userSwitcherViewModel: UserSwitcherViewModel,
- clock: SystemClock,
devicePolicyManager: DevicePolicyManager,
+ bouncerMessageViewModel: BouncerMessageViewModel,
): BouncerViewModel {
return BouncerViewModel(
applicationContext = applicationContext,
@@ -497,8 +405,8 @@
users = userSwitcherViewModel.users,
userSwitcherMenu = userSwitcherViewModel.menu,
actionButton = actionButtonInteractor.actionButton,
- clock = clock,
devicePolicyManager = devicePolicyManager,
+ bouncerMessageViewModel = bouncerMessageViewModel,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
index b42eda1..052fb6b 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PasswordBouncerViewModel.kt
@@ -40,6 +40,7 @@
viewModelScope: CoroutineScope,
isInputEnabled: StateFlow<Boolean>,
interactor: BouncerInteractor,
+ private val onIntentionalUserInput: () -> Unit,
private val inputMethodInteractor: InputMethodInteractor,
private val selectedUserInteractor: SelectedUserInteractor,
) :
@@ -96,12 +97,8 @@
/** Notifies that the user has changed the password input. */
fun onPasswordInputChanged(newPassword: String) {
- if (this.password.value.isEmpty() && newPassword.isNotEmpty()) {
- interactor.clearMessage()
- }
-
if (newPassword.isNotEmpty()) {
- interactor.onIntentionalUserInput()
+ onIntentionalUserInput()
}
_password.value = newPassword
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
index 69f8032..a401600 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PatternBouncerViewModel.kt
@@ -40,6 +40,7 @@
viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
+ private val onIntentionalUserInput: () -> Unit,
) :
AuthMethodBouncerViewModel(
viewModelScope = viewModelScope,
@@ -84,7 +85,7 @@
/** Notifies that the user has started a drag gesture across the dot grid. */
fun onDragStart() {
- interactor.clearMessage()
+ onIntentionalUserInput()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
index e910a92..62da5c0 100644
--- a/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bouncer/ui/viewmodel/PinBouncerViewModel.kt
@@ -41,6 +41,7 @@
viewModelScope: CoroutineScope,
interactor: BouncerInteractor,
isInputEnabled: StateFlow<Boolean>,
+ private val onIntentionalUserInput: () -> Unit,
private val simBouncerInteractor: SimBouncerInteractor,
authenticationMethod: AuthenticationMethodModel,
) :
@@ -131,11 +132,8 @@
/** Notifies that the user clicked on a PIN button with the given digit value. */
fun onPinButtonClicked(input: Int) {
val pinInput = mutablePinInput.value
- if (pinInput.isEmpty()) {
- interactor.clearMessage()
- }
- interactor.onIntentionalUserInput()
+ onIntentionalUserInput()
mutablePinInput.value = pinInput.append(input)
tryAuthenticate(useAutoConfirm = true)
@@ -149,7 +147,6 @@
/** Notifies that the user long-pressed the backspace button. */
fun onBackspaceButtonLongPressed() {
clearInput()
- interactor.clearMessage()
}
/** Notifies that the user clicked the "enter" button. */
@@ -173,7 +170,6 @@
/** Resets the sim screen and shows a default message. */
private fun onResetSimFlow() {
simBouncerInteractor.resetSimPukUserInput()
- interactor.resetMessage()
clearInput()
}
diff --git a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
index 3063ebd..fdd98bec 100644
--- a/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/shared/model/NotificationContainerBounds.kt
@@ -18,12 +18,8 @@
/** Models the bounds of the notification container. */
data class NotificationContainerBounds(
- /** The position of the left of the container in its window coordinate system, in pixels. */
- val left: Float = 0f,
/** The position of the top of the container in its window coordinate system, in pixels. */
val top: Float = 0f,
- /** The position of the right of the container in its window coordinate system, in pixels. */
- val right: Float = 0f,
/** The position of the bottom of the container in its window coordinate system, in pixels. */
val bottom: Float = 0f,
/** Whether any modifications to top/bottom should be smoothly animated. */
diff --git a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
index 964eb6f..578389b 100644
--- a/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
+++ b/packages/SystemUI/src/com/android/systemui/common/ui/ConfigurationState.kt
@@ -54,6 +54,18 @@
}
/**
+ * Returns a [Flow] that emits a dimension pixel size that is kept in sync with the device
+ * configuration.
+ *
+ * @see android.content.res.Resources.getDimensionPixelSize
+ */
+ fun getDimensionPixelOffset(@DimenRes id: Int): Flow<Int> {
+ return configurationController.onDensityOrFontScaleChanged.emitOnStart().map {
+ context.resources.getDimensionPixelOffset(id)
+ }
+ }
+
+ /**
* Returns a [Flow] that emits a color that is kept in sync with the device theme.
*
* @see Utils.getColorAttrDefaultColor
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index bfe751a..afa7c37 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -16,24 +16,36 @@
package com.android.systemui.communal.ui.viewmodel
+import android.appwidget.AppWidgetManager
+import android.appwidget.AppWidgetProviderInfo
+import android.content.Intent
+import android.content.pm.PackageManager
+import android.content.res.Resources
+import android.util.Log
+import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.domain.interactor.CommunalInteractor
import com.android.systemui.communal.domain.interactor.CommunalSettingsInteractor
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.log.CommunalUiEvent
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.media.controls.ui.view.MediaHost
import com.android.systemui.media.dagger.MediaModule
+import com.android.systemui.res.R
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.withContext
/** The view model for communal hub in edit mode. */
@SysUISingleton
@@ -45,6 +57,7 @@
@Named(MediaModule.COMMUNAL_HUB) mediaHost: MediaHost,
private val uiEventLogger: UiEventLogger,
@CommunalLog logBuffer: LogBuffer,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
) : BaseCommunalViewModel(communalInteractor, mediaHost) {
private val logger = Logger(logBuffer, "CommunalEditModeViewModel")
@@ -86,10 +99,77 @@
uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_REORDER_WIDGET_CANCEL)
}
- /** Returns the widget categories to show on communal hub. */
- val getCommunalWidgetCategories: Int
- get() = communalSettingsInteractor.communalWidgetCategories.value
+ /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+ suspend fun onOpenWidgetPicker(
+ resources: Resources,
+ packageManager: PackageManager,
+ activityLauncher: ActivityResultLauncher<Intent>
+ ): Boolean =
+ withContext(backgroundDispatcher) {
+ val widgets = communalInteractor.widgetContent.first()
+ val excludeList = widgets.mapTo(ArrayList()) { it.providerInfo }
+ getWidgetPickerActivityIntent(resources, packageManager, excludeList)?.let {
+ try {
+ activityLauncher.launch(it)
+ return@withContext true
+ } catch (e: Exception) {
+ Log.e(TAG, "Failed to launch widget picker activity", e)
+ }
+ }
+ false
+ }
+
+ private fun getWidgetPickerActivityIntent(
+ resources: Resources,
+ packageManager: PackageManager,
+ excludeList: ArrayList<AppWidgetProviderInfo>
+ ): Intent? {
+ val packageName =
+ getLauncherPackageName(packageManager)
+ ?: run {
+ Log.e(TAG, "Couldn't resolve launcher package name")
+ return@getWidgetPickerActivityIntent null
+ }
+
+ return Intent(Intent.ACTION_PICK).apply {
+ setPackage(packageName)
+ putExtra(
+ EXTRA_DESIRED_WIDGET_WIDTH,
+ resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_width)
+ )
+ putExtra(
+ EXTRA_DESIRED_WIDGET_HEIGHT,
+ resources.getDimensionPixelSize(R.dimen.communal_widget_picker_desired_height)
+ )
+ putExtra(
+ AppWidgetManager.EXTRA_CATEGORY_FILTER,
+ communalSettingsInteractor.communalWidgetCategories.value
+ )
+ putExtra(EXTRA_UI_SURFACE_KEY, EXTRA_UI_SURFACE_VALUE)
+ putParcelableArrayListExtra(EXTRA_ADDED_APP_WIDGETS_KEY, excludeList)
+ }
+ }
+
+ private fun getLauncherPackageName(packageManager: PackageManager): String? {
+ return packageManager
+ .resolveActivity(
+ Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) },
+ PackageManager.MATCH_DEFAULT_ONLY
+ )
+ ?.activityInfo
+ ?.packageName
+ }
/** Sets whether edit mode is currently open */
fun setEditModeOpen(isOpen: Boolean) = communalInteractor.setEditModeOpen(isOpen)
+
+ companion object {
+ private const val TAG = "CommunalEditModeViewModel"
+
+ private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
+ private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
+ private const val EXTRA_UI_SURFACE_KEY = "ui_surface"
+ private const val EXTRA_UI_SURFACE_VALUE = "widgets_hub"
+ const val EXTRA_ADDED_APP_WIDGETS_KEY = "added_app_widgets"
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index b6ad26b..ba18f01 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -16,9 +16,7 @@
package com.android.systemui.communal.widgets
-import android.appwidget.AppWidgetManager
import android.content.Intent
-import android.content.pm.PackageManager
import android.os.Bundle
import android.os.RemoteException
import android.util.Log
@@ -32,6 +30,8 @@
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.ui.Modifier
+import androidx.lifecycle.lifecycleScope
+import com.android.app.tracing.coroutines.launch
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.theme.PlatformTheme
import com.android.internal.logging.UiEventLogger
@@ -43,8 +43,8 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
-import com.android.systemui.res.R
import javax.inject.Inject
+import kotlinx.coroutines.launch
/** An Activity for editing the widgets that appear in hub mode. */
class EditWidgetsActivity
@@ -57,11 +57,8 @@
@CommunalLog logBuffer: LogBuffer,
) : ComponentActivity() {
companion object {
- private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
- private const val EXTRA_DESIRED_WIDGET_WIDTH = "desired_widget_width"
- private const val EXTRA_DESIRED_WIDGET_HEIGHT = "desired_widget_height"
-
private const val TAG = "EditWidgetsActivity"
+ private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
const val EXTRA_PRESELECTED_KEY = "preselected_key"
}
@@ -136,39 +133,13 @@
}
private fun onOpenWidgetPicker() {
- val intent = Intent(Intent.ACTION_MAIN).also { it.addCategory(Intent.CATEGORY_HOME) }
- packageManager
- .resolveActivity(intent, PackageManager.MATCH_DEFAULT_ONLY)
- ?.activityInfo
- ?.packageName
- ?.let { packageName ->
- try {
- addWidgetActivityLauncher.launch(
- Intent(Intent.ACTION_PICK).apply {
- setPackage(packageName)
- putExtra(
- EXTRA_DESIRED_WIDGET_WIDTH,
- resources.getDimensionPixelSize(
- R.dimen.communal_widget_picker_desired_width
- )
- )
- putExtra(
- EXTRA_DESIRED_WIDGET_HEIGHT,
- resources.getDimensionPixelSize(
- R.dimen.communal_widget_picker_desired_height
- )
- )
- putExtra(
- AppWidgetManager.EXTRA_CATEGORY_FILTER,
- communalViewModel.getCommunalWidgetCategories
- )
- }
- )
- } catch (e: Exception) {
- Log.e(TAG, "Failed to launch widget picker activity", e)
- }
- }
- ?: run { Log.e(TAG, "Couldn't resolve launcher package name") }
+ lifecycleScope.launch {
+ communalViewModel.onOpenWidgetPicker(
+ resources,
+ packageManager,
+ addWidgetActivityLauncher
+ )
+ }
}
private fun onEditDone() {
diff --git a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
index 8059993..c4e0ef7 100644
--- a/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/deviceentry/domain/interactor/DeviceEntryFingerprintAuthInteractor.kt
@@ -29,6 +29,8 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
@OptIn(ExperimentalCoroutinesApi::class)
@@ -72,4 +74,14 @@
*/
val isSensorUnderDisplay =
fingerprintPropertyRepository.sensorType.map(FingerprintSensorType::isUdfps)
+
+ /** Whether fingerprint authentication is currently allowed while on the bouncer. */
+ val isFingerprintCurrentlyAllowedOnBouncer =
+ isSensorUnderDisplay.flatMapLatest { sensorBelowDisplay ->
+ if (sensorBelowDisplay) {
+ flowOf(false)
+ } else {
+ isFingerprintAuthCurrentlyAllowed
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
index 7ad5aac..7a56554 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardClockRepository.kt
@@ -113,7 +113,10 @@
override val currentClock: StateFlow<ClockController?> =
currentClockId
- .map { clockRegistry.createCurrentClock() }
+ .map {
+ clockEventController.clock = clockRegistry.createCurrentClock()
+ clockEventController.clock
+ }
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 9a6088d..7f752b4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -231,5 +231,6 @@
private val DEFAULT_DURATION = 500.milliseconds
val TO_GLANCEABLE_HUB_DURATION = 1.seconds
val TO_LOCKSCREEN_DURATION = 1167.milliseconds
+ val TO_GONE_DURATION = DEFAULT_DURATION
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
index 4812e03..89148b0 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardBlueprintViewBinder.kt
@@ -213,9 +213,10 @@
cs: ConstraintSet,
viewModel: KeyguardClockViewModel
) {
- if (!DEBUG || viewModel.clock == null) return
+ val currentClock = viewModel.currentClock.value
+ if (!DEBUG || currentClock == null) return
val smallClockViewId = R.id.lockscreen_clock_view
- val largeClockViewId = viewModel.clock!!.largeClock.layout.views[0].id
+ val largeClockViewId = currentClock.largeClock.layout.views[0].id
Log.i(
TAG,
"applyCsToSmallClock: vis=${cs.getVisibility(smallClockViewId)} " +
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
index 01596ed..fa1fe5e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinder.kt
@@ -19,6 +19,7 @@
import android.transition.TransitionManager
import android.transition.TransitionSet
import android.view.View.INVISIBLE
+import android.view.ViewGroup
import androidx.annotation.VisibleForTesting
import androidx.constraintlayout.helper.widget.Layer
import androidx.constraintlayout.widget.ConstraintLayout
@@ -40,7 +41,8 @@
object KeyguardClockViewBinder {
private val TAG = KeyguardClockViewBinder::class.simpleName!!
-
+ // When changing to new clock, we need to remove old clock views from burnInLayer
+ private var lastClock: ClockController? = null
@JvmStatic
fun bind(
clockSection: ClockSection,
@@ -55,12 +57,11 @@
}
}
keyguardRootView.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.currentClock.collect { currentClock ->
- cleanupClockViews(viewModel.clock, keyguardRootView, viewModel.burnInLayer)
- viewModel.clock = currentClock
+ cleanupClockViews(currentClock, keyguardRootView, viewModel.burnInLayer)
addClockViews(currentClock, keyguardRootView)
updateBurnInLayer(keyguardRootView, viewModel)
applyConstraints(clockSection, keyguardRootView, true)
@@ -76,7 +77,7 @@
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.clockShouldBeCentered.collect { clockShouldBeCentered ->
- viewModel.clock?.let {
+ viewModel.currentClock.value?.let {
// Weather clock also has hasCustomPositionUpdatedAnimation as true
// TODO(b/323020908): remove ID check
if (
@@ -93,7 +94,7 @@
launch {
if (!migrateClocksToBlueprint()) return@launch
viewModel.isAodIconsVisible.collect { isAodIconsVisible ->
- viewModel.clock?.let {
+ viewModel.currentClock.value?.let {
// Weather clock also has hasCustomPositionUpdatedAnimation as true
if (
viewModel.useLargeClock && it.config.id == "DIGITAL_CLOCK_WEATHER"
@@ -132,11 +133,14 @@
}
private fun cleanupClockViews(
- clockController: ClockController?,
+ currentClock: ClockController?,
rootView: ConstraintLayout,
burnInLayer: Layer?
) {
- clockController?.let { clock ->
+ if (lastClock == currentClock) {
+ return
+ }
+ lastClock?.let { clock ->
clock.smallClock.layout.views.forEach {
burnInLayer?.removeView(it)
rootView.removeView(it)
@@ -150,6 +154,7 @@
}
clock.largeClock.layout.views.forEach { rootView.removeView(it) }
}
+ lastClock = currentClock
}
@VisibleForTesting
@@ -157,11 +162,19 @@
clockController: ClockController?,
rootView: ConstraintLayout,
) {
+ // We'll collect the same clock when exiting wallpaper picker without changing clock
+ // so we need to remove clock views from parent before addView again
clockController?.let { clock ->
clock.smallClock.layout.views.forEach {
+ if (it.parent != null) {
+ (it.parent as ViewGroup).removeView(it)
+ }
rootView.addView(it).apply { it.visibility = INVISIBLE }
}
clock.largeClock.layout.views.forEach {
+ if (it.parent != null) {
+ (it.parent as ViewGroup).removeView(it)
+ }
rootView.addView(it).apply { it.visibility = INVISIBLE }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
index b77f0c5..4d0a25f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardSmartspaceViewBinder.kt
@@ -41,7 +41,7 @@
blueprintInteractor: KeyguardBlueprintInteractor,
) {
keyguardRootView.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.STARTED) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
if (!migrateClocksToBlueprint()) return@launch
clockViewModel.hasCustomWeatherDataDisplay.collect { hasCustomWeatherDataDisplay
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 7c76e6a..f60da0e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -90,6 +90,7 @@
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
import com.android.systemui.temporarydisplay.chipbar.ChipbarCoordinator
+import com.android.systemui.util.kotlin.DisposableHandles
import com.android.systemui.util.settings.SecureSettings
import dagger.assisted.Assisted
import dagger.assisted.AssistedInject
@@ -173,7 +174,7 @@
private lateinit var smallClockHostView: FrameLayout
private var smartSpaceView: View? = null
- private val disposables = mutableSetOf<DisposableHandle>()
+ private val disposables = DisposableHandles()
private var isDestroyed = false
private val shortcutsBindings = mutableSetOf<KeyguardQuickAffordanceViewBinder.Binding>()
@@ -183,7 +184,7 @@
init {
coroutineScope = CoroutineScope(applicationScope.coroutineContext + Job())
- disposables.add(DisposableHandle { coroutineScope.cancel() })
+ disposables += DisposableHandle { coroutineScope.cancel() }
if (keyguardBottomAreaRefactor()) {
quickAffordancesCombinedViewModel.enablePreviewMode(
@@ -214,7 +215,7 @@
if (hostToken == null) null else InputTransferToken(hostToken),
"KeyguardPreviewRenderer"
)
- disposables.add(DisposableHandle { host.release() })
+ disposables += DisposableHandle { host.release() }
}
}
@@ -284,7 +285,7 @@
fun destroy() {
isDestroyed = true
lockscreenSmartspaceController.disconnect()
- disposables.forEach { it.dispose() }
+ disposables.dispose()
if (keyguardBottomAreaRefactor()) {
shortcutsBindings.forEach { it.destroy() }
}
@@ -372,7 +373,7 @@
private fun setupKeyguardRootView(previewContext: Context, rootView: FrameLayout) {
val keyguardRootView = KeyguardRootView(previewContext, null)
if (!keyguardBottomAreaRefactor()) {
- disposables.add(
+ disposables +=
KeyguardRootViewBinder.bind(
keyguardRootView,
keyguardRootViewModel,
@@ -387,7 +388,6 @@
null, // device entry haptics not required for preview mode
null, // falsing manager not required for preview mode
)
- )
}
rootView.addView(
keyguardRootView,
@@ -555,14 +555,12 @@
}
}
clockRegistry.registerClockChangeListener(clockChangeListener)
- disposables.add(
- DisposableHandle {
- clockRegistry.unregisterClockChangeListener(clockChangeListener)
- }
- )
+ disposables += DisposableHandle {
+ clockRegistry.unregisterClockChangeListener(clockChangeListener)
+ }
clockController.registerListeners(parentView)
- disposables.add(DisposableHandle { clockController.unregisterListeners() })
+ disposables += DisposableHandle { clockController.unregisterListeners() }
}
val receiver =
@@ -581,7 +579,7 @@
addAction(Intent.ACTION_TIME_CHANGED)
},
)
- disposables.add(DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) })
+ disposables += DisposableHandle { broadcastDispatcher.unregisterReceiver(receiver) }
if (!migrateClocksToBlueprint()) {
val layoutChangeListener =
@@ -602,9 +600,9 @@
}
}
parentView.addOnLayoutChangeListener(layoutChangeListener)
- disposables.add(
- DisposableHandle { parentView.removeOnLayoutChangeListener(layoutChangeListener) }
- )
+ disposables += DisposableHandle {
+ parentView.removeOnLayoutChangeListener(layoutChangeListener)
+ }
}
onClockChanged()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
index 9c9df80..a215efa 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/BaseBlueprintTransition.kt
@@ -41,7 +41,7 @@
private fun excludeClockAndSmartspaceViews(transition: Transition) {
transition.excludeTarget(SmartspaceView::class.java, true)
- clockViewModel.clock?.let { clock ->
+ clockViewModel.currentClock.value?.let { clock ->
clock.largeClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
clock.smallClock.layout.views.forEach { view -> transition.excludeTarget(view, true) }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
index 3adeb2a..c69d868 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/transitions/IntraBlueprintTransition.kt
@@ -57,7 +57,9 @@
when (config.type) {
Type.NoTransition -> {}
Type.DefaultClockStepping ->
- addTransition(clockViewModel.clock?.let { DefaultClockSteppingTransition(it) })
+ addTransition(
+ clockViewModel.currentClock.value?.let { DefaultClockSteppingTransition(it) }
+ )
else -> addTransition(ClockSizeTransition(config, clockViewModel, smartspaceViewModel))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
index a183b72..7847c1c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/ClockSection.kt
@@ -86,7 +86,7 @@
if (!Flags.migrateClocksToBlueprint()) {
return
}
- clockInteractor.clock?.let { clock ->
+ keyguardClockViewModel.currentClock.value?.let { clock ->
constraintSet.applyDeltaFrom(buildConstraints(clock, constraintSet))
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
index 6a3b920..c1b0cc6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultNotificationStackScrollLayoutSection.kt
@@ -26,20 +26,16 @@
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.Flags.centralizedStatusBarHeightFix
import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.LargeScreenHeaderHelper
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import dagger.Lazy
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
/** Single column format for notifications (default for phones) */
class DefaultNotificationStackScrollLayoutSection
@@ -50,12 +46,9 @@
notificationPanelView: NotificationPanelView,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+ notificationStackViewBinder: NotificationStackViewBinder,
private val largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
- @Main mainDispatcher: CoroutineDispatcher,
) :
NotificationStackScrollLayoutSection(
context,
@@ -63,11 +56,8 @@
notificationPanelView,
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- notificationStackSizeCalculator,
- mainDispatcher,
+ sharedNotificationContainerBinder,
+ notificationStackViewBinder,
) {
override fun applyConstraints(constraintSet: ConstraintSet) {
if (!migrateClocksToBlueprint()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
index 5dea7cb..8323502 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/NotificationStackScrollLayoutSection.kt
@@ -31,16 +31,11 @@
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackAppearanceViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
+import com.android.systemui.util.kotlin.DisposableHandles
abstract class NotificationStackScrollLayoutSection
constructor(
@@ -49,14 +44,11 @@
private val notificationPanelView: NotificationPanelView,
private val sharedNotificationContainer: SharedNotificationContainer,
private val sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- private val notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- private val ambientState: AmbientState,
- private val controller: NotificationStackScrollLayoutController,
- private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
- private val mainDispatcher: CoroutineDispatcher,
+ private val sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+ private val notificationStackViewBinder: NotificationStackViewBinder,
) : KeyguardSection() {
private val placeHolderId = R.id.nssl_placeholder
- private val disposableHandles: MutableList<DisposableHandle> = mutableListOf()
+ private val disposableHandles = DisposableHandles()
/**
* Align the notification placeholder bottom to the top of either the lock icon or the ambient
@@ -102,39 +94,20 @@
return
}
- disposeHandles()
- disposableHandles.add(
- SharedNotificationContainerBinder.bind(
+ disposableHandles.dispose()
+ disposableHandles +=
+ sharedNotificationContainerBinder.bind(
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- sceneContainerFlags,
- controller,
- notificationStackSizeCalculator,
- mainImmediateDispatcher = mainDispatcher,
)
- )
if (sceneContainerFlags.isEnabled()) {
- disposableHandles.add(
- NotificationStackAppearanceViewBinder.bind(
- context,
- sharedNotificationContainer,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- mainImmediateDispatcher = mainDispatcher,
- )
- )
+ disposableHandles += notificationStackViewBinder.bindWhileAttached()
}
}
override fun removeViews(constraintLayout: ConstraintLayout) {
- disposeHandles()
+ disposableHandles.dispose()
constraintLayout.removeView(placeHolderId)
}
-
- private fun disposeHandles() {
- disposableHandles.forEach { it.dispose() }
- disposableHandles.clear()
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
index 2545302..4a705a7 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/SplitShadeNotificationStackScrollLayoutSection.kt
@@ -24,19 +24,14 @@
import androidx.constraintlayout.widget.ConstraintSet.START
import androidx.constraintlayout.widget.ConstraintSet.TOP
import com.android.systemui.Flags.migrateClocksToBlueprint
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardSmartspaceViewModel
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.NotificationPanelView
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.NotificationStackViewBinder
+import com.android.systemui.statusbar.notification.stack.ui.viewbinder.SharedNotificationContainerBinder
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
import javax.inject.Inject
-import kotlinx.coroutines.CoroutineDispatcher
/** Large-screen format for notifications, shown as two columns on the device */
class SplitShadeNotificationStackScrollLayoutSection
@@ -47,12 +42,8 @@
notificationPanelView: NotificationPanelView,
sharedNotificationContainer: SharedNotificationContainer,
sharedNotificationContainerViewModel: SharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- private val smartspaceViewModel: KeyguardSmartspaceViewModel,
- @Main mainDispatcher: CoroutineDispatcher,
+ sharedNotificationContainerBinder: SharedNotificationContainerBinder,
+ notificationStackViewBinder: NotificationStackViewBinder,
) :
NotificationStackScrollLayoutSection(
context,
@@ -60,11 +51,8 @@
notificationPanelView,
sharedNotificationContainer,
sharedNotificationContainerViewModel,
- notificationStackAppearanceViewModel,
- ambientState,
- controller,
- notificationStackSizeCalculator,
- mainDispatcher,
+ sharedNotificationContainerBinder,
+ notificationStackViewBinder,
) {
override fun applyConstraints(constraintSet: ConstraintSet) {
if (!migrateClocksToBlueprint()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
index 6184c82..4d3a78d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/transitions/ClockSizeTransition.kt
@@ -216,7 +216,9 @@
captureSmartspace = !viewModel.useLargeClock && smartspaceViewModel.isSmartspaceEnabled
if (viewModel.useLargeClock) {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ viewModel.currentClock.value?.let {
+ it.largeClock.layout.views.forEach { addTarget(it) }
+ }
} else {
addTarget(R.id.lockscreen_clock_view)
}
@@ -276,7 +278,9 @@
if (viewModel.useLargeClock) {
addTarget(R.id.lockscreen_clock_view)
} else {
- viewModel.clock?.let { it.largeClock.layout.views.forEach { addTarget(it) } }
+ viewModel.currentClock.value?.let {
+ it.largeClock.layout.views.forEach { addTarget(it) }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
index f961e08..9c1f077 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AodBurnInViewModel.kt
@@ -169,7 +169,7 @@
provider: Provider<ClockController>?,
): Provider<ClockController>? {
return if (Flags.migrateClocksToBlueprint()) {
- Provider { keyguardClockViewModel.clock }
+ Provider { keyguardClockViewModel.currentClock.value }
} else {
provider
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
new file mode 100644
index 0000000..ec7b931
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModel.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.viewmodel
+
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.keyguard.domain.interactor.FromDreamingTransitionInteractor
+import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.ui.KeyguardTransitionAnimationFlow
+import kotlinx.coroutines.flow.Flow
+import javax.inject.Inject
+
+@SysUISingleton
+class DreamingToGoneTransitionViewModel
+@Inject
+constructor(
+ animationFlow: KeyguardTransitionAnimationFlow,
+) {
+
+ private val transitionAnimation =
+ animationFlow.setup(
+ duration = FromDreamingTransitionInteractor.TO_GONE_DURATION,
+ from = KeyguardState.DREAMING,
+ to = KeyguardState.GONE,
+ )
+
+ /** Lockscreen views alpha */
+ val lockscreenAlpha: Flow<Float> = transitionAnimation.immediatelyTransitionTo(0f)
+
+}
\ No newline at end of file
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
index b6622e5..1c1c33a 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardClockViewModel.kt
@@ -26,7 +26,6 @@
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.shared.ComposeLockscreen
import com.android.systemui.keyguard.shared.model.SettingsClockSize
-import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeMode
@@ -54,8 +53,6 @@
val useLargeClock: Boolean
get() = clockSize.value == LARGE
- var clock: ClockController? by keyguardClockInteractor::clock
-
val clockSize =
combine(keyguardClockInteractor.selectedClockSize, keyguardClockInteractor.clockSize) {
selectedSize,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 55a4025..301f00e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -85,10 +85,12 @@
private val dozingToLockscreenTransitionViewModel: DozingToLockscreenTransitionViewModel,
private val dozingToOccludedTransitionViewModel: DozingToOccludedTransitionViewModel,
private val dreamingToLockscreenTransitionViewModel: DreamingToLockscreenTransitionViewModel,
+ private val dreamingToGoneTransitionViewModel: DreamingToGoneTransitionViewModel,
private val glanceableHubToLockscreenTransitionViewModel:
GlanceableHubToLockscreenTransitionViewModel,
private val goneToAodTransitionViewModel: GoneToAodTransitionViewModel,
private val goneToDozingTransitionViewModel: GoneToDozingTransitionViewModel,
+ private val goneToDreamingTransitionViewModel: GoneToDreamingTransitionViewModel,
private val lockscreenToAodTransitionViewModel: LockscreenToAodTransitionViewModel,
private val lockscreenToDozingTransitionViewModel: LockscreenToDozingTransitionViewModel,
private val lockscreenToDreamingTransitionViewModel: LockscreenToDreamingTransitionViewModel,
@@ -136,14 +138,20 @@
}
.distinctUntilChanged()
+ private val lockscreenToGoneTransitionRunning: Flow<Boolean> =
+ keyguardTransitionInteractor
+ .isInTransitionWhere { from, to -> from == LOCKSCREEN && to == GONE }
+ .onStart { emit(false) }
+
private val alphaOnShadeExpansion: Flow<Float> =
combineTransform(
+ lockscreenToGoneTransitionRunning,
isOnLockscreen,
shadeInteractor.qsExpansion,
shadeInteractor.shadeExpansion,
- ) { isOnLockscreen, qsExpansion, shadeExpansion ->
+ ) { lockscreenToGoneTransitionRunning, isOnLockscreen, qsExpansion, shadeExpansion ->
// Fade out quickly as the shade expands
- if (isOnLockscreen) {
+ if (isOnLockscreen && !lockscreenToGoneTransitionRunning) {
val alpha =
1f -
MathUtils.constrainedMap(
@@ -204,10 +212,12 @@
dozingToGoneTransitionViewModel.lockscreenAlpha(viewState),
dozingToLockscreenTransitionViewModel.lockscreenAlpha,
dozingToOccludedTransitionViewModel.lockscreenAlpha(viewState),
+ dreamingToGoneTransitionViewModel.lockscreenAlpha,
dreamingToLockscreenTransitionViewModel.lockscreenAlpha,
glanceableHubToLockscreenTransitionViewModel.keyguardAlpha,
goneToAodTransitionViewModel.enterFromTopAnimationAlpha,
goneToDozingTransitionViewModel.lockscreenAlpha,
+ goneToDreamingTransitionViewModel.lockscreenAlpha,
lockscreenToAodTransitionViewModel.lockscreenAlpha(viewState),
lockscreenToDozingTransitionViewModel.lockscreenAlpha,
lockscreenToDreamingTransitionViewModel.lockscreenAlpha,
diff --git a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
index 1d820a1..0a880293 100644
--- a/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
+++ b/packages/SystemUI/src/com/android/systemui/people/widget/PeopleSpaceWidgetManager.java
@@ -21,6 +21,9 @@
import static android.app.NotificationManager.INTERRUPTION_FILTER_ALL;
import static android.app.NotificationManager.INTERRUPTION_FILTER_NONE;
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_HOME_SCREEN;
+import static android.appwidget.AppWidgetProviderInfo.WIDGET_CATEGORY_KEYGUARD;
+import static android.appwidget.flags.Flags.generatedPreviews;
import static android.content.Intent.ACTION_BOOT_COMPLETED;
import static android.content.Intent.ACTION_PACKAGE_ADDED;
import static android.content.Intent.ACTION_PACKAGE_REMOVED;
@@ -56,6 +59,7 @@
import android.app.people.PeopleManager;
import android.app.people.PeopleSpaceTile;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
@@ -80,12 +84,15 @@
import android.service.notification.ZenModeConfig;
import android.text.TextUtils;
import android.util.Log;
+import android.util.SparseBooleanArray;
import android.widget.RemoteViews;
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.logging.UiEventLogger;
import com.android.internal.logging.UiEventLoggerImpl;
+import com.android.keyguard.KeyguardUpdateMonitor;
+import com.android.keyguard.KeyguardUpdateMonitorCallback;
import com.android.systemui.Dumpable;
import com.android.systemui.broadcast.BroadcastDispatcher;
import com.android.systemui.dagger.SysUISingleton;
@@ -96,6 +103,8 @@
import com.android.systemui.people.PeopleSpaceUtils;
import com.android.systemui.people.PeopleTileViewHelper;
import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.UserTracker;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.notification.collection.NotificationEntry;
@@ -160,13 +169,27 @@
@GuardedBy("mLock")
public static Map<Integer, PeopleSpaceTile> mTiles = new HashMap<>();
+ @NonNull private final UserTracker mUserTracker;
+ @NonNull private final SparseBooleanArray mUpdatedPreviews = new SparseBooleanArray();
+ @NonNull private final KeyguardUpdateMonitorCallback mKeyguardUpdateMonitorCallback =
+ new KeyguardUpdateMonitorCallback() {
+ @Override
+ public void onUserUnlocked() {
+ if (DEBUG) {
+ Log.d(TAG, "onUserUnlocked " + mUserTracker.getUserId());
+ }
+ updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ }
+ };
+
@Inject
public PeopleSpaceWidgetManager(Context context, LauncherApps launcherApps,
CommonNotifCollection notifCollection,
PackageManager packageManager, Optional<Bubbles> bubblesOptional,
UserManager userManager, NotificationManager notificationManager,
BroadcastDispatcher broadcastDispatcher, @Background Executor bgExecutor,
- DumpManager dumpManager) {
+ DumpManager dumpManager, @NonNull UserTracker userTracker,
+ @NonNull KeyguardUpdateMonitor keyguardUpdateMonitor) {
if (DEBUG) Log.d(TAG, "constructor");
mContext = context;
mAppWidgetManager = AppWidgetManager.getInstance(context);
@@ -187,6 +210,8 @@
mBroadcastDispatcher = broadcastDispatcher;
mBgExecutor = bgExecutor;
dumpManager.registerNormalDumpable(TAG, this);
+ mUserTracker = userTracker;
+ keyguardUpdateMonitor.registerCallback(mKeyguardUpdateMonitorCallback);
}
/** Initializes {@PeopleSpaceWidgetManager}. */
@@ -246,7 +271,7 @@
CommonNotifCollection notifCollection, PackageManager packageManager,
Optional<Bubbles> bubblesOptional, UserManager userManager, BackupManager backupManager,
INotificationManager iNotificationManager, NotificationManager notificationManager,
- @Background Executor executor) {
+ @Background Executor executor, UserTracker userTracker) {
mContext = context;
mAppWidgetManager = appWidgetManager;
mIPeopleManager = iPeopleManager;
@@ -262,6 +287,7 @@
mManager = this;
mSharedPrefs = PreferenceManager.getDefaultSharedPreferences(context);
mBgExecutor = executor;
+ mUserTracker = userTracker;
}
/**
@@ -1407,4 +1433,32 @@
Trace.traceEnd(Trace.TRACE_TAG_APP);
}
+
+ @VisibleForTesting
+ void updateGeneratedPreviewForUser(UserHandle user) {
+ if (!generatedPreviews() || mUpdatedPreviews.get(user.getIdentifier())
+ || !mUserManager.isUserUnlocked(user)) {
+ return;
+ }
+
+ // The widget provider may be disabled on SystemUI implementers, e.g. TvSystemUI.
+ ComponentName provider = new ComponentName(mContext, PeopleSpaceWidgetProvider.class);
+ List<AppWidgetProviderInfo> infos = mAppWidgetManager.getInstalledProvidersForPackage(
+ mContext.getPackageName(), user);
+ if (infos.stream().noneMatch(info -> info.provider.equals(provider))) {
+ return;
+ }
+
+ if (DEBUG) {
+ Log.d(TAG, "Updating People Space widget preview for user " + user.getIdentifier());
+ }
+ boolean success = mAppWidgetManager.setWidgetPreview(
+ provider, WIDGET_CATEGORY_HOME_SCREEN | WIDGET_CATEGORY_KEYGUARD,
+ new RemoteViews(mContext.getPackageName(),
+ R.layout.people_space_placeholder_layout));
+ if (DEBUG && !success) {
+ Log.d(TAG, "Failed to update generated preview for user " + user.getIdentifier());
+ }
+ mUpdatedPreviews.put(user.getIdentifier(), success);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
index 5d2aeef..b34b370 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/PagedTileLayout.java
@@ -432,6 +432,9 @@
for (int i = 0; i < NP; i++) {
mPages.get(i).removeAllViews();
}
+ if (mPageIndicator != null) {
+ mPageIndicator.setNumPages(numPages);
+ }
if (NP == numPages) {
return;
}
@@ -443,7 +446,6 @@
mLogger.d("Removing page");
mPages.remove(mPages.size() - 1);
}
- mPageIndicator.setNumPages(mPages.size());
setAdapter(mAdapter);
mAdapter.notifyDataSetChanged();
if (mPageToRestore != NO_PAGE) {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
index 7a7ee59..00757b7 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanel.java
@@ -127,8 +127,9 @@
}
- void initialize(QSLogger qsLogger) {
+ void initialize(QSLogger qsLogger, boolean usingMediaPlayer) {
mQsLogger = qsLogger;
+ mUsingMediaPlayer = usingMediaPlayer;
mTileLayout = getOrCreateTileLayout();
if (mUsingMediaPlayer) {
@@ -163,22 +164,25 @@
}
protected void setHorizontalContentContainerClipping() {
- mHorizontalContentContainer.setClipChildren(true);
- mHorizontalContentContainer.setClipToPadding(false);
- // Don't clip on the top, that way, secondary pages tiles can animate up
- // Clipping coordinates should be relative to this view, not absolute (parent coordinates)
- mHorizontalContentContainer.addOnLayoutChangeListener(
- (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
- if ((right - left) != (oldRight - oldLeft)
- || ((bottom - top) != (oldBottom - oldTop))) {
- mClippingRect.right = right - left;
- mClippingRect.bottom = bottom - top;
- mHorizontalContentContainer.setClipBounds(mClippingRect);
- }
- });
- mClippingRect.left = 0;
- mClippingRect.top = -1000;
- mHorizontalContentContainer.setClipBounds(mClippingRect);
+ if (mHorizontalContentContainer != null) {
+ mHorizontalContentContainer.setClipChildren(true);
+ mHorizontalContentContainer.setClipToPadding(false);
+ // Don't clip on the top, that way, secondary pages tiles can animate up
+ // Clipping coordinates should be relative to this view, not absolute
+ // (parent coordinates)
+ mHorizontalContentContainer.addOnLayoutChangeListener(
+ (v, left, top, right, bottom, oldLeft, oldTop, oldRight, oldBottom) -> {
+ if ((right - left) != (oldRight - oldLeft)
+ || ((bottom - top) != (oldBottom - oldTop))) {
+ mClippingRect.right = right - left;
+ mClippingRect.bottom = bottom - top;
+ mHorizontalContentContainer.setClipBounds(mClippingRect);
+ }
+ });
+ mClippingRect.left = 0;
+ mClippingRect.top = -1000;
+ mHorizontalContentContainer.setClipBounds(mClippingRect);
+ }
}
/**
@@ -412,7 +416,7 @@
}
private void updateHorizontalLinearLayoutMargins() {
- if (mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
+ if (mUsingMediaPlayer && mHorizontalLinearLayout != null && !displayMediaMarginsOnMedia()) {
LayoutParams lp = (LayoutParams) mHorizontalLinearLayout.getLayoutParams();
lp.bottomMargin = Math.max(mMediaTotalBottomMargin - getPaddingBottom(), 0);
mHorizontalLinearLayout.setLayoutParams(lp);
@@ -461,6 +465,11 @@
/** Call when orientation has changed and MediaHost needs to be adjusted. */
private void reAttachMediaHost(ViewGroup hostView, boolean horizontal) {
if (!mUsingMediaPlayer) {
+ // If the host view was attached, detach it.
+ ViewGroup parent = (ViewGroup) hostView.getParent();
+ if (parent != null) {
+ parent.removeView(hostView);
+ }
return;
}
mMediaHostView = hostView;
@@ -492,8 +501,10 @@
public void setExpanded(boolean expanded) {
if (mExpanded == expanded) return;
mExpanded = expanded;
- if (!mExpanded && mTileLayout instanceof PagedTileLayout) {
- ((PagedTileLayout) mTileLayout).setCurrentItem(0, false);
+ if (!mExpanded && mTileLayout instanceof PagedTileLayout tilesLayout) {
+ // Use post, so it will wait until the view is attached. If the view is not attached,
+ // it will not populate corresponding views (and will not do it later when attached).
+ tilesLayout.post(() -> tilesLayout.setCurrentItem(0, false));
}
}
@@ -616,7 +627,10 @@
if (horizontal != mUsingHorizontalLayout || force) {
Log.d(getDumpableTag(), "setUsingHorizontalLayout: " + horizontal + ", " + force);
mUsingHorizontalLayout = horizontal;
- ViewGroup newParent = horizontal ? mHorizontalContentContainer : this;
+ // The tile layout should be reparented if horizontal and we are using media. If not
+ // using media, the parent should always be this.
+ ViewGroup newParent =
+ horizontal && mUsingMediaPlayer ? mHorizontalContentContainer : this;
switchAllContentToParent(newParent, mTileLayout);
reAttachMediaHost(mediaHostView, horizontal);
if (needsDynamicRowsAndColumns()) {
@@ -624,7 +638,9 @@
mTileLayout.setMaxColumns(horizontal ? 2 : 4);
}
updateMargins(mediaHostView);
- mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+ if (mHorizontalLinearLayout != null) {
+ mHorizontalLinearLayout.setVisibility(horizontal ? View.VISIBLE : View.GONE);
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
index 5e12b9d..d8e8187 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/QSPanelControllerBase.java
@@ -167,7 +167,7 @@
@Override
protected void onInit() {
- mView.initialize(mQSLogger);
+ mView.initialize(mQSLogger, mUsingMediaPlayer);
mQSLogger.logAllTilesChangeListening(mView.isListening(), mView.getDumpableTag(), "");
mHost.addCallback(mQSHostCallback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
index c1b2037..6710504 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/adapter/QSSceneAdapter.kt
@@ -23,16 +23,21 @@
import androidx.annotation.VisibleForTesting
import androidx.asynclayoutinflater.view.AsyncLayoutInflater
import com.android.settingslib.applications.InterestingConfigChanges
+import com.android.systemui.Dumpable
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.dump.DumpManager
import com.android.systemui.plugins.qs.QSContainerController
import com.android.systemui.qs.QSContainerImpl
import com.android.systemui.qs.QSImpl
import com.android.systemui.qs.dagger.QSSceneComponent
import com.android.systemui.res.R
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.util.kotlin.sample
+import java.io.PrintWriter
import javax.inject.Inject
import javax.inject.Provider
import kotlin.coroutines.resume
@@ -107,11 +112,17 @@
}
/** State for appearing QQS from Lockscreen or Gone */
- data class Unsquishing(override val squishiness: Float) : State {
+ data class UnsquishingQQS(override val squishiness: Float) : State {
override val isVisible = true
override val expansion = 0f
}
+ /** State for appearing QS from Lockscreen or Gone, used in Split shade */
+ data class UnsquishingQS(override val squishiness: Float) : State {
+ override val isVisible = true
+ override val expansion = 1f
+ }
+
companion object {
// These are special cases of the expansion.
val QQS = Expanding(0f)
@@ -129,22 +140,28 @@
constructor(
private val qsSceneComponentFactory: QSSceneComponent.Factory,
private val qsImplProvider: Provider<QSImpl>,
+ shadeInteractor: ShadeInteractor,
+ dumpManager: DumpManager,
@Main private val mainDispatcher: CoroutineDispatcher,
@Application applicationScope: CoroutineScope,
private val configurationInteractor: ConfigurationInteractor,
private val asyncLayoutInflaterFactory: (Context) -> AsyncLayoutInflater,
-) : QSContainerController, QSSceneAdapter {
+) : QSContainerController, QSSceneAdapter, Dumpable {
@Inject
constructor(
qsSceneComponentFactory: QSSceneComponent.Factory,
qsImplProvider: Provider<QSImpl>,
+ shadeInteractor: ShadeInteractor,
+ dumpManager: DumpManager,
@Main dispatcher: CoroutineDispatcher,
@Application scope: CoroutineScope,
configurationInteractor: ConfigurationInteractor,
) : this(
qsSceneComponentFactory,
qsImplProvider,
+ shadeInteractor,
+ dumpManager,
dispatcher,
scope,
configurationInteractor,
@@ -182,6 +199,7 @@
)
init {
+ dumpManager.registerDumpable(this)
applicationScope.launch {
launch {
state.sample(_isCustomizing, ::Pair).collect { (state, customizing) ->
@@ -210,6 +228,11 @@
it.second.applyBottomNavBarToCustomizerPadding(it.first)
}
}
+ launch {
+ shadeInteractor.shadeMode.collect {
+ qsImpl.value?.setInSplitShade(it == ShadeMode.Split)
+ }
+ }
}
}
@@ -256,9 +279,17 @@
private fun QSImpl.applyState(state: QSSceneAdapter.State) {
setQsVisible(state.isVisible)
- setExpanded(state.isVisible)
+ setExpanded(state.isVisible && state.expansion > 0f)
setListening(state.isVisible)
setQsExpansion(state.expansion, 1f, 0f, state.squishiness)
- setTransitionToFullShadeProgress(false, 1f, state.squishiness)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.apply {
+ println("Last state: ${state.value}")
+ println("Customizing: ${isCustomizing.value}")
+ println("QQS height: $qqsHeight")
+ println("QS height: $qsHeight")
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index 34f66b8..c695d4c 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -48,6 +48,8 @@
qsSceneAdapter.isCustomizing.map { customizing ->
if (customizing) {
mapOf<UserAction, UserActionResult>(Back to UserActionResult(Scenes.QuickSettings))
+ // TODO(b/330200163) Add an Up from Bottom to be able to collapse the shade
+ // while customizing
} else {
mapOf(
Back to UserActionResult(Scenes.Shade),
diff --git a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
index d0ff338..7c1a2c0 100644
--- a/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
+++ b/packages/SystemUI/src/com/android/systemui/recents/OverviewProxyService.java
@@ -86,7 +86,6 @@
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.dagger.qualifiers.Main;
import com.android.systemui.dump.DumpManager;
-import com.android.systemui.flags.FeatureFlags;
import com.android.systemui.keyguard.KeyguardUnlockAnimationController;
import com.android.systemui.keyguard.KeyguardWmStateRefactor;
import com.android.systemui.keyguard.WakefulnessLifecycle;
@@ -146,7 +145,6 @@
private static final long MAX_BACKOFF_MILLIS = 10 * 60 * 1000;
private final Context mContext;
- private final FeatureFlags mFeatureFlags;
private final SceneContainerFlags mSceneContainerFlags;
private final Executor mMainExecutor;
private final ShellInterface mShellInterface;
@@ -209,8 +207,10 @@
@Override
public void onStatusBarTouchEvent(MotionEvent event) {
verifyCallerAndClearCallingIdentity("onStatusBarTouchEvent", () -> {
- // TODO move this logic to message queue
- if (event.getActionMasked() == ACTION_DOWN) {
+ if (mSceneContainerFlags.isEnabled()) {
+ //TODO(b/329863123) implement latency tracking for shade scene
+ Log.i(TAG_OPS, "Scene container enabled. Latency tracking not started.");
+ } else if (event.getActionMasked() == ACTION_DOWN) {
mShadeViewControllerLazy.get().startExpandLatencyTracking();
}
mHandler.post(() -> {
@@ -600,7 +600,6 @@
KeyguardUnlockAnimationController sysuiUnlockAnimationController,
InWindowLauncherUnlockAnimationManager inWindowLauncherUnlockAnimationManager,
AssistUtils assistUtils,
- FeatureFlags featureFlags,
SceneContainerFlags sceneContainerFlags,
DumpManager dumpManager,
Optional<UnfoldTransitionProgressForwarder> unfoldTransitionProgressForwarder,
@@ -613,7 +612,6 @@
}
mContext = context;
- mFeatureFlags = featureFlags;
mSceneContainerFlags = sceneContainerFlags;
mMainExecutor = mainExecutor;
mShellInterface = shellInterface;
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.kt
new file mode 100644
index 0000000..abdbd68
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotActionsProvider.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.screenshot
+
+import android.content.Context
+import android.content.Intent
+import android.graphics.drawable.Drawable
+import android.net.Uri
+import android.os.UserHandle
+import androidx.appcompat.content.res.AppCompatResources
+import com.android.systemui.res.R
+import javax.inject.Inject
+
+/**
+ * Provides static actions for screenshots. This class can be overridden by a vendor-specific SysUI
+ * implementation.
+ */
+interface ScreenshotActionsProvider {
+ data class ScreenshotAction(
+ val icon: Drawable?,
+ val text: String?,
+ val overrideTransition: Boolean,
+ val retrieveIntent: (Uri) -> Intent
+ )
+
+ fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent
+ fun getActions(context: Context, user: UserHandle): List<ScreenshotAction>
+}
+
+class DefaultScreenshotActionsProvider @Inject constructor() : ScreenshotActionsProvider {
+ override fun getPreviewAction(context: Context, uri: Uri, user: UserHandle): Intent {
+ return ActionIntentCreator.createEdit(uri, context)
+ }
+
+ override fun getActions(
+ context: Context,
+ user: UserHandle
+ ): List<ScreenshotActionsProvider.ScreenshotAction> {
+ val editAction =
+ ScreenshotActionsProvider.ScreenshotAction(
+ AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_edit),
+ context.resources.getString(R.string.screenshot_edit_label),
+ true
+ ) { uri ->
+ ActionIntentCreator.createEdit(uri, context)
+ }
+ val shareAction =
+ ScreenshotActionsProvider.ScreenshotAction(
+ AppCompatResources.getDrawable(context, R.drawable.ic_screenshot_share),
+ context.resources.getString(R.string.screenshot_share_label),
+ false
+ ) { uri ->
+ ActionIntentCreator.createShare(uri)
+ }
+ return listOf(editAction, shareAction)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
new file mode 100644
index 0000000..9354fd2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ScreenshotShelfViewProxy.kt
@@ -0,0 +1,226 @@
+/*
+ * 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
+
+import android.animation.Animator
+import android.animation.AnimatorListenerAdapter
+import android.app.Notification
+import android.content.Context
+import android.graphics.Bitmap
+import android.graphics.Rect
+import android.view.KeyEvent
+import android.view.LayoutInflater
+import android.view.ScrollCaptureResponse
+import android.view.View
+import android.view.ViewTreeObserver
+import android.view.WindowInsets
+import android.window.OnBackInvokedCallback
+import android.window.OnBackInvokedDispatcher
+import com.android.internal.logging.UiEventLogger
+import com.android.systemui.log.DebugLogger.debugLog
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.LogConfig.DEBUG_ACTIONS
+import com.android.systemui.screenshot.LogConfig.DEBUG_DISMISS
+import com.android.systemui.screenshot.LogConfig.DEBUG_INPUT
+import com.android.systemui.screenshot.LogConfig.DEBUG_WINDOW
+import com.android.systemui.screenshot.ScreenshotEvent.SCREENSHOT_DISMISSED_OTHER
+import com.android.systemui.screenshot.scroll.ScrollCaptureController
+import com.android.systemui.screenshot.ui.ScreenshotAnimationController
+import com.android.systemui.screenshot.ui.ScreenshotShelfView
+import com.android.systemui.screenshot.ui.binder.ScreenshotShelfViewBinder
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+
+/** Controls the screenshot view and viewModel. */
+class ScreenshotShelfViewProxy
+@AssistedInject
+constructor(
+ private val logger: UiEventLogger,
+ private val viewModel: ScreenshotViewModel,
+ private val staticActionsProvider: ScreenshotActionsProvider,
+ @Assisted private val context: Context,
+ @Assisted private val displayId: Int
+) : ScreenshotViewProxy {
+ override val view: ScreenshotShelfView =
+ LayoutInflater.from(context).inflate(R.layout.screenshot_shelf, null) as ScreenshotShelfView
+ override val screenshotPreview: View
+ override var packageName: String = ""
+ override var callbacks: ScreenshotView.ScreenshotViewCallback? = null
+ override var screenshot: ScreenshotData? = null
+ set(value) {
+ viewModel.setScreenshotBitmap(value?.bitmap)
+ field = value
+ }
+
+ override val isAttachedToWindow
+ get() = view.isAttachedToWindow
+ override var isDismissing = false
+ override var isPendingSharedTransition = false
+
+ private val animationController = ScreenshotAnimationController(view)
+
+ init {
+ ScreenshotShelfViewBinder.bind(view, viewModel, LayoutInflater.from(context))
+ addPredictiveBackListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+ setOnKeyListener { requestDismissal(SCREENSHOT_DISMISSED_OTHER) }
+ debugLog(DEBUG_WINDOW) { "adding OnComputeInternalInsetsListener" }
+ screenshotPreview = view.screenshotPreview
+ }
+
+ override fun reset() {
+ animationController.cancel()
+ isPendingSharedTransition = false
+ viewModel.setScreenshotBitmap(null)
+ viewModel.setActions(listOf())
+ }
+ override fun updateInsets(insets: WindowInsets) {}
+ override fun updateOrientation(insets: WindowInsets) {}
+
+ override fun createScreenshotDropInAnimation(screenRect: Rect, showFlash: Boolean): Animator {
+ return animationController.getEntranceAnimation()
+ }
+
+ override fun addQuickShareChip(quickShareAction: Notification.Action) {}
+
+ override fun setChipIntents(imageData: ScreenshotController.SavedImageData) {
+ val staticActions =
+ staticActionsProvider.getActions(context, imageData.owner).map {
+ ActionButtonViewModel(it.icon, it.text) {
+ val intent = it.retrieveIntent(imageData.uri)
+ debugLog(DEBUG_ACTIONS) { "Action tapped: $intent" }
+ isPendingSharedTransition = true
+ callbacks?.onAction(intent, imageData.owner, it.overrideTransition)
+ }
+ }
+
+ viewModel.setActions(staticActions)
+ }
+
+ override fun requestDismissal(event: ScreenshotEvent) {
+ debugLog(DEBUG_DISMISS) { "screenshot dismissal requested: $event" }
+
+ // If we're already animating out, don't restart the animation
+ if (isDismissing) {
+ debugLog(DEBUG_DISMISS) { "Already dismissing, ignoring duplicate command $event" }
+ return
+ }
+ logger.log(event, 0, packageName)
+ val animator = animationController.getExitAnimation()
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ isDismissing = true
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ isDismissing = false
+ callbacks?.onDismiss()
+ }
+ }
+ )
+ animator.start()
+ }
+
+ override fun showScrollChip(packageName: String, onClick: Runnable) {}
+
+ override fun hideScrollChip() {}
+
+ override fun prepareScrollingTransition(
+ response: ScrollCaptureResponse,
+ screenBitmap: Bitmap,
+ newScreenshot: Bitmap,
+ screenshotTakenInPortrait: Boolean,
+ onTransitionPrepared: Runnable,
+ ) {}
+
+ override fun startLongScreenshotTransition(
+ transitionDestination: Rect,
+ onTransitionEnd: Runnable,
+ longScreenshot: ScrollCaptureController.LongScreenshot
+ ) {}
+
+ override fun restoreNonScrollingUi() {}
+
+ override fun stopInputListening() {}
+
+ override fun requestFocus() {
+ view.requestFocus()
+ }
+
+ override fun announceForAccessibility(string: String) = view.announceForAccessibility(string)
+
+ override fun prepareEntranceAnimation(runnable: Runnable) {
+ view.viewTreeObserver.addOnPreDrawListener(
+ object : ViewTreeObserver.OnPreDrawListener {
+ override fun onPreDraw(): Boolean {
+ debugLog(DEBUG_WINDOW) { "onPreDraw: startAnimation" }
+ view.viewTreeObserver.removeOnPreDrawListener(this)
+ runnable.run()
+ return true
+ }
+ }
+ )
+ }
+
+ private fun addPredictiveBackListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+ val onBackInvokedCallback = OnBackInvokedCallback {
+ debugLog(DEBUG_INPUT) { "Predictive Back callback dispatched" }
+ onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+ }
+ view.addOnAttachStateChangeListener(
+ object : View.OnAttachStateChangeListener {
+ override fun onViewAttachedToWindow(v: View) {
+ debugLog(DEBUG_INPUT) { "Registering Predictive Back callback" }
+ view
+ .findOnBackInvokedDispatcher()
+ ?.registerOnBackInvokedCallback(
+ OnBackInvokedDispatcher.PRIORITY_DEFAULT,
+ onBackInvokedCallback
+ )
+ }
+
+ override fun onViewDetachedFromWindow(view: View) {
+ debugLog(DEBUG_INPUT) { "Unregistering Predictive Back callback" }
+ view
+ .findOnBackInvokedDispatcher()
+ ?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
+ }
+ }
+ )
+ }
+ private fun setOnKeyListener(onDismissRequested: (ScreenshotEvent) -> Unit) {
+ view.setOnKeyListener(
+ object : View.OnKeyListener {
+ override fun onKey(view: View, keyCode: Int, event: KeyEvent): Boolean {
+ if (keyCode == KeyEvent.KEYCODE_BACK || keyCode == KeyEvent.KEYCODE_ESCAPE) {
+ debugLog(DEBUG_INPUT) { "onKeyEvent: $keyCode" }
+ onDismissRequested.invoke(SCREENSHOT_DISMISSED_OTHER)
+ return true
+ }
+ return false
+ }
+ }
+ )
+ }
+
+ @AssistedFactory
+ interface Factory : ScreenshotViewProxy.Factory {
+ override fun getProxy(context: Context, displayId: Int): ScreenshotShelfViewProxy
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
index cdb9abb..9118ee1 100644
--- a/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/dagger/ScreenshotModule.java
@@ -16,16 +16,23 @@
package com.android.systemui.screenshot.dagger;
-import android.app.Service;
+import static com.android.systemui.Flags.screenshotShelfUi;
+import android.app.Service;
+import android.view.accessibility.AccessibilityManager;
+
+import com.android.systemui.dagger.SysUISingleton;
+import com.android.systemui.screenshot.DefaultScreenshotActionsProvider;
import com.android.systemui.screenshot.ImageCapture;
import com.android.systemui.screenshot.ImageCaptureImpl;
import com.android.systemui.screenshot.LegacyScreenshotViewProxy;
import com.android.systemui.screenshot.RequestProcessor;
+import com.android.systemui.screenshot.ScreenshotActionsProvider;
import com.android.systemui.screenshot.ScreenshotPolicy;
import com.android.systemui.screenshot.ScreenshotPolicyImpl;
import com.android.systemui.screenshot.ScreenshotProxyService;
import com.android.systemui.screenshot.ScreenshotRequestProcessor;
+import com.android.systemui.screenshot.ScreenshotShelfViewProxy;
import com.android.systemui.screenshot.ScreenshotSoundController;
import com.android.systemui.screenshot.ScreenshotSoundControllerImpl;
import com.android.systemui.screenshot.ScreenshotSoundProvider;
@@ -34,6 +41,7 @@
import com.android.systemui.screenshot.TakeScreenshotService;
import com.android.systemui.screenshot.appclips.AppClipsScreenshotHelperService;
import com.android.systemui.screenshot.appclips.AppClipsService;
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel;
import dagger.Binds;
import dagger.Module;
@@ -85,9 +93,25 @@
abstract ScreenshotSoundController bindScreenshotSoundController(
ScreenshotSoundControllerImpl screenshotSoundProviderImpl);
+ @Binds
+ abstract ScreenshotActionsProvider bindScreenshotActionsProvider(
+ DefaultScreenshotActionsProvider defaultScreenshotActionsProvider);
+
+ @Provides
+ @SysUISingleton
+ static ScreenshotViewModel providesScreenshotViewModel(
+ AccessibilityManager accessibilityManager) {
+ return new ScreenshotViewModel(accessibilityManager);
+ }
+
@Provides
static ScreenshotViewProxy.Factory providesScreenshotViewProxyFactory(
+ ScreenshotShelfViewProxy.Factory shelfScreenshotViewProxyFactory,
LegacyScreenshotViewProxy.Factory legacyScreenshotViewProxyFactory) {
- return legacyScreenshotViewProxyFactory;
+ if (screenshotShelfUi()) {
+ return shelfScreenshotViewProxyFactory;
+ } else {
+ return legacyScreenshotViewProxyFactory;
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
new file mode 100644
index 0000000..2c17873
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotAnimationController.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.Animator
+import android.animation.AnimatorListenerAdapter
+import android.animation.ValueAnimator
+import android.view.View
+
+class ScreenshotAnimationController(private val view: View) {
+ private var animator: Animator? = null
+
+ fun getEntranceAnimation(): Animator {
+ val animator = ValueAnimator.ofFloat(0f, 1f)
+ animator.addUpdateListener { view.alpha = it.animatedFraction }
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ view.alpha = 0f
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ view.alpha = 1f
+ }
+ }
+ )
+ this.animator = animator
+ return animator
+ }
+
+ fun getExitAnimation(): Animator {
+ val animator = ValueAnimator.ofFloat(1f, 0f)
+ animator.addUpdateListener { view.alpha = it.animatedValue as Float }
+ animator.addListener(
+ object : AnimatorListenerAdapter() {
+ override fun onAnimationStart(animator: Animator) {
+ view.alpha = 1f
+ }
+ override fun onAnimationEnd(animator: Animator) {
+ view.alpha = 0f
+ }
+ }
+ )
+ this.animator = animator
+ return animator
+ }
+
+ fun cancel() {
+ animator?.cancel()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
new file mode 100644
index 0000000..747ad4f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/ScreenshotShelfView.kt
@@ -0,0 +1,33 @@
+/*
+ * 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.content.Context
+import android.util.AttributeSet
+import android.widget.ImageView
+import androidx.constraintlayout.widget.ConstraintLayout
+import com.android.systemui.res.R
+
+class ScreenshotShelfView(context: Context, attrs: AttributeSet? = null) :
+ ConstraintLayout(context, attrs) {
+ lateinit var screenshotPreview: ImageView
+
+ override fun onFinishInflate() {
+ super.onFinishInflate()
+ screenshotPreview = requireViewById(R.id.screenshot_preview)
+ }
+}
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
new file mode 100644
index 0000000..a5825b5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ActionButtonViewBinder.kt
@@ -0,0 +1,64 @@
+/*
+ * 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.binder
+
+import android.view.View
+import android.widget.ImageView
+import android.widget.LinearLayout
+import android.widget.TextView
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ActionButtonViewModel
+
+object ActionButtonViewBinder {
+ /** Binds the given view to the given view-model */
+ 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.icon)
+ textView.text = viewModel.name
+ setMargins(iconView, textView, viewModel.name?.isNotEmpty() ?: false)
+ if (viewModel.onClicked != null) {
+ view.setOnClickListener { viewModel.onClicked.invoke() }
+ } else {
+ view.setOnClickListener(null)
+ }
+ view.visibility = View.VISIBLE
+ view.alpha = 1f
+ }
+
+ private fun setMargins(iconView: View, textView: View, hasText: Boolean) {
+ val iconParams = iconView.layoutParams as LinearLayout.LayoutParams
+ val textParams = textView.layoutParams as LinearLayout.LayoutParams
+ if (hasText) {
+ iconParams.marginStart = iconView.dpToPx(R.dimen.overlay_action_chip_padding_start)
+ iconParams.marginEnd = iconView.dpToPx(R.dimen.overlay_action_chip_spacing)
+ textParams.marginStart = 0
+ textParams.marginEnd = textView.dpToPx(R.dimen.overlay_action_chip_padding_end)
+ } else {
+ val paddingHorizontal =
+ iconView.dpToPx(R.dimen.overlay_action_chip_icon_only_padding_horizontal)
+ iconParams.marginStart = paddingHorizontal
+ iconParams.marginEnd = paddingHorizontal
+ }
+ iconView.layoutParams = iconParams
+ textView.layoutParams = textParams
+ }
+
+ private fun View.dpToPx(dimenId: Int): Int {
+ return this.resources.getDimensionPixelSize(dimenId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
new file mode 100644
index 0000000..3bcd52c
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/binder/ScreenshotShelfViewBinder.kt
@@ -0,0 +1,90 @@
+/*
+ * 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.binder
+
+import android.view.LayoutInflater
+import android.view.View
+import android.view.ViewGroup
+import android.widget.ImageView
+import android.widget.LinearLayout
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.lifecycleScope
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.screenshot.ui.viewmodel.ScreenshotViewModel
+import com.android.systemui.util.children
+import kotlinx.coroutines.launch
+
+object ScreenshotShelfViewBinder {
+ fun bind(
+ view: ViewGroup,
+ viewModel: ScreenshotViewModel,
+ layoutInflater: LayoutInflater,
+ ) {
+ val previewView: ImageView = view.requireViewById(R.id.screenshot_preview)
+ val previewBorder = view.requireViewById<View>(R.id.screenshot_preview_border)
+ previewView.clipToOutline = true
+ val actionsContainer: LinearLayout = view.requireViewById(R.id.screenshot_actions)
+ view.requireViewById<View>(R.id.screenshot_dismiss_button).visibility =
+ if (viewModel.showDismissButton) View.VISIBLE else View.GONE
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ repeatOnLifecycle(Lifecycle.State.STARTED) {
+ launch {
+ viewModel.preview.collect { bitmap ->
+ if (bitmap != null) {
+ previewView.setImageBitmap(bitmap)
+ previewView.visibility = View.VISIBLE
+ previewBorder.visibility = View.VISIBLE
+ } else {
+ previewView.visibility = View.GONE
+ previewBorder.visibility = View.GONE
+ }
+ }
+ }
+ launch {
+ viewModel.actions.collect { actions ->
+ if (actions.isNotEmpty()) {
+ view
+ .requireViewById<View>(R.id.actions_container_background)
+ .visibility = View.VISIBLE
+ }
+ val viewPool = actionsContainer.children.toList()
+ actionsContainer.removeAllViews()
+ val actionButtons =
+ List(actions.size) {
+ viewPool.getOrElse(it) {
+ layoutInflater.inflate(
+ R.layout.overlay_action_chip,
+ actionsContainer,
+ false
+ )
+ }
+ }
+ actionButtons.zip(actions).forEach {
+ actionsContainer.addView(it.first)
+ ActionButtonViewBinder.bind(it.first, it.second)
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
new file mode 100644
index 0000000..6ee9705
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ActionButtonViewModel.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.drawable.Drawable
+
+data class ActionButtonViewModel(
+ val icon: Drawable?,
+ val name: String?,
+ val onClicked: (() -> Unit)?
+)
diff --git a/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
new file mode 100644
index 0000000..3a652d9
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/screenshot/ui/viewmodel/ScreenshotViewModel.kt
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.screenshot.ui.viewmodel
+
+import android.graphics.Bitmap
+import android.view.accessibility.AccessibilityManager
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class ScreenshotViewModel(private val accessibilityManager: AccessibilityManager) {
+ private val _preview = MutableStateFlow<Bitmap?>(null)
+ val preview: StateFlow<Bitmap?> = _preview
+ private val _actions = MutableStateFlow(emptyList<ActionButtonViewModel>())
+ val actions: StateFlow<List<ActionButtonViewModel>> = _actions
+ val showDismissButton: Boolean
+ get() = accessibilityManager.isEnabled
+
+ fun setScreenshotBitmap(bitmap: Bitmap?) {
+ _preview.value = bitmap
+ }
+
+ fun setActions(actions: List<ActionButtonViewModel>) {
+ _actions.value = actions
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
index 3a2a081..9cb920a 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java
@@ -3257,7 +3257,6 @@
mKeyguardStatusViewController.setStatusAccessibilityImportance(mode);
}
- @Override
public void performHapticFeedback(int constant) {
mVibratorHelper.performHapticFeedback(mView, constant);
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 8ba0544..8dbcead 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -1280,18 +1280,20 @@
mScrimController.setScrimCornerRadius(radius);
- // Convert global clipping coordinates to local ones,
- // relative to NotificationStackScrollLayout
- int nsslLeft = calculateNsslLeft(left);
- int nsslRight = calculateNsslRight(right);
- int nsslTop = getNotificationsClippingTopBounds(top);
- int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
- int bottomRadius = mSplitShadeEnabled ? radius : 0;
- // TODO (b/265193930): remove dependency on NPVC
- int topRadius = mSplitShadeEnabled
- && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
- mNotificationStackScrollLayoutController.setRoundedClippingBounds(
- nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+ if (!SceneContainerFlag.isEnabled()) {
+ // Convert global clipping coordinates to local ones,
+ // relative to NotificationStackScrollLayout
+ int nsslLeft = calculateNsslLeft(left);
+ int nsslRight = calculateNsslRight(right);
+ int nsslTop = getNotificationsClippingTopBounds(top);
+ int nsslBottom = bottom - mNotificationStackScrollLayoutController.getTop();
+ int bottomRadius = mSplitShadeEnabled ? radius : 0;
+ // TODO (b/265193930): remove dependency on NPVC
+ int topRadius = mSplitShadeEnabled
+ && mPanelViewControllerLazy.get().isExpandingFromHeadsUp() ? 0 : radius;
+ mNotificationStackScrollLayoutController.setRoundedClippingBounds(
+ nsslLeft, nsslTop, nsslRight, nsslBottom, topRadius, bottomRadius);
+ }
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
index 0a57b64..813df11 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeController.java
@@ -232,6 +232,13 @@
/** Called when a launch animation ends. */
void onLaunchAnimationEnd(boolean launchIsFullScreen);
+ /**
+ * Performs haptic feedback from a view with a haptic feedback constant.
+ *
+ * @param constant One of android.view.HapticFeedbackConstants
+ */
+ void performHapticFeedback(int constant);
+
/** Sets the listener for when the visibility of the shade changes. */
default void setVisibilityListener(ShadeVisibilityListener listener) {}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
index 093690f..d703a27 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerEmptyImpl.kt
@@ -63,4 +63,5 @@
override fun onStatusBarTouch(event: MotionEvent?) {}
override fun onLaunchAnimationCancelled(isLaunchForActivity: Boolean) {}
override fun onLaunchAnimationEnd(launchIsFullScreen: Boolean) {}
+ override fun performHapticFeedback(constant: Int) {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
index d99d607..5f5e5ce 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java
@@ -271,6 +271,11 @@
}
@Override
+ public void performHapticFeedback(int constant) {
+ getNpvc().performHapticFeedback(constant);
+ }
+
+ @Override
public void instantCollapseShade() {
getNpvc().instantCollapse();
runPostCollapseActions();
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
index 177c3db..c20efea 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerSceneImpl.kt
@@ -33,6 +33,7 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
import dagger.Lazy
@@ -62,6 +63,7 @@
private val deviceEntryInteractor: DeviceEntryInteractor,
private val notificationStackScrollLayout: NotificationStackScrollLayout,
@ShadeTouchLog private val touchLog: LogBuffer,
+ private val vibratorHelper: VibratorHelper,
commandQueue: CommandQueue,
statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
notificationShadeWindowController: NotificationShadeWindowController,
@@ -249,4 +251,8 @@
// The only call to this doesn't happen with migrateClocksToBlueprint() enabled
throw UnsupportedOperationException()
}
+
+ override fun performHapticFeedback(constant: Int) {
+ vibratorHelper.performHapticFeedback(notificationStackScrollLayout, constant)
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
index d90bb0b..9902a32 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewController.kt
@@ -44,6 +44,7 @@
fun disableHeader(state1: Int, state2: Int, animated: Boolean)
/** If the latency tracker is enabled, begins tracking expand latency. */
+ @Deprecated("No longer supported. Do not add new calls to this.")
fun startExpandLatencyTracking()
/** Sets the alpha value of the shade to a value between 0 and 255. */
@@ -57,13 +58,14 @@
fun setAlphaChangeAnimationEndAction(r: Runnable)
/** Sets Qs ScrimEnabled and updates QS state. */
+ @Deprecated("Does nothing when scene container is enabled.")
fun setQsScrimEnabled(qsScrimEnabled: Boolean)
/** Sets the top spacing for the ambient indicator. */
fun setAmbientIndicationTop(ambientIndicationTop: Int, ambientTextVisible: Boolean)
/** Updates notification panel-specific flags on [SysUiState]. */
- fun updateSystemUiStateFlags()
+ @Deprecated("Does nothing when scene container is enabled.") fun updateSystemUiStateFlags()
/** Ensures that the touchable region is updated. */
fun updateTouchableRegion()
@@ -105,16 +107,6 @@
@Deprecated("No longer supported. Do not add new calls to this.")
fun finishInputFocusTransfer(velocity: Float)
- /**
- * Performs haptic feedback from a view with a haptic feedback constant.
- *
- * The implementation of this method should use the [android.view.View.performHapticFeedback]
- * method with the provided constant.
- *
- * @param[constant] One of [android.view.HapticFeedbackConstants]
- */
- fun performHapticFeedback(constant: Int)
-
/** Returns the ShadeHeadsUpTracker. */
val shadeHeadsUpTracker: ShadeHeadsUpTracker
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
index 69849e8..93c3772 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewControllerEmptyImpl.kt
@@ -84,8 +84,6 @@
override fun startInputFocusTransfer() {}
override fun cancelInputFocusTransfer() {}
override fun finishInputFocusTransfer(velocity: Float) {}
- override fun performHapticFeedback(constant: Int) {}
-
override val shadeHeadsUpTracker = ShadeHeadsUpTrackerEmptyImpl()
override val shadeFoldAnimator = ShadeFoldAnimatorEmptyImpl()
@Deprecated("Use SceneInteractor.currentScene instead.")
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
index ea549f2..24b7533 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
@@ -66,11 +66,13 @@
deviceEntryInteractor.isUnlocked,
deviceEntryInteractor.canSwipeToEnter,
shadeInteractor.shadeMode,
- ) { isUnlocked, canSwipeToDismiss, shadeMode ->
+ qsSceneAdapter.isCustomizing
+ ) { isUnlocked, canSwipeToDismiss, shadeMode, isCustomizing ->
destinationScenes(
isUnlocked = isUnlocked,
canSwipeToDismiss = canSwipeToDismiss,
shadeMode = shadeMode,
+ isCustomizing = isCustomizing
)
}
.stateIn(
@@ -81,6 +83,7 @@
isUnlocked = deviceEntryInteractor.isUnlocked.value,
canSwipeToDismiss = deviceEntryInteractor.canSwipeToEnter.value,
shadeMode = shadeInteractor.shadeMode.value,
+ isCustomizing = qsSceneAdapter.isCustomizing.value,
),
)
@@ -120,6 +123,7 @@
isUnlocked: Boolean,
canSwipeToDismiss: Boolean?,
shadeMode: ShadeMode,
+ isCustomizing: Boolean,
): Map<UserAction, UserActionResult> {
val up =
when {
@@ -131,7 +135,9 @@
val down = Scenes.QuickSettings.takeIf { shadeMode is ShadeMode.Single }
return buildMap {
- this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ if (!isCustomizing) {
+ this[Swipe(SwipeDirection.Up)] = UserActionResult(up)
+ } // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
down?.let { this[Swipe(SwipeDirection.Down)] = UserActionResult(down) }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
index a12b970..da8c1be 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyboardShortcutListSearch.java
@@ -560,11 +560,6 @@
Pair.create(
KeyEvent.KEYCODE_TAB,
KeyEvent.META_SHIFT_ON | KeyEvent.META_ALT_ON))),
- /* Hide and (re)show taskbar: Meta + T */
- new ShortcutKeyGroupMultiMappingInfo(
- context.getString(R.string.group_system_hide_reshow_taskbar),
- Arrays.asList(
- Pair.create(KeyEvent.KEYCODE_T, KeyEvent.META_META_ON))),
/* Access notification shade: Meta + N */
new ShortcutKeyGroupMultiMappingInfo(
context.getString(R.string.group_system_access_notification_shade),
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index c904621..815236e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -757,8 +757,8 @@
mRotateTextViewController.updateIndication(
INDICATION_TYPE_ADAPTIVE_AUTH,
new KeyguardIndication.Builder()
- .setMessage(mContext
- .getString(R.string.kg_prompt_after_adaptive_auth_lock))
+ .setMessage(mContext.getString(
+ R.string.keyguard_indication_after_adaptive_auth_lock))
.setTextColor(mInitialTextColorState)
.build(),
true);
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 9479762..f2c593d 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
@@ -812,6 +812,10 @@
} else {
mDebugTextUsedYPositions.clear();
}
+
+ mDebugPaint.setColor(Color.DKGRAY);
+ canvas.drawPath(mRoundedClipPath, mDebugPaint);
+
int y = 0;
drawDebugInfo(canvas, y, Color.RED, /* label= */ "y = " + y);
@@ -843,14 +847,14 @@
drawDebugInfo(canvas, y, Color.LTGRAY,
/* label= */ "mAmbientState.getStackY() + mAmbientState.getStackHeight() = " + y);
- y = (int) mAmbientState.getStackY() + mContentHeight;
- drawDebugInfo(canvas, y, Color.MAGENTA,
- /* label= */ "mAmbientState.getStackY() + mContentHeight = " + y);
-
y = (int) (mAmbientState.getStackY() + mIntrinsicContentHeight);
drawDebugInfo(canvas, y, Color.YELLOW,
/* label= */ "mAmbientState.getStackY() + mIntrinsicContentHeight = " + y);
+ y = mContentHeight;
+ drawDebugInfo(canvas, y, Color.MAGENTA,
+ /* label= */ "mContentHeight = " + y);
+
drawDebugInfo(canvas, mRoundedRectClippingBottom, Color.DKGRAY,
/* label= */ "mRoundedRectClippingBottom) = " + y);
}
@@ -4940,6 +4944,9 @@
println(pw, "intrinsicPadding", mIntrinsicPadding);
println(pw, "topPadding", mTopPadding);
println(pw, "bottomPadding", mBottomPadding);
+ dumpRoundedRectClipping(pw);
+ println(pw, "requestedClipBounds", mRequestedClipBounds);
+ println(pw, "isClipped", mIsClipped);
println(pw, "translationX", getTranslationX());
println(pw, "translationY", getTranslationY());
println(pw, "translationZ", getTranslationZ());
@@ -4994,6 +5001,15 @@
});
}
+ private void dumpRoundedRectClipping(IndentingPrintWriter pw) {
+ pw.append("roundedRectClipping{l=").print(mRoundedRectClippingLeft);
+ pw.append(" t=").print(mRoundedRectClippingTop);
+ pw.append(" r=").print(mRoundedRectClippingRight);
+ pw.append(" b=").print(mRoundedRectClippingBottom);
+ pw.append("} topRadius=").print(mBgCornerRadii[0]);
+ pw.append(" bottomRadius=").println(mBgCornerRadii[4]);
+ }
+
private void dumpFooterViewVisibility(IndentingPrintWriter pw) {
FooterViewRefactor.assertInLegacyMode();
final boolean showDismissView = shouldShowDismissView();
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
index 9efe632..79ba25e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/data/repository/NotificationStackAppearanceRepository.kt
@@ -17,8 +17,8 @@
package com.android.systemui.statusbar.notification.stack.data.repository
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
import javax.inject.Inject
import kotlinx.coroutines.flow.MutableStateFlow
@@ -26,7 +26,7 @@
@SysUISingleton
class NotificationStackAppearanceRepository @Inject constructor() {
/** The bounds of the notification stack in the current scene. */
- val stackBounds = MutableStateFlow(NotificationContainerBounds())
+ val stackBounds = MutableStateFlow(StackBounds())
/**
* The height in px of the contents of notification stack. Depending on the number of
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 08df473..f05d017 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
@@ -17,13 +17,19 @@
package com.android.systemui.statusbar.notification.stack.domain.interactor
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
import com.android.systemui.statusbar.notification.stack.data.repository.NotificationStackAppearanceRepository
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOf
/** An interactor which controls the appearance of the NSSL */
@SysUISingleton
@@ -31,9 +37,30 @@
@Inject
constructor(
private val repository: NotificationStackAppearanceRepository,
+ shadeInteractor: ShadeInteractor,
) {
/** The bounds of the notification stack in the current scene. */
- val stackBounds: StateFlow<NotificationContainerBounds> = repository.stackBounds.asStateFlow()
+ val stackBounds: StateFlow<StackBounds> = repository.stackBounds.asStateFlow()
+
+ /**
+ * Whether the stack is expanding from GONE-with-HUN to SHADE
+ *
+ * TODO(b/296118689): implement this to match legacy QSController logic
+ */
+ private val isExpandingFromHeadsUp: Flow<Boolean> = flowOf(false)
+
+ /** The rounding of the notification stack. */
+ val stackRounding: Flow<StackRounding> =
+ combine(
+ shadeInteractor.shadeMode,
+ isExpandingFromHeadsUp,
+ ) { shadeMode, isExpandingFromHeadsUp ->
+ StackRounding(
+ roundTop = !(shadeMode == ShadeMode.Split && isExpandingFromHeadsUp),
+ roundBottom = shadeMode != ShadeMode.Single,
+ )
+ }
+ .distinctUntilChanged()
/**
* The height in px of the contents of notification stack. Depending on the number of
@@ -59,7 +86,7 @@
val syntheticScroll: Flow<Float> = repository.syntheticScroll.asStateFlow()
/** Sets the position of the notification stack in the current scene. */
- fun setStackBounds(bounds: NotificationContainerBounds) {
+ fun setStackBounds(bounds: StackBounds) {
check(bounds.top <= bounds.bottom) { "Invalid bounds: $bounds" }
repository.stackBounds.value = bounds
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
new file mode 100644
index 0000000..1fc9a18
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackBounds.kt
@@ -0,0 +1,32 @@
+/*
+ * 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.stack.shared.model
+
+/** Models the bounds of the notification stack. */
+data class StackBounds(
+ /** The position of the left of the stack in its window coordinate system, in pixels. */
+ val left: Float = 0f,
+ /** The position of the top of the stack in its window coordinate system, in pixels. */
+ val top: Float = 0f,
+ /** The position of the right of the stack in its window coordinate system, in pixels. */
+ val right: Float = 0f,
+ /** The position of the bottom of the stack in its window coordinate system, in pixels. */
+ val bottom: Float = 0f,
+) {
+ /** The current height of the notification container. */
+ val height: Float = bottom - top
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
new file mode 100644
index 0000000..0c92b50
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackClipping.kt
@@ -0,0 +1,20 @@
+/*
+ * 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.stack.shared.model
+
+/** Models the clipping rounded rectangle of the notification stack */
+data class StackClipping(val bounds: StackBounds, val rounding: StackRounding)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
new file mode 100644
index 0000000..ddc5d7ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/shared/model/StackRounding.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.shared.model
+
+/** Models the corner rounds of the notification stack. */
+data class StackRounding(
+ /** Whether the top corners of the notification stack should be rounded. */
+ val roundTop: Boolean = false,
+ /** Whether the bottom corners of the notification stack should be rounded. */
+ val roundBottom: Boolean = false,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
deleted file mode 100644
index f10e5f1..0000000
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackAppearanceViewBinder.kt
+++ /dev/null
@@ -1,99 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.statusbar.notification.stack.ui.viewbinder
-
-import android.content.Context
-import android.util.TypedValue
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.lifecycle.repeatWhenAttached
-import com.android.systemui.statusbar.notification.stack.AmbientState
-import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
-import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
-import kotlin.math.roundToInt
-import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.launch
-
-/** Binds the shared notification container to its view-model. */
-object NotificationStackAppearanceViewBinder {
- const val SCRIM_CORNER_RADIUS = 32f
-
- @JvmStatic
- fun bind(
- context: Context,
- view: SharedNotificationContainer,
- viewModel: NotificationStackAppearanceViewModel,
- ambientState: AmbientState,
- controller: NotificationStackScrollLayoutController,
- @Main mainImmediateDispatcher: CoroutineDispatcher,
- ): DisposableHandle {
- return view.repeatWhenAttached(mainImmediateDispatcher) {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- viewModel.stackBounds.collect { bounds ->
- val viewLeft = controller.view.left
- val viewTop = controller.view.top
- controller.setRoundedClippingBounds(
- bounds.left.roundToInt() - viewLeft,
- bounds.top.roundToInt() - viewTop,
- bounds.right.roundToInt() - viewLeft,
- bounds.bottom.roundToInt() - viewTop,
- SCRIM_CORNER_RADIUS.dpToPx(context),
- 0,
- )
- }
- }
-
- launch {
- viewModel.contentTop.collect {
- controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
- }
- }
-
- launch {
- var wasExpanding = false
- viewModel.expandFraction.collect { expandFraction ->
- val nowExpanding = expandFraction != 0f && expandFraction != 1f
- if (nowExpanding && !wasExpanding) {
- controller.onExpansionStarted()
- }
- ambientState.expansionFraction = expandFraction
- controller.expandedHeight = expandFraction * controller.view.height
- if (!nowExpanding && wasExpanding) {
- controller.onExpansionStopped()
- }
- wasExpanding = nowExpanding
- }
- }
-
- launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
- }
- }
- }
-
- private fun Float.dpToPx(context: Context): Int {
- return TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- this,
- context.resources.displayMetrics
- )
- .roundToInt()
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
new file mode 100644
index 0000000..1a34bb4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationStackViewBinder.kt
@@ -0,0 +1,102 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.notification.stack.ui.viewbinder
+
+import androidx.lifecycle.Lifecycle
+import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.common.ui.ConfigurationState
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.notification.stack.AmbientState
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
+import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController
+import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationStackAppearanceViewModel
+import javax.inject.Inject
+import kotlin.math.roundToInt
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.coroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.launch
+
+/** Binds the NSSL/Controller/AmbientState to their ViewModel. */
+@SysUISingleton
+class NotificationStackViewBinder
+@Inject
+constructor(
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+ private val ambientState: AmbientState,
+ private val view: NotificationStackScrollLayout,
+ private val controller: NotificationStackScrollLayoutController,
+ private val viewModel: NotificationStackAppearanceViewModel,
+ private val configuration: ConfigurationState,
+) {
+
+ fun bindWhileAttached(): DisposableHandle {
+ return view.repeatWhenAttached(mainImmediateDispatcher) {
+ repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
+ }
+ }
+
+ suspend fun bind() = coroutineScope {
+ launch {
+ combine(viewModel.stackClipping, clipRadius, ::Pair).collect { (clipping, clipRadius) ->
+ val (bounds, rounding) = clipping
+ val viewLeft = controller.view.left
+ val viewTop = controller.view.top
+ controller.setRoundedClippingBounds(
+ bounds.left.roundToInt() - viewLeft,
+ bounds.top.roundToInt() - viewTop,
+ bounds.right.roundToInt() - viewLeft,
+ bounds.bottom.roundToInt() - viewTop,
+ if (rounding.roundTop) clipRadius else 0,
+ if (rounding.roundBottom) clipRadius else 0,
+ )
+ }
+ }
+
+ launch {
+ viewModel.contentTop.collect {
+ controller.updateTopPadding(it, controller.isAddOrRemoveAnimationPending)
+ }
+ }
+
+ launch {
+ var wasExpanding = false
+ viewModel.expandFraction.collect { expandFraction ->
+ val nowExpanding = expandFraction != 0f && expandFraction != 1f
+ if (nowExpanding && !wasExpanding) {
+ controller.onExpansionStarted()
+ }
+ ambientState.expansionFraction = expandFraction
+ controller.expandedHeight = expandFraction * controller.view.height
+ if (!nowExpanding && wasExpanding) {
+ controller.onExpansionStopped()
+ }
+ wasExpanding = nowExpanding
+ }
+ }
+
+ launch { viewModel.isScrollable.collect { controller.setScrollingEnabled(it) } }
+ }
+
+ private val clipRadius: Flow<Int>
+ get() = configuration.getDimensionPixelOffset(R.dimen.notification_scrim_corner_radius)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 7c76ddb..6db6719 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -20,6 +20,7 @@
import android.view.WindowInsets
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.BurnInParameters
import com.android.systemui.keyguard.ui.viewmodel.ViewStateAccessor
@@ -30,6 +31,8 @@
import com.android.systemui.statusbar.notification.stack.NotificationStackSizeCalculator
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.SharedNotificationContainerViewModel
+import com.android.systemui.util.kotlin.DisposableHandles
+import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.MutableStateFlow
@@ -38,18 +41,23 @@
import kotlinx.coroutines.launch
/** Binds the shared notification container to its view-model. */
-object SharedNotificationContainerBinder {
+@SysUISingleton
+class SharedNotificationContainerBinder
+@Inject
+constructor(
+ private val sceneContainerFlags: SceneContainerFlags,
+ private val controller: NotificationStackScrollLayoutController,
+ private val notificationStackSizeCalculator: NotificationStackSizeCalculator,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
- @JvmStatic
fun bind(
view: SharedNotificationContainer,
viewModel: SharedNotificationContainerViewModel,
- sceneContainerFlags: SceneContainerFlags,
- controller: NotificationStackScrollLayoutController,
- notificationStackSizeCalculator: NotificationStackSizeCalculator,
- @Main mainImmediateDispatcher: CoroutineDispatcher,
): DisposableHandle {
- val disposableHandle =
+ val disposables = DisposableHandles()
+
+ disposables +=
view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
@@ -72,24 +80,6 @@
}
}
- // Required to capture keyguard media changes and ensure the notification count is correct
- val layoutChangeListener =
- object : View.OnLayoutChangeListener {
- override fun onLayoutChange(
- view: View,
- left: Int,
- top: Int,
- right: Int,
- bottom: Int,
- oldLeft: Int,
- oldTop: Int,
- oldRight: Int,
- oldBottom: Int
- ) {
- viewModel.notificationStackChanged()
- }
- }
-
val burnInParams = MutableStateFlow(BurnInParameters())
val viewState =
ViewStateAccessor(
@@ -100,7 +90,7 @@
* For animation sensitive coroutines, immediately run just like applicationScope does
* instead of doing a post() to the main thread. This extra delay can cause visible jitter.
*/
- val disposableHandleMainImmediate =
+ disposables +=
view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
launch {
@@ -167,7 +157,8 @@
}
}
- controller.setOnHeightChangedRunnable(Runnable { viewModel.notificationStackChanged() })
+ controller.setOnHeightChangedRunnable { viewModel.notificationStackChanged() }
+ disposables += DisposableHandle { controller.setOnHeightChangedRunnable(null) }
view.setOnApplyWindowInsetsListener { v: View, insets: WindowInsets ->
val insetTypes = WindowInsets.Type.systemBars() or WindowInsets.Type.displayCutout()
@@ -176,16 +167,16 @@
}
insets
}
- view.addOnLayoutChangeListener(layoutChangeListener)
+ disposables += DisposableHandle { view.setOnApplyWindowInsetsListener(null) }
- return object : DisposableHandle {
- override fun dispose() {
- disposableHandle.dispose()
- disposableHandleMainImmediate.dispose()
- controller.setOnHeightChangedRunnable(null)
- view.setOnApplyWindowInsetsListener(null)
- view.removeOnLayoutChangeListener(layoutChangeListener)
+ // Required to capture keyguard media changes and ensure the notification count is correct
+ val layoutChangeListener =
+ View.OnLayoutChangeListener { _, _, _, _, _, _, _, _, _ ->
+ viewModel.notificationStackChanged()
}
- }
+ view.addOnLayoutChangeListener(layoutChangeListener)
+ disposables += DisposableHandle { view.removeOnLayoutChangeListener(layoutChangeListener) }
+
+ return disposables
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
index b6167e1..a7cbc33 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationStackAppearanceViewModel.kt
@@ -18,7 +18,6 @@
package com.android.systemui.statusbar.notification.stack.ui.viewmodel
import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.systemui.common.shared.model.NotificationContainerBounds
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dump.DumpManager
@@ -27,6 +26,7 @@
import com.android.systemui.scene.shared.model.Scenes.Shade
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackClipping
import com.android.systemui.util.kotlin.FlowDumperImpl
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -83,8 +83,13 @@
.dumpWhileCollecting("expandFraction")
/** The bounds of the notification stack in the current scene. */
- val stackBounds: Flow<NotificationContainerBounds> =
- stackAppearanceInteractor.stackBounds.dumpValue("stackBounds")
+ val stackClipping: Flow<StackClipping> =
+ combine(
+ stackAppearanceInteractor.stackBounds,
+ stackAppearanceInteractor.stackRounding,
+ ::StackClipping
+ )
+ .dumpWhileCollecting("stackClipping")
/** The y-coordinate in px of top of the contents of the notification stack. */
val contentTop: StateFlow<Float> = stackAppearanceInteractor.contentTop.dumpValue("contentTop")
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 9e2497d..bd83121 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
@@ -24,6 +24,8 @@
import com.android.systemui.scene.shared.flag.SceneContainerFlags
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
+import com.android.systemui.statusbar.notification.stack.shared.model.StackBounds
+import com.android.systemui.statusbar.notification.stack.shared.model.StackRounding
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -61,12 +63,17 @@
right: Float,
bottom: Float,
) {
- val notificationContainerBounds =
- NotificationContainerBounds(top = top, bottom = bottom, left = left, right = right)
- keyguardInteractor.setNotificationContainerBounds(notificationContainerBounds)
- interactor.setStackBounds(notificationContainerBounds)
+ keyguardInteractor.setNotificationContainerBounds(
+ NotificationContainerBounds(top = top, bottom = bottom)
+ )
+ interactor.setStackBounds(
+ StackBounds(top = top, bottom = bottom, left = left, right = right)
+ )
}
+ /** Corner rounding of the stack */
+ val stackRounding: Flow<StackRounding> = interactor.stackRounding
+
/**
* The height in px of the contents of notification stack. Depending on the number of
* notifications, this can exceed the space available on screen to show notifications, at which
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
index 0db5c64..665fc0a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacks.java
@@ -537,7 +537,7 @@
@VisibleForTesting
void vibrateOnNavigationKeyDown() {
- mShadeViewController.performHapticFeedback(
+ mShadeController.performHapticFeedback(
HapticFeedbackConstants.GESTURE_START
);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
new file mode 100644
index 0000000..de036ea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/DisposableHandles.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.util.kotlin
+
+import kotlinx.coroutines.DisposableHandle
+
+/** A mutable collection of [DisposableHandle] objects that is itself a [DisposableHandle] */
+class DisposableHandles : DisposableHandle {
+ private val handles = mutableListOf<DisposableHandle>()
+
+ /** Add the provided handles to this collection. */
+ fun add(vararg handles: DisposableHandle) {
+ this.handles.addAll(handles)
+ }
+
+ /** Same as [add] */
+ operator fun plusAssign(handle: DisposableHandle) {
+ this.handles.add(handle)
+ }
+
+ /** Same as [add] */
+ operator fun plusAssign(handles: Iterable<DisposableHandle>) {
+ this.handles.addAll(handles)
+ }
+
+ /** [dispose] the current contents, then [add] the provided [handles] */
+ fun replaceAll(vararg handles: DisposableHandle) {
+ dispose()
+ add(*handles)
+ }
+
+ /** Dispose of all added handles and empty this collection. */
+ override fun dispose() {
+ handles.forEach { it.dispose() }
+ handles.clear()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
index d134e60..155102c9 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/MediaDevicesModule.kt
@@ -21,7 +21,6 @@
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepository
import com.android.settingslib.volume.data.repository.MediaControllerRepositoryImpl
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
@@ -52,13 +51,6 @@
@Provides
@SysUISingleton
- fun provideLocalMediaInteractor(
- repository: LocalMediaRepository,
- @Application scope: CoroutineScope,
- ): LocalMediaInteractor = LocalMediaInteractor(repository, scope)
-
- @Provides
- @SysUISingleton
fun provideMediaDeviceSessionRepository(
intentsReceiver: AudioManagerEventsReceiver,
mediaSessionManager: MediaSessionManager,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
index 11b4690..e052f24 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/data/repository/LocalMediaRepositoryFactory.kt
@@ -15,15 +15,12 @@
*/
package com.android.systemui.volume.panel.component.mediaoutput.data.repository
-import android.media.MediaRouter2Manager
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import com.android.settingslib.volume.data.repository.LocalMediaRepositoryImpl
import com.android.settingslib.volume.shared.AudioManagerEventsReceiver
import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.media.controls.util.LocalMediaManagerFactory
import javax.inject.Inject
-import kotlin.coroutines.CoroutineContext
import kotlinx.coroutines.CoroutineScope
interface LocalMediaRepositoryFactory {
@@ -35,18 +32,14 @@
@Inject
constructor(
private val eventsReceiver: AudioManagerEventsReceiver,
- private val mediaRouter2Manager: MediaRouter2Manager,
private val localMediaManagerFactory: LocalMediaManagerFactory,
@Application private val coroutineScope: CoroutineScope,
- @Background private val backgroundCoroutineContext: CoroutineContext,
) : LocalMediaRepositoryFactory {
override fun create(packageName: String?): LocalMediaRepository =
LocalMediaRepositoryImpl(
eventsReceiver,
localMediaManagerFactory.create(packageName),
- mediaRouter2Manager,
coroutineScope,
- backgroundCoroutineContext,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
new file mode 100644
index 0000000..b0c8a4a
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/interactor/MediaDeviceSessionInteractor.kt
@@ -0,0 +1,108 @@
+/*
+ * 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.session.MediaController
+import android.media.session.PlaybackState
+import android.os.Handler
+import com.android.settingslib.volume.data.repository.MediaControllerChange
+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.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import javax.inject.Inject
+import kotlin.coroutines.CoroutineContext
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.FlowCollector
+import kotlinx.coroutines.flow.filterIsInstance
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.withContext
+
+/** Allows to observe and change [MediaDeviceSession] state. */
+@OptIn(ExperimentalCoroutinesApi::class)
+@VolumePanelScope
+class MediaDeviceSessionInteractor
+@Inject
+constructor(
+ @Background private val backgroundCoroutineContext: CoroutineContext,
+ @Background private val backgroundHandler: Handler,
+ private val mediaControllerRepository: MediaControllerRepository,
+) {
+
+ /** [PlaybackState] changes for the [MediaDeviceSession]. */
+ fun playbackState(session: MediaDeviceSession): Flow<PlaybackState?> {
+ return stateChanges(session) {
+ emit(MediaControllerChange.PlaybackStateChanged(it.playbackState))
+ }
+ .filterIsInstance(MediaControllerChange.PlaybackStateChanged::class)
+ .map { it.state }
+ }
+
+ /** [MediaController.PlaybackInfo] changes for the [MediaDeviceSession]. */
+ fun playbackInfo(session: MediaDeviceSession): Flow<MediaController.PlaybackInfo?> {
+ return stateChanges(session) {
+ emit(MediaControllerChange.AudioInfoChanged(it.playbackInfo))
+ }
+ .filterIsInstance(MediaControllerChange.AudioInfoChanged::class)
+ .map { it.info }
+ }
+
+ private fun stateChanges(
+ session: MediaDeviceSession,
+ onStart: suspend FlowCollector<MediaControllerChange>.(controller: MediaController) -> Unit,
+ ): Flow<MediaControllerChange?> =
+ mediaControllerRepository.activeSessions
+ .flatMapLatest { controllers ->
+ val controller: MediaController =
+ findControllerForSession(controllers, session)
+ ?: return@flatMapLatest flowOf(null)
+ controller.stateChanges(backgroundHandler).onStart { onStart(controller) }
+ }
+ .flowOn(backgroundCoroutineContext)
+
+ /** Set [MediaDeviceSession] volume to [volume]. */
+ suspend fun setSessionVolume(mediaDeviceSession: MediaDeviceSession, volume: Int): Boolean {
+ if (!mediaDeviceSession.canAdjustVolume) {
+ return false
+ }
+ return withContext(backgroundCoroutineContext) {
+ val controller =
+ findControllerForSession(
+ mediaControllerRepository.activeSessions.value,
+ mediaDeviceSession,
+ )
+ if (controller == null) {
+ false
+ } else {
+ controller.setVolumeTo(volume, 0)
+ true
+ }
+ }
+ }
+
+ private fun findControllerForSession(
+ controllers: Collection<MediaController>,
+ mediaDeviceSession: MediaDeviceSession,
+ ): MediaController? =
+ controllers.firstOrNull { it.sessionToken == mediaDeviceSession.sessionToken }
+}
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 cb16abe..ea4c082 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,23 +33,15 @@
private val mediaOutputDialogManager: MediaOutputDialogManager,
) {
- fun onBarClick(session: MediaDeviceSession, expandable: Expandable) {
- when (session) {
- is MediaDeviceSession.Active -> {
- mediaOutputDialogManager.createAndShowWithController(
- session.packageName,
- false,
- expandable.dialogController()
- )
- }
- is MediaDeviceSession.Inactive -> {
- mediaOutputDialogManager.createAndShowForSystemRouting(
- expandable.dialogController()
- )
- }
- else -> {
- /* do nothing */
- }
+ fun onBarClick(session: MediaDeviceSession, isPlaybackActive: Boolean, expandable: Expandable) {
+ if (isPlaybackActive) {
+ mediaOutputDialogManager.createAndShowWithController(
+ session.packageName,
+ false,
+ expandable.dialogController()
+ )
+ } else {
+ 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 0f53437..e60139e 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
@@ -17,17 +17,16 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.interactor
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.MediaControllerChange
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.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSessions
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import javax.inject.Inject
import kotlin.coroutines.CoroutineContext
@@ -38,12 +37,9 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.distinctUntilChanged
-import kotlinx.coroutines.flow.filterIsInstance
import kotlinx.coroutines.flow.flatMapLatest
-import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext
@@ -58,35 +54,40 @@
private val packageManager: PackageManager,
@VolumePanelScope private val coroutineScope: CoroutineScope,
@Background private val backgroundCoroutineContext: CoroutineContext,
- @Background private val backgroundHandler: Handler,
- mediaControllerRepository: MediaControllerRepository
+ mediaControllerRepository: MediaControllerRepository,
) {
- /** Current [MediaDeviceSession]. Emits when the session playback changes. */
- val mediaDeviceSession: StateFlow<MediaDeviceSession> =
- mediaControllerRepository.activeLocalMediaController
- .flatMapLatest { it?.mediaDeviceSession() ?: flowOf(MediaDeviceSession.Inactive) }
- .flowOn(backgroundCoroutineContext)
- .stateIn(coroutineScope, SharingStarted.Eagerly, MediaDeviceSession.Inactive)
+ private val activeMediaControllers: Flow<MediaControllers> =
+ mediaControllerRepository.activeSessions
+ .map { getMediaControllers(it) }
+ .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
- private fun MediaController.mediaDeviceSession(): Flow<MediaDeviceSession> {
- return stateChanges(backgroundHandler)
- .onStart { emit(MediaControllerChange.PlaybackStateChanged(playbackState)) }
- .filterIsInstance<MediaControllerChange.PlaybackStateChanged>()
+ /** [MediaDeviceSessions] that contains currently active sessions. */
+ val activeMediaDeviceSessions: Flow<MediaDeviceSessions> =
+ activeMediaControllers.map {
+ MediaDeviceSessions(
+ local = it.local?.mediaDeviceSession(),
+ remote = it.remote?.mediaDeviceSession()
+ )
+ }
+
+ /** Returns the default [MediaDeviceSession] from [activeMediaDeviceSessions] */
+ val defaultActiveMediaSession: StateFlow<MediaDeviceSession?> =
+ activeMediaControllers
.map {
- MediaDeviceSession.Active(
- appLabel = getApplicationLabel(packageName)
- ?: return@map MediaDeviceSession.Inactive,
- packageName = packageName,
- sessionToken = sessionToken,
- playbackState = playbackState,
- )
+ when {
+ it.local?.playbackState?.isActive == true -> it.local.mediaDeviceSession()
+ it.remote?.playbackState?.isActive == true -> it.remote.mediaDeviceSession()
+ it.local != null -> it.local.mediaDeviceSession()
+ else -> null
+ }
}
- }
+ .flowOn(backgroundCoroutineContext)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
private val localMediaRepository: SharedFlow<LocalMediaRepository> =
- mediaDeviceSession
- .map { (it as? MediaDeviceSession.Active)?.packageName }
+ defaultActiveMediaSession
+ .map { it?.packageName }
.distinctUntilChanged()
.map { localMediaRepositoryFactory.create(it) }
.shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
@@ -111,6 +112,54 @@
}
}
+ /** Finds local and remote media controllers. */
+ private fun getMediaControllers(
+ controllers: Collection<MediaController>,
+ ): MediaControllers {
+ var localController: MediaController? = null
+ var remoteController: MediaController? = null
+ val remoteMediaSessions: MutableSet<String> = mutableSetOf()
+ for (controller in controllers) {
+ val playbackInfo: MediaController.PlaybackInfo = controller.playbackInfo ?: continue
+ when (playbackInfo.playbackType) {
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_REMOTE -> {
+ // MediaController can't be local if there is a remote one for the same package
+ if (localController?.packageName.equals(controller.packageName)) {
+ localController = null
+ }
+ if (!remoteMediaSessions.contains(controller.packageName)) {
+ remoteMediaSessions.add(controller.packageName)
+ if (remoteController == null) {
+ remoteController = controller
+ }
+ }
+ }
+ MediaController.PlaybackInfo.PLAYBACK_TYPE_LOCAL -> {
+ if (controller.packageName in remoteMediaSessions) continue
+ if (localController != null) continue
+ localController = controller
+ }
+ }
+ }
+ return MediaControllers(local = localController, remote = remoteController)
+ }
+
+ private suspend fun MediaController.mediaDeviceSession(): MediaDeviceSession? {
+ return MediaDeviceSession(
+ packageName = packageName,
+ sessionToken = sessionToken,
+ canAdjustVolume =
+ playbackInfo != null &&
+ playbackInfo?.volumeControl != VolumeProvider.VOLUME_CONTROL_FIXED,
+ appLabel = getApplicationLabel(packageName) ?: return null
+ )
+ }
+
+ private data class MediaControllers(
+ val local: MediaController?,
+ val remote: MediaController?,
+ )
+
private companion object {
const val TAG = "MediaOutputInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
index 1bceee9..2a2ce79 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSession.kt
@@ -17,26 +17,15 @@
package com.android.systemui.volume.panel.component.mediaoutput.domain.model
import android.media.session.MediaSession
-import android.media.session.PlaybackState
/** Represents media playing on the connected device. */
-sealed interface MediaDeviceSession {
+data class MediaDeviceSession(
+ val appLabel: CharSequence,
+ val packageName: String,
+ val sessionToken: MediaSession.Token,
+ val canAdjustVolume: Boolean,
+)
- /** Media is playing. */
- data class Active(
- val appLabel: CharSequence,
- val packageName: String,
- val sessionToken: MediaSession.Token,
- val playbackState: PlaybackState?,
- ) : MediaDeviceSession
-
- /** Media is not playing. */
- data object Inactive : MediaDeviceSession
-
- /** Current media state is unknown yet. */
- data object Unknown : MediaDeviceSession
-}
-
-/** Returns true when the audio is playing for the [MediaDeviceSession]. */
-fun MediaDeviceSession.isPlaying(): Boolean =
- this is MediaDeviceSession.Active && playbackState?.isActive == true
+/** Returns true when [other] controls the same sessions as [this]. */
+fun MediaDeviceSession.isTheSameSession(other: MediaDeviceSession?): Boolean =
+ sessionToken == other?.sessionToken
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
new file mode 100644
index 0000000..ddc0784
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/mediaoutput/domain/model/MediaDeviceSessions.kt
@@ -0,0 +1,31 @@
+/*
+ * 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.model
+
+/** Models a pair of local and remote [MediaDeviceSession]s. */
+data class MediaDeviceSessions(
+ val local: MediaDeviceSession?,
+ val remote: MediaDeviceSession?,
+) {
+
+ companion object {
+ /** Returns [MediaDeviceSessions.local]. */
+ val Local: (MediaDeviceSessions) -> MediaDeviceSession? = { it.local }
+ /** Returns [MediaDeviceSessions.remote]. */
+ val Remote: (MediaDeviceSessions) -> MediaDeviceSession? = { it.remote }
+ }
+}
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 d49cb1e..2530a3a 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
@@ -17,24 +17,30 @@
package com.android.systemui.volume.panel.component.mediaoutput.ui.viewmodel
import android.content.Context
+import android.media.session.PlaybackState
import com.android.systemui.animation.Expandable
import com.android.systemui.common.shared.model.Color
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
+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
import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
/** Models the UI of the Media Output Volume Panel component. */
+@OptIn(ExperimentalCoroutinesApi::class)
@VolumePanelScope
class MediaOutputViewModel
@Inject
@@ -43,25 +49,36 @@
@VolumePanelScope private val coroutineScope: CoroutineScope,
private val volumePanelViewModel: VolumePanelViewModel,
private val actionsInteractor: MediaOutputActionsInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
interactor: MediaOutputInteractor,
) {
- private val mediaDeviceSession: StateFlow<MediaDeviceSession> =
- interactor.mediaDeviceSession.stateIn(
- coroutineScope,
- SharingStarted.Eagerly,
- MediaDeviceSession.Unknown,
- )
+ private val sessionWithPlayback: StateFlow<SessionWithPlayback?> =
+ interactor.defaultActiveMediaSession
+ .flatMapLatest { session ->
+ if (session == null) {
+ flowOf(null)
+ } else {
+ mediaDeviceSessionInteractor.playbackState(session).map { playback ->
+ playback?.let { SessionWithPlayback(session, it) }
+ }
+ }
+ }
+ .stateIn(
+ coroutineScope,
+ SharingStarted.Eagerly,
+ null,
+ )
val connectedDeviceViewModel: StateFlow<ConnectedDeviceViewModel?> =
- combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+ combine(sessionWithPlayback, interactor.currentConnectedDevice) {
mediaDeviceSession,
currentConnectedDevice ->
ConnectedDeviceViewModel(
- if (mediaDeviceSession.isPlaying()) {
+ if (mediaDeviceSession?.playback?.isActive == true) {
context.getString(
R.string.media_output_label_title,
- (mediaDeviceSession as MediaDeviceSession.Active).appLabel
+ mediaDeviceSession.session.appLabel
)
} else {
context.getString(R.string.media_output_title_without_playing)
@@ -76,10 +93,10 @@
)
val deviceIconViewModel: StateFlow<DeviceIconViewModel?> =
- combine(mediaDeviceSession, interactor.currentConnectedDevice) {
+ combine(sessionWithPlayback, interactor.currentConnectedDevice) {
mediaDeviceSession,
currentConnectedDevice ->
- if (mediaDeviceSession.isPlaying()) {
+ if (mediaDeviceSession?.playback?.isActive == true) {
val icon =
currentConnectedDevice?.icon?.let { Icon.Loaded(it, null) }
?: Icon.Resource(
@@ -112,7 +129,14 @@
)
fun onBarClick(expandable: Expandable) {
- actionsInteractor.onBarClick(mediaDeviceSession.value, expandable)
+ sessionWithPlayback.value?.let {
+ actionsInteractor.onBarClick(it.session, it.playback.isActive, expandable)
+ }
volumePanelViewModel.dismissPanel()
}
+
+ private data class SessionWithPlayback(
+ val session: MediaDeviceSession,
+ val playback: PlaybackState,
+ )
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
deleted file mode 100644
index 6b62074..0000000
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/domain/interactor/CastVolumeInteractor.kt
+++ /dev/null
@@ -1,48 +0,0 @@
-/*
- * 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.volume.domain.interactor
-
-import com.android.settingslib.volume.domain.interactor.LocalMediaInteractor
-import com.android.settingslib.volume.domain.model.RoutingSession
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.stateIn
-
-/** Provides a remote media casting state. */
-@VolumePanelScope
-class CastVolumeInteractor
-@Inject
-constructor(
- @VolumePanelScope private val coroutineScope: CoroutineScope,
- private val localMediaInteractor: LocalMediaInteractor,
-) {
-
- /** Returns a list of [RoutingSession] to show in the UI. */
- val remoteRoutingSessions: StateFlow<List<RoutingSession>> =
- localMediaInteractor.remoteRoutingSessions
- .map { it.filter { routingSession -> routingSession.isVolumeSeekBarEnabled } }
- .stateIn(coroutineScope, SharingStarted.Eagerly, emptyList())
-
- /** Sets [routingSession] volume to [volume]. */
- suspend fun setVolume(routingSession: RoutingSession, volume: Int) {
- localMediaInteractor.adjustSessionVolume(routingSession.routingSessionInfo.id, volume)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
index 1b73208..d49442c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/AudioStreamSliderViewModel.kt
@@ -80,7 +80,7 @@
) { model, isEnabled, ringerMode ->
model.toState(isEnabled, ringerMode)
}
- .stateIn(coroutineScope, SharingStarted.Eagerly, EmptyState)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
override fun onValueChanged(state: SliderState, newValue: Float) {
val audioViewModel = state as? State
@@ -163,17 +163,6 @@
val audioStreamModel: AudioStreamModel,
) : SliderState
- private data object EmptyState : SliderState {
- override val value: Float = 0f
- override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
- override val icon: Icon? = null
- override val valueText: String = ""
- override val label: String = ""
- override val disabledMessage: String? = null
- override val a11yStep: Int = 0
- override val isEnabled: Boolean = true
- }
-
@AssistedFactory
interface Factory {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
index 86b2d73..0f240b3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/CastVolumeSliderViewModel.kt
@@ -17,11 +17,11 @@
package com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel
import android.content.Context
-import com.android.settingslib.volume.domain.model.RoutingSession
+import android.media.session.MediaController.PlaybackInfo
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.res.R
-import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
import com.android.systemui.volume.panel.component.volume.domain.interactor.VolumeSliderInteractor
import dagger.assisted.Assisted
import dagger.assisted.AssistedFactory
@@ -30,30 +30,29 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.mapNotNull
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
class CastVolumeSliderViewModel
@AssistedInject
constructor(
- @Assisted private val routingSession: RoutingSession,
+ @Assisted private val session: MediaDeviceSession,
@Assisted private val coroutineScope: CoroutineScope,
private val context: Context,
- mediaOutputInteractor: MediaOutputInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
private val volumeSliderInteractor: VolumeSliderInteractor,
- private val castVolumeInteractor: CastVolumeInteractor,
) : SliderViewModel {
- private val volumeRange = 0..routingSession.routingSessionInfo.volumeMax
-
override val slider: StateFlow<SliderState> =
- combine(mediaOutputInteractor.currentConnectedDevice) { _ -> getCurrentState() }
- .stateIn(coroutineScope, SharingStarted.Eagerly, getCurrentState())
+ mediaDeviceSessionInteractor
+ .playbackInfo(session)
+ .mapNotNull { it?.getCurrentState() }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, SliderState.Empty)
override fun onValueChanged(state: SliderState, newValue: Float) {
coroutineScope.launch {
- castVolumeInteractor.setVolume(routingSession, newValue.roundToInt())
+ mediaDeviceSessionInteractor.setSessionVolume(session, newValue.roundToInt())
}
}
@@ -61,15 +60,16 @@
// do nothing because this action isn't supported for Cast sliders.
}
- private fun getCurrentState(): State =
- State(
- value = routingSession.routingSessionInfo.volume.toFloat(),
+ private fun PlaybackInfo.getCurrentState(): State {
+ val volumeRange = 0..maxVolume
+ return State(
+ value = currentVolume.toFloat(),
valueRange = volumeRange.first.toFloat()..volumeRange.last.toFloat(),
icon = Icon.Resource(R.drawable.ic_cast, null),
valueText =
SliderViewModel.formatValue(
volumeSliderInteractor.processVolumeToValue(
- volume = routingSession.routingSessionInfo.volume,
+ volume = currentVolume,
volumeRange = volumeRange,
)
),
@@ -77,6 +77,7 @@
isEnabled = true,
a11yStep = 1
)
+ }
private data class State(
override val value: Float,
@@ -95,7 +96,7 @@
interface Factory {
fun create(
- routingSession: RoutingSession,
+ session: MediaDeviceSession,
coroutineScope: CoroutineScope,
): CastVolumeSliderViewModel
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
index b87d0a7..3dca272 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/slider/ui/viewmodel/SliderState.kt
@@ -36,4 +36,15 @@
*/
val a11yStep: Int
val disabledMessage: String?
+
+ data object Empty : SliderState {
+ override val value: Float = 0f
+ override val valueRange: ClosedFloatingPointRange<Float> = 0f..1f
+ override val icon: Icon? = null
+ override val valueText: String = ""
+ override val label: String = ""
+ override val disabledMessage: String? = null
+ override val a11yStep: Int = 0
+ override val isEnabled: Boolean = true
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
index aaee24b..4e9a456 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/volume/ui/viewmodel/AudioVolumeComponentViewModel.kt
@@ -18,9 +18,10 @@
import android.media.AudioManager
import com.android.settingslib.volume.shared.model.AudioStream
+import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaDeviceSessionInteractor
import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor
-import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isPlaying
-import com.android.systemui.volume.panel.component.volume.domain.interactor.CastVolumeInteractor
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.MediaDeviceSession
+import com.android.systemui.volume.panel.component.mediaoutput.domain.model.isTheSameSession
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.AudioStreamSliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.CastVolumeSliderViewModel
import com.android.systemui.volume.panel.component.volume.slider.ui.viewmodel.SliderViewModel
@@ -29,17 +30,15 @@
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.combineTransform
+import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
-import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
-import kotlinx.coroutines.flow.transformLatest
import kotlinx.coroutines.launch
/**
@@ -52,50 +51,34 @@
@Inject
constructor(
@VolumePanelScope private val scope: CoroutineScope,
- castVolumeInteractor: CastVolumeInteractor,
mediaOutputInteractor: MediaOutputInteractor,
+ private val mediaDeviceSessionInteractor: MediaDeviceSessionInteractor,
private val streamSliderViewModelFactory: AudioStreamSliderViewModel.Factory,
private val castVolumeSliderViewModelFactory: CastVolumeSliderViewModel.Factory,
) {
- private val remoteSessionsViewModels: Flow<List<SliderViewModel>> =
- castVolumeInteractor.remoteRoutingSessions.transformLatest { routingSessions ->
- coroutineScope {
- emit(
- routingSessions.map { routingSession ->
- castVolumeSliderViewModelFactory.create(routingSession, this)
- }
- )
- }
- }
- private val streamViewModels: Flow<List<SliderViewModel>> =
- flowOf(
- listOf(
- AudioStream(AudioManager.STREAM_MUSIC),
- AudioStream(AudioManager.STREAM_VOICE_CALL),
- AudioStream(AudioManager.STREAM_RING),
- AudioStream(AudioManager.STREAM_NOTIFICATION),
- AudioStream(AudioManager.STREAM_ALARM),
- )
- )
- .transformLatest { streams ->
- coroutineScope {
- emit(
- streams.map { stream ->
- streamSliderViewModelFactory.create(
- AudioStreamSliderViewModel.FactoryAudioStreamWrapper(stream),
- this,
- )
- }
- )
- }
- }
-
val sliderViewModels: StateFlow<List<SliderViewModel>> =
- combine(remoteSessionsViewModels, streamViewModels) {
- remoteSessionsViewModels,
- streamViewModels ->
- remoteSessionsViewModels + streamViewModels
+ combineTransform(
+ mediaOutputInteractor.activeMediaDeviceSessions,
+ mediaOutputInteractor.defaultActiveMediaSession,
+ ) { activeSessions, defaultSession ->
+ coroutineScope {
+ val viewModels = buildList {
+ if (defaultSession?.isTheSameSession(activeSessions.remote) == true) {
+ addRemoteViewModelIfNeeded(this, activeSessions.remote)
+ addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+ } else {
+ addStreamViewModel(this, AudioManager.STREAM_MUSIC)
+ addRemoteViewModelIfNeeded(this, activeSessions.remote)
+ }
+
+ addStreamViewModel(this, AudioManager.STREAM_VOICE_CALL)
+ addStreamViewModel(this, AudioManager.STREAM_RING)
+ addStreamViewModel(this, AudioManager.STREAM_NOTIFICATION)
+ addStreamViewModel(this, AudioManager.STREAM_ALARM)
+ }
+ emit(viewModels)
+ }
}
.stateIn(scope, SharingStarted.Eagerly, emptyList())
@@ -103,12 +86,41 @@
val isExpanded: StateFlow<Boolean> =
merge(
- mutableIsExpanded.onStart { emit(false) },
- mediaOutputInteractor.mediaDeviceSession.map { !it.isPlaying() },
+ mutableIsExpanded,
+ mediaOutputInteractor.defaultActiveMediaSession.flatMapLatest {
+ if (it == null) flowOf(true)
+ else mediaDeviceSessionInteractor.playbackState(it).map { it?.isActive != true }
+ },
)
.stateIn(scope, SharingStarted.Eagerly, false)
fun onExpandedChanged(isExpanded: Boolean) {
scope.launch { mutableIsExpanded.emit(isExpanded) }
}
+
+ private fun CoroutineScope.addRemoteViewModelIfNeeded(
+ list: MutableList<SliderViewModel>,
+ remoteMediaDeviceSession: MediaDeviceSession?
+ ) {
+ if (remoteMediaDeviceSession?.canAdjustVolume == true) {
+ val viewModel =
+ castVolumeSliderViewModelFactory.create(
+ remoteMediaDeviceSession,
+ this,
+ )
+ list.add(viewModel)
+ }
+ }
+
+ private fun CoroutineScope.addStreamViewModel(
+ list: MutableList<SliderViewModel>,
+ stream: Int,
+ ) {
+ val viewModel =
+ streamSliderViewModelFactory.create(
+ AudioStreamSliderViewModel.FactoryAudioStreamWrapper(AudioStream(stream)),
+ this,
+ )
+ list.add(viewModel)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
index 206babf..09675e2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/complication/ComplicationViewModelTransformerTest.java
@@ -23,6 +23,7 @@
import android.testing.AndroidTestingRunner;
+import androidx.lifecycle.ViewModel;
import androidx.test.filters.SmallTest;
import com.android.systemui.SysuiTestCase;
@@ -56,7 +57,8 @@
MockitoAnnotations.initMocks(this);
when(mFactory.create(Mockito.any(), Mockito.any())).thenReturn(mComponent);
when(mComponent.getViewModelProvider()).thenReturn(mViewModelProvider);
- when(mViewModelProvider.get(Mockito.any(), Mockito.any())).thenReturn(mViewModel);
+ when(mViewModelProvider.get(Mockito.any(), Mockito.<Class<ViewModel>>any()))
+ .thenReturn(mViewModel);
}
/**
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
index 5dd37ae..66aa572 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/binder/KeyguardClockViewBinderTest.kt
@@ -131,7 +131,6 @@
whenever(clock.smallClock).thenReturn(smallClock)
whenever(largeClock.layout).thenReturn(largeClockFaceLayout)
whenever(smallClock.layout).thenReturn(smallClockFaceLayout)
- whenever(clockViewModel.clock).thenReturn(clock)
currentClock.value = clock
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
index a63b221..db0c0bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/people/widget/PeopleSpaceWidgetManagerTest.java
@@ -80,6 +80,8 @@
import android.app.people.PeopleManager;
import android.app.people.PeopleSpaceTile;
import android.appwidget.AppWidgetManager;
+import android.appwidget.AppWidgetProviderInfo;
+import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
import android.content.SharedPreferences;
@@ -101,11 +103,12 @@
import androidx.preference.PreferenceManager;
import androidx.test.filters.SmallTest;
-import com.android.systemui.res.R;
import com.android.systemui.SysuiTestCase;
import com.android.systemui.people.PeopleBackupFollowUpJob;
import com.android.systemui.people.PeopleSpaceUtils;
import com.android.systemui.people.SharedPreferencesHelper;
+import com.android.systemui.res.R;
+import com.android.systemui.settings.FakeUserTracker;
import com.android.systemui.statusbar.NotificationListener;
import com.android.systemui.statusbar.NotificationListener.NotificationHandler;
import com.android.systemui.statusbar.SbnBuilder;
@@ -265,6 +268,8 @@
private final FakeExecutor mFakeExecutor = new FakeExecutor(mClock);
+ private final FakeUserTracker mUserTracker = new FakeUserTracker();
+
@Before
public void setUp() throws Exception {
MockitoAnnotations.initMocks(this);
@@ -272,7 +277,7 @@
mManager = new PeopleSpaceWidgetManager(mContext, mAppWidgetManager, mIPeopleManager,
mPeopleManager, mLauncherApps, mNotifCollection, mPackageManager,
Optional.of(mBubbles), mUserManager, mBackupManager, mINotificationManager,
- mNotificationManager, mFakeExecutor);
+ mNotificationManager, mFakeExecutor, mUserTracker);
mManager.attach(mListenerService);
verify(mListenerService).addNotificationHandler(mListenerCaptor.capture());
@@ -309,6 +314,12 @@
.setId(1)
.setShortcutInfo(mShortcutInfo)
.build();
+
+ AppWidgetProviderInfo providerInfo = new AppWidgetProviderInfo();
+ providerInfo.provider = new ComponentName("com.android.systemui.tests",
+ "com.android.systemui.people.widget.PeopleSpaceWidgetProvider");
+ when(mAppWidgetManager.getInstalledProvidersForPackage(anyString(), any()))
+ .thenReturn(List.of(providerInfo));
}
@Test
@@ -1562,6 +1573,43 @@
String.valueOf(WIDGET_ID_WITH_KEY_IN_OPTIONS));
}
+ @Test
+ public void testUpdateGeneratedPreview_flagDisabled() {
+ mSetFlagsRule.disableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_userLocked() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(false);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(0)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_userUnlocked() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+ when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+ }
+
+ @Test
+ public void testUpdateGeneratedPreview_doesNotSetTwice() {
+ mSetFlagsRule.enableFlags(android.appwidget.flags.Flags.FLAG_GENERATED_PREVIEWS);
+ when(mUserManager.isUserUnlocked(mUserTracker.getUserHandle())).thenReturn(true);
+ when(mAppWidgetManager.setWidgetPreview(any(), anyInt(), any())).thenReturn(true);
+
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ mManager.updateGeneratedPreviewForUser(mUserTracker.getUserHandle());
+ verify(mAppWidgetManager, times(1)).setWidgetPreview(any(), anyInt(), any());
+ }
+
private void setFinalField(String fieldName, int value) {
try {
Field field = NotificationManager.Policy.class.getDeclaredField(fieldName);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
index cc48640..5c6ed70 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QSPanelTest.kt
@@ -21,6 +21,7 @@
import android.testing.ViewUtils
import android.view.ContextThemeWrapper
import android.view.View
+import android.view.ViewGroup
import android.view.ViewGroup.LayoutParams.MATCH_PARENT
import android.view.accessibility.AccessibilityNodeInfo
import android.widget.FrameLayout
@@ -71,7 +72,7 @@
qsPanel = QSPanel(themedContext, null)
qsPanel.mUsingMediaPlayer = true
- qsPanel.initialize(qsLogger)
+ qsPanel.initialize(qsLogger, true)
// QSPanel inflates a footer inside of it, mocking it here
footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
qsPanel.addView(footer, MATCH_PARENT, 100)
@@ -218,6 +219,62 @@
verify(tile).addCallback(record.callback)
}
+ @Test
+ fun initializedWithNoMedia_tileLayoutParentIsAlwaysQsPanel() {
+ lateinit var panel: QSPanel
+ lateinit var tileLayout: View
+ testableLooper.runWithLooper {
+ panel = QSPanel(themedContext, null)
+ panel.mUsingMediaPlayer = true
+
+ panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+ tileLayout = panel.orCreateTileLayout as View
+ // QSPanel inflates a footer inside of it, mocking it here
+ footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+ panel.addView(footer, MATCH_PARENT, 100)
+ panel.onFinishInflate()
+ // Provides a parent with non-zero size for QSPanel
+ ViewUtils.attachView(panel)
+ }
+ val mockMediaHost = mock(ViewGroup::class.java)
+
+ panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+
+ assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+ panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+ assertThat(tileLayout.parent).isSameInstanceAs(panel)
+
+ ViewUtils.detachView(panel)
+ }
+
+ @Test
+ fun initializeWithNoMedia_mediaNeverAttached() {
+ lateinit var panel: QSPanel
+ testableLooper.runWithLooper {
+ panel = QSPanel(themedContext, null)
+ panel.mUsingMediaPlayer = true
+
+ panel.initialize(qsLogger, /* usingMediaPlayer= */ false)
+ panel.orCreateTileLayout as View
+ // QSPanel inflates a footer inside of it, mocking it here
+ footer = LinearLayout(themedContext).apply { id = R.id.qs_footer }
+ panel.addView(footer, MATCH_PARENT, 100)
+ panel.onFinishInflate()
+ // Provides a parent with non-zero size for QSPanel
+ ViewUtils.attachView(panel)
+ }
+ val mockMediaHost = FrameLayout(themedContext)
+
+ panel.setUsingHorizontalLayout(false, mockMediaHost, true)
+ assertThat(mockMediaHost.parent).isNull()
+
+ panel.setUsingHorizontalLayout(true, mockMediaHost, true)
+ assertThat(mockMediaHost.parent).isNull()
+
+ ViewUtils.detachView(panel)
+ }
+
private infix fun View.isLeftOf(other: View): Boolean {
val rect = Rect()
getBoundsOnScreen(rect)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
index 3fba393..e5369fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/QuickQSPanelTest.kt
@@ -36,7 +36,7 @@
testableLooper.runWithLooper {
quickQSPanel = QuickQSPanel(mContext, null)
- quickQSPanel.initialize(qsLogger)
+ quickQSPanel.initialize(qsLogger, true)
quickQSPanel.onFinishInflate()
// Provides a parent with non-zero size for QSPanel
diff --git a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
index 10d6ebf..1313227 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/recents/OverviewProxyServiceTest.kt
@@ -21,7 +21,7 @@
import android.content.pm.PackageManager
import android.content.pm.ResolveInfo
import android.os.PowerManager
-import android.os.Process;
+import android.os.Process
import android.os.UserHandle
import android.testing.AndroidTestingRunner
import android.testing.TestableContext
@@ -34,8 +34,6 @@
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
-import com.android.systemui.flags.FakeFeatureFlags
-import com.android.systemui.flags.Flags
import com.android.systemui.keyguard.KeyguardUnlockAnimationController
import com.android.systemui.keyguard.WakefulnessLifecycle
import com.android.systemui.keyguard.ui.view.InWindowLauncherUnlockAnimationManager
@@ -96,7 +94,6 @@
private val displayTracker = FakeDisplayTracker(mContext)
private val fakeSystemClock = FakeSystemClock()
private val sysUiState = SysUiState(displayTracker, kosmos.sceneContainerPlugin)
- private val featureFlags = FakeFeatureFlags()
private val wakefulnessLifecycle =
WakefulnessLifecycle(mContext, null, fakeSystemClock, dumpManager)
@@ -121,8 +118,7 @@
@Mock
private lateinit var unfoldTransitionProgressForwarder:
Optional<UnfoldTransitionProgressForwarder>
- @Mock
- private lateinit var broadcastDispatcher: BroadcastDispatcher
+ @Mock private lateinit var broadcastDispatcher: BroadcastDispatcher
@Before
fun setUp() {
@@ -205,16 +201,14 @@
@Test
fun connectToOverviewService_primaryUser_expectBindService() {
- val mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Process::class.java)
- .startMocking()
+ val mockitoSession =
+ ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
try {
`when`(Process.myUserHandle()).thenReturn(UserHandle.SYSTEM)
val spyContext = spy(context)
val ops = createOverviewProxyService(spyContext)
ops.startConnectionToCurrentUser()
- verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(),
- anyInt(), any())
+ verify(spyContext, atLeast(1)).bindServiceAsUser(any(), any(), anyInt(), any())
} finally {
mockitoSession.finishMocking()
}
@@ -222,22 +216,20 @@
@Test
fun connectToOverviewService_nonPrimaryUser_expectNoBindService() {
- val mockitoSession = ExtendedMockito.mockitoSession()
- .spyStatic(Process::class.java)
- .startMocking()
+ val mockitoSession =
+ ExtendedMockito.mockitoSession().spyStatic(Process::class.java).startMocking()
try {
`when`(Process.myUserHandle()).thenReturn(UserHandle.of(12345))
val spyContext = spy(context)
val ops = createOverviewProxyService(spyContext)
ops.startConnectionToCurrentUser()
- verify(spyContext, times(0)).bindServiceAsUser(any(), any(),
- anyInt(), any())
+ verify(spyContext, times(0)).bindServiceAsUser(any(), any(), anyInt(), any())
} finally {
mockitoSession.finishMocking()
}
}
- private fun createOverviewProxyService(ctx: Context) : OverviewProxyService {
+ private fun createOverviewProxyService(ctx: Context): OverviewProxyService {
return OverviewProxyService(
ctx,
executor,
@@ -257,7 +249,6 @@
sysuiUnlockAnimationController,
inWindowLauncherUnlockAnimationManager,
assistUtils,
- featureFlags,
FakeSceneContainerFlags(),
dumpManager,
unfoldTransitionProgressForwarder,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
index d5c4053..8e8dd4d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/CentralSurfacesCommandQueueCallbacksTest.java
@@ -188,7 +188,7 @@
public void vibrateOnNavigationKeyDown_usesPerformHapticFeedback() {
mSbcqCallbacks.vibrateOnNavigationKeyDown();
- verify(mShadeViewController).performHapticFeedback(
+ verify(mShadeController).performHapticFeedback(
HapticFeedbackConstants.GESTURE_START
);
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
index 6ef7419..ba07a84 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/biometrics/data/repository/FacePropertyRepositoryKosmos.kt
@@ -19,4 +19,5 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
-val Kosmos.facePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.fakeFacePropertyRepository by Fixture { FakeFacePropertyRepository() }
+val Kosmos.facePropertyRepository by Fixture { fakeFacePropertyRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
index 27803b2..c065545 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/BouncerInteractorKosmos.kt
@@ -16,7 +16,6 @@
package com.android.systemui.bouncer.domain.interactor
-import android.content.applicationContext
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.classifier.domain.interactor.falsingInteractor
@@ -29,12 +28,10 @@
val Kosmos.bouncerInteractor by Fixture {
BouncerInteractor(
applicationScope = testScope.backgroundScope,
- applicationContext = applicationContext,
repository = bouncerRepository,
authenticationInteractor = authenticationInteractor,
deviceEntryFaceAuthInteractor = deviceEntryFaceAuthInteractor,
falsingInteractor = falsingInteractor,
powerInteractor = powerInteractor,
- simBouncerInteractor = simBouncerInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
index 8ed9f45..02b79af 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/domain/interactor/SimBouncerInteractorKosmos.kt
@@ -38,7 +38,7 @@
telephonyManager = telephonyManager,
resources = mainResources,
keyguardUpdateMonitor = keyguardUpdateMonitor,
- euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager,
+ euiccManager = applicationContext.getSystemService(Context.EUICC_SERVICE) as EuiccManager?,
mobileConnectionsRepository = mobileConnectionsRepository,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
new file mode 100644
index 0000000..4b64416
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerMessageViewModelKosmos.kt
@@ -0,0 +1,51 @@
+/*
+ * 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.bouncer.ui.viewmodel
+
+import android.content.applicationContext
+import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
+import com.android.systemui.bouncer.shared.flag.composeBouncerFlags
+import com.android.systemui.deviceentry.domain.interactor.biometricMessageInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFaceAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryFingerprintAuthInteractor
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
+import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.bouncerMessageViewModel by
+ Kosmos.Fixture {
+ BouncerMessageViewModel(
+ applicationContext = applicationContext,
+ applicationScope = testScope.backgroundScope,
+ bouncerInteractor = bouncerInteractor,
+ simBouncerInteractor = simBouncerInteractor,
+ authenticationInteractor = authenticationInteractor,
+ selectedUser = userSwitcherViewModel.selectedUser,
+ clock = systemClock,
+ biometricMessageInteractor = biometricMessageInteractor,
+ faceAuthInteractor = deviceEntryFaceAuthInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ fingerprintInteractor = deviceEntryFingerprintAuthInteractor,
+ flags = composeBouncerFlags,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
index 6d97238..0f6c7cf 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bouncer/ui/viewmodel/BouncerViewModelKosmos.kt
@@ -14,6 +14,8 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.bouncer.ui.viewmodel
import android.content.applicationContext
@@ -30,7 +32,7 @@
import com.android.systemui.user.domain.interactor.selectedUserInteractor
import com.android.systemui.user.ui.viewmodel.userSwitcherViewModel
import com.android.systemui.util.mockito.mock
-import com.android.systemui.util.time.systemClock
+import kotlinx.coroutines.ExperimentalCoroutinesApi
val Kosmos.bouncerViewModel by Fixture {
BouncerViewModel(
@@ -47,7 +49,7 @@
users = userSwitcherViewModel.users,
userSwitcherMenu = userSwitcherViewModel.menu,
actionButton = bouncerActionButtonInteractor.actionButton,
- clock = systemClock,
devicePolicyManager = mock(),
+ bouncerMessageViewModel = bouncerMessageViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.kt
new file mode 100644
index 0000000..f389142
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/DreamingToGoneTransitionViewModelKosmos.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.keyguard.ui.viewmodel
+
+import com.android.systemui.keyguard.ui.keyguardTransitionAnimationFlow
+import com.android.systemui.kosmos.Kosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+val Kosmos.dreamingToGoneTransitionViewModel by
+ Kosmos.Fixture {
+ DreamingToGoneTransitionViewModel(
+ animationFlow = keyguardTransitionAnimationFlow,
+ )
+ }
\ No newline at end of file
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index a863edf..a84899e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -46,10 +46,12 @@
dozingToGoneTransitionViewModel = dozingToGoneTransitionViewModel,
dozingToLockscreenTransitionViewModel = dozingToLockscreenTransitionViewModel,
dozingToOccludedTransitionViewModel = dozingToOccludedTransitionViewModel,
+ dreamingToGoneTransitionViewModel = dreamingToGoneTransitionViewModel,
dreamingToLockscreenTransitionViewModel = dreamingToLockscreenTransitionViewModel,
glanceableHubToLockscreenTransitionViewModel = glanceableHubToLockscreenTransitionViewModel,
goneToAodTransitionViewModel = goneToAodTransitionViewModel,
goneToDozingTransitionViewModel = goneToDozingTransitionViewModel,
+ goneToDreamingTransitionViewModel = goneToDreamingTransitionViewModel,
lockscreenToAodTransitionViewModel = lockscreenToAodTransitionViewModel,
lockscreenToDozingTransitionViewModel = lockscreenToDozingTransitionViewModel,
lockscreenToDreamingTransitionViewModel = lockscreenToDreamingTransitionViewModel,
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
index f4acf4d..16c5b72 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeControllerKosmos.kt
@@ -31,6 +31,7 @@
import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.CommandQueue
import com.android.systemui.statusbar.NotificationShadeWindowController
+import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.notification.row.NotificationGutsManager
import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayout
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.windowRootViewVisibilityInteractor
@@ -52,6 +53,7 @@
notificationStackScrollLayout = mock<NotificationStackScrollLayout>(),
deviceEntryInteractor = deviceEntryInteractor,
touchLog = mock<LogBuffer>(),
+ vibratorHelper = mock<VibratorHelper>(),
commandQueue = mock<CommandQueue>(),
statusBarKeyguardViewManager = mock<StatusBarKeyguardViewManager>(),
notificationShadeWindowController = mock<NotificationShadeWindowController>(),
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
index 546a1e0..5605d10 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/NotificationStackAppearanceInteractorKosmos.kt
@@ -18,10 +18,12 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.statusbar.notification.stack.data.repository.notificationStackAppearanceRepository
val Kosmos.notificationStackAppearanceInteractor by Fixture {
NotificationStackAppearanceInteractor(
repository = notificationStackAppearanceRepository,
+ shadeInteractor = shadeInteractor,
)
}
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
new file mode 100644
index 0000000..5db1724
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/MediaControllerKosmos.kt
@@ -0,0 +1,82 @@
+/*
+ * 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
+
+import android.content.packageManager
+import android.content.pm.ApplicationInfo
+import android.media.AudioAttributes
+import android.media.session.MediaController
+import android.media.session.MediaSession
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.util.mockito.any
+import com.android.systemui.util.mockito.eq
+import com.android.systemui.util.mockito.mock
+import com.android.systemui.util.mockito.whenever
+
+private const val LOCAL_PACKAGE = "local.test.pkg"
+var Kosmos.localMediaController: MediaController by
+ Kosmos.Fixture {
+ val appInfo: ApplicationInfo = mock {
+ whenever(loadLabel(any())).thenReturn("local_media_controller_label")
+ }
+ whenever(packageManager.getApplicationInfo(eq(LOCAL_PACKAGE), any<Int>()))
+ .thenReturn(appInfo)
+
+ 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(sessionToken).thenReturn(localSessionToken)
+ }
+ }
+
+private const val REMOTE_PACKAGE = "remote.test.pkg"
+var Kosmos.remoteMediaController: MediaController by
+ Kosmos.Fixture {
+ val appInfo: ApplicationInfo = mock {
+ whenever(loadLabel(any())).thenReturn("remote_media_controller_label")
+ }
+ whenever(packageManager.getApplicationInfo(eq(REMOTE_PACKAGE), any<Int>()))
+ .thenReturn(appInfo)
+
+ 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(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 3938f77..fa3a19b 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
@@ -18,7 +18,6 @@
import android.content.packageManager
import android.content.pm.ApplicationInfo
-import android.media.session.MediaController
import android.os.Handler
import android.testing.TestableLooper
import com.android.systemui.kosmos.Kosmos
@@ -32,11 +31,10 @@
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
-var Kosmos.mediaController: MediaController by Kosmos.Fixture { mock {} }
-
val Kosmos.localMediaRepository by Kosmos.Fixture { FakeLocalMediaRepository() }
val Kosmos.localMediaRepositoryFactory: LocalMediaRepositoryFactory by
Kosmos.Fixture { FakeLocalMediaRepositoryFactory { localMediaRepository } }
@@ -56,6 +54,14 @@
},
testScope.backgroundScope,
testScope.testScheduler,
+ mediaControllerRepository,
+ )
+ }
+
+val Kosmos.mediaDeviceSessionInteractor by
+ Kosmos.Fixture {
+ MediaDeviceSessionInteractor(
+ testScope.testScheduler,
Handler(TestableLooper.get(testCase).looper),
mediaControllerRepository,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
index 284bd55..909be75 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeLocalMediaRepository.kt
@@ -17,7 +17,6 @@
package com.android.systemui.volume.data.repository
import com.android.settingslib.media.MediaDevice
-import com.android.settingslib.volume.data.model.RoutingSession
import com.android.settingslib.volume.data.repository.LocalMediaRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
@@ -25,35 +24,11 @@
class FakeLocalMediaRepository : LocalMediaRepository {
- private val volumeBySession: MutableMap<String?, Int> = mutableMapOf()
-
- private val mutableMediaDevices = MutableStateFlow<List<MediaDevice>>(emptyList())
- override val mediaDevices: StateFlow<List<MediaDevice>>
- get() = mutableMediaDevices.asStateFlow()
-
private val mutableCurrentConnectedDevice = MutableStateFlow<MediaDevice?>(null)
override val currentConnectedDevice: StateFlow<MediaDevice?>
get() = mutableCurrentConnectedDevice.asStateFlow()
- private val mutableRemoteRoutingSessions = MutableStateFlow<List<RoutingSession>>(emptyList())
- override val remoteRoutingSessions: StateFlow<List<RoutingSession>>
- get() = mutableRemoteRoutingSessions.asStateFlow()
-
- fun updateMediaDevices(devices: List<MediaDevice>) {
- mutableMediaDevices.value = devices
- }
-
fun updateCurrentConnectedDevice(device: MediaDevice?) {
mutableCurrentConnectedDevice.value = device
}
-
- fun updateRemoteRoutingSessions(sessions: List<RoutingSession>) {
- mutableRemoteRoutingSessions.value = sessions
- }
-
- fun getSessionVolume(sessionId: String?): Int = volumeBySession.getOrDefault(sessionId, 0)
-
- override suspend fun adjustSessionVolume(sessionId: String?, volume: Int) {
- volumeBySession[sessionId] = volume
- }
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
index 6d52e52..8ab5bd90 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeMediaControllerRepository.kt
@@ -24,11 +24,11 @@
class FakeMediaControllerRepository : MediaControllerRepository {
- private val mutableActiveLocalMediaController = MutableStateFlow<MediaController?>(null)
- override val activeLocalMediaController: StateFlow<MediaController?> =
- mutableActiveLocalMediaController.asStateFlow()
+ private val mutableActiveSessions = MutableStateFlow<List<MediaController>>(emptyList())
+ override val activeSessions: StateFlow<List<MediaController>>
+ get() = mutableActiveSessions.asStateFlow()
- fun setActiveLocalMediaController(controller: MediaController?) {
- mutableActiveLocalMediaController.value = controller
+ fun setActiveSessions(sessions: List<MediaController>) {
+ mutableActiveSessions.value = sessions
}
}
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
index 4e14dee..880a687 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityManagerService.java
@@ -993,6 +993,12 @@
intent.getStringExtra(Intent.EXTRA_SETTING_PREVIOUS_VALUE),
intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
}
+ } else if (Settings.Secure.ACCESSIBILITY_QS_TARGETS.equals(which)) {
+ if (!android.view.accessibility.Flags.a11yQsShortcut()) {
+ return;
+ }
+ restoreAccessibilityQsTargets(
+ intent.getStringExtra(Intent.EXTRA_SETTING_NEW_VALUE));
}
}
}
@@ -2131,6 +2137,29 @@
onUserStateChangedLocked(userState);
}
+ /**
+ * User could configure accessibility shortcut during the SUW before restoring user data.
+ * Merges the current value and the new value to make sure we don't lost the setting the user's
+ * preferences of accessibility qs shortcut updated in SUW are not lost.
+ *
+ * Called only during settings restore; currently supports only the owner user
+ * TODO: http://b/22388012
+ */
+ private void restoreAccessibilityQsTargets(String newValue) {
+ synchronized (mLock) {
+ final AccessibilityUserState userState = getUserStateLocked(UserHandle.USER_SYSTEM);
+ final Set<String> mergedTargets = userState.getA11yQsTargets();
+ readColonDelimitedStringToSet(newValue, str -> str, mergedTargets,
+ /* doMerge = */ true);
+
+ userState.updateA11yQsTargetLocked(mergedTargets);
+ persistColonDelimitedSetToSettingLocked(Settings.Secure.ACCESSIBILITY_QS_TARGETS,
+ UserHandle.USER_SYSTEM, mergedTargets, str -> str);
+ scheduleNotifyClientsOfServicesStateChangeLocked(userState);
+ onUserStateChangedLocked(userState);
+ }
+ }
+
private int getClientStateLocked(AccessibilityUserState userState) {
return userState.getClientStateLocked(
mUiAutomationManager.canIntrospect(),
diff --git a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
index 9a1d379..7008e8e 100644
--- a/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
+++ b/services/accessibility/java/com/android/server/accessibility/AccessibilityUserState.java
@@ -112,6 +112,10 @@
* TileService's or the a11y framework tile component names (e.g.
* {@link AccessibilityShortcutController#COLOR_INVERSION_TILE_COMPONENT_NAME}) instead of the
* A11y Feature's component names.
+ * <p/>
+ * In addition, {@link #mA11yTilesInQsPanel} stores what's on the QS Panel, whereas
+ * {@link #mAccessibilityQsTargets} stores the targets that configured qs as their shortcut and
+ * also grant full device control permission.
*/
private final ArraySet<ComponentName> mA11yTilesInQsPanel = new ArraySet<>();
diff --git a/services/core/java/com/android/server/display/AutomaticBrightnessController.java b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
index 4032514..4aab9d2 100644
--- a/services/core/java/com/android/server/display/AutomaticBrightnessController.java
+++ b/services/core/java/com/android/server/display/AutomaticBrightnessController.java
@@ -54,6 +54,7 @@
import com.android.internal.os.BackgroundThread;
import com.android.server.EventLogTags;
import com.android.server.display.brightness.BrightnessEvent;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
import java.io.PrintWriter;
import java.lang.annotation.Retention;
@@ -252,6 +253,7 @@
// Controls Brightness range (including High Brightness Mode).
private final BrightnessRangeController mBrightnessRangeController;
+ private final BrightnessClamperController mBrightnessClamperController;
// Throttles (caps) maximum allowed brightness
private final BrightnessThrottler mBrightnessThrottler;
@@ -287,7 +289,8 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessModeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
this(new Injector(), callbacks, looper, sensorManager, lightSensor,
brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin, brightnessMax,
dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -297,7 +300,7 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, context, brightnessModeController,
brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
- userNits
+ userNits, brightnessClamperController
);
}
@@ -313,9 +316,10 @@
HysteresisLevels screenBrightnessThresholds,
HysteresisLevels ambientBrightnessThresholdsIdle,
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
- BrightnessRangeController brightnessModeController,
+ BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
mInjector = injector;
mClock = injector.createClock();
mContext = context;
@@ -358,7 +362,8 @@
mPendingForegroundAppPackageName = null;
mForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
mPendingForegroundAppCategory = ApplicationInfo.CATEGORY_UNDEFINED;
- mBrightnessRangeController = brightnessModeController;
+ mBrightnessRangeController = brightnessRangeController;
+ mBrightnessClamperController = brightnessClamperController;
mBrightnessThrottler = brightnessThrottler;
mBrightnessMappingStrategyMap = brightnessMappingStrategyMap;
@@ -791,7 +796,7 @@
mAmbientBrightnessThresholds.getDarkeningThreshold(lux);
}
mBrightnessRangeController.onAmbientLuxChange(mAmbientLux);
-
+ mBrightnessClamperController.onAmbientLuxChange(mAmbientLux);
// If the short term model was invalidated and the change is drastic enough, reset it.
mShortTermModel.maybeReset(mAmbientLux);
diff --git a/services/core/java/com/android/server/display/DisplayDeviceConfig.java b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
index 4116669..04e7f77 100644
--- a/services/core/java/com/android/server/display/DisplayDeviceConfig.java
+++ b/services/core/java/com/android/server/display/DisplayDeviceConfig.java
@@ -61,6 +61,7 @@
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholds;
import com.android.server.display.config.IntegerArray;
+import com.android.server.display.config.LowBrightnessData;
import com.android.server.display.config.LuxThrottling;
import com.android.server.display.config.NitsMap;
import com.android.server.display.config.NonNegativeFloatToFloatPoint;
@@ -555,6 +556,24 @@
* <majorVersion>2</majorVersion>
* <minorVersion>0</minorVersion>
* </usiVersion>
+ * <lowBrightness enabled="true">
+ * <transitionPoint>0.1</transitionPoint>
+ *
+ * <nits>0.2</nits>
+ * <nits>2.0</nits>
+ * <nits>500.0</nits>
+ * <nits>1000.0</nits>
+ *
+ * <backlight>0</backlight>
+ * <backlight>0.0001</backlight>
+ * <backlight>0.5</backlight>
+ * <backlight>1.0</backlight>
+ *
+ * <brightness>0</brightness>
+ * <brightness>0.1</brightness>
+ * <brightness>0.5</brightness>
+ * <brightness>1.0</brightness>
+ * </lowBrightness>
* <screenBrightnessCapForWearBedtimeMode>0.1</screenBrightnessCapForWearBedtimeMode>
* <idleScreenRefreshRateTimeout>
* <luxThresholds>
@@ -568,6 +587,8 @@
* </point>
* </luxThresholds>
* </idleScreenRefreshRateTimeout>
+ *
+ *
* </displayConfiguration>
* }
* </pre>
@@ -732,6 +753,7 @@
private Spline mBacklightToBrightnessSpline;
private Spline mBacklightToNitsSpline;
private Spline mNitsToBacklightSpline;
+
private List<String> mQuirks;
private boolean mIsHighBrightnessModeEnabled = false;
private HighBrightnessModeData mHbmData;
@@ -872,6 +894,9 @@
@Nullable
private HdrBrightnessData mHdrBrightnessData;
+ @Nullable
+ public LowBrightnessData mLowBrightnessData;
+
/**
* Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
*/
@@ -1814,6 +1839,15 @@
}
/**
+ *
+ * @return true if low brightness mode is enabled
+ */
+ @VisibleForTesting
+ public boolean getLbmEnabled() {
+ return mLowBrightnessData != null;
+ }
+
+ /**
* @return Maximum screen brightness setting when screen brightness capped in Wear Bedtime mode.
*/
public float getBrightnessCapForWearBedtimeMode() {
@@ -1952,6 +1986,8 @@
+ "mUsiVersion= " + mHostUsiVersion + "\n"
+ "mHdrBrightnessData= " + mHdrBrightnessData + "\n"
+ "mBrightnessCapForWearBedtimeMode= " + mBrightnessCapForWearBedtimeMode
+ + "\n"
+ + (mLowBrightnessData != null ? mLowBrightnessData.toString() : "")
+ "}";
}
@@ -2002,6 +2038,9 @@
loadDensityMapping(config);
loadBrightnessDefaultFromDdcXml(config);
loadBrightnessConstraintsFromConfigXml();
+ if (mFlags.isEvenDimmerEnabled()) {
+ mLowBrightnessData = LowBrightnessData.loadConfig(config);
+ }
loadBrightnessMap(config);
loadThermalThrottlingConfig(config);
loadPowerThrottlingConfigData(config);
@@ -2793,6 +2832,18 @@
// These splines are used to convert from the system brightness value to the HAL backlight
// value
private void createBacklightConversionSplines() {
+ if (mLowBrightnessData != null) {
+ mBrightnessToBacklightSpline = mLowBrightnessData.mBrightnessToBacklight;
+ mBacklightToBrightnessSpline = mLowBrightnessData.mBacklightToBrightness;
+ mBacklightToNitsSpline = mLowBrightnessData.mBacklightToNits;
+ mNitsToBacklightSpline = mLowBrightnessData.mNitsToBacklight;
+
+ mNits = mLowBrightnessData.mNits;
+ mBrightness = mLowBrightnessData.mBrightness;
+ mBacklight = mLowBrightnessData.mBacklight;
+ return;
+ }
+
mBrightness = new float[mBacklight.length];
for (int i = 0; i < mBrightness.length; i++) {
mBrightness[i] = MathUtils.map(mBacklight[0],
diff --git a/services/core/java/com/android/server/display/DisplayPowerController.java b/services/core/java/com/android/server/display/DisplayPowerController.java
index 87d017c..90ad8c0 100644
--- a/services/core/java/com/android/server/display/DisplayPowerController.java
+++ b/services/core/java/com/android/server/display/DisplayPowerController.java
@@ -1165,7 +1165,8 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, mContext, mBrightnessRangeController,
mBrightnessThrottler, mDisplayDeviceConfig.getAmbientHorizonShort(),
- mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits);
+ mDisplayDeviceConfig.getAmbientHorizonLong(), userLux, userNits,
+ mBrightnessClamperController);
mDisplayBrightnessController.setAutomaticBrightnessController(
mAutomaticBrightnessController);
@@ -2479,6 +2480,7 @@
public void setBrightnessToFollow(float leadDisplayBrightness, float nits, float ambientLux,
boolean slowChange) {
mBrightnessRangeController.onAmbientLuxChange(ambientLux);
+ mBrightnessClamperController.onAmbientLuxChange(ambientLux);
if (nits == BrightnessMappingStrategy.INVALID_NITS) {
mDisplayBrightnessController.setBrightnessToFollow(leadDisplayBrightness, slowChange);
} else {
@@ -3176,7 +3178,9 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessModeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
+
return new AutomaticBrightnessController(callbacks, looper, sensorManager, lightSensor,
brightnessMappingStrategyMap, lightSensorWarmUpTime, brightnessMin,
brightnessMax, dozeScaleFactor, lightSensorRate, initialLightSensorRate,
@@ -3186,7 +3190,7 @@
screenBrightnessThresholds, ambientBrightnessThresholdsIdle,
screenBrightnessThresholdsIdle, context, brightnessModeController,
brightnessThrottler, ambientLightHorizonShort, ambientLightHorizonLong, userLux,
- userNits);
+ userNits, brightnessClamperController);
}
BrightnessMappingStrategy getDefaultModeBrightnessMapper(Context context,
diff --git a/services/core/java/com/android/server/display/LocalDisplayAdapter.java b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
index b2fd9ed..3b3a03b 100644
--- a/services/core/java/com/android/server/display/LocalDisplayAdapter.java
+++ b/services/core/java/com/android/server/display/LocalDisplayAdapter.java
@@ -37,6 +37,7 @@
import android.os.Trace;
import android.util.DisplayUtils;
import android.util.LongSparseArray;
+import android.util.MathUtils;
import android.util.Slog;
import android.util.SparseArray;
import android.view.Display;
@@ -78,6 +79,13 @@
private static final String UNIQUE_ID_PREFIX = "local:";
private static final String PROPERTY_EMULATOR_CIRCULAR = "ro.boot.emulator.circular";
+ // Min and max strengths for even dimmer feature.
+ private static final float EVEN_DIMMER_MIN_STRENGTH = 0.0f;
+ private static final float EVEN_DIMMER_MAX_STRENGTH = 70.0f; // not too dim yet.
+ private static final float BRIGHTNESS_MIN = 0.0f;
+ // The brightness at which we start using color matrices rather than backlight,
+ // to dim the display
+ private static final float BACKLIGHT_COLOR_TRANSITION_POINT = 0.1f;
private final LongSparseArray<LocalDisplayDevice> mDevices = new LongSparseArray<>();
@@ -91,6 +99,8 @@
private Context mOverlayContext;
+ private int mEvenDimmerStrength = -1;
+
// Called with SyncRoot lock held.
LocalDisplayAdapter(DisplayManagerService.SyncRoot syncRoot, Context context,
Handler handler, Listener listener, DisplayManagerFlags flags,
@@ -928,6 +938,10 @@
final float nits = backlightToNits(backlight);
final float sdrNits = backlightToNits(sdrBacklight);
+ if (getFeatureFlags().isEvenDimmerEnabled()) {
+ applyColorMatrixBasedDimming(brightnessState);
+ }
+
mBacklightAdapter.setBacklight(sdrBacklight, sdrNits, backlight, nits);
Trace.traceCounter(Trace.TRACE_TAG_POWER,
"ScreenBrightness",
@@ -974,6 +988,22 @@
}
}
}
+
+ private void applyColorMatrixBasedDimming(float brightnessState) {
+ int strength = (int) (MathUtils.constrainedMap(
+ EVEN_DIMMER_MAX_STRENGTH, EVEN_DIMMER_MIN_STRENGTH, // to this range
+ BRIGHTNESS_MIN, BACKLIGHT_COLOR_TRANSITION_POINT, // from this range
+ brightnessState) + 0.5); // map this (+ rounded up)
+
+ if (mEvenDimmerStrength < 0 // uninitialised
+ || MathUtils.abs(mEvenDimmerStrength - strength) > 1
+ || strength <= 1) {
+ mEvenDimmerStrength = strength;
+ }
+
+ // TODO: use `enabled` and `mRbcStrength` to set color matrices here
+ // TODO: boolean enabled = mEvenDimmerStrength > 0.0f;
+ }
};
}
return null;
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
index 18e8fab..d8a4500 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessClamperController.java
@@ -189,6 +189,13 @@
mModifiers.forEach(BrightnessStateModifier::stop);
}
+ /**
+ * Notifies modifiers that ambient lux has changed.
+ * @param ambientLux current lux, debounced
+ */
+ public void onAmbientLuxChange(float ambientLux) {
+ mModifiers.forEach(modifier -> modifier.onAmbientLuxChange(ambientLux));
+ }
// Called in DisplayControllerHandler
private void recalculateBrightnessCap() {
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
index 7f1f7a9..a91bb59 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessLowLuxModifier.java
@@ -39,21 +39,21 @@
* Class used to prevent the screen brightness dipping below a certain value, based on current
* lux conditions and user preferred minimum.
*/
-public class BrightnessLowLuxModifier implements
- BrightnessStateModifier {
+public class BrightnessLowLuxModifier extends BrightnessModifier {
// To enable these logs, run:
// 'adb shell setprop persist.log.tag.BrightnessLowLuxModifier DEBUG && adb reboot'
private static final String TAG = "BrightnessLowLuxModifier";
private static final boolean DEBUG = DebugUtils.isDebuggable(TAG);
+ private static final float MIN_NITS = 2.0f;
private final SettingsObserver mSettingsObserver;
private final ContentResolver mContentResolver;
private final Handler mHandler;
private final BrightnessClamperController.ClamperChangeListener mChangeListener;
- protected float mSettingNitsLowerBound = PowerManager.BRIGHTNESS_MIN;
private int mReason;
private float mBrightnessLowerBound;
private boolean mIsActive;
+ private float mAmbientLux;
@VisibleForTesting
BrightnessLowLuxModifier(Handler handler,
@@ -78,17 +78,17 @@
int userId = UserHandle.USER_CURRENT;
float settingNitsLowerBound = Settings.Secure.getFloatForUser(
mContentResolver, Settings.Secure.EVEN_DIMMER_MIN_NITS,
- /* def= */ PowerManager.BRIGHTNESS_MIN, userId);
+ /* def= */ MIN_NITS, userId);
- boolean isActive = Settings.Secure.getIntForUser(mContentResolver,
+ boolean isActive = Settings.Secure.getFloatForUser(mContentResolver,
Settings.Secure.EVEN_DIMMER_ACTIVATED,
- /* def= */ 0, userId) == 1;
+ /* def= */ 0, userId) == 1.0f;
- // TODO: luxBasedNitsLowerBound = mMinNitsToLuxSpline(currentLux);
- float luxBasedNitsLowerBound = 0.0f;
+ // TODO: luxBasedNitsLowerBound = mMinLuxToNitsSpline(currentLux);
+ float luxBasedNitsLowerBound = 2.0f;
- // TODO: final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
- // luxBasedNitsLowerBound) : PowerManager.BRIGHTNESS_MIN;
+ final float nitsLowerBound = isActive ? Math.max(settingNitsLowerBound,
+ luxBasedNitsLowerBound) : MIN_NITS;
final int reason = settingNitsLowerBound > luxBasedNitsLowerBound
? BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND
@@ -104,8 +104,13 @@
mReason = reason;
if (DEBUG) {
Slog.i(TAG, "isActive: " + isActive
- + ", settingNitsLowerBound: " + settingNitsLowerBound
- + ", lowerBound: " + brightnessLowerBound);
+ + ", brightnessLowerBound: " + brightnessLowerBound
+ + ", mAmbientLux: " + mAmbientLux
+ + ", mReason: " + (
+ mReason == BrightnessReason.MODIFIER_MIN_USER_SET_LOWER_BOUND ? "minSetting"
+ : "lux")
+ + ", nitsLowerBound: " + nitsLowerBound
+ );
}
mBrightnessLowerBound = brightnessLowerBound;
mChangeListener.onChanged();
@@ -132,6 +137,22 @@
}
@Override
+ boolean shouldApply(DisplayManagerInternal.DisplayPowerRequest request) {
+ return mIsActive;
+ }
+
+ @Override
+ float getBrightnessAdjusted(float currentBrightness,
+ DisplayManagerInternal.DisplayPowerRequest request) {
+ return Math.max(mBrightnessLowerBound, currentBrightness);
+ }
+
+ @Override
+ int getModifier() {
+ return mReason;
+ }
+
+ @Override
public void apply(DisplayManagerInternal.DisplayPowerRequest request,
DisplayBrightnessState.Builder stateBuilder) {
stateBuilder.setMinBrightness(mBrightnessLowerBound);
@@ -150,10 +171,16 @@
}
@Override
+ public void onAmbientLuxChange(float ambientLux) {
+ mAmbientLux = ambientLux;
+ recalculateLowerBound();
+ }
+
+ @Override
public void dump(PrintWriter pw) {
pw.println("BrightnessLowLuxModifier:");
- pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound);
pw.println(" mIsActive=" + mIsActive);
+ pw.println(" mBrightnessLowerBound=" + mBrightnessLowerBound);
pw.println(" mReason=" + mReason);
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
index be8fa5a..2a3dd87 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessModifier.java
@@ -68,4 +68,9 @@
public void stop() {
// do nothing
}
+
+ @Override
+ public void onAmbientLuxChange(float ambientLux) {
+ // do nothing
+ }
}
diff --git a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
index 441ba8f..2234258 100644
--- a/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
+++ b/services/core/java/com/android/server/display/brightness/clamper/BrightnessStateModifier.java
@@ -42,4 +42,10 @@
* Called when stopped. Listeners can be unregistered here.
*/
void stop();
+
+ /**
+ * Allows modifiers to react to ambient lux changes.
+ * @param ambientLux current debounced lux.
+ */
+ void onAmbientLuxChange(float ambientLux);
}
diff --git a/services/core/java/com/android/server/display/config/LowBrightnessData.java b/services/core/java/com/android/server/display/config/LowBrightnessData.java
new file mode 100644
index 0000000..aa82533
--- /dev/null
+++ b/services/core/java/com/android/server/display/config/LowBrightnessData.java
@@ -0,0 +1,142 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.display.config;
+
+import android.annotation.Nullable;
+import android.util.Slog;
+import android.util.Spline;
+
+import com.android.internal.annotations.VisibleForTesting;
+
+import java.util.Arrays;
+import java.util.List;
+
+/**
+ * Brightness config for low brightness mode
+ */
+public class LowBrightnessData {
+ private static final String TAG = "LowBrightnessData";
+
+ /**
+ * Brightness value at which lower brightness methods are used.
+ */
+ public final float mTransitionPoint;
+
+ /**
+ * Nits array, maps to mBacklight
+ */
+ public final float[] mNits;
+
+ /**
+ * Backlight array, maps to mBrightness and mNits
+ */
+ public final float[] mBacklight;
+
+ /**
+ * Brightness array, maps to mBacklight
+ */
+ public final float[] mBrightness;
+ /**
+ * Spline, mapping between backlight and nits
+ */
+ public final Spline mBacklightToNits;
+ /**
+ * Spline, mapping between nits and backlight
+ */
+ public final Spline mNitsToBacklight;
+ /**
+ * Spline, mapping between brightness and backlight
+ */
+ public final Spline mBrightnessToBacklight;
+ /**
+ * Spline, mapping between backlight and brightness
+ */
+ public final Spline mBacklightToBrightness;
+
+ @VisibleForTesting
+ public LowBrightnessData(float transitionPoint, float[] nits,
+ float[] backlight, float[] brightness, Spline backlightToNits,
+ Spline nitsToBacklight, Spline brightnessToBacklight, Spline backlightToBrightness) {
+ mTransitionPoint = transitionPoint;
+ mNits = nits;
+ mBacklight = backlight;
+ mBrightness = brightness;
+ mBacklightToNits = backlightToNits;
+ mNitsToBacklight = nitsToBacklight;
+ mBrightnessToBacklight = brightnessToBacklight;
+ mBacklightToBrightness = backlightToBrightness;
+ }
+
+ @Override
+ public String toString() {
+ return "LowBrightnessData {"
+ + "mTransitionPoint: " + mTransitionPoint
+ + ", mNits: " + Arrays.toString(mNits)
+ + ", mBacklight: " + Arrays.toString(mBacklight)
+ + ", mBrightness: " + Arrays.toString(mBrightness)
+ + ", mBacklightToNits: " + mBacklightToNits
+ + ", mNitsToBacklight: " + mNitsToBacklight
+ + ", mBrightnessToBacklight: " + mBrightnessToBacklight
+ + ", mBacklightToBrightness: " + mBacklightToBrightness
+ + "} ";
+ }
+
+ /**
+ * Loads LowBrightnessData from DisplayConfiguration
+ */
+ @Nullable
+ public static LowBrightnessData loadConfig(DisplayConfiguration config) {
+ final LowBrightnessMode lbm = config.getLowBrightness();
+ if (lbm == null) {
+ return null;
+ }
+
+ boolean lbmIsEnabled = lbm.getEnabled();
+ if (!lbmIsEnabled) {
+ return null;
+ }
+
+ List<Float> nitsList = lbm.getNits();
+ List<Float> backlightList = lbm.getBacklight();
+ List<Float> brightnessList = lbm.getBrightness();
+ float transitionPoints = lbm.getTransitionPoint().floatValue();
+
+ if (nitsList.isEmpty()
+ || backlightList.size() != brightnessList.size()
+ || backlightList.size() != nitsList.size()) {
+ Slog.e(TAG, "Invalid low brightness array lengths");
+ return null;
+ }
+
+ float[] nits = new float[nitsList.size()];
+ float[] backlight = new float[nitsList.size()];
+ float[] brightness = new float[nitsList.size()];
+
+ for (int i = 0; i < nitsList.size(); i++) {
+ nits[i] = nitsList.get(i);
+ backlight[i] = backlightList.get(i);
+ brightness[i] = brightnessList.get(i);
+ }
+
+ return new LowBrightnessData(transitionPoints, nits, backlight, brightness,
+ Spline.createSpline(backlight, nits),
+ Spline.createSpline(nits, backlight),
+ Spline.createSpline(brightness, backlight),
+ Spline.createSpline(backlight, brightness)
+ );
+ }
+}
diff --git a/services/core/java/com/android/server/input/KeyboardLayoutManager.java b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
index 283e692..6610081 100644
--- a/services/core/java/com/android/server/input/KeyboardLayoutManager.java
+++ b/services/core/java/com/android/server/input/KeyboardLayoutManager.java
@@ -459,13 +459,16 @@
for (ResolveInfo resolveInfo : pm.queryBroadcastReceiversAsUser(intent,
PackageManager.GET_META_DATA | PackageManager.MATCH_DIRECT_BOOT_AWARE
| PackageManager.MATCH_DIRECT_BOOT_UNAWARE, UserHandle.USER_SYSTEM)) {
+ if (resolveInfo == null || resolveInfo.activityInfo == null) {
+ continue;
+ }
final ActivityInfo activityInfo = resolveInfo.activityInfo;
final int priority = resolveInfo.priority;
visitKeyboardLayoutsInPackage(pm, activityInfo, null, priority, visitor);
}
}
- private void visitKeyboardLayout(String keyboardLayoutDescriptor,
+ private void visitKeyboardLayout(@NonNull String keyboardLayoutDescriptor,
KeyboardLayoutVisitor visitor) {
KeyboardLayoutDescriptor d = KeyboardLayoutDescriptor.parse(keyboardLayoutDescriptor);
if (d != null) {
@@ -482,8 +485,8 @@
}
}
- private void visitKeyboardLayoutsInPackage(PackageManager pm, ActivityInfo receiver,
- String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
+ private void visitKeyboardLayoutsInPackage(PackageManager pm, @NonNull ActivityInfo receiver,
+ @Nullable String keyboardName, int requestedPriority, KeyboardLayoutVisitor visitor) {
Bundle metaData = receiver.metaData;
if (metaData == null) {
return;
@@ -1415,7 +1418,7 @@
return packageName + "/" + receiverName + "/" + keyboardName;
}
- public static KeyboardLayoutDescriptor parse(String descriptor) {
+ public static KeyboardLayoutDescriptor parse(@NonNull String descriptor) {
int pos = descriptor.indexOf('/');
if (pos < 0 || pos + 1 == descriptor.length()) {
return null;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index d0a83a6..cfd64c4 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -1248,7 +1248,15 @@
mService.publishLocalService();
IInputMethodManager.Stub service;
if (Flags.useZeroJankProxy()) {
- service = new ZeroJankProxy(mService.mHandler::post, mService);
+ service =
+ new ZeroJankProxy(
+ mService.mHandler::post,
+ mService,
+ () -> {
+ synchronized (ImfLock.class) {
+ return mService.isInputShown();
+ }
+ });
} else {
service = mService;
}
diff --git a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
index 396192e..136ab42c 100644
--- a/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
+++ b/services/core/java/com/android/server/inputmethod/ZeroJankProxy.java
@@ -46,7 +46,6 @@
import android.os.RemoteException;
import android.os.ResultReceiver;
import android.os.ShellCallback;
-import android.util.ExceptionUtils;
import android.util.Slog;
import android.view.WindowManager;
import android.view.inputmethod.CursorAnchorInfo;
@@ -77,6 +76,7 @@
import java.util.concurrent.CompletableFuture;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
+import java.util.function.BooleanSupplier;
/**
* A proxy that processes all {@link IInputMethodManager} calls asynchronously.
@@ -86,10 +86,12 @@
private final IInputMethodManager mInner;
private final Executor mExecutor;
+ private final BooleanSupplier mIsInputShown;
- ZeroJankProxy(Executor executor, IInputMethodManager inner) {
+ ZeroJankProxy(Executor executor, IInputMethodManager inner, BooleanSupplier isInputShown) {
mInner = inner;
mExecutor = executor;
+ mIsInputShown = isInputShown;
}
private void offload(ThrowingRunnable r) {
@@ -163,8 +165,19 @@
int lastClickTooType, ResultReceiver resultReceiver,
@SoftInputShowHideReason int reason)
throws RemoteException {
- offload(() -> mInner.showSoftInput(client, windowToken, statsToken, flags, lastClickTooType,
- resultReceiver, reason));
+ offload(
+ () -> {
+ if (!mInner.showSoftInput(
+ client,
+ windowToken,
+ statsToken,
+ flags,
+ lastClickTooType,
+ resultReceiver,
+ reason)) {
+ sendResultReceiverFailure(resultReceiver);
+ }
+ });
return true;
}
@@ -173,11 +186,24 @@
@Nullable ImeTracker.Token statsToken, @InputMethodManager.HideFlags int flags,
ResultReceiver resultReceiver, @SoftInputShowHideReason int reason)
throws RemoteException {
- offload(() -> mInner.hideSoftInput(client, windowToken, statsToken, flags, resultReceiver,
- reason));
+ offload(
+ () -> {
+ if (!mInner.hideSoftInput(
+ client, windowToken, statsToken, flags, resultReceiver, reason)) {
+ sendResultReceiverFailure(resultReceiver);
+ }
+ });
return true;
}
+ private void sendResultReceiverFailure(ResultReceiver resultReceiver) {
+ resultReceiver.send(
+ mIsInputShown.getAsBoolean()
+ ? InputMethodManager.RESULT_UNCHANGED_SHOWN
+ : InputMethodManager.RESULT_UNCHANGED_HIDDEN,
+ null);
+ }
+
@Override
@EnforcePermission(Manifest.permission.TEST_INPUT_METHOD)
public void hideSoftInputFromServerForTest() throws RemoteException {
diff --git a/services/core/java/com/android/server/media/MediaSessionRecord.java b/services/core/java/com/android/server/media/MediaSessionRecord.java
index a9a8272..5b3934e 100644
--- a/services/core/java/com/android/server/media/MediaSessionRecord.java
+++ b/services/core/java/com/android/server/media/MediaSessionRecord.java
@@ -687,27 +687,20 @@
private static String toVolumeControlTypeString(
@VolumeProvider.ControlType int volumeControlType) {
- switch (volumeControlType) {
- case VOLUME_CONTROL_FIXED:
- return "FIXED";
- case VOLUME_CONTROL_RELATIVE:
- return "RELATIVE";
- case VOLUME_CONTROL_ABSOLUTE:
- return "ABSOLUTE";
- default:
- return TextUtils.formatSimple("unknown(%d)", volumeControlType);
- }
+ return switch (volumeControlType) {
+ case VOLUME_CONTROL_FIXED -> "FIXED";
+ case VOLUME_CONTROL_RELATIVE -> "RELATIVE";
+ case VOLUME_CONTROL_ABSOLUTE -> "ABSOLUTE";
+ default -> TextUtils.formatSimple("unknown(%d)", volumeControlType);
+ };
}
private static String toVolumeTypeString(@PlaybackInfo.PlaybackType int volumeType) {
- switch (volumeType) {
- case PLAYBACK_TYPE_LOCAL:
- return "LOCAL";
- case PLAYBACK_TYPE_REMOTE:
- return "REMOTE";
- default:
- return TextUtils.formatSimple("unknown(%d)", volumeType);
- }
+ return switch (volumeType) {
+ case PLAYBACK_TYPE_LOCAL -> "LOCAL";
+ case PLAYBACK_TYPE_REMOTE -> "REMOTE";
+ default -> TextUtils.formatSimple("unknown(%d)", volumeType);
+ };
}
@Override
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 18b495b..25095ed 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -4328,7 +4328,9 @@
@GuardedBy("mUidRulesFirstLock")
private boolean updateUidStateUL(int uid, int procState, long procStateSeq,
@ProcessCapability int capability) {
- Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL");
+ Trace.traceBegin(Trace.TRACE_TAG_NETWORK, "updateUidStateUL: " + uid + "/"
+ + ActivityManager.procStateToString(procState) + "/" + procStateSeq + "/"
+ + ActivityManager.getCapabilitiesSummary(capability));
try {
final UidState oldUidState = mUidState.get(uid);
if (oldUidState != null && procStateSeq < oldUidState.procStateSeq) {
diff --git a/services/core/java/com/android/server/notification/PreferencesHelper.java b/services/core/java/com/android/server/notification/PreferencesHelper.java
index 4f3cdbc..50ca984 100644
--- a/services/core/java/com/android/server/notification/PreferencesHelper.java
+++ b/services/core/java/com/android/server/notification/PreferencesHelper.java
@@ -310,6 +310,7 @@
parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY),
parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE),
bubblePref);
+ r.bubblePreference = bubblePref;
r.priority = parser.getAttributeInt(null, ATT_PRIORITY, DEFAULT_PRIORITY);
r.visibility = parser.getAttributeInt(null, ATT_VISIBILITY, DEFAULT_VISIBILITY);
r.showBadge = parser.getAttributeBoolean(null, ATT_SHOW_BADGE, DEFAULT_SHOW_BADGE);
@@ -676,7 +677,7 @@
* @param bubblePreference whether bubbles are allowed.
*/
public void setBubblesAllowed(String pkg, int uid, int bubblePreference) {
- boolean changed = false;
+ boolean changed;
synchronized (mPackagePreferences) {
PackagePreferences p = getOrCreatePackagePreferencesLocked(pkg, uid);
changed = p.bubblePreference != bubblePreference;
diff --git a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
index 28682e3..37023e1 100644
--- a/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
+++ b/services/core/java/com/android/server/ondeviceintelligence/OnDeviceIntelligenceManagerService.java
@@ -37,8 +37,8 @@
import android.content.Context;
import android.content.pm.PackageManager;
import android.content.pm.ServiceInfo;
-import android.os.Binder;
import android.content.res.Resources;
+import android.os.Binder;
import android.os.Bundle;
import android.os.Handler;
import android.os.IBinder;
@@ -360,7 +360,7 @@
synchronized (mLock) {
if (mRemoteOnDeviceIntelligenceService == null) {
String serviceName = getServiceNames()[0];
- validateService(serviceName, false);
+ Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, false));
mRemoteOnDeviceIntelligenceService = new RemoteOnDeviceIntelligenceService(mContext,
ComponentName.unflattenFromString(serviceName),
UserHandle.SYSTEM.getIdentifier());
@@ -410,7 +410,7 @@
synchronized (mLock) {
if (mRemoteInferenceService == null) {
String serviceName = getServiceNames()[1];
- validateService(serviceName, true);
+ Binder.withCleanCallingIdentity(() -> validateServiceElevated(serviceName, true));
mRemoteInferenceService = new RemoteOnDeviceSandboxedInferenceService(mContext,
ComponentName.unflattenFromString(serviceName),
UserHandle.SYSTEM.getIdentifier());
@@ -457,11 +457,10 @@
};
}
- @GuardedBy("mLock")
- private void validateService(String serviceName, boolean checkIsolated)
+ private static void validateServiceElevated(String serviceName, boolean checkIsolated)
throws RemoteException {
if (TextUtils.isEmpty(serviceName)) {
- throw new RuntimeException("");
+ throw new IllegalArgumentException("Received null/empty service name : " + serviceName);
}
ComponentName serviceComponent = ComponentName.unflattenFromString(
serviceName);
@@ -501,8 +500,7 @@
}
}
- @GuardedBy("mLock")
- private boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
+ private static boolean isIsolatedService(@NonNull ServiceInfo serviceInfo) {
return (serviceInfo.flags & ServiceInfo.FLAG_ISOLATED_PROCESS) != 0
&& (serviceInfo.flags & ServiceInfo.FLAG_EXTERNAL_SERVICE) == 0;
}
diff --git a/services/core/java/com/android/server/pm/LauncherAppsService.java b/services/core/java/com/android/server/pm/LauncherAppsService.java
index c6bb99e..20b669b 100644
--- a/services/core/java/com/android/server/pm/LauncherAppsService.java
+++ b/services/core/java/com/android/server/pm/LauncherAppsService.java
@@ -18,12 +18,12 @@
import static android.Manifest.permission.READ_FRAME_BUFFER;
import static android.app.ActivityOptions.KEY_SPLASH_SCREEN_THEME;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
+import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.app.AppOpsManager.MODE_ALLOWED;
import static android.app.AppOpsManager.MODE_IGNORED;
import static android.app.AppOpsManager.OP_ARCHIVE_ICON_OVERLAY;
import static android.app.AppOpsManager.OP_UNARCHIVAL_CONFIRMATION;
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_ALLOWED;
-import static android.app.ActivityOptions.MODE_BACKGROUND_ACTIVITY_START_SYSTEM_DEFINED;
import static android.app.PendingIntent.FLAG_IMMUTABLE;
import static android.app.PendingIntent.FLAG_MUTABLE;
import static android.app.PendingIntent.FLAG_UPDATE_CURRENT;
@@ -555,12 +555,6 @@
return false;
}
- if (!mRoleManager
- .getRoleHoldersAsUser(
- RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
- .contains(callingPackage.getPackageName())) {
- return false;
- }
if (mContext.checkPermission(
Manifest.permission.ACCESS_HIDDEN_PROFILES_FULL,
callingPid,
@@ -569,6 +563,13 @@
return true;
}
+ if (!mRoleManager
+ .getRoleHoldersAsUser(
+ RoleManager.ROLE_HOME, UserHandle.getUserHandleForUid(callingUid))
+ .contains(callingPackage.getPackageName())) {
+ return false;
+ }
+
// TODO(b/321988638): add option to disable with a flag
return mContext.checkPermission(
android.Manifest.permission.ACCESS_HIDDEN_PROFILES,
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 6b12781..3a0f7fb 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -261,11 +261,6 @@
// Step 1: always destroy app profiles.
mAppDataHelper.destroyAppProfilesLIF(packageName);
- // Everything else is preserved if the DELETE_KEEP_DATA flag is on
- if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
- return;
- }
-
final AndroidPackage pkg;
final SharedUserSetting sus;
synchronized (mPm.mLock) {
@@ -282,9 +277,20 @@
resolvedPkg = PackageImpl.buildFakeForDeletion(packageName, ps.getVolumeUuid());
}
+ int appDataDeletionFlags = FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL;
+ // Personal data is preserved if the DELETE_KEEP_DATA flag is on
+ if ((flags & PackageManager.DELETE_KEEP_DATA) != 0) {
+ if ((flags & PackageManager.DELETE_ARCHIVE) != 0) {
+ mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+ appDataDeletionFlags | Installer.FLAG_CLEAR_CACHE_ONLY);
+ mAppDataHelper.clearAppDataLIF(resolvedPkg, userId,
+ appDataDeletionFlags | Installer.FLAG_CLEAR_CODE_CACHE_ONLY);
+ }
+ return;
+ }
+
// Step 2: destroy app data.
- mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId,
- FLAG_STORAGE_DE | FLAG_STORAGE_CE | FLAG_STORAGE_EXTERNAL);
+ mAppDataHelper.destroyAppDataLIF(resolvedPkg, userId, appDataDeletionFlags);
if (userId != UserHandle.USER_ALL) {
ps.setCeDataInode(-1, userId);
ps.setDeDataInode(-1, userId);
diff --git a/services/core/java/com/android/server/policy/PhoneWindowManager.java b/services/core/java/com/android/server/policy/PhoneWindowManager.java
index 9e31748..12a5892 100644
--- a/services/core/java/com/android/server/policy/PhoneWindowManager.java
+++ b/services/core/java/com/android/server/policy/PhoneWindowManager.java
@@ -1905,6 +1905,7 @@
accessibilityManager.performSystemAction(
AccessibilityService.GLOBAL_ACTION_ACCESSIBILITY_ALL_APPS);
}
+ dismissKeyboardShortcutsMenu();
}
private void toggleNotificationPanel() {
@@ -3478,13 +3479,6 @@
return true;
}
break;
- case KeyEvent.KEYCODE_T:
- if (firstDown && event.isMetaPressed()) {
- toggleTaskbar();
- logKeyboardSystemsEvent(event, KeyboardLogEvent.TOGGLE_TASKBAR);
- return true;
- }
- break;
case KeyEvent.KEYCODE_DEL:
case KeyEvent.KEYCODE_ESCAPE:
if (firstDown && event.isMetaPressed()) {
@@ -4735,7 +4729,7 @@
if (down) {
// There may have other embedded activities on the same Task. Try to move the
// focus before processing the back event.
- mWindowManagerInternal.moveFocusToTopEmbeddedWindowIfNeeded();
+ mWindowManagerInternal.moveFocusToAdjacentEmbeddedActivityIfNeeded();
mBackKeyHandled = false;
} else {
if (!hasLongPressOnBackBehavior()) {
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index 23f9743..17e6996 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -11012,6 +11012,20 @@
}
/**
+ * Returns the {@link #createTime} if the top window is the `base` window. Note that do not
+ * use the window creation time because the window could be re-created when the activity
+ * relaunched if configuration changed.
+ * <p>
+ * Otherwise, return the creation time of the top window.
+ */
+ long getLastWindowCreateTime() {
+ final WindowState window = getWindow(win -> true);
+ return window != null && window.mAttrs.type != TYPE_BASE_APPLICATION
+ ? window.getCreateTime()
+ : createTime;
+ }
+
+ /**
* Adjust the source rect hint in {@link #pictureInPictureArgs} by window bounds since
* it is relative to its root view (see also b/235599028).
* It is caller's responsibility to make sure this is called exactly once when we update
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index e3ac35c..48d78f5 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -165,7 +165,7 @@
}
// Move focus to the top embedded window if possible
- if (mWindowManagerService.moveFocusToTopEmbeddedWindow(window)) {
+ if (mWindowManagerService.moveFocusToAdjacentEmbeddedWindow(window)) {
window = wmService.getFocusedWindowLocked();
if (window == null) {
Slog.e(TAG, "New focused window is null, returning null.");
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index 55dc30c..4c282bd 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -1274,7 +1274,8 @@
if (!isLeafTaskFragment()) {
final ActivityRecord top = topRunningActivity();
final ActivityRecord resumedActivity = getResumedActivity();
- if (resumedActivity != null && top.getTaskFragment() != this) {
+ if (resumedActivity != null
+ && (top.getTaskFragment() != this || !canBeResumed(resuming))) {
// Pausing the resumed activity because it is occluded by other task fragment.
if (startPausing(false /* uiSleeping*/, resuming, reason)) {
someActivityPaused[0]++;
@@ -3753,11 +3754,9 @@
// Boost the adjacent TaskFragment for dimmer if needed.
final TaskFragment taskFragment = wc.asTaskFragment();
if (taskFragment != null && taskFragment.isEmbedded()) {
- taskFragment.mDimmerSurfaceBoosted = false;
final TaskFragment adjacentTf = taskFragment.getAdjacentTaskFragment();
if (adjacentTf != null && adjacentTf.shouldBoostDimmer()) {
adjacentTf.assignLayer(t, layer++);
- adjacentTf.mDimmerSurfaceBoosted = true;
}
}
diff --git a/services/core/java/com/android/server/wm/TaskFragment.java b/services/core/java/com/android/server/wm/TaskFragment.java
index 3cf561c..dc0e034 100644
--- a/services/core/java/com/android/server/wm/TaskFragment.java
+++ b/services/core/java/com/android/server/wm/TaskFragment.java
@@ -216,9 +216,6 @@
Dimmer mDimmer = Dimmer.DIMMER_REFACTOR
? new SmoothDimmer(this) : new LegacyDimmer(this);
- /** {@code true} if the dimmer surface is boosted. {@code false} otherwise. */
- boolean mDimmerSurfaceBoosted;
-
/** Apply the dim layer on the embedded TaskFragment. */
static final int EMBEDDED_DIM_AREA_TASK_FRAGMENT = 0;
diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java
index acc6330..daf8129 100644
--- a/services/core/java/com/android/server/wm/WindowManagerInternal.java
+++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java
@@ -1068,9 +1068,9 @@
public abstract void clearBlockedApps();
/**
- * Moves the current focus to the top activity window if the top activity is embedded.
+ * Moves the current focus to the adjacent activity if it has the latest created window.
*/
- public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded();
+ public abstract boolean moveFocusToAdjacentEmbeddedActivityIfNeeded();
/**
* Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current
diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java
index 207b1bb..2934574 100644
--- a/services/core/java/com/android/server/wm/WindowManagerService.java
+++ b/services/core/java/com/android/server/wm/WindowManagerService.java
@@ -8700,14 +8700,14 @@
}
@Override
- public boolean moveFocusToTopEmbeddedWindowIfNeeded() {
+ public boolean moveFocusToAdjacentEmbeddedActivityIfNeeded() {
synchronized (mGlobalLock) {
final WindowState focusedWindow = getFocusedWindow();
if (focusedWindow == null) {
return false;
}
- if (moveFocusToTopEmbeddedWindow(focusedWindow)) {
+ if (moveFocusToAdjacentEmbeddedWindow(focusedWindow)) {
// Sync the input transactions to ensure the input focus updates as well.
syncInputTransactions(false);
return true;
@@ -9219,9 +9219,10 @@
}
/**
- * Move focus to the top embedded window if possible.
+ * Move focus to the adjacent embedded activity if the adjacent activity is more recently
+ * created or has a window more recently added.
*/
- boolean moveFocusToTopEmbeddedWindow(@NonNull WindowState focusedWindow) {
+ boolean moveFocusToAdjacentEmbeddedWindow(@NonNull WindowState focusedWindow) {
final TaskFragment taskFragment = focusedWindow.getTaskFragment();
if (taskFragment == null) {
// Skip if not an Activity window.
@@ -9233,31 +9234,25 @@
return false;
}
- if (taskFragment.mDimmerSurfaceBoosted) {
- // Skip if the TaskFragment currently has dimmer surface boosted.
+ if (!focusedWindow.mActivityRecord.isEmbedded()) {
+ // Skip if the focused activity is not embedded
return false;
}
- final ActivityRecord topActivity =
- taskFragment.getTask().topRunningActivity(true /* focusableOnly */);
- if (topActivity == null || topActivity == focusedWindow.mActivityRecord) {
- // Skip if the focused activity is already the top-most activity on the Task.
+ final TaskFragment adjacentTaskFragment = taskFragment.getAdjacentTaskFragment();
+ final ActivityRecord adjacentTopActivity =
+ adjacentTaskFragment != null ? adjacentTaskFragment.topRunningActivity() : null;
+ if (adjacentTopActivity == null) {
return false;
}
- if (!topActivity.isEmbedded()) {
- // Skip if the top activity is not embedded
+ if (adjacentTopActivity.getLastWindowCreateTime()
+ < focusedWindow.mActivityRecord.getLastWindowCreateTime()) {
+ // Skip if the current focus activity has more recently active window.
return false;
}
- final TaskFragment topTaskFragment = topActivity.getTaskFragment();
- if (topTaskFragment.isIsolatedNav()
- && taskFragment.getAdjacentTaskFragment() == topTaskFragment) {
- // Skip if the top TaskFragment is adjacent to current focus and is set to isolated nav.
- return false;
- }
-
- moveFocusToActivity(topActivity);
+ moveFocusToActivity(adjacentTopActivity);
return !focusedWindow.isFocused();
}
diff --git a/services/core/java/com/android/server/wm/WindowState.java b/services/core/java/com/android/server/wm/WindowState.java
index 2b337ae..c0cf97d 100644
--- a/services/core/java/com/android/server/wm/WindowState.java
+++ b/services/core/java/com/android/server/wm/WindowState.java
@@ -364,6 +364,7 @@
private boolean mDragResizing;
private boolean mDragResizingChangeReported = true;
private boolean mRedrawForSyncReported = true;
+ private long mCreateTime = System.currentTimeMillis();
/**
* Used to assosciate a given set of state changes sent from MSG_RESIZED
@@ -1714,6 +1715,10 @@
: DEFAULT_DISPATCHING_TIMEOUT_MILLIS;
}
+ long getCreateTime() {
+ return mCreateTime;
+ }
+
/**
* Returns true if, at any point, the application token associated with this window has actually
* displayed any windows. This is most useful with the "starting up" window to determine if any
diff --git a/services/core/xsd/display-device-config/display-device-config.xsd b/services/core/xsd/display-device-config/display-device-config.xsd
index d0df2b2..1f54518 100644
--- a/services/core/xsd/display-device-config/display-device-config.xsd
+++ b/services/core/xsd/display-device-config/display-device-config.xsd
@@ -162,6 +162,10 @@
<xs:element type="usiVersion" name="usiVersion">
<xs:annotation name="final"/>
</xs:element>
+ <xs:element type="lowBrightnessMode" name="lowBrightness">
+ <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+ <xs:annotation name="final"/>
+ </xs:element>
<!-- Maximum screen brightness setting when screen brightness capped in
Wear Bedtime mode. This must be a non-negative decimal within the range defined by
the first and the last brightness value in screenBrightnessMap. -->
@@ -172,6 +176,7 @@
<xs:element type="idleScreenRefreshRateTimeout" name="idleScreenRefreshRateTimeout" minOccurs="0">
<xs:annotation name="final"/>
</xs:element>
+
</xs:sequence>
</xs:complexType>
</xs:element>
@@ -216,6 +221,21 @@
</xs:restriction>
</xs:simpleType>
+ <xs:complexType name="lowBrightnessMode">
+ <xs:sequence>
+ <xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
+ maxOccurs="1">
+ </xs:element>
+ <xs:element name="nits" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ <xs:element name="backlight" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ <xs:element name="brightness" type="xs:float" maxOccurs="unbounded">
+ </xs:element>
+ </xs:sequence>
+ <xs:attribute name="enabled" type="xs:boolean" use="optional"/>
+ </xs:complexType>
+
<xs:complexType name="highBrightnessMode">
<xs:all>
<xs:element name="transitionPoint" type="nonNegativeDecimal" minOccurs="1"
diff --git a/services/core/xsd/display-device-config/schema/current.txt b/services/core/xsd/display-device-config/schema/current.txt
index 00dc908..c39c3d7 100644
--- a/services/core/xsd/display-device-config/schema/current.txt
+++ b/services/core/xsd/display-device-config/schema/current.txt
@@ -113,6 +113,7 @@
method public com.android.server.display.config.HighBrightnessMode getHighBrightnessMode();
method public final com.android.server.display.config.IdleScreenRefreshRateTimeout getIdleScreenRefreshRateTimeout();
method public final com.android.server.display.config.SensorDetails getLightSensor();
+ method public final com.android.server.display.config.LowBrightnessMode getLowBrightness();
method public com.android.server.display.config.LuxThrottling getLuxThrottling();
method @Nullable public final String getName();
method public com.android.server.display.config.PowerThrottlingConfig getPowerThrottlingConfig();
@@ -149,6 +150,7 @@
method public void setHighBrightnessMode(com.android.server.display.config.HighBrightnessMode);
method public final void setIdleScreenRefreshRateTimeout(com.android.server.display.config.IdleScreenRefreshRateTimeout);
method public final void setLightSensor(com.android.server.display.config.SensorDetails);
+ method public final void setLowBrightness(com.android.server.display.config.LowBrightnessMode);
method public void setLuxThrottling(com.android.server.display.config.LuxThrottling);
method public final void setName(@Nullable String);
method public void setPowerThrottlingConfig(com.android.server.display.config.PowerThrottlingConfig);
@@ -248,6 +250,17 @@
method public java.util.List<java.math.BigInteger> getItem();
}
+ public class LowBrightnessMode {
+ ctor public LowBrightnessMode();
+ method public java.util.List<java.lang.Float> getBacklight();
+ method public java.util.List<java.lang.Float> getBrightness();
+ method public boolean getEnabled();
+ method public java.util.List<java.lang.Float> getNits();
+ method public java.math.BigDecimal getTransitionPoint();
+ method public void setEnabled(boolean);
+ method public void setTransitionPoint(java.math.BigDecimal);
+ }
+
public class LuxThrottling {
ctor public LuxThrottling();
method @NonNull public final java.util.List<com.android.server.display.config.BrightnessLimitMap> getBrightnessLimitMap();
diff --git a/services/devicepolicy/Android.bp b/services/devicepolicy/Android.bp
index 8dfa685..da965bb 100644
--- a/services/devicepolicy/Android.bp
+++ b/services/devicepolicy/Android.bp
@@ -24,5 +24,6 @@
"app-compat-annotations",
"service-permission.stubs.system_server",
"device_policy_aconfig_flags_lib",
+ "androidx.annotation_annotation",
],
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
index f3b164c..94c1374 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/OverlayPackagesProvider.java
@@ -25,15 +25,16 @@
import static android.content.pm.PackageManager.GET_META_DATA;
import static com.android.internal.util.Preconditions.checkArgument;
-import static com.android.internal.util.Preconditions.checkNotNull;
-import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpResources;
+import static com.android.server.devicepolicy.DevicePolicyManagerService.dumpApps;
import static java.util.Objects.requireNonNull;
+import android.annotation.ArrayRes;
import android.annotation.NonNull;
import android.annotation.UserIdInt;
import android.app.admin.DeviceAdminReceiver;
import android.app.admin.DevicePolicyManager;
+import android.app.admin.flags.Flags;
import android.app.role.RoleManager;
import android.content.ComponentName;
import android.content.Context;
@@ -67,13 +68,16 @@
protected static final String TAG = "OverlayPackagesProvider";
private static final Map<String, String> sActionToMetadataKeyMap = new HashMap<>();
- {
+
+ static {
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_USER, REQUIRED_APP_MANAGED_USER);
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_PROFILE, REQUIRED_APP_MANAGED_PROFILE);
sActionToMetadataKeyMap.put(ACTION_PROVISION_MANAGED_DEVICE, REQUIRED_APP_MANAGED_DEVICE);
}
+
private static final Set<String> sAllowedActions = new HashSet<>();
- {
+
+ static {
sAllowedActions.add(ACTION_PROVISION_MANAGED_USER);
sAllowedActions.add(ACTION_PROVISION_MANAGED_PROFILE);
sAllowedActions.add(ACTION_PROVISION_MANAGED_DEVICE);
@@ -83,8 +87,13 @@
private final Context mContext;
private final Injector mInjector;
+ private final RecursiveStringArrayResourceResolver mRecursiveStringArrayResourceResolver;
+
public OverlayPackagesProvider(Context context) {
- this(context, new DefaultInjector());
+ this(
+ context,
+ new DefaultInjector(),
+ new RecursiveStringArrayResourceResolver(context.getResources()));
}
@VisibleForTesting
@@ -113,8 +122,8 @@
public String getDevicePolicyManagementRoleHolderPackageName(Context context) {
return Binder.withCleanCallingIdentity(() -> {
RoleManager roleManager = context.getSystemService(RoleManager.class);
- List<String> roleHolders =
- roleManager.getRoleHolders(RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
+ List<String> roleHolders = roleManager.getRoleHolders(
+ RoleManager.ROLE_DEVICE_POLICY_MANAGEMENT);
if (roleHolders.isEmpty()) {
return null;
}
@@ -124,17 +133,20 @@
}
@VisibleForTesting
- OverlayPackagesProvider(Context context, Injector injector) {
+ OverlayPackagesProvider(Context context, Injector injector,
+ RecursiveStringArrayResourceResolver recursiveStringArrayResourceResolver) {
mContext = context;
- mPm = checkNotNull(context.getPackageManager());
- mInjector = checkNotNull(injector);
+ mPm = requireNonNull(context.getPackageManager());
+ mInjector = requireNonNull(injector);
+ mRecursiveStringArrayResourceResolver = requireNonNull(
+ recursiveStringArrayResourceResolver);
}
/**
* Computes non-required apps. All the system apps with a launcher that are not in
* the required set of packages, and all mainline modules that are not declared as required
* via metadata in their manifests, will be considered as non-required apps.
- *
+ * <p>
* Note: If an app is mistakenly listed as both required and disallowed, it will be treated as
* disallowed.
*
@@ -176,12 +188,12 @@
/**
* Returns a subset of {@code packageNames} whose packages are mainline modules declared as
* required apps via their app metadata.
+ *
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_USER
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_DEVICE
* @see DevicePolicyManager#REQUIRED_APP_MANAGED_PROFILE
*/
- private Set<String> getRequiredAppsMainlineModules(
- Set<String> packageNames,
+ private Set<String> getRequiredAppsMainlineModules(Set<String> packageNames,
String provisioningAction) {
final Set<String> result = new HashSet<>();
for (String packageName : packageNames) {
@@ -225,8 +237,8 @@
}
private boolean isApkInApexMainlineModule(String packageName) {
- final String apexPackageName =
- mInjector.getActiveApexPackageNameContainingPackage(packageName);
+ final String apexPackageName = mInjector.getActiveApexPackageNameContainingPackage(
+ packageName);
return apexPackageName != null;
}
@@ -274,112 +286,94 @@
}
private Set<String> getRequiredAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.required_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.required_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.required_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.required_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.required_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.required_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getDisallowedAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.disallowed_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.disallowed_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.disallowed_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.disallowed_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.disallowed_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.disallowed_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getVendorRequiredAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.vendor_required_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.vendor_required_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.vendor_required_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
- }
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_required_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_required_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_required_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
}
private Set<String> getVendorDisallowedAppsSet(String provisioningAction) {
- final int resId;
- switch (provisioningAction) {
- case ACTION_PROVISION_MANAGED_USER:
- resId = R.array.vendor_disallowed_apps_managed_user;
- break;
- case ACTION_PROVISION_MANAGED_PROFILE:
- resId = R.array.vendor_disallowed_apps_managed_profile;
- break;
- case ACTION_PROVISION_MANAGED_DEVICE:
- resId = R.array.vendor_disallowed_apps_managed_device;
- break;
- default:
- throw new IllegalArgumentException("Provisioning type "
- + provisioningAction + " not supported.");
+ final int resId = switch (provisioningAction) {
+ case ACTION_PROVISION_MANAGED_USER -> R.array.vendor_disallowed_apps_managed_user;
+ case ACTION_PROVISION_MANAGED_PROFILE -> R.array.vendor_disallowed_apps_managed_profile;
+ case ACTION_PROVISION_MANAGED_DEVICE -> R.array.vendor_disallowed_apps_managed_device;
+ default -> throw new IllegalArgumentException(
+ "Provisioning type " + provisioningAction + " not supported.");
+ };
+ return resolveStringArray(resId);
+ }
+
+ private Set<String> resolveStringArray(@ArrayRes int resId) {
+ if (Flags.isRecursiveRequiredAppMergingEnabled()) {
+ return mRecursiveStringArrayResourceResolver.resolve(mContext.getPackageName(), resId);
+ } else {
+ return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
}
- return new ArraySet<>(Arrays.asList(mContext.getResources().getStringArray(resId)));
}
void dump(IndentingPrintWriter pw) {
pw.println("OverlayPackagesProvider");
pw.increaseIndent();
- dumpResources(pw, mContext, "required_apps_managed_device",
- R.array.required_apps_managed_device);
- dumpResources(pw, mContext, "required_apps_managed_user",
- R.array.required_apps_managed_user);
- dumpResources(pw, mContext, "required_apps_managed_profile",
- R.array.required_apps_managed_profile);
+ dumpApps(pw, "required_apps_managed_device",
+ resolveStringArray(R.array.required_apps_managed_device).toArray(String[]::new));
+ dumpApps(pw, "required_apps_managed_user",
+ resolveStringArray(R.array.required_apps_managed_user).toArray(String[]::new));
+ dumpApps(pw, "required_apps_managed_profile",
+ resolveStringArray(R.array.required_apps_managed_profile).toArray(String[]::new));
- dumpResources(pw, mContext, "disallowed_apps_managed_device",
- R.array.disallowed_apps_managed_device);
- dumpResources(pw, mContext, "disallowed_apps_managed_user",
- R.array.disallowed_apps_managed_user);
- dumpResources(pw, mContext, "disallowed_apps_managed_device",
- R.array.disallowed_apps_managed_device);
+ dumpApps(pw, "disallowed_apps_managed_device",
+ resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
+ dumpApps(pw, "disallowed_apps_managed_user",
+ resolveStringArray(R.array.disallowed_apps_managed_user).toArray(String[]::new));
+ dumpApps(pw, "disallowed_apps_managed_device",
+ resolveStringArray(R.array.disallowed_apps_managed_device).toArray(String[]::new));
- dumpResources(pw, mContext, "vendor_required_apps_managed_device",
- R.array.vendor_required_apps_managed_device);
- dumpResources(pw, mContext, "vendor_required_apps_managed_user",
- R.array.vendor_required_apps_managed_user);
- dumpResources(pw, mContext, "vendor_required_apps_managed_profile",
- R.array.vendor_required_apps_managed_profile);
+ dumpApps(pw, "vendor_required_apps_managed_device",
+ resolveStringArray(R.array.vendor_required_apps_managed_device).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_required_apps_managed_user",
+ resolveStringArray(R.array.vendor_required_apps_managed_user).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_required_apps_managed_profile",
+ resolveStringArray(R.array.vendor_required_apps_managed_profile).toArray(
+ String[]::new));
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_user",
- R.array.vendor_disallowed_apps_managed_user);
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_device",
- R.array.vendor_disallowed_apps_managed_device);
- dumpResources(pw, mContext, "vendor_disallowed_apps_managed_profile",
- R.array.vendor_disallowed_apps_managed_profile);
+ dumpApps(pw, "vendor_disallowed_apps_managed_user",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_user).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_disallowed_apps_managed_device",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_device).toArray(
+ String[]::new));
+ dumpApps(pw, "vendor_disallowed_apps_managed_profile",
+ resolveStringArray(R.array.vendor_disallowed_apps_managed_profile).toArray(
+ String[]::new));
pw.decreaseIndent();
}
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
new file mode 100644
index 0000000..935e051
--- /dev/null
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/RecursiveStringArrayResourceResolver.java
@@ -0,0 +1,147 @@
+/*
+ * 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.
+ */
+
+package com.android.server.devicepolicy;
+
+import android.annotation.SuppressLint;
+import android.content.res.Resources;
+
+import androidx.annotation.ArrayRes;
+
+import java.util.ArrayList;
+import java.util.Collection;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Objects;
+import java.util.Set;
+
+/**
+ * A class encapsulating all the logic for recursive string-array resource resolution.
+ */
+public class RecursiveStringArrayResourceResolver {
+ private static final String IMPORT_PREFIX = "#import:";
+ private static final String SEPARATOR = "/";
+ private static final String PWP = ".";
+
+ private final Resources mResources;
+
+ /**
+ * @param resources Android resource access object to use when resolving resources
+ */
+ public RecursiveStringArrayResourceResolver(Resources resources) {
+ this.mResources = resources;
+ }
+
+ /**
+ * Resolves a given {@code <string-array/>} resource specified via
+ * {@param rootId} in {@param pkg}. During resolution all values prefixed with
+ * {@link #IMPORT_PREFIX} are expanded and injected
+ * into the final list at the position of the import statement,
+ * pushing all the following values (and their expansions) down.
+ * Circular imports are tracked and skipped to avoid infinite resolution loops without losing
+ * data.
+ *
+ * <p>
+ * The import statements are expected in a form of
+ * "{@link #IMPORT_PREFIX}{package}{@link #SEPARATOR}{resourceName}"
+ * If the resource being imported is from the same package, its package can be specified as a
+ * {@link #PWP} shorthand `.`
+ * > e.g.:
+ * > {@code "#import:com.android.internal/disallowed_apps_managed_user"}
+ * > {@code "#import:./disallowed_apps_managed_user"}
+ *
+ * <p>
+ * Any incorrect or unresolvable import statement
+ * will cause the entire resolution to fail with an error.
+ *
+ * @param pkg the package owning the resource
+ * @param rootId the id of the {@code <string-array>} resource within {@param pkg} to start the
+ * resolution from
+ * @return a flattened list of all the resolved string array values from the root resource
+ * as well as all the imported arrays
+ */
+ public Set<String> resolve(String pkg, @ArrayRes int rootId) {
+ return resolve(List.of(), pkg, rootId);
+ }
+
+ /**
+ * A version of resolve that tracks already imported resources
+ * to avoid circular imports and wasted work.
+ *
+ * @param cache a list of already resolved packages to be skipped for further resolution
+ */
+ private Set<String> resolve(Collection<String> cache, String pkg, @ArrayRes int rootId) {
+ final var strings = mResources.getStringArray(rootId);
+ final var runningCache = new ArrayList<>(cache);
+
+ final var result = new HashSet<String>();
+ for (var string : strings) {
+ final String ref;
+ if (string.startsWith(IMPORT_PREFIX)) {
+ ref = string.substring(IMPORT_PREFIX.length());
+ } else {
+ ref = null;
+ }
+
+ if (ref == null) {
+ result.add(string);
+ } else if (!runningCache.contains(ref)) {
+ final var next = resolveImport(runningCache, pkg, ref);
+ runningCache.addAll(next);
+ result.addAll(next);
+ }
+ }
+ return result;
+ }
+
+ /**
+ * Resolves an import of the {@code <string-array>} resource
+ * in the context of {@param importingPackage} by the provided {@param ref}.
+ *
+ * @param cache a list of already resolved packages to be passed along into chained
+ * {@link #resolve} calls
+ * @param importingPackage the package that owns the resource which defined the import being
+ * processed.
+ * It is also used to expand all {@link #PWP} shorthands in
+ * {@param ref}
+ * @param ref reference to the resource to be imported in a form of
+ * "{package}{@link #SEPARATOR}{resourceName}".
+ * e.g.: {@code com.android.internal/disallowed_apps_managed_user}
+ */
+ private Set<String> resolveImport(
+ Collection<String> cache,
+ String importingPackage,
+ String ref) {
+ final var chunks = ref.split(SEPARATOR, 2);
+ final var pkg = chunks[0];
+ final var name = chunks[1];
+ final String resolvedPkg;
+ if (Objects.equals(pkg, PWP)) {
+ resolvedPkg = importingPackage;
+ } else {
+ resolvedPkg = pkg;
+ }
+ @SuppressLint("DiscouragedApi") final var importId = mResources.getIdentifier(
+ /* name = */ name,
+ /* defType = */ "array",
+ /* defPackage = */ resolvedPkg);
+ if (importId == 0) {
+ throw new Resources.NotFoundException(
+ /* name= */ String.format("%s:array/%s", resolvedPkg, name));
+ }
+ return resolve(cache, resolvedPkg, importId);
+ }
+}
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
index cea65b5..9f46d0ba 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodManagerServiceWindowGainedFocusTest.java
@@ -198,7 +198,9 @@
@Test
public void startInputOrWindowGainedFocus_userNotRunning() throws RemoteException {
- when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false);
+ // Run blockingly on ServiceThread to avoid that interfering with our stubbing.
+ mServiceThread.getThreadHandler().runWithScissors(
+ () -> when(mMockUserManagerInternal.isUserRunning(anyInt())).thenReturn(false), 0);
assertThat(
startInputOrWindowGainedFocus(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
index b0f7bfa..54de64e 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/AutomaticBrightnessControllerTest.java
@@ -52,6 +52,7 @@
import androidx.test.filters.SmallTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.server.display.brightness.clamper.BrightnessClamperController;
import com.android.server.testutils.OffsettableClock;
import org.junit.After;
@@ -96,6 +97,8 @@
@Mock HysteresisLevels mScreenBrightnessThresholdsIdle;
@Mock Handler mNoOpHandler;
@Mock BrightnessRangeController mBrightnessRangeController;
+ @Mock
+ BrightnessClamperController mBrightnessClamperController;
@Mock BrightnessThrottler mBrightnessThrottler;
@Before
@@ -161,7 +164,8 @@
mAmbientBrightnessThresholdsIdle, mScreenBrightnessThresholdsIdle,
mContext, mBrightnessRangeController, mBrightnessThrottler,
useHorizon ? AMBIENT_LIGHT_HORIZON_SHORT : 1,
- useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits
+ useHorizon ? AMBIENT_LIGHT_HORIZON_LONG : 10000, userLux, userNits,
+ mBrightnessClamperController
);
when(mBrightnessRangeController.getCurrentBrightnessMax()).thenReturn(
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
index 35b69f8..73a2f65 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayDeviceConfigTest.java
@@ -44,6 +44,7 @@
import android.hardware.display.DisplayManagerInternal;
import android.os.PowerManager;
import android.os.Temperature;
+import android.platform.test.annotations.RequiresFlagsEnabled;
import android.provider.Settings;
import android.util.SparseArray;
import android.util.Spline;
@@ -57,6 +58,7 @@
import com.android.server.display.config.IdleScreenRefreshRateTimeoutLuxThresholdPoint;
import com.android.server.display.config.ThermalStatus;
import com.android.server.display.feature.DisplayManagerFlags;
+import com.android.server.display.feature.flags.Flags;
import org.junit.Before;
import org.junit.Test;
@@ -380,7 +382,7 @@
public void testInvalidLuxThrottling() throws Exception {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getInvalidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
Map<DisplayDeviceConfig.BrightnessLimitMapType, Map<Float, Float>> luxThrottlingData =
mDisplayDeviceConfig.getLuxThrottlingData();
@@ -588,7 +590,7 @@
public void testProximitySensorWithEmptyValuesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getProxSensorWithEmptyValues(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
assertNull(mDisplayDeviceConfig.getProximitySensor());
}
@@ -596,7 +598,7 @@
public void testProximitySensorWithRefreshRatesFromDisplayConfig() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(
getContent(getValidLuxThrottling(), getValidProxSensorWithRefreshRateAndVsyncRate(),
- /* includeIdleMode= */ true));
+ /* includeIdleMode= */ true, /* enableEvenDimmer */ false));
assertEquals("test_proximity_sensor",
mDisplayDeviceConfig.getProximitySensor().type);
assertEquals("Test Proximity Sensor",
@@ -784,7 +786,7 @@
@Test
public void testBrightnessRamps_IdleFallsBackToConfigInteractive() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertEquals(mDisplayDeviceConfig.getBrightnessRampDecreaseMaxMillis(), 3000);
assertEquals(mDisplayDeviceConfig.getBrightnessRampIncreaseMaxMillis(), 2000);
@@ -801,14 +803,14 @@
@Test
public void testBrightnessCapForWearBedtimeMode() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertEquals(0.1f, mDisplayDeviceConfig.getBrightnessCapForWearBedtimeMode(), ZERO_DELTA);
}
@Test
public void testAutoBrightnessBrighteningLevels() throws IOException {
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertArrayEquals(new float[]{0.0f, 80},
mDisplayDeviceConfig.getAutoBrightnessBrighteningLevelsLux(
@@ -871,7 +873,7 @@
when(mFlags.areAutoBrightnessModesEnabled()).thenReturn(false);
setupDisplayDeviceConfigFromConfigResourceFile();
setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
- getValidProxSensor(), /* includeIdleMode= */ false));
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ false));
assertArrayEquals(new float[]{brightnessIntToFloat(50), brightnessIntToFloat(100),
brightnessIntToFloat(150)},
@@ -904,6 +906,18 @@
assertFalse(mDisplayDeviceConfig.isAutoBrightnessAvailable());
}
+ @RequiresFlagsEnabled(Flags.FLAG_EVEN_DIMMER)
+ @Test
+ public void testEvenDimmer() throws IOException {
+ when(mFlags.isEvenDimmerEnabled()).thenReturn(true);
+ setupDisplayDeviceConfigFromDisplayConfigFile(getContent(getValidLuxThrottling(),
+ getValidProxSensor(), /* includeIdleMode= */ false, /* enableEvenDimmer */ true));
+
+ assertTrue(mDisplayDeviceConfig.getLbmEnabled());
+ assertEquals(0.0001f, mDisplayDeviceConfig.getBacklightFromBrightness(0.1f), ZERO_DELTA);
+ assertEquals(0.2f, mDisplayDeviceConfig.getNitsFromBacklight(0.0f), ZERO_DELTA);
+ }
+
private String getValidLuxThrottling() {
return "<luxThrottling>\n"
+ " <brightnessLimitMap>\n"
@@ -1229,11 +1243,11 @@
private String getContent() {
return getContent(getValidLuxThrottling(), getValidProxSensor(),
- /* includeIdleMode= */ true);
+ /* includeIdleMode= */ true, false);
}
private String getContent(String brightnessCapConfig, String proxSensor,
- boolean includeIdleMode) {
+ boolean includeIdleMode, boolean enableEvenDimmer) {
return "<?xml version='1.0' encoding='utf-8' standalone='yes' ?>\n"
+ "<displayConfiguration>\n"
+ "<name>Example Display</name>\n"
@@ -1603,6 +1617,7 @@
+ "<majorVersion>2</majorVersion>\n"
+ "<minorVersion>0</minorVersion>\n"
+ "</usiVersion>\n"
+ + evenDimmerConfig(enableEvenDimmer)
+ "<screenBrightnessCapForWearBedtimeMode>"
+ "0.1"
+ "</screenBrightnessCapForWearBedtimeMode>"
@@ -1621,6 +1636,24 @@
+ "</displayConfiguration>\n";
}
+ private String evenDimmerConfig(boolean enabled) {
+ return (enabled ? "<lowBrightness enabled=\"true\">" : "<lowBrightness enabled=\"false\">")
+ + " <transitionPoint>0.1</transitionPoint>\n"
+ + " <nits>0.2</nits>\n"
+ + " <nits>2.0</nits>\n"
+ + " <nits>500.0</nits>\n"
+ + " <nits>1000.0</nits>\n"
+ + " <backlight>0</backlight>\n"
+ + " <backlight>0.0001</backlight>\n"
+ + " <backlight>0.5</backlight>\n"
+ + " <backlight>1.0</backlight>\n"
+ + " <brightness>0</brightness>\n"
+ + " <brightness>0.1</brightness>\n"
+ + " <brightness>0.5</brightness>\n"
+ + " <brightness>1.0</brightness>\n"
+ + "</lowBrightness>";
+ }
+
private void mockDeviceConfigs() {
when(mResources.getFloat(com.android.internal.R.dimen
.config_screenBrightnessSettingDefaultFloat)).thenReturn(0.5f);
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
index 01598ae..740ffc9 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayPowerControllerTest.java
@@ -1184,7 +1184,8 @@
/* ambientLightHorizonShort= */ anyInt(),
/* ambientLightHorizonLong= */ anyInt(),
eq(lux),
- eq(nits)
+ eq(nits),
+ any(BrightnessClamperController.class)
);
}
@@ -2121,7 +2122,8 @@
HysteresisLevels screenBrightnessThresholdsIdle, Context context,
BrightnessRangeController brightnessRangeController,
BrightnessThrottler brightnessThrottler, int ambientLightHorizonShort,
- int ambientLightHorizonLong, float userLux, float userNits) {
+ int ambientLightHorizonLong, float userLux, float userNits,
+ BrightnessClamperController brightnessClamperController) {
return mAutomaticBrightnessController;
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
index ac7d1f5..e4a7d98 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
+++ b/services/tests/displayservicetests/src/com/android/server/display/brightness/clamper/BrightnessLowLuxModifierTest.kt
@@ -65,7 +65,7 @@
Settings.Secure.putIntForUser(context.contentResolver,
Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
Settings.Secure.putFloatForUser(context.contentResolver,
- Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.7f, userId)
+ Settings.Secure.EVEN_DIMMER_MIN_NITS, 30.0f, userId)
modifier.recalculateLowerBound()
testHandler.flush()
assertThat(modifier.isActive).isTrue()
@@ -81,11 +81,22 @@
Settings.Secure.EVEN_DIMMER_ACTIVATED, 1, userId)
Settings.Secure.putFloatForUser(context.contentResolver,
Settings.Secure.EVEN_DIMMER_MIN_NITS, 0.0f, userId)
- modifier.recalculateLowerBound()
+ modifier.onAmbientLuxChange(3000.0f)
testHandler.flush()
assertThat(modifier.isActive).isTrue()
// Test restriction from lux setting
assertThat(modifier.brightnessReason).isEqualTo(BrightnessReason.MODIFIER_MIN_LUX)
}
+
+ @Test
+ fun testSettingOffDisablesModifier() {
+ Settings.Secure.putIntForUser(context.contentResolver,
+ Settings.Secure.EVEN_DIMMER_ACTIVATED, 0, userId)
+ assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+ modifier.onAmbientLuxChange(3000.0f)
+ testHandler.flush()
+ assertThat(modifier.isActive).isFalse()
+ assertThat(modifier.brightnessLowerBound).isEqualTo(PowerManager.BRIGHTNESS_MIN)
+ }
}
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 37967fa..65986ea 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -62,6 +62,7 @@
"cts-wm-util",
"platform-compat-test-rules",
"mockito-target-minus-junit4",
+ "mockito-kotlin2",
"platform-test-annotations",
"ShortcutManagerTestUtils",
"truth",
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
index b2ecea1..53c460c 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityManagerServiceTest.java
@@ -50,9 +50,11 @@
import android.accessibilityservice.IAccessibilityServiceClient;
import android.app.PendingIntent;
import android.app.RemoteAction;
+import android.content.BroadcastReceiver;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.IntentFilter;
import android.content.pm.ApplicationInfo;
import android.content.pm.PackageManager;
import android.content.pm.ResolveInfo;
@@ -67,6 +69,7 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.UserHandle;
+import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
import android.platform.test.flag.junit.DeviceFlagsValueProvider;
@@ -74,6 +77,7 @@
import android.testing.AndroidTestingRunner;
import android.testing.TestableContext;
import android.testing.TestableLooper;
+import android.util.ArrayMap;
import android.util.ArraySet;
import android.view.Display;
import android.view.DisplayAdjustments;
@@ -123,6 +127,7 @@
import java.util.ArrayList;
import java.util.HashSet;
+import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Set;
@@ -1464,6 +1469,52 @@
AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString());
}
+ @Test
+ @RequiresFlagsEnabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void restoreAccessibilityQsTargets_a11yQsTargetsRestored() {
+ String daltonizerTile =
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
+ String colorInversionTile =
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
+ final AccessibilityUserState userState = new AccessibilityUserState(
+ UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+ userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+ Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+ .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+ sendBroadcastToAccessibilityManagerService(intent);
+ mTestableLooper.processAllMessages();
+
+ assertThat(mA11yms.mUserStates.get(UserHandle.USER_SYSTEM).getA11yQsTargets())
+ .containsExactlyElementsIn(Set.of(daltonizerTile, colorInversionTile));
+ }
+
+ @Test
+ @RequiresFlagsDisabled(android.view.accessibility.Flags.FLAG_A11Y_QS_SHORTCUT)
+ public void restoreAccessibilityQsTargets_a11yQsTargetsNotRestored() {
+ String daltonizerTile =
+ AccessibilityShortcutController.DALTONIZER_COMPONENT_NAME.flattenToString();
+ String colorInversionTile =
+ AccessibilityShortcutController.COLOR_INVERSION_COMPONENT_NAME.flattenToString();
+ final AccessibilityUserState userState = new AccessibilityUserState(
+ UserHandle.USER_SYSTEM, mTestableContext, mA11yms);
+ userState.updateA11yQsTargetLocked(Set.of(daltonizerTile));
+ mA11yms.mUserStates.put(UserHandle.USER_SYSTEM, userState);
+
+ Intent intent = new Intent(Intent.ACTION_SETTING_RESTORED)
+ .addFlags(Intent.FLAG_RECEIVER_REGISTERED_ONLY)
+ .putExtra(Intent.EXTRA_SETTING_NAME, Settings.Secure.ACCESSIBILITY_QS_TARGETS)
+ .putExtra(Intent.EXTRA_SETTING_NEW_VALUE, colorInversionTile);
+ sendBroadcastToAccessibilityManagerService(intent);
+ mTestableLooper.processAllMessages();
+
+ assertThat(userState.getA11yQsTargets())
+ .containsExactlyElementsIn(Set.of(daltonizerTile));
+ }
+
private static AccessibilityServiceInfo mockAccessibilityServiceInfo(
ComponentName componentName) {
return mockAccessibilityServiceInfo(
@@ -1542,6 +1593,14 @@
mA11yms.getCurrentUserState().updateTileServiceMapForAccessibilityServiceLocked();
}
+ private void sendBroadcastToAccessibilityManagerService(Intent intent) {
+ if (!mTestableContext.getBroadcastReceivers().containsKey(intent.getAction())) {
+ return;
+ }
+ mTestableContext.getBroadcastReceivers().get(intent.getAction()).forEach(
+ broadcastReceiver -> broadcastReceiver.onReceive(mTestableContext, intent));
+ }
+
public static class FakeInputFilter extends AccessibilityInputFilter {
FakeInputFilter(Context context,
AccessibilityManagerService service) {
@@ -1552,6 +1611,7 @@
private static class A11yTestableContext extends TestableContext {
private final Context mMockContext;
+ private final Map<String, List<BroadcastReceiver>> mBroadcastReceivers = new ArrayMap<>();
A11yTestableContext(Context base) {
super(base);
@@ -1563,8 +1623,29 @@
mMockContext.startActivityAsUser(intent, options, user);
}
+ @Override
+ public Intent registerReceiverAsUser(BroadcastReceiver receiver, UserHandle user,
+ IntentFilter filter, String broadcastPermission, Handler scheduler) {
+ Iterator<String> actions = filter.actionsIterator();
+ if (actions != null) {
+ while (actions.hasNext()) {
+ String action = actions.next();
+ List<BroadcastReceiver> actionReceivers =
+ mBroadcastReceivers.getOrDefault(action, new ArrayList<>());
+ actionReceivers.add(receiver);
+ mBroadcastReceivers.put(action, actionReceivers);
+ }
+ }
+ return super.registerReceiverAsUser(
+ receiver, user, filter, broadcastPermission, scheduler);
+ }
+
Context getMockContext() {
return mMockContext;
}
+
+ Map<String, List<BroadcastReceiver>> getBroadcastReceivers() {
+ return mBroadcastReceivers;
+ }
}
}
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
index 4f6fc3d..0a696ef 100644
--- a/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/OverlayPackagesProviderTest.java
@@ -47,7 +47,7 @@
import androidx.annotation.NonNull;
import androidx.test.InstrumentationRegistry;
-import androidx.test.runner.AndroidJUnit4;
+import androidx.test.ext.junit.runners.AndroidJUnit4;
import com.android.internal.R;
@@ -67,9 +67,7 @@
/**
* Run this test with:
- *
* {@code atest FrameworksServicesTests:com.android.server.devicepolicy.OwnersTest}
- *
*/
@RunWith(AndroidJUnit4.class)
public class OverlayPackagesProviderTest {
@@ -87,8 +85,8 @@
private FakePackageManager mPackageManager;
private String[] mSystemAppsWithLauncher;
- private Set<String> mRegularMainlineModules = new HashSet<>();
- private Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
+ private final Set<String> mRegularMainlineModules = new HashSet<>();
+ private final Map<String, String> mMainlineModuleToDeclaredMetadataMap = new HashMap<>();
private OverlayPackagesProvider mHelper;
@Before
@@ -115,7 +113,8 @@
setVendorDisallowedAppsManagedUser();
mRealResources = InstrumentationRegistry.getTargetContext().getResources();
- mHelper = new OverlayPackagesProvider(mTestContext, mInjector);
+ mHelper = new OverlayPackagesProvider(mTestContext, mInjector,
+ new RecursiveStringArrayResourceResolver(mResources));
}
@Test
@@ -213,7 +212,7 @@
}
/**
- * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+ * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
*/
@Test
public void testAllowedAndDisallowedAtTheSameTimeManagedUser() {
@@ -224,7 +223,7 @@
}
/**
- * @see {@link #testAllowedAndDisallowedAtTheSameTimeManagedDevice}
+ * @see #testAllowedAndDisallowedAtTheSameTimeManagedDevice
*/
@Test
public void testAllowedAndDisallowedAtTheSameTimeManagedProfile() {
@@ -447,7 +446,7 @@
}
private void setSystemInputMethods(String... packageNames) {
- List<InputMethodInfo> inputMethods = new ArrayList<InputMethodInfo>();
+ List<InputMethodInfo> inputMethods = new ArrayList<>();
for (String packageName : packageNames) {
ApplicationInfo aInfo = new ApplicationInfo();
aInfo.flags = ApplicationInfo.FLAG_SYSTEM;
@@ -467,6 +466,7 @@
mSystemAppsWithLauncher = apps;
}
+ @SafeVarargs
private <T> Set<T> setFromArray(T... array) {
if (array == null) {
return null;
@@ -475,6 +475,7 @@
}
class FakePackageManager extends MockPackageManager {
+ @NonNull
@Override
public List<ResolveInfo> queryIntentActivitiesAsUser(Intent intent, int flags, int userId) {
assertWithMessage("Expected an intent with action ACTION_MAIN")
diff --git a/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
new file mode 100644
index 0000000..647f6c7
--- /dev/null
+++ b/services/tests/servicestests/src/com/android/server/devicepolicy/RecursiveStringArrayResourceResolverTest.kt
@@ -0,0 +1,96 @@
+/*
+ * 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.server.devicepolicy
+
+import android.annotation.ArrayRes
+import android.content.res.Resources
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.google.common.truth.Truth.assertWithMessage
+import com.google.errorprone.annotations.CanIgnoreReturnValue
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+
+/**
+ * Run this test with:
+ * `atest FrameworksServicesTests:com.android.server.devicepolicy.RecursiveStringArrayResourceResolverTest`
+ */
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class RecursiveStringArrayResourceResolverTest {
+ private companion object {
+ const val PACKAGE = "com.android.test"
+ const val ROOT_RESOURCE = "my_root_resource"
+ const val SUB_RESOURCE = "my_sub_resource"
+ const val EXTERNAL_PACKAGE = "com.external.test"
+ const val EXTERNAL_RESOURCE = "my_external_resource"
+ }
+
+ private val mResources = mock<Resources>()
+ private val mTarget = RecursiveStringArrayResourceResolver(mResources)
+
+ /**
+ * Mocks [Resources.getIdentifier] and [Resources.getStringArray] to return [values] and reference under a generated ID.
+ * @receiver mocked [Resources] container to configure
+ * @param pkg package name to "contain" mocked resource
+ * @param name mocked resource name
+ * @param values string-array resource values to return when mock is queried
+ * @return generated resource ID
+ */
+ @ArrayRes
+ @CanIgnoreReturnValue
+ private fun Resources.mockStringArrayResource(pkg: String, name: String, vararg values: String): Int {
+ val anId = (pkg + name).hashCode()
+ println("Mocking Resources::getIdentifier(name=\"$name\", defType=\"array\", defPackage=\"$pkg\") -> $anId")
+ whenever(getIdentifier(eq(name), eq("array"), eq(pkg))).thenReturn(anId)
+ println("Mocking Resources::getStringArray(id=$anId) -> ${values.asList()}")
+ whenever(getStringArray(eq(anId))).thenReturn(values)
+ return anId
+ }
+
+ @Test
+ fun testCanResolveTheArrayWithoutImports() {
+ val values = arrayOf("app.a", "app.b")
+ val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+ val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId = */ mockId)
+
+ assertWithMessage("Values are resolved correctly")
+ .that(actual).containsExactlyElementsIn(values)
+ }
+
+ @Test
+ fun testCanResolveTheArrayWithImports() {
+ val externalValues = arrayOf("ext.a", "ext.b", "#import:$PACKAGE/$SUB_RESOURCE")
+ mResources.mockStringArrayResource(pkg = EXTERNAL_PACKAGE, name = EXTERNAL_RESOURCE, values = externalValues)
+ val subValues = arrayOf("sub.a", "sub.b")
+ mResources.mockStringArrayResource(pkg = PACKAGE, name = SUB_RESOURCE, values = subValues)
+ val values = arrayOf("app.a", "#import:./$SUB_RESOURCE", "app.b", "#import:$EXTERNAL_PACKAGE/$EXTERNAL_RESOURCE", "app.c")
+ val mockId = mResources.mockStringArrayResource(pkg = PACKAGE, name = ROOT_RESOURCE, values = values)
+
+ val actual = mTarget.resolve(/* pkg= */ PACKAGE, /* rootId= */ mockId)
+
+ assertWithMessage("Values are resolved correctly")
+ .that(actual).containsExactlyElementsIn((externalValues + subValues + values)
+ .filterNot { it.startsWith("#import:") }
+ .toSet())
+ }
+}
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
index bfc47fd..cee6cdb 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/PreferencesHelperTest.java
@@ -3962,6 +3962,20 @@
}
@Test
+ public void testReadXml_existingPackage_bubblePrefsRestored() throws Exception {
+ mHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_ALL);
+ assertEquals(BUBBLE_PREFERENCE_ALL, mHelper.getBubblePreference(PKG_O, UID_O));
+
+ mXmlHelper.setBubblesAllowed(PKG_O, UID_O, BUBBLE_PREFERENCE_NONE);
+ assertEquals(BUBBLE_PREFERENCE_NONE, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+
+ ByteArrayOutputStream stream = writeXmlAndPurge(PKG_O, UID_O, false, UserHandle.USER_ALL);
+ loadStreamXml(stream, true, UserHandle.USER_ALL);
+
+ assertEquals(BUBBLE_PREFERENCE_ALL, mXmlHelper.getBubblePreference(PKG_O, UID_O));
+ }
+
+ @Test
public void testUpdateNotificationChannel_fixedPermission() {
List<UserInfo> users = ImmutableList.of(new UserInfo(UserHandle.USER_SYSTEM, "user0", 0));
when(mPermissionHelper.isPermissionFixed(PKG_O, 0)).thenReturn(true);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
index 8cbcc22..5861d88 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorManagerServiceTest.java
@@ -500,7 +500,8 @@
InOrder batteryVerifier = inOrder(mBatteryStatsMock);
batteryVerifier.verify(mBatteryStatsMock)
.noteVibratorOn(UID, oneShotDuration + mVibrationConfig.getRampDownDurationMs());
- batteryVerifier.verify(mBatteryStatsMock).noteVibratorOff(UID);
+ batteryVerifier
+ .verify(mBatteryStatsMock, timeout(TEST_TIMEOUT_MILLIS)).noteVibratorOff(UID);
}
@Test
diff --git a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
index 0a29dfb..60716cb 100644
--- a/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
+++ b/services/tests/wmtests/src/com/android/server/policy/ShortcutLoggingTests.java
@@ -95,8 +95,6 @@
new int[]{KeyEvent.KEYCODE_NOTIFICATION},
KeyboardLogEvent.TOGGLE_NOTIFICATION_PANEL, KeyEvent.KEYCODE_NOTIFICATION,
0},
- {"Meta + T -> Toggle Taskbar", new int[]{META_KEY, KeyEvent.KEYCODE_T},
- KeyboardLogEvent.TOGGLE_TASKBAR, KeyEvent.KEYCODE_T, META_ON},
{"Meta + Ctrl + S -> Take Screenshot",
new int[]{META_KEY, CTRL_KEY, KeyEvent.KEYCODE_S},
KeyboardLogEvent.TAKE_SCREENSHOT, KeyEvent.KEYCODE_S, META_ON | CTRL_ON},
diff --git a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
index c29547f..b9e87dc 100644
--- a/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/BackNavigationControllerTests.java
@@ -633,18 +633,23 @@
@Test
public void testAdjacentFocusInActivityEmbedding() {
mSetFlagsRule.enableFlags(Flags.FLAG_EMBEDDED_ACTIVITY_BACK_NAV_FLAG);
- Task task = createTask(mDefaultDisplay);
- TaskFragment primary = createTaskFragmentWithActivity(task);
- TaskFragment secondary = createTaskFragmentWithActivity(task);
- primary.setAdjacentTaskFragment(secondary);
- secondary.setAdjacentTaskFragment(primary);
+ final Task task = createTask(mDefaultDisplay);
+ final TaskFragment primaryTf = createTaskFragmentWithActivity(task);
+ final TaskFragment secondaryTf = createTaskFragmentWithActivity(task);
+ final ActivityRecord primaryActivity = primaryTf.getTopMostActivity();
+ final ActivityRecord secondaryActivity = secondaryTf.getTopMostActivity();
+ primaryTf.setAdjacentTaskFragment(secondaryTf);
+ secondaryTf.setAdjacentTaskFragment(primaryTf);
- WindowState windowState = mock(WindowState.class);
+ final WindowState windowState = mock(WindowState.class);
+ windowState.mActivityRecord = primaryActivity;
doReturn(windowState).when(mWm).getFocusedWindowLocked();
- doReturn(primary).when(windowState).getTaskFragment();
+ doReturn(primaryTf).when(windowState).getTaskFragment();
+ doReturn(1L).when(primaryActivity).getLastWindowCreateTime();
+ doReturn(2L).when(secondaryActivity).getLastWindowCreateTime();
startBackNavigation();
- verify(mWm).moveFocusToActivity(any());
+ verify(mWm).moveFocusToActivity(eq(secondaryActivity));
}
/**
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
index 5360a10..6b1bf26bf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskFragmentTest.java
@@ -887,20 +887,14 @@
assertEquals(winLeftTop, mDisplayContent.mCurrentFocus);
if (Flags.embeddedActivityBackNavFlag()) {
- // Send request to move the focus to top window from the left window.
- assertTrue(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
- // The focus should change.
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+ // Move focus if the adjacent activity is more recently active.
+ doReturn(1L).when(appLeftTop).getLastWindowCreateTime();
+ doReturn(2L).when(appRightTop).getLastWindowCreateTime();
+ assertTrue(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
- // Send request to move the focus to top window from the right window.
- assertFalse(mWm.moveFocusToTopEmbeddedWindow(winRightTop));
- // The focus should NOT change.
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
-
- // Do not move focus if the dim is boosted.
- taskFragmentLeft.mDimmerSurfaceBoosted = true;
- assertFalse(mWm.moveFocusToTopEmbeddedWindow(winLeftTop));
- assertEquals(winRightTop, mDisplayContent.mCurrentFocus);
+ // Do not move the focus if the adjacent activity is less recently active.
+ doReturn(3L).when(appLeftTop).getLastWindowCreateTime();
+ assertFalse(mWm.moveFocusToAdjacentEmbeddedWindow(winLeftTop));
}
}
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
index 3bd6496..a88680a 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskTests.java
@@ -1945,6 +1945,21 @@
assertEquals(2, finishCount[0]);
}
+ @Test
+ public void testPauseActivityWhenHasEmptyLeafTaskFragment() {
+ // Creating a task that has a RESUMED activity and an empty TaskFragment.
+ final Task task = new TaskBuilder(mSupervisor).setCreateActivity(true).build();
+ final ActivityRecord activity = task.getTopMostActivity();
+ new TaskFragmentBuilder(mAtm).setParentTask(task).build();
+ activity.setState(ActivityRecord.State.RESUMED, "test");
+
+ // Ensure the activity is paused if cannot be resumed.
+ doReturn(false).when(task).canBeResumed(any());
+ mSupervisor.mUserLeaving = true;
+ task.pauseActivityIfNeeded(null /* resuming */, "test");
+ verify(task).startPausing(eq(true) /* userLeaving */, anyBoolean(), any(), any());
+ }
+
private Task getTestTask() {
return new TaskBuilder(mSupervisor).setCreateActivity(true).build();
}