diff options
132 files changed, 4206 insertions, 686 deletions
diff --git a/Ravenwood.bp b/Ravenwood.bp index 4e360759c137..c73e04896173 100644 --- a/Ravenwood.bp +++ b/Ravenwood.bp @@ -145,6 +145,16 @@ java_library { } java_library { + name: "services.fakes.ravenwood-jarjar", + installable: false, + srcs: [":services.fakes-sources"], + libs: [ + "services.core.ravenwood", + ], + jarjar_rules: ":ravenwood-services-jarjar-rules", +} + +java_library { name: "mockito-ravenwood-prebuilt", installable: false, static_libs: [ @@ -189,6 +199,7 @@ android_ravenwood_libgroup { "ravenwood-helper-runtime", "hoststubgen-helper-runtime.ravenwood", "services.core.ravenwood-jarjar", + "services.fakes.ravenwood-jarjar", // Provide runtime versions of utils linked in below "junit", diff --git a/apex/jobscheduler/service/aconfig/device_idle.aconfig b/apex/jobscheduler/service/aconfig/device_idle.aconfig index fc24b3075f14..e4cb5ad81ba0 100644 --- a/apex/jobscheduler/service/aconfig/device_idle.aconfig +++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig @@ -4,5 +4,5 @@ flag { name: "disable_wakelocks_in_light_idle" namespace: "backstage_power" description: "Disable wakelocks for background apps while Light Device Idle is active" - bug: "299329948" + bug: "326607666" } diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig index ef9ac73d6f8e..5e6d3775f6a2 100644 --- a/apex/jobscheduler/service/aconfig/job.aconfig +++ b/apex/jobscheduler/service/aconfig/job.aconfig @@ -4,7 +4,7 @@ flag { name: "batch_active_bucket_jobs" namespace: "backstage_power" description: "Include jobs in the ACTIVE bucket in the job batching effort. Don't let them run as freely as they're ready." - bug: "299329948" + bug: "326607666" } flag { diff --git a/core/api/current.txt b/core/api/current.txt index bc89f1222bc4..ac98a944722d 100644 --- a/core/api/current.txt +++ b/core/api/current.txt @@ -3350,7 +3350,6 @@ package android.accessibilityservice { method @FlaggedApi("android.view.accessibility.a11y_overlay_callbacks") public void attachAccessibilityOverlayToWindow(int, @NonNull android.view.SurfaceControl, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer); method public boolean clearCache(); method public boolean clearCachedSubtree(@NonNull android.view.accessibility.AccessibilityNodeInfo); - method @FlaggedApi("android.view.accessibility.braille_display_hid") public void clearTestBrailleDisplayController(); method public final void disableSelf(); method public final boolean dispatchGesture(@NonNull android.accessibilityservice.GestureDescription, @Nullable android.accessibilityservice.AccessibilityService.GestureResultCallback, @Nullable android.os.Handler); method public android.view.accessibility.AccessibilityNodeInfo findFocus(int); @@ -3386,7 +3385,6 @@ package android.accessibilityservice { method public boolean setCacheEnabled(boolean); method public void setGestureDetectionPassthroughRegion(int, @NonNull android.graphics.Region); method public final void setServiceInfo(android.accessibilityservice.AccessibilityServiceInfo); - method @FlaggedApi("android.view.accessibility.braille_display_hid") public void setTestBrailleDisplayController(@NonNull android.accessibilityservice.BrailleDisplayController); method public void setTouchExplorationPassthroughRegion(int, @NonNull android.graphics.Region); method public void takeScreenshot(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); method public void takeScreenshotOfWindow(int, @NonNull java.util.concurrent.Executor, @NonNull android.accessibilityservice.AccessibilityService.TakeScreenshotCallback); @@ -13510,7 +13508,7 @@ package android.content.pm { field public static final int FLAG_USE_APP_ZYGOTE = 8; // 0x8 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CAMERA}, anyOf={android.Manifest.permission.CAMERA}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CAMERA = 64; // 0x40 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_CONNECTED_DEVICE}, anyOf={android.Manifest.permission.BLUETOOTH_ADVERTISE, android.Manifest.permission.BLUETOOTH_CONNECT, android.Manifest.permission.BLUETOOTH_SCAN, android.Manifest.permission.CHANGE_NETWORK_STATE, android.Manifest.permission.CHANGE_WIFI_STATE, android.Manifest.permission.CHANGE_WIFI_MULTICAST_STATE, android.Manifest.permission.NFC, android.Manifest.permission.TRANSMIT_IR, android.Manifest.permission.UWB_RANGING}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_CONNECTED_DEVICE = 16; // 0x10 - field @Deprecated @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 + field @RequiresPermission(value=android.Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1; // 0x1 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_HEALTH}, anyOf={android.Manifest.permission.ACTIVITY_RECOGNITION, android.Manifest.permission.BODY_SENSORS, android.Manifest.permission.HIGH_SAMPLING_RATE_SENSORS}) public static final int FOREGROUND_SERVICE_TYPE_HEALTH = 256; // 0x100 field @RequiresPermission(allOf={android.Manifest.permission.FOREGROUND_SERVICE_LOCATION}, anyOf={android.Manifest.permission.ACCESS_COARSE_LOCATION, android.Manifest.permission.ACCESS_FINE_LOCATION}, conditional=true) public static final int FOREGROUND_SERVICE_TYPE_LOCATION = 8; // 0x8 field public static final int FOREGROUND_SERVICE_TYPE_MANIFEST = -1; // 0xffffffff @@ -39801,6 +39799,7 @@ package android.security.keystore { method @Deprecated public boolean isInsideSecureHardware(); method public boolean isInvalidatedByBiometricEnrollment(); method public boolean isTrustedUserPresenceRequired(); + method @FlaggedApi("android.security.keyinfo_unlocked_device_required") public boolean isUnlockedDeviceRequired(); method public boolean isUserAuthenticationRequired(); method public boolean isUserAuthenticationRequirementEnforcedBySecureHardware(); method public boolean isUserAuthenticationValidWhileOnBody(); diff --git a/core/api/system-current.txt b/core/api/system-current.txt index 2fec0ad8ba1a..67ccd9d86c83 100644 --- a/core/api/system-current.txt +++ b/core/api/system-current.txt @@ -1647,14 +1647,12 @@ package android.app.ambientcontext { method public int getDensityLevel(); method @NonNull public java.time.Instant getEndTime(); method public int getEventType(); - method @FlaggedApi("android.app.ambient_heart_rate") @IntRange(from=0xffffffff) public int getRatePerMinute(); method @NonNull public java.time.Instant getStartTime(); method @NonNull public android.os.PersistableBundle getVendorData(); method public void writeToParcel(@NonNull android.os.Parcel, int); field @NonNull public static final android.os.Parcelable.Creator<android.app.ambientcontext.AmbientContextEvent> CREATOR; field public static final int EVENT_BACK_DOUBLE_TAP = 3; // 0x3 field public static final int EVENT_COUGH = 1; // 0x1 - field @FlaggedApi("android.app.ambient_heart_rate") public static final int EVENT_HEART_RATE = 4; // 0x4 field public static final int EVENT_SNORE = 2; // 0x2 field public static final int EVENT_UNKNOWN = 0; // 0x0 field public static final int EVENT_VENDOR_WEARABLE_START = 100000; // 0x186a0 @@ -1665,7 +1663,6 @@ package android.app.ambientcontext { field public static final int LEVEL_MEDIUM_HIGH = 4; // 0x4 field public static final int LEVEL_MEDIUM_LOW = 2; // 0x2 field public static final int LEVEL_UNKNOWN = 0; // 0x0 - field @FlaggedApi("android.app.ambient_heart_rate") public static final int RATE_PER_MINUTE_UNKNOWN = -1; // 0xffffffff } public static final class AmbientContextEvent.Builder { @@ -1675,7 +1672,6 @@ package android.app.ambientcontext { method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setDensityLevel(int); method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEndTime(@NonNull java.time.Instant); method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setEventType(int); - method @FlaggedApi("android.app.ambient_heart_rate") @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setRatePerMinute(@IntRange(from=0xffffffff) int); method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setStartTime(@NonNull java.time.Instant); method @NonNull public android.app.ambientcontext.AmbientContextEvent.Builder setVendorData(@NonNull android.os.PersistableBundle); } diff --git a/core/java/android/accessibilityservice/AccessibilityService.java b/core/java/android/accessibilityservice/AccessibilityService.java index 42c32723fd72..d70fa19a4468 100644 --- a/core/java/android/accessibilityservice/AccessibilityService.java +++ b/core/java/android/accessibilityservice/AccessibilityService.java @@ -81,7 +81,6 @@ import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.util.Collections; import java.util.List; -import java.util.Objects; import java.util.concurrent.Executor; import java.util.function.Consumer; import java.util.function.IntConsumer; @@ -853,7 +852,6 @@ public abstract class AccessibilityService extends Service { private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers = new SparseArray<>(0); private BrailleDisplayController mBrailleDisplayController; - private BrailleDisplayController mTestBrailleDisplayController; private int mGestureStatusCallbackSequence; @@ -3650,46 +3648,10 @@ public abstract class AccessibilityService extends Service { public BrailleDisplayController getBrailleDisplayController() { BrailleDisplayController.checkApiFlagIsEnabled(); synchronized (mLock) { - if (mTestBrailleDisplayController != null) { - return mTestBrailleDisplayController; - } - if (mBrailleDisplayController == null) { mBrailleDisplayController = new BrailleDisplayControllerImpl(this, mLock); } return mBrailleDisplayController; } } - - /** - * Set the {@link BrailleDisplayController} implementation that will be returned by - * {@link #getBrailleDisplayController}, to allow this accessibility service to test its - * interaction with BrailleDisplayController without requiring a real Braille display. - * - * <p>For full test fidelity, ensure that this test-only implementation follows the same - * behavior specified in the documentation for {@link BrailleDisplayController}, including - * thrown exceptions. - * - * @param controller A test-only implementation of {@link BrailleDisplayController}. - */ - @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) - public void setTestBrailleDisplayController(@NonNull BrailleDisplayController controller) { - BrailleDisplayController.checkApiFlagIsEnabled(); - Objects.requireNonNull(controller); - synchronized (mLock) { - mTestBrailleDisplayController = controller; - } - } - - /** - * Clears the {@link BrailleDisplayController} previously set by - * {@link #setTestBrailleDisplayController}. - */ - @FlaggedApi(android.view.accessibility.Flags.FLAG_BRAILLE_DISPLAY_HID) - public void clearTestBrailleDisplayController() { - BrailleDisplayController.checkApiFlagIsEnabled(); - synchronized (mLock) { - mTestBrailleDisplayController = null; - } - } } diff --git a/core/java/android/app/ActivityThread.java b/core/java/android/app/ActivityThread.java index 074f7e993eb4..41151c0dc647 100644 --- a/core/java/android/app/ActivityThread.java +++ b/core/java/android/app/ActivityThread.java @@ -717,7 +717,7 @@ public final class ActivityThread extends ClientTransactionHandler } activity.mMainThread.handleActivityConfigurationChanged( ActivityClientRecord.this, overrideConfig, newDisplayId, - false /* alwaysReportChange */); + mActivityWindowInfo, false /* alwaysReportChange */); } @Override @@ -6659,11 +6659,12 @@ public final class ActivityThread extends ClientTransactionHandler /** * Sets the supplied {@code overrideConfig} as pending for the {@code token}. Calling * this method prevents any calls to - * {@link #handleActivityConfigurationChanged(ActivityClientRecord, Configuration, int)} from - * processing any configurations older than {@code overrideConfig}. + * {@link #handleActivityConfigurationChanged(ActivityClientRecord, Configuration, int, + * ActivityWindowInfo)} from processing any configurations older than {@code overrideConfig}. */ @Override - public void updatePendingActivityConfiguration(IBinder token, Configuration overrideConfig) { + public void updatePendingActivityConfiguration(@NonNull IBinder token, + @NonNull Configuration overrideConfig) { synchronized (mPendingOverrideConfigs) { final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(token); if (pendingOverrideConfig != null @@ -6680,9 +6681,10 @@ public final class ActivityThread extends ClientTransactionHandler } @Override - public void handleActivityConfigurationChanged(ActivityClientRecord r, - @NonNull Configuration overrideConfig, int displayId) { - handleActivityConfigurationChanged(r, overrideConfig, displayId, + public void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo) { + handleActivityConfigurationChanged(r, overrideConfig, displayId, activityWindowInfo, // This is the only place that uses alwaysReportChange=true. The entry point should // be from ActivityConfigurationChangeItem or MoveToDisplayItem, so the server side // has confirmed the activity should handle the configuration instead of relaunch. @@ -6700,9 +6702,11 @@ public final class ActivityThread extends ClientTransactionHandler * @param overrideConfig Activity override config. * @param displayId Id of the display where activity was moved to, -1 if there was no move and * value didn't change. + * @param activityWindowInfo the window info of the given activity. */ - void handleActivityConfigurationChanged(ActivityClientRecord r, - @NonNull Configuration overrideConfig, int displayId, boolean alwaysReportChange) { + void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo, boolean alwaysReportChange) { synchronized (mPendingOverrideConfigs) { final Configuration pendingOverrideConfig = mPendingOverrideConfigs.get(r.token); if (overrideConfig.isOtherSeqNewer(pendingOverrideConfig)) { @@ -6735,6 +6739,8 @@ public final class ActivityThread extends ClientTransactionHandler // Perform updates. r.overrideConfig = overrideConfig; + r.mActivityWindowInfo = activityWindowInfo; + // TODO(b/287582673): notify on ActivityWindowInfo change final ViewRootImpl viewRoot = r.activity.mDecor != null ? r.activity.mDecor.getViewRootImpl() : null; diff --git a/core/java/android/app/ClientTransactionHandler.java b/core/java/android/app/ClientTransactionHandler.java index 4c92dee6ff17..b5b3669c1d80 100644 --- a/core/java/android/app/ClientTransactionHandler.java +++ b/core/java/android/app/ClientTransactionHandler.java @@ -167,11 +167,12 @@ public abstract class ClientTransactionHandler { /** Set pending activity configuration in case it will be updated by other transaction item. */ public abstract void updatePendingActivityConfiguration(@NonNull IBinder token, - Configuration overrideConfig); + @NonNull Configuration overrideConfig); /** Deliver activity (override) configuration change. */ public abstract void handleActivityConfigurationChanged(@NonNull ActivityClientRecord r, - Configuration overrideConfig, int displayId); + @NonNull Configuration overrideConfig, int displayId, + @NonNull ActivityWindowInfo activityWindowInfo); /** Deliver {@link android.window.WindowContextInfo} change. */ public abstract void handleWindowContextInfoChanged(@NonNull IBinder clientToken, diff --git a/core/java/android/app/ForegroundServiceTypePolicy.java b/core/java/android/app/ForegroundServiceTypePolicy.java index 7e06735791ff..d1e517bbd03c 100644 --- a/core/java/android/app/ForegroundServiceTypePolicy.java +++ b/core/java/android/app/ForegroundServiceTypePolicy.java @@ -62,7 +62,6 @@ import android.content.pm.ServiceInfo.ForegroundServiceType; import android.hardware.usb.UsbAccessory; import android.hardware.usb.UsbDevice; import android.hardware.usb.UsbManager; -import android.os.Build; import android.os.RemoteException; import android.os.ServiceManager; import android.os.UserHandle; @@ -128,14 +127,10 @@ public abstract class ForegroundServiceTypePolicy { * The FGS type enforcement: * deprecating the {@link android.content.pm.ServiceInfo#FOREGROUND_SERVICE_TYPE_DATA_SYNC}. * - * <p>Starting a FGS with this type from apps with targetSdkVersion - * {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} or later will result in a warning - * in the log. - * * @hide */ @ChangeId - @EnabledAfter(targetSdkVersion = Build.VERSION_CODES.UPSIDE_DOWN_CAKE) + @Disabled @Overridable public static final long FGS_TYPE_DATA_SYNC_DEPRECATION_CHANGE_ID = 255039210L; diff --git a/core/java/android/app/ambient_context.aconfig b/core/java/android/app/ambient_context.aconfig deleted file mode 100644 index 3f73da216b9f..000000000000 --- a/core/java/android/app/ambient_context.aconfig +++ /dev/null @@ -1,8 +0,0 @@ -package: "android.app" - -flag { - namespace: "biometrics_integration" - name: "ambient_heart_rate" - description: "Feature flag for adding heart rate api to ambient context." - bug: "318309481" -} diff --git a/core/java/android/app/ambientcontext/AmbientContextEvent.java b/core/java/android/app/ambientcontext/AmbientContextEvent.java index 5ab7991c6326..13d959c79cd2 100644 --- a/core/java/android/app/ambientcontext/AmbientContextEvent.java +++ b/core/java/android/app/ambientcontext/AmbientContextEvent.java @@ -16,9 +16,7 @@ package android.app.ambientcontext; -import android.annotation.FlaggedApi; import android.annotation.IntDef; -import android.annotation.IntRange; import android.annotation.NonNull; import android.annotation.SystemApi; import android.os.Parcelable; @@ -70,14 +68,6 @@ public final class AmbientContextEvent implements Parcelable { public static final int EVENT_BACK_DOUBLE_TAP = 3; /** - * The integer indicating a heart rate measurement was done. - * - * @see #getRatePerMinute - */ - @Event @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) - public static final int EVENT_HEART_RATE = 4; - - /** * Integer indicating the start of wearable vendor defined events that can be detected. * These depend on the vendor implementation. */ @@ -89,19 +79,12 @@ public final class AmbientContextEvent implements Parcelable { */ public static final String KEY_VENDOR_WEARABLE_EVENT_NAME = "wearable_event_name"; - /** - * Default value for {@link #getRatePerMinute}. Indicates that the rate of the event is unknown. - */ - @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) - public static final int RATE_PER_MINUTE_UNKNOWN = -1; - /** @hide */ @IntDef(prefix = { "EVENT_" }, value = { EVENT_UNKNOWN, EVENT_COUGH, EVENT_SNORE, EVENT_BACK_DOUBLE_TAP, - EVENT_HEART_RATE, EVENT_VENDOR_WEARABLE_START, }) @Retention(RetentionPolicy.SOURCE) @@ -187,16 +170,6 @@ public final class AmbientContextEvent implements Parcelable { return new PersistableBundle(); } - /** - * Rate per minute of the event during the start to end time. - * - * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown. - */ - private final @IntRange(from = -1) int mRatePerMinute; - private static int defaultRatePerMinute() { - return RATE_PER_MINUTE_UNKNOWN; - } - // Code below generated by codegen v1.0.23. @@ -206,8 +179,6 @@ public final class AmbientContextEvent implements Parcelable { // // To regenerate run: // $ codegen $ANDROID_BUILD_TOP/frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java - // then manually add @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) back to flagged - // APIs. // // To exclude the generated code from IntelliJ auto-formatting enable (one-time): // Settings > Editor > Code Style > Formatter Control @@ -220,7 +191,6 @@ public final class AmbientContextEvent implements Parcelable { EVENT_COUGH, EVENT_SNORE, EVENT_BACK_DOUBLE_TAP, - EVENT_HEART_RATE, EVENT_VENDOR_WEARABLE_START }) @Retention(RetentionPolicy.SOURCE) @@ -239,8 +209,6 @@ public final class AmbientContextEvent implements Parcelable { return "EVENT_SNORE"; case EVENT_BACK_DOUBLE_TAP: return "EVENT_BACK_DOUBLE_TAP"; - case EVENT_HEART_RATE: - return "EVENT_HEART_RATE"; case EVENT_VENDOR_WEARABLE_START: return "EVENT_VENDOR_WEARABLE_START"; default: return Integer.toHexString(value); @@ -287,8 +255,7 @@ public final class AmbientContextEvent implements Parcelable { @NonNull Instant endTime, @LevelValue int confidenceLevel, @LevelValue int densityLevel, - @NonNull PersistableBundle vendorData, - @IntRange(from = -1) int ratePerMinute) { + @NonNull PersistableBundle vendorData) { this.mEventType = eventType; com.android.internal.util.AnnotationValidations.validate( EventCode.class, null, mEventType); @@ -307,10 +274,6 @@ public final class AmbientContextEvent implements Parcelable { this.mVendorData = vendorData; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mVendorData); - this.mRatePerMinute = ratePerMinute; - com.android.internal.util.AnnotationValidations.validate( - IntRange.class, null, mRatePerMinute, - "from", -1); // onConstructed(); // You can define this method to get a callback } @@ -367,17 +330,6 @@ public final class AmbientContextEvent implements Parcelable { return mVendorData; } - /** - * Rate per minute of the event during the start to end time. - * - * @return the rate per minute, or {@link #RATE_PER_MINUTE_UNKNOWN} if the rate is unknown. - */ - @DataClass.Generated.Member - @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) - public @IntRange(from = -1) int getRatePerMinute() { - return mRatePerMinute; - } - @Override @DataClass.Generated.Member public String toString() { @@ -390,8 +342,7 @@ public final class AmbientContextEvent implements Parcelable { "endTime = " + mEndTime + ", " + "confidenceLevel = " + mConfidenceLevel + ", " + "densityLevel = " + mDensityLevel + ", " + - "vendorData = " + mVendorData + ", " + - "ratePerMinute = " + mRatePerMinute + + "vendorData = " + mVendorData + " }"; } @@ -429,7 +380,6 @@ public final class AmbientContextEvent implements Parcelable { dest.writeInt(mConfidenceLevel); dest.writeInt(mDensityLevel); dest.writeTypedObject(mVendorData, flags); - dest.writeInt(mRatePerMinute); } @Override @@ -449,7 +399,6 @@ public final class AmbientContextEvent implements Parcelable { int confidenceLevel = in.readInt(); int densityLevel = in.readInt(); PersistableBundle vendorData = (PersistableBundle) in.readTypedObject(PersistableBundle.CREATOR); - int ratePerMinute = in.readInt(); this.mEventType = eventType; com.android.internal.util.AnnotationValidations.validate( @@ -469,10 +418,6 @@ public final class AmbientContextEvent implements Parcelable { this.mVendorData = vendorData; com.android.internal.util.AnnotationValidations.validate( NonNull.class, null, mVendorData); - this.mRatePerMinute = ratePerMinute; - com.android.internal.util.AnnotationValidations.validate( - IntRange.class, null, mRatePerMinute, - "from", -1); // onConstructed(); // You can define this method to get a callback } @@ -504,7 +449,6 @@ public final class AmbientContextEvent implements Parcelable { private @LevelValue int mConfidenceLevel; private @LevelValue int mDensityLevel; private @NonNull PersistableBundle mVendorData; - private @IntRange(from = -1) int mRatePerMinute; private long mBuilderFieldsSet = 0L; @@ -581,22 +525,10 @@ public final class AmbientContextEvent implements Parcelable { return this; } - /** - * Rate per minute of the event during the start to end time. - */ - @DataClass.Generated.Member - @FlaggedApi(android.app.Flags.FLAG_AMBIENT_HEART_RATE) - public @NonNull Builder setRatePerMinute(@IntRange(from = -1) int value) { - checkNotUsed(); - mBuilderFieldsSet |= 0x40; - mRatePerMinute = value; - return this; - } - /** Builds the instance. This builder should not be touched after calling this! */ public @NonNull AmbientContextEvent build() { checkNotUsed(); - mBuilderFieldsSet |= 0x80; // Mark builder used + mBuilderFieldsSet |= 0x40; // Mark builder used if ((mBuilderFieldsSet & 0x1) == 0) { mEventType = defaultEventType(); @@ -616,22 +548,18 @@ public final class AmbientContextEvent implements Parcelable { if ((mBuilderFieldsSet & 0x20) == 0) { mVendorData = defaultVendorData(); } - if ((mBuilderFieldsSet & 0x40) == 0) { - mRatePerMinute = defaultRatePerMinute(); - } AmbientContextEvent o = new AmbientContextEvent( mEventType, mStartTime, mEndTime, mConfidenceLevel, mDensityLevel, - mVendorData, - mRatePerMinute); + mVendorData); return o; } private void checkNotUsed() { - if ((mBuilderFieldsSet & 0x80) != 0) { + if ((mBuilderFieldsSet & 0x40) != 0) { throw new IllegalStateException( "This Builder should not be reused. Use a new Builder instance instead"); } @@ -639,10 +567,10 @@ public final class AmbientContextEvent implements Parcelable { } @DataClass.Generated( - time = 1705575046107L, + time = 1709014715064L, codegenVersion = "1.0.23", sourceFile = "frameworks/base/core/java/android/app/ambientcontext/AmbientContextEvent.java", - inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final @android.app.ambientcontext.AmbientContextEvent.Event @android.annotation.FlaggedApi int EVENT_HEART_RATE\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final @android.annotation.FlaggedApi int RATE_PER_MINUTE_UNKNOWN\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate final @android.annotation.IntRange int mRatePerMinute\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nprivate static int defaultRatePerMinute()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") + inputSignatures = "public static final int EVENT_UNKNOWN\npublic static final int EVENT_COUGH\npublic static final int EVENT_SNORE\npublic static final int EVENT_BACK_DOUBLE_TAP\npublic static final int EVENT_VENDOR_WEARABLE_START\npublic static final java.lang.String KEY_VENDOR_WEARABLE_EVENT_NAME\npublic static final int LEVEL_UNKNOWN\npublic static final int LEVEL_LOW\npublic static final int LEVEL_MEDIUM_LOW\npublic static final int LEVEL_MEDIUM\npublic static final int LEVEL_MEDIUM_HIGH\npublic static final int LEVEL_HIGH\nprivate final @android.app.ambientcontext.AmbientContextEvent.EventCode int mEventType\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mStartTime\nprivate final @com.android.internal.util.DataClass.ParcelWith(com.android.internal.util.Parcelling.BuiltIn.ForInstant.class) @android.annotation.NonNull java.time.Instant mEndTime\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mConfidenceLevel\nprivate final @android.app.ambientcontext.AmbientContextEvent.LevelValue int mDensityLevel\nprivate final @android.annotation.NonNull android.os.PersistableBundle mVendorData\nprivate static int defaultEventType()\nprivate static @android.annotation.NonNull java.time.Instant defaultStartTime()\nprivate static @android.annotation.NonNull java.time.Instant defaultEndTime()\nprivate static int defaultConfidenceLevel()\nprivate static int defaultDensityLevel()\nprivate static android.os.PersistableBundle defaultVendorData()\nclass AmbientContextEvent extends java.lang.Object implements [android.os.Parcelable]\n@com.android.internal.util.DataClass(genBuilder=true, genConstructor=false, genHiddenConstDefs=true, genParcelable=true, genToString=true)") @Deprecated private void __metadata() {} diff --git a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java index bc8fac5fa0ce..48ea846e8d50 100644 --- a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java +++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java @@ -29,6 +29,7 @@ import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import android.window.ActivityWindowInfo; import java.util.Objects; @@ -49,11 +50,13 @@ public class ActivityConfigurationChangeItem extends ActivityTransactionItem { } @Override - public void execute(@NonNull ClientTransactionHandler client, @Nullable ActivityClientRecord r, + public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r, @NonNull PendingTransactionActions pendingActions) { // TODO(lifecycler): detect if PIP or multi-window mode changed and report it here. Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityConfigChanged"); - client.handleActivityConfigurationChanged(r, mConfiguration, INVALID_DISPLAY); + client.handleActivityConfigurationChanged(r, mConfiguration, INVALID_DISPLAY, + // TODO(b/287582673): add ActivityWindowInfo + new ActivityWindowInfo()); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } diff --git a/core/java/android/app/servertransaction/MoveToDisplayItem.java b/core/java/android/app/servertransaction/MoveToDisplayItem.java index 1353d1679427..0702c4594075 100644 --- a/core/java/android/app/servertransaction/MoveToDisplayItem.java +++ b/core/java/android/app/servertransaction/MoveToDisplayItem.java @@ -28,6 +28,7 @@ import android.content.res.Configuration; import android.os.IBinder; import android.os.Parcel; import android.os.Trace; +import android.window.ActivityWindowInfo; import java.util.Objects; @@ -39,6 +40,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { private int mTargetDisplayId; private Configuration mConfiguration; + private ActivityWindowInfo mActivityWindowInfo; @Override public void preExecute(@NonNull ClientTransactionHandler client) { @@ -52,7 +54,8 @@ public class MoveToDisplayItem extends ActivityTransactionItem { public void execute(@NonNull ClientTransactionHandler client, @NonNull ActivityClientRecord r, @NonNull PendingTransactionActions pendingActions) { Trace.traceBegin(TRACE_TAG_ACTIVITY_MANAGER, "activityMovedToDisplay"); - client.handleActivityConfigurationChanged(r, mConfiguration, mTargetDisplayId); + client.handleActivityConfigurationChanged(r, mConfiguration, mTargetDisplayId, + mActivityWindowInfo); Trace.traceEnd(TRACE_TAG_ACTIVITY_MANAGER); } @@ -69,7 +72,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { /** Obtain an instance initialized with provided params. */ @NonNull public static MoveToDisplayItem obtain(@NonNull IBinder activityToken, int targetDisplayId, - @NonNull Configuration configuration) { + @NonNull Configuration configuration, @NonNull ActivityWindowInfo activityWindowInfo) { MoveToDisplayItem instance = ObjectPool.obtain(MoveToDisplayItem.class); if (instance == null) { instance = new MoveToDisplayItem(); @@ -77,6 +80,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { instance.setActivityToken(activityToken); instance.mTargetDisplayId = targetDisplayId; instance.mConfiguration = new Configuration(configuration); + instance.mActivityWindowInfo = new ActivityWindowInfo(activityWindowInfo); return instance; } @@ -86,6 +90,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super.recycle(); mTargetDisplayId = 0; mConfiguration = null; + mActivityWindowInfo = null; ObjectPool.recycle(this); } @@ -97,6 +102,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super.writeToParcel(dest, flags); dest.writeInt(mTargetDisplayId); dest.writeTypedObject(mConfiguration, flags); + dest.writeTypedObject(mActivityWindowInfo, flags); } /** Read from Parcel. */ @@ -104,6 +110,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { super(in); mTargetDisplayId = in.readInt(); mConfiguration = in.readTypedObject(Configuration.CREATOR); + mActivityWindowInfo = in.readTypedObject(ActivityWindowInfo.CREATOR); } public static final @NonNull Creator<MoveToDisplayItem> CREATOR = new Creator<>() { @@ -126,7 +133,8 @@ public class MoveToDisplayItem extends ActivityTransactionItem { } final MoveToDisplayItem other = (MoveToDisplayItem) o; return mTargetDisplayId == other.mTargetDisplayId - && Objects.equals(mConfiguration, other.mConfiguration); + && Objects.equals(mConfiguration, other.mConfiguration) + && Objects.equals(mActivityWindowInfo, other.mActivityWindowInfo); } @Override @@ -135,6 +143,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { result = 31 * result + super.hashCode(); result = 31 * result + mTargetDisplayId; result = 31 * result + mConfiguration.hashCode(); + result = 31 * result + Objects.hashCode(mActivityWindowInfo); return result; } @@ -142,6 +151,7 @@ public class MoveToDisplayItem extends ActivityTransactionItem { public String toString() { return "MoveToDisplayItem{" + super.toString() + ",targetDisplayId=" + mTargetDisplayId - + ",configuration=" + mConfiguration + "}"; + + ",configuration=" + mConfiguration + + ",activityWindowInfo=" + mActivityWindowInfo + "}"; } } diff --git a/core/java/android/content/ClipData.java b/core/java/android/content/ClipData.java index 728c350bfb51..b42133939f28 100644 --- a/core/java/android/content/ClipData.java +++ b/core/java/android/content/ClipData.java @@ -169,6 +169,8 @@ import java.util.List; */ @android.ravenwood.annotation.RavenwoodKeepWholeClass public class ClipData implements Parcelable { + private static final String TAG = "ClipData"; + static final String[] MIMETYPES_TEXT_PLAIN = new String[] { ClipDescription.MIMETYPE_TEXT_PLAIN }; static final String[] MIMETYPES_TEXT_HTML = new String[] { @@ -476,7 +478,6 @@ public class ClipData implements Parcelable { * @return Returns the item's textual representation. */ //BEGIN_INCLUDE(coerceToText) - @android.ravenwood.annotation.RavenwoodThrow public CharSequence coerceToText(Context context) { // If this Item has an explicit textual value, simply return that. CharSequence text = getText(); @@ -484,13 +485,20 @@ public class ClipData implements Parcelable { return text; } + // Gracefully handle cases where resolver isn't available + ContentResolver resolver = null; + try { + resolver = context.getContentResolver(); + } catch (Exception e) { + Log.w(TAG, "Failed to obtain ContentResolver: " + e); + } + // If this Item has a URI value, try using that. Uri uri = getUri(); - if (uri != null) { + if (uri != null && resolver != null) { // First see if the URI can be opened as a plain text stream // (of any sub-type). If so, this is the best textual // representation for it. - final ContentResolver resolver = context.getContentResolver(); AssetFileDescriptor descr = null; FileInputStream stream = null; InputStreamReader reader = null; @@ -499,7 +507,7 @@ public class ClipData implements Parcelable { // Ask for a stream of the desired type. descr = resolver.openTypedAssetFileDescriptor(uri, "text/*", null); } catch (SecurityException e) { - Log.w("ClipData", "Failure opening stream", e); + Log.w(TAG, "Failure opening stream", e); } catch (FileNotFoundException|RuntimeException e) { // Unable to open content URI as text... not really an // error, just something to ignore. @@ -519,7 +527,7 @@ public class ClipData implements Parcelable { return builder.toString(); } catch (IOException e) { // Something bad has happened. - Log.w("ClipData", "Failure loading text", e); + Log.w(TAG, "Failure loading text", e); return e.toString(); } } @@ -528,7 +536,8 @@ public class ClipData implements Parcelable { IoUtils.closeQuietly(stream); IoUtils.closeQuietly(reader); } - + } + if (uri != null) { // If we couldn't open the URI as a stream, use the URI itself as a textual // representation (but not for "content", "android.resource" or "file" schemes). final String scheme = uri.getScheme(); @@ -704,7 +713,7 @@ public class ClipData implements Parcelable { } } catch (SecurityException e) { - Log.w("ClipData", "Failure opening stream", e); + Log.w(TAG, "Failure opening stream", e); } catch (FileNotFoundException e) { // Unable to open content URI as text... not really an @@ -712,7 +721,7 @@ public class ClipData implements Parcelable { } catch (IOException e) { // Something bad has happened. - Log.w("ClipData", "Failure loading text", e); + Log.w(TAG, "Failure loading text", e); return Html.escapeHtml(e.toString()); } finally { @@ -1123,7 +1132,7 @@ public class ClipData implements Parcelable { * * @hide */ - @android.ravenwood.annotation.RavenwoodThrow + @android.ravenwood.annotation.RavenwoodKeep public void prepareToLeaveProcess(boolean leavingPackage) { // Assume that callers are going to be granting permissions prepareToLeaveProcess(leavingPackage, Intent.FLAG_GRANT_READ_URI_PERMISSION); @@ -1134,7 +1143,7 @@ public class ClipData implements Parcelable { * * @hide */ - @android.ravenwood.annotation.RavenwoodThrow + @android.ravenwood.annotation.RavenwoodReplace public void prepareToLeaveProcess(boolean leavingPackage, int intentFlags) { final int size = mItems.size(); for (int i = 0; i < size; i++) { @@ -1154,6 +1163,11 @@ public class ClipData implements Parcelable { } } + /** @hide */ + public void prepareToLeaveProcess$ravenwood(boolean leavingPackage, int intentFlags) { + // No process boundaries on Ravenwood; ignored + } + /** {@hide} */ @android.ravenwood.annotation.RavenwoodThrow public void prepareToEnterProcess(AttributionSource source) { diff --git a/core/java/android/content/ClipboardManager.java b/core/java/android/content/ClipboardManager.java index 107f1078b11e..2fabcbae9bbb 100644 --- a/core/java/android/content/ClipboardManager.java +++ b/core/java/android/content/ClipboardManager.java @@ -50,6 +50,7 @@ import java.util.Objects; * </div> */ @SystemService(Context.CLIPBOARD_SERVICE) +@android.ravenwood.annotation.RavenwoodKeepWholeClass public class ClipboardManager extends android.text.ClipboardManager { /** @@ -143,6 +144,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION) + @android.ravenwood.annotation.RavenwoodThrow public boolean areClipboardAccessNotificationsEnabled() { try { return mService.areClipboardAccessNotificationsEnabledForUser(mContext.getUserId()); @@ -159,6 +161,7 @@ public class ClipboardManager extends android.text.ClipboardManager { */ @SystemApi @RequiresPermission(Manifest.permission.MANAGE_CLIPBOARD_ACCESS_NOTIFICATION) + @android.ravenwood.annotation.RavenwoodThrow public void setClipboardAccessNotificationsEnabled(boolean enable) { try { mService.setClipboardAccessNotificationsEnabledForUser(enable, mContext.getUserId()); diff --git a/core/java/android/content/pm/ServiceInfo.java b/core/java/android/content/pm/ServiceInfo.java index 9c6aab4bc9fb..5b0cee75e591 100644 --- a/core/java/android/content/pm/ServiceInfo.java +++ b/core/java/android/content/pm/ServiceInfo.java @@ -163,25 +163,12 @@ public class ServiceInfo extends ComponentInfo * Because of this, developers must make sure to stop the foreground service even if * {@link android.app.Service#onTimeout(int, int)} is not called on such versions. * - * <p>Apps targeting API level {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and - * later should <b>NOT</b> use this type: calling - * {@link android.app.Service#startForeground(int, android.app.Notification, int)} with - * this type on devices running {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} is - * still allowed, but it may throw an {@link android.app.InvalidForegroundServiceTypeException} - * in future platform releases. - * - * <p class="note"> - * Use the {@link android.app.job.JobInfo.Builder#setUserInitiated(boolean)} API for - * user-initiated, network data transfers. - * - * @deprecated Use {@link android.app.job.JobInfo.Builder} APIs or alternate FGS types - * (like {@link #FOREGROUND_SERVICE_TYPE_MEDIA_PROCESSING}) applicable to your use-case. + * @see android.app.Service#onTimeout(int, int) */ @RequiresPermission( value = Manifest.permission.FOREGROUND_SERVICE_DATA_SYNC, conditional = true ) - @Deprecated public static final int FOREGROUND_SERVICE_TYPE_DATA_SYNC = 1 << 0; /** diff --git a/core/java/android/os/HandlerThread.java b/core/java/android/os/HandlerThread.java index 36730cb07344..f852d3cd69b2 100644 --- a/core/java/android/os/HandlerThread.java +++ b/core/java/android/os/HandlerThread.java @@ -19,6 +19,8 @@ package android.os; import android.annotation.NonNull; import android.annotation.Nullable; +import java.util.concurrent.Executor; + /** * A {@link Thread} that has a {@link Looper}. * The {@link Looper} can then be used to create {@link Handler}s. @@ -30,7 +32,8 @@ public class HandlerThread extends Thread { int mPriority; int mTid = -1; Looper mLooper; - private @Nullable Handler mHandler; + private volatile @Nullable Handler mHandler; + private volatile @Nullable Executor mExecutor; public HandlerThread(String name) { super(name); @@ -131,6 +134,18 @@ public class HandlerThread extends Thread { } /** + * @return a shared {@link Executor} associated with this thread + * @hide + */ + @NonNull + public Executor getThreadExecutor() { + if (mExecutor == null) { + mExecutor = new HandlerExecutor(getThreadHandler()); + } + return mExecutor; + } + + /** * Quits the handler thread's looper. * <p> * Causes the handler thread's looper to terminate without processing any diff --git a/core/java/android/security/flags.aconfig b/core/java/android/security/flags.aconfig index 76314546b4f0..5e7edda31c19 100644 --- a/core/java/android/security/flags.aconfig +++ b/core/java/android/security/flags.aconfig @@ -31,6 +31,13 @@ flag { } flag { + name: "keyinfo_unlocked_device_required" + namespace: "hardware_backed_security" + description: "Add the API android.security.keystore.KeyInfo#isUnlockedDeviceRequired()" + bug: "296475382" +} + +flag { name: "deprecate_fsv_sig" namespace: "hardware_backed_security" description: "Feature flag for deprecating .fsv_sig" diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java index b5f3b9a8fa2d..333cbb39d9c7 100644 --- a/core/java/android/view/ViewRootImpl.java +++ b/core/java/android/view/ViewRootImpl.java @@ -95,6 +95,7 @@ import static android.view.WindowManagerGlobal.RELAYOUT_RES_SURFACE_CHANGED; import static android.view.accessibility.Flags.fixMergedContentChangeEvent; import static android.view.accessibility.Flags.forceInvertColor; import static android.view.accessibility.Flags.reduceWindowContentChangedEventThrottle; +import static android.view.flags.Flags.toolkitFrameRateTypingReadOnly; import static android.view.flags.Flags.toolkitMetricsForFrameRateDecision; import static android.view.flags.Flags.toolkitSetFrameRateReadOnly; import static android.view.inputmethod.InputMethodEditorTraceProto.InputMethodClientsTraceProto.ClientSideProto.IME_FOCUS_CONTROLLER; @@ -1061,9 +1062,6 @@ public final class ViewRootImpl implements ViewParent, * the variables below are used to determine whther a dVRR feature should be enabled */ - // Used to determine whether to suppress boost on typing - private boolean mShouldSuppressBoostOnTyping = false; - /** * A temporary object used so relayoutWindow can return the latest SyncSeqId * system. The SyncSeqId system was designed to work without synchronous relayout @@ -1117,10 +1115,12 @@ public final class ViewRootImpl implements ViewParent, private static boolean sToolkitSetFrameRateReadOnlyFlagValue; private static boolean sToolkitMetricsForFrameRateDecisionFlagValue; + private static boolean sToolkitFrameRateTypingReadOnlyFlagValue; static { sToolkitSetFrameRateReadOnlyFlagValue = toolkitSetFrameRateReadOnly(); sToolkitMetricsForFrameRateDecisionFlagValue = toolkitMetricsForFrameRateDecision(); + sToolkitFrameRateTypingReadOnlyFlagValue = toolkitFrameRateTypingReadOnly(); } // The latest input event from the gesture that was used to resolve the pointer icon. @@ -12417,7 +12417,8 @@ public final class ViewRootImpl implements ViewParent, boolean desiredAction = motionEventAction == MotionEvent.ACTION_DOWN || motionEventAction == MotionEvent.ACTION_MOVE || motionEventAction == MotionEvent.ACTION_UP; - boolean undesiredType = windowType == TYPE_INPUT_METHOD && mShouldSuppressBoostOnTyping; + boolean undesiredType = windowType == TYPE_INPUT_METHOD + && sToolkitFrameRateTypingReadOnlyFlagValue; // use toolkitSetFrameRate flag to gate the change return desiredAction && !undesiredType && shouldEnableDvrr() && getFrameRateBoostOnTouchEnabled(); diff --git a/core/java/android/view/flags/refresh_rate_flags.aconfig b/core/java/android/view/flags/refresh_rate_flags.aconfig index 9d613bcae29a..05cabd56f532 100644 --- a/core/java/android/view/flags/refresh_rate_flags.aconfig +++ b/core/java/android/view/flags/refresh_rate_flags.aconfig @@ -74,4 +74,12 @@ flag { description: "Feature flag for setting frame rate based on velocity" bug: "239979904" is_fixed_read_only: true +} + +flag { + name: "toolkit_frame_rate_typing_read_only" + namespace: "toolkit" + description: "Feature flag for suppressing boost on typing" + bug: "239979904" + is_fixed_read_only: true }
\ No newline at end of file diff --git a/core/java/android/widget/RemoteViews.java b/core/java/android/widget/RemoteViews.java index 13dc4efb374d..0e5747d0e445 100644 --- a/core/java/android/widget/RemoteViews.java +++ b/core/java/android/widget/RemoteViews.java @@ -3874,7 +3874,7 @@ public class RemoteViews implements Parcelable, Filter { } } - private static class SetDrawInstructionAction extends Action { + private class SetDrawInstructionAction extends Action { @Nullable private final DrawInstructions mInstructions; @@ -3909,6 +3909,15 @@ public class RemoteViews implements Parcelable, Filter { } try (ByteArrayInputStream is = new ByteArrayInputStream(bytes.get(0))) { player.setDocument(new RemoteComposeDocument(is)); + player.addClickListener((viewId, metadata) -> { + mActions.forEach(action -> { + if (viewId == action.mViewId + && action instanceof SetOnClickResponse setOnClickResponse) { + setOnClickResponse.mResponse.handleViewInteraction( + player, params.handler); + } + }); + }); } catch (IOException e) { Log.e(LOG_TAG, "Failed to render draw instructions", e); } @@ -6051,16 +6060,6 @@ public class RemoteViews implements Parcelable, Filter { RemoteViews rvToApply = getRemoteViewsToApply(context, size); View result = inflateView(context, rvToApply, directParent, params.applyThemeResId, params.colorResources); - if (result instanceof RemoteComposePlayer player) { - player.addClickListener((viewId, metadata) -> { - mActions.forEach(action -> { - if (viewId == action.mViewId - && action instanceof SetOnClickResponse setOnClickResponse) { - setOnClickResponse.mResponse.handleViewInteraction(player, params.handler); - } - }); - }); - } rvToApply.performApply(result, rootParent, params); return result; } diff --git a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig index 82067defd336..254f4f77c100 100644 --- a/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig +++ b/core/java/android/window/flags/large_screen_experiences_app_compat.aconfig @@ -77,3 +77,10 @@ flag { bug: "309593314" is_fixed_read_only: true } + +flag { + name: "letterbox_background_wallpaper" + namespace: "large_screen_experiences_app_compat" + description: "Whether the blurred letterbox wallpaper background is enabled by default" + bug: "297195682" +} diff --git a/core/res/res/values/attrs_manifest.xml b/core/res/res/values/attrs_manifest.xml index b2e0be7c2201..c882938b63ce 100644 --- a/core/res/res/values/attrs_manifest.xml +++ b/core/res/res/values/attrs_manifest.xml @@ -1618,15 +1618,13 @@ <!-- Data (photo, file, account) upload/download, backup/restore, import/export, fetch, transfer over network between device and cloud. - <p><b>THIS TYPE IS DEPRECATED.</b> - <p><em>Note: For apps with <code>targetSdkVersion</code> - {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} and above, this type should - <b>NOT</b> be used: calling - {@link android.app.Service#startForeground(int, android.app.Notification, int)} - with this type on devices running - {@link android.os.Build.VERSION_CODES#VANILLA_ICE_CREAM} is still allowed, but it may - throw an {@link android.app.InvalidForegroundServiceTypeException} in future platform - releases.</em> + <p>For apps with <code>targetSdkVersion</code> + {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} and above, this type should NOT + be used: calling + {@link android.app.Service#startForeground(int, android.app.Notification, int)} with + this type on devices running {@link android.os.Build.VERSION_CODES#UPSIDE_DOWN_CAKE} + is still allowed, but calling it with this type on devices running future platform + releases may get a {@link android.app.InvalidForegroundServiceTypeException}. --> <flag name="dataSync" value="0x01" /> <!-- Music, video, news or other media play. diff --git a/core/res/res/values/config.xml b/core/res/res/values/config.xml index 1f06b0b7c62b..6134e788be82 100644 --- a/core/res/res/values/config.xml +++ b/core/res/res/values/config.xml @@ -6967,4 +6967,16 @@ <!-- Whether to use file hashes cache in watchlist--> <bool name="config_watchlistUseFileHashesCache">false</bool> + + <!-- Name of the package responsible to handle Contextual Search. --> + <string name="config_defaultContextualSearchPackageName" translatable="false" /> + + <!-- The key containing the entrypoint for Contextual Search. --> + <string name="config_defaultContextualSearchKey" translatable="false" /> + + <!-- The key containing the branching boolean for Contextual Search. --> + <string name="config_defaultContextualSearchEnabled" translatable="false" /> + + <!-- The key containing the branching boolean for legacy Search. --> + <string name="config_defaultContextualSearchLegacyEnabled" translatable="false" /> </resources> diff --git a/core/res/res/values/symbols.xml b/core/res/res/values/symbols.xml index b36b1d63cbf2..2f5183fc1455 100644 --- a/core/res/res/values/symbols.xml +++ b/core/res/res/values/symbols.xml @@ -5368,4 +5368,8 @@ <java-symbol type="bool" name="config_evenDimmerEnabled" /> <java-symbol type="bool" name="config_watchlistUseFileHashesCache" /> + <java-symbol type="string" name="config_defaultContextualSearchPackageName" /> + <java-symbol type="string" name="config_defaultContextualSearchKey" /> + <java-symbol type="string" name="config_defaultContextualSearchEnabled" /> + <java-symbol type="string" name="config_defaultContextualSearchLegacyEnabled" /> </resources> diff --git a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java index d95834fc0f4a..64c17bdfa731 100644 --- a/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java +++ b/core/tests/coretests/src/android/app/activity/ActivityThreadTest.java @@ -395,11 +395,13 @@ public class ActivityThreadTest { olderConfig.seq = seq + 1; final ActivityClientRecord r = getActivityClientRecord(activity); - activityThread.handleActivityConfigurationChanged(r, olderConfig, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, olderConfig, INVALID_DISPLAY, + new ActivityWindowInfo()); assertEquals(numOfConfig, activity.mNumOfConfigChanges); assertEquals(olderConfig.orientation, activity.mConfig.orientation); - activityThread.handleActivityConfigurationChanged(r, newerConfig, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, newerConfig, INVALID_DISPLAY, + new ActivityWindowInfo()); assertEquals(numOfConfig + 1, activity.mNumOfConfigChanges); assertEquals(newerConfig.orientation, activity.mConfig.orientation); }); @@ -417,7 +419,7 @@ public class ActivityThreadTest { config.orientation = ORIENTATION_PORTRAIT; activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity), - config, INVALID_DISPLAY); + config, INVALID_DISPLAY, new ActivityWindowInfo()); }); final IApplicationThread appThread = activityThread.getApplicationThread(); @@ -488,7 +490,7 @@ public class ActivityThreadTest { config.orientation = ORIENTATION_PORTRAIT; activityThread.handleActivityConfigurationChanged(getActivityClientRecord(activity), - config, INVALID_DISPLAY); + config, INVALID_DISPLAY, new ActivityWindowInfo()); }); final int numOfConfig = activity.mNumOfConfigChanges; @@ -618,7 +620,7 @@ public class ActivityThreadTest { activityThread.updatePendingActivityConfiguration(activity.getActivityToken(), newActivityConfig); activityThread.handleActivityConfigurationChanged(r, newActivityConfig, - INVALID_DISPLAY); + INVALID_DISPLAY, new ActivityWindowInfo()); assertEquals("Virtual display orientation must not change when activity" + " configuration orientation changes.", @@ -783,8 +785,8 @@ public class ActivityThreadTest { /** * Calls {@link ActivityThread#handleActivityConfigurationChanged(ActivityClientRecord, - * Configuration, int)} to try to push activity configuration to the activity for the given - * sequence number. + * Configuration, int, ActivityWindowInfo)} to try to push activity configuration to the + * activity for the given sequence number. * <p> * It uses orientation to push the configuration and it tries a different orientation if the * first attempt doesn't make through, to rule out the possibility that the previous @@ -803,7 +805,8 @@ public class ActivityThreadTest { Configuration config = new Configuration(); config.orientation = ORIENTATION_PORTRAIT; config.seq = seq; - activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY, + new ActivityWindowInfo()); if (activity.mNumOfConfigChanges > numOfConfig) { return config.seq; @@ -812,7 +815,8 @@ public class ActivityThreadTest { config = new Configuration(); config.orientation = ORIENTATION_LANDSCAPE; config.seq = seq + 1; - activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY); + activityThread.handleActivityConfigurationChanged(r, config, INVALID_DISPLAY, + new ActivityWindowInfo()); return config.seq; } diff --git a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java index 30545f994f01..85a1b4ee3ebd 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java +++ b/core/tests/coretests/src/android/app/servertransaction/ClientTransactionItemTest.java @@ -177,7 +177,7 @@ public class ClientTransactionItemTest { @Test public void testMoveToDisplayItem_getContextToUpdate() { final MoveToDisplayItem item = MoveToDisplayItem - .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration); + .obtain(mActivityToken, DEFAULT_DISPLAY, mConfiguration, new ActivityWindowInfo()); final Context context = item.getContextToUpdate(mHandler); assertEquals(mActivity, context); diff --git a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java index a8466bb092c8..906558f7603b 100644 --- a/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/ObjectPoolTests.java @@ -157,7 +157,8 @@ public class ObjectPoolTests { @Test public void testRecycleMoveToDisplayItem() { - testRecycle(() -> MoveToDisplayItem.obtain(mActivityToken, 4, config())); + testRecycle(() -> MoveToDisplayItem.obtain(mActivityToken, 4, config(), + new ActivityWindowInfo())); } @Test diff --git a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java index 9743e84b9349..dbb090fe795b 100644 --- a/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java +++ b/core/tests/coretests/src/android/app/servertransaction/TransactionParcelTests.java @@ -110,8 +110,11 @@ public class TransactionParcelTests { @Test public void testMoveToDisplay() { // Write to parcel + final ActivityWindowInfo activityWindowInfo = new ActivityWindowInfo(); + activityWindowInfo.set(true /* isEmbedded */, new Rect(0, 0, 500, 1000), + new Rect(0, 0, 500, 500)); MoveToDisplayItem item = MoveToDisplayItem.obtain(mActivityToken, 4 /* targetDisplayId */, - config()); + config(), activityWindowInfo); writeAndPrepareForReading(item); // Read from parcel and assert diff --git a/keystore/java/android/security/KeyStore.java b/keystore/java/android/security/KeyStore.java index 11b827117aa3..bd9abec22325 100644 --- a/keystore/java/android/security/KeyStore.java +++ b/keystore/java/android/security/KeyStore.java @@ -21,12 +21,14 @@ import android.os.Build; import android.os.StrictMode; /** - * @hide This should not be made public in its present form because it - * assumes that private and secret key bytes are available and would - * preclude the use of hardware crypto. + * This class provides some constants and helper methods related to Android's Keystore service. + * This class was originally much larger, but its functionality was superseded by other classes. + * It now just contains a few remaining pieces for which the users haven't been updated yet. + * You may be looking for {@link java.security.KeyStore} instead. + * + * @hide */ public class KeyStore { - private static final String TAG = "KeyStore"; // ResponseCodes - see system/security/keystore/include/keystore/keystore.h @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) @@ -42,50 +44,6 @@ public class KeyStore { return KEY_STORE; } - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public byte[] get(String key) { - return null; - } - - /** @hide */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean delete(String key) { - return false; - } - - /** - * List uids of all keys that are auth bound to the current user. - * Only system is allowed to call this method. - * @hide - * @deprecated This function always returns null. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public int[] listUidsOfAuthBoundKeys() { - return null; - } - - - /** - * @hide - * @deprecated This function has no effect. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public boolean unlock(String password) { - return false; - } - - /** - * - * @return - * @deprecated This function always returns true. - * @hide - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.P, trackingBug = 115609023) - public boolean isEmpty() { - return true; - } - /** * Add an authentication record to the keystore authorization table. * @@ -105,13 +63,4 @@ public class KeyStore { public void onDeviceOffBody() { AndroidKeyStoreMaintenance.onDeviceOffBody(); } - - /** - * Returns a {@link KeyStoreException} corresponding to the provided keystore/keymaster error - * code. - */ - @UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553) - public static KeyStoreException getKeyStoreException(int errorCode) { - return new KeyStoreException(-10000, "Should not be called."); - } } diff --git a/keystore/java/android/security/keystore/KeyInfo.java b/keystore/java/android/security/keystore/KeyInfo.java index f50efd2c3328..5cffe46936a2 100644 --- a/keystore/java/android/security/keystore/KeyInfo.java +++ b/keystore/java/android/security/keystore/KeyInfo.java @@ -16,6 +16,7 @@ package android.security.keystore; +import android.annotation.FlaggedApi; import android.annotation.NonNull; import android.annotation.Nullable; @@ -81,6 +82,7 @@ public class KeyInfo implements KeySpec { private final @KeyProperties.AuthEnum int mUserAuthenticationType; private final boolean mUserAuthenticationRequirementEnforcedBySecureHardware; private final boolean mUserAuthenticationValidWhileOnBody; + private final boolean mUnlockedDeviceRequired; private final boolean mTrustedUserPresenceRequired; private final boolean mInvalidatedByBiometricEnrollment; private final boolean mUserConfirmationRequired; @@ -107,6 +109,7 @@ public class KeyInfo implements KeySpec { @KeyProperties.AuthEnum int userAuthenticationType, boolean userAuthenticationRequirementEnforcedBySecureHardware, boolean userAuthenticationValidWhileOnBody, + boolean unlockedDeviceRequired, boolean trustedUserPresenceRequired, boolean invalidatedByBiometricEnrollment, boolean userConfirmationRequired, @@ -132,6 +135,7 @@ public class KeyInfo implements KeySpec { mUserAuthenticationRequirementEnforcedBySecureHardware = userAuthenticationRequirementEnforcedBySecureHardware; mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody; + mUnlockedDeviceRequired = unlockedDeviceRequired; mTrustedUserPresenceRequired = trustedUserPresenceRequired; mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment; mUserConfirmationRequired = userConfirmationRequired; @@ -275,6 +279,20 @@ public class KeyInfo implements KeySpec { } /** + * Returns {@code true} if the key is authorized to be used only when the device is unlocked. + * + * <p>This authorization applies only to secret key and private key operations. Public key + * operations are not restricted. + * + * @see KeyGenParameterSpec.Builder#setUnlockedDeviceRequired(boolean) + * @see KeyProtection.Builder#setUnlockedDeviceRequired(boolean) + */ + @FlaggedApi(android.security.Flags.FLAG_KEYINFO_UNLOCKED_DEVICE_REQUIRED) + public boolean isUnlockedDeviceRequired() { + return mUnlockedDeviceRequired; + } + + /** * Returns {@code true} if the key is authorized to be used only for messages confirmed by the * user. * diff --git a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java index 97592b44ba2e..2682eb657963 100644 --- a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java +++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java @@ -93,6 +93,7 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { long userAuthenticationValidityDurationSeconds = 0; boolean userAuthenticationRequired = true; boolean userAuthenticationValidWhileOnBody = false; + boolean unlockedDeviceRequired = false; boolean trustedUserPresenceRequired = false; boolean trustedUserConfirmationRequired = false; int remainingUsageCount = KeyProperties.UNRESTRICTED_USAGE_COUNT; @@ -184,6 +185,9 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { + userAuthenticationValidityDurationSeconds + " seconds"); } break; + case KeymasterDefs.KM_TAG_UNLOCKED_DEVICE_REQUIRED: + unlockedDeviceRequired = true; + break; case KeymasterDefs.KM_TAG_ALLOW_WHILE_ON_BODY: userAuthenticationValidWhileOnBody = KeyStore2ParameterUtils.isSecureHardware(a.securityLevel); @@ -257,6 +261,7 @@ public class AndroidKeyStoreSecretKeyFactorySpi extends SecretKeyFactorySpi { : keymasterSwEnforcedUserAuthenticators, userAuthenticationRequirementEnforcedBySecureHardware, userAuthenticationValidWhileOnBody, + unlockedDeviceRequired, trustedUserPresenceRequired, invalidatedByBiometricEnrollment, trustedUserConfirmationRequired, diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt index d319e4cc9ef9..a7b5c36215cf 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/BottomSheet.kt @@ -16,8 +16,15 @@ package com.android.credentialmanager.common.ui +import android.credentials.flags.Flags +import androidx.compose.animation.animateContentSize import androidx.compose.foundation.background -import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.Box +import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.padding +import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.material3.ExperimentalMaterial3Api +import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect import androidx.compose.runtime.rememberCoroutineScope @@ -25,6 +32,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.graphics.Color import com.android.compose.rememberSystemUiController import com.android.compose.theme.LocalAndroidColorScheme +import androidx.compose.ui.unit.dp import com.android.credentialmanager.common.material.ModalBottomSheetLayout import com.android.credentialmanager.common.material.ModalBottomSheetValue import com.android.credentialmanager.common.material.rememberModalBottomSheetState @@ -34,40 +42,68 @@ import kotlinx.coroutines.launch /** Draws a modal bottom sheet with the same styles and effects shared by various flows. */ @Composable +@OptIn(ExperimentalMaterial3Api::class) fun ModalBottomSheet( - sheetContent: @Composable ColumnScope.() -> Unit, - onDismiss: () -> Unit, - isInitialRender: Boolean, - onInitialRenderComplete: () -> Unit, - isAutoSelectFlow: Boolean, + sheetContent: @Composable () -> Unit, + onDismiss: () -> Unit, + isInitialRender: Boolean, + onInitialRenderComplete: () -> Unit, + isAutoSelectFlow: Boolean, ) { - val scope = rememberCoroutineScope() - val state = rememberModalBottomSheetState( - initialValue = if (isAutoSelectFlow) ModalBottomSheetValue.Expanded - else ModalBottomSheetValue.Hidden, - skipHalfExpanded = true - ) - val sysUiController = rememberSystemUiController() - if (state.targetValue == ModalBottomSheetValue.Hidden || isAutoSelectFlow) { - setTransparentSystemBarsColor(sysUiController) + if (Flags.selectorUiImprovementsEnabled()) { + val state = androidx.compose.material3.rememberModalBottomSheetState( + skipPartiallyExpanded = true + ) + androidx.compose.material3.ModalBottomSheet( + onDismissRequest = onDismiss, + containerColor = LocalAndroidColorScheme.current.surfaceBright, + sheetState = state, + content = { + Box( + modifier = Modifier + .animateContentSize() + .wrapContentHeight() + .fillMaxWidth() + ) { + sheetContent() + } + }, + scrimColor = MaterialTheme.colorScheme.scrim.copy(alpha = .32f), + shape = EntryShape.TopRoundedCorner, + dragHandle = null, + // Never take over the full screen. We always want to leave some top scrim space + // for exiting and viewing the underlying app to help a user gain context. + modifier = Modifier.padding(top = 56.dp), + ) } else { - setBottomSheetSystemBarsColor(sysUiController) - } - ModalBottomSheetLayout( - sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright, - modifier = Modifier.background(Color.Transparent), - sheetState = state, - sheetContent = sheetContent, - sheetShape = EntryShape.TopRoundedCorner, - ) {} - LaunchedEffect(state.currentValue, state.targetValue) { - if (state.currentValue == ModalBottomSheetValue.Hidden) { - if (isInitialRender) { - onInitialRenderComplete() - scope.launch { state.show() } - } else if (state.targetValue == ModalBottomSheetValue.Hidden) { - // Only dismiss ui when the motion is downwards - onDismiss() + val scope = rememberCoroutineScope() + val state = rememberModalBottomSheetState( + initialValue = if (isAutoSelectFlow) ModalBottomSheetValue.Expanded + else ModalBottomSheetValue.Hidden, + skipHalfExpanded = true + ) + val sysUiController = rememberSystemUiController() + if (state.targetValue == ModalBottomSheetValue.Hidden || isAutoSelectFlow) { + setTransparentSystemBarsColor(sysUiController) + } else { + setBottomSheetSystemBarsColor(sysUiController) + } + ModalBottomSheetLayout( + sheetBackgroundColor = LocalAndroidColorScheme.current.surfaceBright, + modifier = Modifier.background(Color.Transparent), + sheetState = state, + sheetContent = { sheetContent() }, + sheetShape = EntryShape.TopRoundedCorner, + ) {} + LaunchedEffect(state.currentValue, state.targetValue) { + if (state.currentValue == ModalBottomSheetValue.Hidden) { + if (isInitialRender) { + onInitialRenderComplete() + scope.launch { state.show() } + } else if (state.targetValue == ModalBottomSheetValue.Hidden) { + // Only dismiss ui when the motion is downwards + onDismiss() + } } } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt index bdfe39920d44..c68ae8b168fb 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Cards.kt @@ -18,7 +18,10 @@ package com.android.credentialmanager.common.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.ColumnScope +import androidx.compose.foundation.layout.WindowInsets import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.layout.asPaddingValues +import androidx.compose.foundation.layout.navigationBars import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.lazy.LazyColumn @@ -66,6 +69,9 @@ fun SheetContainerCard( horizontalAlignment = Alignment.CenterHorizontally, content = content, verticalArrangement = contentVerticalArrangement, + // The bottom sheet overlaps with the navigation bars but make sure the actual content + // in the bottom sheet does not. + contentPadding = WindowInsets.navigationBars.asPaddingValues(), ) } } diff --git a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt index a6253b8d4e07..8ff17e0d333a 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/common/ui/Entry.kt @@ -29,13 +29,10 @@ import androidx.compose.foundation.layout.wrapContentSize import androidx.compose.material.icons.Icons import androidx.compose.material.icons.filled.ArrowBack import androidx.compose.material.icons.outlined.Lock -import androidx.compose.material3.ExperimentalMaterial3Api import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.SuggestionChip import androidx.compose.material3.SuggestionChipDefaults -import androidx.compose.material3.TopAppBar -import androidx.compose.material3.TopAppBarDefaults import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf @@ -278,31 +275,6 @@ fun ActionEntry( } /** - * A single row of leading icon and text describing a benefit of passkeys, used by the - * [com.android.credentialmanager.createflow.PasskeyIntroCard]. - */ -@Composable -fun PasskeyBenefitRow( - leadingIconPainter: Painter, - text: String, -) { - Row( - horizontalArrangement = Arrangement.spacedBy(16.dp), - verticalAlignment = Alignment.CenterVertically, - modifier = Modifier.fillMaxWidth() - ) { - Icon( - modifier = Modifier.size(24.dp), - painter = leadingIconPainter, - tint = LocalAndroidColorScheme.current.onSurfaceVariant, - // Decorative purpose only. - contentDescription = null, - ) - BodyMediumText(text = text) - } -} - -/** * A single row of one or two CTA buttons for continuing or cancelling the current step. */ @Composable @@ -327,40 +299,36 @@ fun CtaButtonRow( } } -@OptIn(ExperimentalMaterial3Api::class) @Composable fun MoreOptionTopAppBar( text: String, onNavigationIconClicked: () -> Unit, bottomPadding: Dp, ) { - TopAppBar( - title = { - LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp)) - }, - navigationIcon = { - IconButton( + Row( + modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding), + verticalAlignment = Alignment.CenterVertically, + ) { + IconButton( modifier = Modifier.padding(top = 8.dp, bottom = 8.dp, start = 4.dp).size(48.dp), onClick = onNavigationIconClicked - ) { - Box( + ) { + Box( modifier = Modifier.size(48.dp), contentAlignment = Alignment.Center, - ) { - Icon( + ) { + Icon( imageVector = Icons.Filled.ArrowBack, contentDescription = stringResource( - R.string.accessibility_back_arrow_button + R.string.accessibility_back_arrow_button ), modifier = Modifier.size(24.dp).autoMirrored(), tint = LocalAndroidColorScheme.current.onSurfaceVariant, - ) - } + ) } - }, - colors = TopAppBarDefaults.topAppBarColors(containerColor = Color.Transparent), - modifier = Modifier.padding(top = 12.dp, bottom = bottomPadding) - ) + } + LargeTitleText(text = text, modifier = Modifier.padding(horizontal = 4.dp)) + } } private fun Modifier.autoMirrored() = composed { diff --git a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt index 4ed84b908865..72775500e7c5 100644 --- a/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt +++ b/packages/CredentialManager/src/com/android/credentialmanager/getflow/GetCredentialComponents.kt @@ -653,4 +653,4 @@ fun EmptyAuthEntrySnackBarScreen( contentText = stringResource(R.string.no_sign_in_info_in, lastLocked.providerDisplayName), ) onLog(GetCredentialEvent.CREDMAN_GET_CRED_SCREEN_EMPTY_AUTH_SNACKBAR_SCREEN) -}
\ No newline at end of file +} diff --git a/packages/CredentialManager/tests/robotests/Android.bp b/packages/CredentialManager/tests/robotests/Android.bp index baebfeb399f2..75a0dcce0b9e 100644 --- a/packages/CredentialManager/tests/robotests/Android.bp +++ b/packages/CredentialManager/tests/robotests/Android.bp @@ -37,7 +37,7 @@ android_robolectric_test { ":CredentialManagerScreenshotTestFiles", ], - // Do not add any libraries here, instead add them to the ScreenshotTestStub + // Do not add any libraries here, instead add them to the ScreenshotTestRobo static_libs: [ "androidx.compose.runtime_runtime", "androidx.test.uiautomator_uiautomator", @@ -45,6 +45,7 @@ android_robolectric_test { "inline-mockito-robolectric-prebuilt", "platform-parametric-runner-lib", "uiautomator-helpers", + "flag-junit-base", ], libs: [ "android.test.runner", diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..81860e538275 --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..8c1fff7df8ab --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..4eb025fcd190 --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..c709f934f78d --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..278c13f6c7da --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..cb85df350ccf --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..2eca70741a3c --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png Binary files differnew file mode 100644 index 000000000000..7ee91b3705df --- /dev/null +++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png diff --git a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt index a0e1fed0ac96..e609d0c5c008 100644 --- a/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt +++ b/packages/CredentialManager/tests/robotests/screenshot/src/com/android/credentialmanager/GetCredScreenshotTest.kt @@ -16,7 +16,10 @@ package com.android.credentialmanager +import android.credentials.flags.Flags import android.content.Context +import android.platform.test.flag.junit.SetFlagsRule +import androidx.compose.ui.test.isPopup import com.android.credentialmanager.getflow.RequestDisplayInfo import com.android.credentialmanager.model.CredentialType import com.android.credentialmanager.model.get.ProviderInfo @@ -59,8 +62,11 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) { CredentialManagerGoldenImagePathManager(getEmulatedDevicePathConfig(emulationSpec)) ) + @get:Rule val setFlagsRule: SetFlagsRule = SetFlagsRule() + @Test - fun singleCredentialScreen() { + fun singleCredentialScreen_M3BottomSheetDisabled() { + setFlagsRule.disableFlags(Flags.FLAG_SELECTOR_UI_IMPROVEMENTS_ENABLED) val providerInfoList = buildProviderInfoList() val providerDisplayInfo = toProviderDisplayInfo(providerInfoList) val activeEntry = toActiveEntry(providerDisplayInfo) @@ -86,6 +92,39 @@ class GetCredScreenshotTest(emulationSpec: DeviceEmulationSpec) { } } + @Test + fun singleCredentialScreen_M3BottomSheetEnabled() { + setFlagsRule.enableFlags(Flags.FLAG_SELECTOR_UI_IMPROVEMENTS_ENABLED) + val providerInfoList = buildProviderInfoList() + val providerDisplayInfo = toProviderDisplayInfo(providerInfoList) + val activeEntry = toActiveEntry(providerDisplayInfo) + screenshotRule.screenshotTest( + "singleCredentialScreen_newM3BottomSheet", + // M3's ModalBottomSheet lives in a new window, meaning we have two windows with + // a root. Hence use a different matcher `isPopup`. + viewFinder = { screenshotRule.composeRule.onNode(isPopup()) }, + ) { + ModalBottomSheet( + sheetContent = { + PrimarySelectionCard( + requestDisplayInfo = REQUEST_DISPLAY_INFO, + providerDisplayInfo = providerDisplayInfo, + providerInfoList = providerInfoList, + activeEntry = activeEntry, + onEntrySelected = {}, + onConfirm = {}, + onMoreOptionSelected = {}, + onLog = {}, + ) + }, + isInitialRender = true, + onDismiss = {}, + onInitialRenderComplete = {}, + isAutoSelectFlow = false, + ) + } + } + private fun buildProviderInfoList(): List<ProviderInfo> { val context = ApplicationProvider.getApplicationContext<Context>() val provider1 = ProviderInfo( diff --git a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt index 988afd70aaed..a395266ba5f9 100644 --- a/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt +++ b/packages/SettingsLib/SpaPrivileged/src/com/android/settingslib/spaprivileged/model/app/AppListRepository.kt @@ -26,6 +26,7 @@ import android.content.pm.PackageManager import android.content.pm.PackageManager.ApplicationInfoFlags import android.content.pm.ResolveInfo import android.os.SystemProperties +import android.util.Log import com.android.internal.R import com.android.settingslib.spaprivileged.framework.common.userManager import kotlinx.coroutines.async @@ -85,19 +86,24 @@ class AppListRepositoryImpl( userId: Int, loadInstantApps: Boolean, matchAnyUserForAdmin: Boolean, - ): List<ApplicationInfo> = coroutineScope { - val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } - val hideWhenDisabledPackagesDeferred = async { - context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) - } - val installedApplicationsAsUser = - getInstalledApplications(userId, matchAnyUserForAdmin) + ): List<ApplicationInfo> = try { + coroutineScope { + val hiddenSystemModulesDeferred = async { packageManager.getHiddenSystemModules() } + val hideWhenDisabledPackagesDeferred = async { + context.resources.getStringArray(R.array.config_hideWhenDisabled_packageNames) + } + val installedApplicationsAsUser = + getInstalledApplications(userId, matchAnyUserForAdmin) - val hiddenSystemModules = hiddenSystemModulesDeferred.await() - val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() - installedApplicationsAsUser.filter { app -> - app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages) + val hiddenSystemModules = hiddenSystemModulesDeferred.await() + val hideWhenDisabledPackages = hideWhenDisabledPackagesDeferred.await() + installedApplicationsAsUser.filter { app -> + app.isInAppList(loadInstantApps, hiddenSystemModules, hideWhenDisabledPackages) + } } + } catch (e: Exception) { + Log.e(TAG, "loadApps failed", e) + emptyList() } private suspend fun getInstalledApplications( @@ -210,6 +216,8 @@ class AppListRepositoryImpl( } companion object { + private const val TAG = "AppListRepository" + private fun ApplicationInfo.isInAppList( showInstantApps: Boolean, hiddenSystemModules: Set<String>, diff --git a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt index efd53a4c9c23..c60ce41be87e 100644 --- a/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt +++ b/packages/SettingsLib/SpaPrivileged/tests/src/com/android/settingslib/spaprivileged/model/app/AppListRepositoryTest.kt @@ -28,6 +28,8 @@ import android.content.pm.PackageManager.ResolveInfoFlags import android.content.pm.ResolveInfo import android.content.pm.UserInfo import android.content.res.Resources +import android.os.BadParcelableException +import android.os.DeadObjectException import android.os.UserManager import android.platform.test.flag.junit.SetFlagsRule import androidx.test.core.app.ApplicationProvider @@ -44,6 +46,7 @@ import org.mockito.kotlin.any import org.mockito.kotlin.argumentCaptor import org.mockito.kotlin.doAnswer import org.mockito.kotlin.doReturn +import org.mockito.kotlin.doThrow import org.mockito.kotlin.eq import org.mockito.kotlin.mock import org.mockito.kotlin.spy @@ -311,6 +314,19 @@ class AppListRepositoryTest { } @Test + fun loadApps_hasException_returnEmptyList() = runTest { + packageManager.stub { + on { + getInstalledApplicationsAsUser(any<ApplicationInfoFlags>(), eq(ADMIN_USER_ID)) + } doThrow BadParcelableException(DeadObjectException()) + } + + val appList = repository.loadApps(userId = ADMIN_USER_ID) + + assertThat(appList).isEmpty() + } + + @Test fun showSystemPredicate_showSystem() = runTest { val app = SYSTEM_APP diff --git a/packages/SettingsLib/aconfig/settingslib.aconfig b/packages/SettingsLib/aconfig/settingslib.aconfig index 6a1ee3a3c623..54c5a14702f6 100644 --- a/packages/SettingsLib/aconfig/settingslib.aconfig +++ b/packages/SettingsLib/aconfig/settingslib.aconfig @@ -43,4 +43,11 @@ flag { namespace: "pixel_cross_device_control" description: "Gates whether to enable LE audio private broadcast sharing via QR code" bug: "308368124" -}
\ No newline at end of file +} + +flag { + name: "enable_hide_exclusively_managed_bluetooth_device" + namespace: "dck_framework" + description: "Hide exclusively managed Bluetooth devices in BT settings menu." + bug: "324475542" +} diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java index 6ee403d50751..bd27c896a3c4 100644 --- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java +++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java @@ -19,6 +19,7 @@ package com.android.settingslib.bluetooth; import static android.bluetooth.BluetoothProfile.CONNECTION_POLICY_FORBIDDEN; import android.annotation.CallbackExecutor; +import android.annotation.IntDef; import android.bluetooth.BluetoothAdapter; import android.bluetooth.BluetoothClass; import android.bluetooth.BluetoothCsipSetCoordinator; @@ -35,6 +36,7 @@ import android.bluetooth.BluetoothProfile; import android.bluetooth.BluetoothProfile.ServiceListener; import android.content.ContentResolver; import android.content.Context; +import android.content.Intent; import android.database.ContentObserver; import android.net.Uri; import android.os.Build; @@ -52,6 +54,8 @@ import com.android.settingslib.R; import com.google.common.collect.ImmutableList; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.Arrays; @@ -71,6 +75,18 @@ import java.util.stream.Collectors; * result callback. */ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + public static final String ACTION_LE_AUDIO_SHARING_STATE_CHANGE = + "com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE"; + public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE"; + public static final int BROADCAST_STATE_UNKNOWN = 0; + public static final int BROADCAST_STATE_ON = 1; + public static final int BROADCAST_STATE_OFF = 2; + @Retention(RetentionPolicy.SOURCE) + @IntDef( + prefix = {"BROADCAST_STATE_"}, + value = {BROADCAST_STATE_UNKNOWN, BROADCAST_STATE_ON, BROADCAST_STATE_OFF}) + public @interface BroadcastState {} + private static final String SETTINGS_PKG = "com.android.settings"; private static final String TAG = "LocalBluetoothLeBroadcast"; private static final boolean DEBUG = BluetoothUtils.D; @@ -89,7 +105,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Settings.Secure.getUriFor( Settings.Secure.BLUETOOTH_LE_BROADCAST_IMPROVE_COMPATIBILITY), }; - private final Context mContext; private final CachedBluetoothDeviceManager mDeviceManager; private BluetoothLeBroadcast mServiceBroadcast; @@ -200,6 +215,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId); } setLatestBluetoothLeBroadcastMetadata(metadata); + notifyBroadcastStateChange(BROADCAST_STATE_ON); } @Override @@ -212,7 +228,7 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { + ", broadcastId = " + broadcastId); } - + notifyBroadcastStateChange(BROADCAST_STATE_OFF); stopLocalSourceReceivers(); resetCacheInfo(); } @@ -1005,10 +1021,6 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { /** Update fallback active device if needed. */ public void updateFallbackActiveDeviceIfNeeded() { - if (!isEnabled(null)) { - Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to no ongoing broadcast"); - return; - } if (mServiceBroadcastAssistant == null) { Log.d(TAG, "Skip updateFallbackActiveDeviceIfNeeded due to assistant profile is null"); return; @@ -1078,4 +1090,15 @@ public class LocalBluetoothLeBroadcast implements LocalBluetoothProfile { "bluetooth_le_broadcast_fallback_active_group_id", BluetoothCsipSetCoordinator.GROUP_ID_INVALID); } + + private void notifyBroadcastStateChange(@BroadcastState int state) { + if (!mContext.getPackageName().equals(SETTINGS_PKG)) { + Log.d(TAG, "Skip notifyBroadcastStateChange, not triggered by Settings."); + return; + } + Intent intent = new Intent(ACTION_LE_AUDIO_SHARING_STATE_CHANGE); + intent.putExtra(EXTRA_LE_AUDIO_SHARING_STATE, state); + intent.setPackage(mContext.getPackageName()); + mContext.sendBroadcast(intent); + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt index 2a4658bc69a1..a5c63be3c987 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/data/repository/SpatializerRepository.kt @@ -18,33 +18,71 @@ package com.android.settingslib.media.data.repository import android.media.AudioDeviceAttributes import android.media.Spatializer +import androidx.concurrent.futures.DirectExecutor import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.channels.awaitClose +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.flow.flowOn +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.launch import kotlinx.coroutines.withContext interface SpatializerRepository { + /** Returns true when head tracking is enabled and false the otherwise. */ + val isHeadTrackingAvailable: StateFlow<Boolean> + /** * Returns true when Spatial audio feature is supported for the [audioDeviceAttributes] and * false the otherwise. */ - suspend fun isAvailableForDevice(audioDeviceAttributes: AudioDeviceAttributes): Boolean + suspend fun isSpatialAudioAvailableForDevice( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean /** Returns a list [AudioDeviceAttributes] that are compatible with spatial audio. */ - suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> + suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> + + /** Adds a [audioDeviceAttributes] to [getSpatialAudioCompatibleDevices] list. */ + suspend fun addSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + + /** Removes a [audioDeviceAttributes] from [getSpatialAudioCompatibleDevices] list. */ + suspend fun removeSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) - /** Adds a [audioDeviceAttributes] to [getCompatibleDevices] list. */ - suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + /** Checks if the head tracking is enabled for the [audioDeviceAttributes]. */ + suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean - /** Removes a [audioDeviceAttributes] to [getCompatibleDevices] list. */ - suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) + /** Sets head tracking [isEnabled] for the [audioDeviceAttributes]. */ + suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) } class SpatializerRepositoryImpl( private val spatializer: Spatializer, + coroutineScope: CoroutineScope, private val backgroundContext: CoroutineContext, ) : SpatializerRepository { - override suspend fun isAvailableForDevice( + override val isHeadTrackingAvailable: StateFlow<Boolean> = + callbackFlow { + val listener = + Spatializer.OnHeadTrackerAvailableListener { _, available -> + launch { send(available) } + } + spatializer.addOnHeadTrackerAvailableListener(DirectExecutor.INSTANCE, listener) + awaitClose { spatializer.removeOnHeadTrackerAvailableListener(listener) } + } + .onStart { emit(spatializer.isHeadTrackerAvailable) } + .flowOn(backgroundContext) + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), false) + + override suspend fun isSpatialAudioAvailableForDevice( audioDeviceAttributes: AudioDeviceAttributes ): Boolean { return withContext(backgroundContext) { @@ -52,18 +90,36 @@ class SpatializerRepositoryImpl( } } - override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> = + override suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> = withContext(backgroundContext) { spatializer.compatibleAudioDevices } - override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { + override suspend fun addSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { withContext(backgroundContext) { spatializer.addCompatibleAudioDevice(audioDeviceAttributes) } } - override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { + override suspend fun removeSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { withContext(backgroundContext) { spatializer.removeCompatibleAudioDevice(audioDeviceAttributes) } } + + override suspend fun isHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = + withContext(backgroundContext) { spatializer.isHeadTrackerEnabled(audioDeviceAttributes) } + + override suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) { + withContext(backgroundContext) { + spatializer.setHeadTrackerEnabled(isEnabled, audioDeviceAttributes) + } + } } diff --git a/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt index c3cc340d9cd8..0347403cb385 100644 --- a/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt +++ b/packages/SettingsLib/src/com/android/settingslib/media/domain/interactor/SpatializerInteractor.kt @@ -18,22 +18,40 @@ package com.android.settingslib.media.domain.interactor import android.media.AudioDeviceAttributes import com.android.settingslib.media.data.repository.SpatializerRepository +import kotlinx.coroutines.flow.StateFlow class SpatializerInteractor(private val repository: SpatializerRepository) { - suspend fun isAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean = - repository.isAvailableForDevice(audioDeviceAttributes) + /** Checks if head tracking is available. */ + val isHeadTrackingAvailable: StateFlow<Boolean> + get() = repository.isHeadTrackingAvailable + + suspend fun isSpatialAudioAvailable(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.isSpatialAudioAvailableForDevice(audioDeviceAttributes) /** Checks if spatial audio is enabled for the [audioDeviceAttributes]. */ - suspend fun isEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = - repository.getCompatibleDevices().contains(audioDeviceAttributes) + suspend fun isSpatialAudioEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.getSpatialAudioCompatibleDevices().contains(audioDeviceAttributes) - /** Enblaes or disables spatial audio for [audioDeviceAttributes]. */ - suspend fun setEnabled(audioDeviceAttributes: AudioDeviceAttributes, isEnabled: Boolean) { + /** Enables or disables spatial audio for [audioDeviceAttributes]. */ + suspend fun setSpatialAudioEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean + ) { if (isEnabled) { - repository.addCompatibleDevice(audioDeviceAttributes) + repository.addSpatialAudioCompatibleDevice(audioDeviceAttributes) } else { - repository.removeCompatibleDevice(audioDeviceAttributes) + repository.removeSpatialAudioCompatibleDevice(audioDeviceAttributes) } } + + /** Checks if head tracking is enabled for the [audioDeviceAttributes]. */ + suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean = + repository.isHeadTrackingEnabled(audioDeviceAttributes) + + /** Enables or disables head tracking for the [audioDeviceAttributes]. */ + suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean, + ) = repository.setHeadTrackingEnabled(audioDeviceAttributes, isEnabled) } diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt deleted file mode 100644 index 3f52f2494dfc..000000000000 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/FakeSpatializerRepository.kt +++ /dev/null @@ -1,45 +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.media.domain.interactor - -import android.media.AudioDeviceAttributes -import com.android.settingslib.media.data.repository.SpatializerRepository - -class FakeSpatializerRepository : SpatializerRepository { - - private val availabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = mutableMapOf() - private val compatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf() - - override suspend fun isAvailableForDevice( - audioDeviceAttributes: AudioDeviceAttributes - ): Boolean = availabilityByDevice.getOrDefault(audioDeviceAttributes, false) - - override suspend fun getCompatibleDevices(): Collection<AudioDeviceAttributes> = - compatibleDevices - - override suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { - compatibleDevices.add(audioDeviceAttributes) - } - - override suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes) { - compatibleDevices.remove(audioDeviceAttributes) - } - - fun setIsAvailable(audioDeviceAttributes: AudioDeviceAttributes, isAvailable: Boolean) { - availabilityByDevice[audioDeviceAttributes] = isAvailable - } -} diff --git a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt b/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt deleted file mode 100644 index a44baeb174bf..000000000000 --- a/packages/SettingsLib/tests/integ/src/com/android/settingslib/media/domain/interactor/SpatializerInteractorTest.kt +++ /dev/null @@ -1,56 +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.media.domain.interactor - -import android.media.AudioDeviceAttributes -import androidx.test.ext.junit.runners.AndroidJUnit4 -import androidx.test.filters.SmallTest -import com.google.common.truth.Truth.assertThat -import kotlinx.coroutines.test.TestScope -import kotlinx.coroutines.test.runTest -import org.junit.Test -import org.junit.runner.RunWith - -@SmallTest -@RunWith(AndroidJUnit4::class) -class SpatializerInteractorTest { - - private val testScope = TestScope() - private val underTest = SpatializerInteractor(FakeSpatializerRepository()) - - @Test - fun setEnabledFalse_isEnabled_false() { - testScope.runTest { - underTest.setEnabled(deviceAttributes, false) - - assertThat(underTest.isEnabled(deviceAttributes)).isFalse() - } - } - - @Test - fun setEnabledTrue_isEnabled_true() { - testScope.runTest { - underTest.setEnabled(deviceAttributes, true) - - assertThat(underTest.isEnabled(deviceAttributes)).isTrue() - } - } - - private companion object { - val deviceAttributes = AudioDeviceAttributes(0, 0, "test_device") - } -} diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java index 37052673eb4d..70ba415abde5 100644 --- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java +++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/deviceinfo/WifiMacAddressPreferenceControllerTest.java @@ -20,9 +20,7 @@ import static com.google.common.truth.Truth.assertThat; import static com.google.common.truth.Truth.assertWithMessage; import static org.mockito.Mockito.doReturn; -import static org.mockito.Mockito.never; import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.verify; import android.annotation.SuppressLint; import android.content.Context; @@ -94,19 +92,6 @@ public class WifiMacAddressPreferenceControllerTest { } @Test - public void updateConnectivity_notAvailable_notCalled() { - boolean mCalled = false; - mController = spy(new ConcreteWifiMacAddressPreferenceController(mContext, mLifecycle) { - @Override - public boolean isAvailable() { - return false; - } - }); - mController.displayPreference(mScreen); - verify(mController, never()).updateConnectivity(); - } - - @Test public void updateConnectivity_null_setMacUnavailable() { doReturn(null).when(mWifiManager).getFactoryMacAddresses(); mController.displayPreference(mScreen); diff --git a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java index ea46c0cee6b9..ee81813b4245 100644 --- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java +++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java @@ -300,6 +300,8 @@ public final class RingtonePickerActivity extends AlertActivity implements } }; installTask.execute(data.getData()); + } else if (requestCode == ADD_FILE_REQUEST_CODE && resultCode == RESULT_CANCELED) { + setupAlert(); } } diff --git a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt index 62dd4ac8c230..ef15c8461b95 100644 --- a/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt +++ b/packages/SystemUI/compose/core/src/com/android/compose/PlatformSlider.kt @@ -152,7 +152,10 @@ fun PlatformSlider( modifier = Modifier.fillMaxHeight() .weight(1f) - .padding(start = { paddingStart.roundToPx() }), + .padding( + start = { paddingStart.roundToPx() }, + end = { sliderHeight.roundToPx() / 2 }, + ), contentAlignment = Alignment.CenterStart, ) { labelComposable(isDragging) diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt index be5aa8a4c3b9..7535a51675e3 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalContainer.kt @@ -9,7 +9,7 @@ import androidx.compose.runtime.DisposableEffect import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Modifier -import androidx.compose.ui.unit.dp +import androidx.compose.ui.res.dimensionResource import com.android.compose.animation.scene.Edge import com.android.compose.animation.scene.ElementKey import com.android.compose.animation.scene.FixedSizeEdgeDetector @@ -29,6 +29,7 @@ import com.android.systemui.communal.shared.model.ObservableCommunalTransitionSt import com.android.systemui.communal.ui.compose.extensions.allowGestures import com.android.systemui.communal.ui.viewmodel.BaseCommunalViewModel import com.android.systemui.communal.ui.viewmodel.CommunalViewModel +import com.android.systemui.res.R import kotlinx.coroutines.flow.map import kotlinx.coroutines.flow.transform @@ -91,7 +92,10 @@ fun CommunalContainer( SceneTransitionLayout( state = sceneTransitionLayoutState, modifier = modifier.fillMaxSize().allowGestures(allowed = touchesAllowed), - swipeSourceDetector = FixedSizeEdgeDetector(ContainerDimensions.EdgeSwipeSize), + swipeSourceDetector = + FixedSizeEdgeDetector( + dimensionResource(id = R.dimen.communal_gesture_initiation_width) + ), ) { scene( TransitionSceneKey.Blank, @@ -167,7 +171,3 @@ fun ObservableTransitionState.toModel(): ObservableCommunalTransitionState { ) } } - -object ContainerDimensions { - val EdgeSwipeSize = 40.dp -} diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt index 66cef86fb773..6875bc544a55 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt @@ -40,7 +40,6 @@ import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.wrapContentHeight import androidx.compose.foundation.rememberScrollState import androidx.compose.foundation.verticalScroll -import androidx.compose.material3.MaterialTheme import androidx.compose.material3.windowsizeclass.WindowWidthSizeClass import androidx.compose.runtime.Composable import androidx.compose.runtime.LaunchedEffect @@ -51,6 +50,7 @@ import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.platform.LocalDensity import androidx.compose.ui.platform.LocalLifecycleOwner +import androidx.compose.ui.res.colorResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.SceneScope import com.android.compose.animation.scene.TransitionState @@ -62,6 +62,7 @@ import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.qs.footer.ui.compose.FooterActions import com.android.systemui.qs.ui.viewmodel.QuickSettingsSceneViewModel +import com.android.systemui.res.R import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.composable.ComposableScene import com.android.systemui.scene.ui.composable.asComposeAware @@ -168,7 +169,7 @@ private fun SceneScope.QuickSettingsScene( modifier = Modifier.element(Shade.Elements.BackgroundScrim) .fillMaxSize() - .background(MaterialTheme.colorScheme.scrim) + .background(colorResource(R.color.shade_scrim_background_dark)) ) Column( horizontalAlignment = Alignment.CenterHorizontally, 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 2e0ce42ee713..8484b7f5273f 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 @@ -26,7 +26,6 @@ import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.height import androidx.compose.foundation.layout.padding import androidx.compose.foundation.shape.RoundedCornerShape -import androidx.compose.material3.MaterialTheme import androidx.compose.runtime.Composable import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue @@ -37,6 +36,7 @@ import androidx.compose.ui.Modifier import androidx.compose.ui.layout.Layout import androidx.compose.ui.layout.layout import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.res.colorResource import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp import com.android.compose.animation.scene.ElementKey @@ -171,7 +171,7 @@ private fun SceneScope.ShadeScene( modifier = modifier .element(Shade.Elements.BackgroundScrim) - .background(MaterialTheme.colorScheme.scrim), + .background(colorResource(R.color.shade_scrim_background_dark)), ) Box { Layout( diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt index d40126198c33..c08eb94f25c0 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/bottombar/ui/BottomBarComponent.kt @@ -19,17 +19,17 @@ package com.android.systemui.volume.panel.component.bottombar.ui import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth -import androidx.compose.foundation.layout.height +import androidx.compose.foundation.layout.heightIn +import androidx.compose.material3.Button import androidx.compose.material3.ButtonDefaults import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.OutlinedButton import androidx.compose.material3.Text import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier import androidx.compose.ui.res.stringResource import androidx.compose.ui.unit.dp -import com.android.compose.PlatformButton -import com.android.compose.PlatformOutlinedButton import com.android.systemui.res.R import com.android.systemui.volume.panel.component.bottombar.ui.viewmodel.BottomBarViewModel import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope @@ -47,11 +47,11 @@ constructor( @Composable override fun VolumePanelComposeScope.Content(modifier: Modifier) { Row( - modifier = modifier.height(if (isLargeScreen) 54.dp else 48.dp).fillMaxWidth(), + modifier = modifier.heightIn(min = if (isLargeScreen) 54.dp else 48.dp).fillMaxWidth(), horizontalArrangement = Arrangement.SpaceBetween, verticalAlignment = Alignment.CenterVertically, ) { - PlatformOutlinedButton( + OutlinedButton( onClick = viewModel::onSettingsClicked, colors = ButtonDefaults.outlinedButtonColors( @@ -60,8 +60,8 @@ constructor( ) { Text(text = stringResource(R.string.volume_panel_dialog_settings_button)) } - PlatformButton(onClick = viewModel::onDoneClicked) { - Text(text = stringResource(R.string.inline_done_button)) + Button(onClick = viewModel::onDoneClicked) { + Text(stringResource(R.string.inline_done_button)) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt index d49fed5d6e10..b3fcc305e6b5 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/mediaoutput/ui/composable/MediaOutputComponent.kt @@ -27,6 +27,7 @@ import androidx.compose.animation.scaleOut import androidx.compose.animation.slideInVertically import androidx.compose.animation.slideOutVertically import androidx.compose.animation.togetherWith +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column @@ -46,7 +47,6 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier -import androidx.compose.ui.text.style.TextOverflow import androidx.compose.ui.unit.dp import com.android.compose.animation.Expandable import com.android.systemui.common.ui.compose.Icon @@ -78,8 +78,8 @@ constructor( color = MaterialTheme.colorScheme.surface, shape = RoundedCornerShape(28.dp), onClick = { viewModel.onBarClick(it) }, - ) { - Row { + ) { _ -> + Row(verticalAlignment = Alignment.CenterVertically) { connectedDeviceViewModel?.let { ConnectedDeviceText(it) } deviceIconViewModel?.let { ConnectedDeviceIcon(it) } @@ -90,26 +90,23 @@ constructor( @Composable private fun RowScope.ConnectedDeviceText(connectedDeviceViewModel: ConnectedDeviceViewModel) { Column( - modifier = - Modifier.weight(1f) - .padding(start = 24.dp, top = 20.dp, bottom = 20.dp) - .fillMaxHeight(), + modifier = Modifier.weight(1f).padding(start = 24.dp), verticalArrangement = Arrangement.spacedBy(4.dp), ) { Text( - connectedDeviceViewModel.label.toString(), + modifier = Modifier.basicMarquee(), + text = connectedDeviceViewModel.label.toString(), style = MaterialTheme.typography.labelMedium, color = MaterialTheme.colorScheme.onSurfaceVariant, maxLines = 1, - overflow = TextOverflow.Ellipsis, ) connectedDeviceViewModel.deviceName?.let { Text( - it.toString(), + modifier = Modifier.basicMarquee(), + text = it.toString(), style = MaterialTheme.typography.titleMedium, color = MaterialTheme.colorScheme.onSurface, maxLines = 1, - overflow = TextOverflow.Ellipsis, ) } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt index ddc9252a5a4a..4d810dfce89d 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/ColumnVolumeSliders.kt @@ -36,8 +36,6 @@ import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.padding import androidx.compose.foundation.layout.size -import androidx.compose.foundation.rememberScrollState -import androidx.compose.foundation.verticalScroll import androidx.compose.material3.Icon import androidx.compose.material3.IconButton import androidx.compose.material3.IconButtonDefaults @@ -70,7 +68,7 @@ fun ColumnVolumeSliders( require(viewModels.isNotEmpty()) var isExpanded: Boolean by remember(isExpandable) { mutableStateOf(!isExpandable) } val transition = updateTransition(isExpanded, label = "CollapsableSliders") - Column(modifier = modifier.verticalScroll(rememberScrollState())) { + Column(modifier = modifier) { Row( modifier = Modifier.fillMaxWidth(), horizontalArrangement = Arrangement.spacedBy(8.dp), diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt index 0d94bb06c06f..18a62dca3769 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/component/volume/ui/composable/VolumeSlider.kt @@ -20,6 +20,7 @@ import androidx.compose.animation.AnimatedVisibility import androidx.compose.animation.animateContentSize import androidx.compose.animation.expandVertically import androidx.compose.animation.shrinkVertically +import androidx.compose.foundation.basicMarquee import androidx.compose.foundation.layout.Column import androidx.compose.material3.MaterialTheme import androidx.compose.material3.Text @@ -59,7 +60,12 @@ fun VolumeSlider( colors = sliderColors, label = { Column(modifier = Modifier.animateContentSize()) { - Text(state.label, style = MaterialTheme.typography.titleMedium) + Text( + modifier = Modifier.basicMarquee(), + text = state.label, + style = MaterialTheme.typography.titleMedium, + maxLines = 1, + ) state.disabledMessage?.let { message -> AnimatedVisibility( @@ -67,7 +73,12 @@ fun VolumeSlider( enter = expandVertically { it }, exit = shrinkVertically { it }, ) { - Text(text = message, style = MaterialTheme.typography.bodySmall) + Text( + modifier = Modifier.basicMarquee(), + text = message, + style = MaterialTheme.typography.bodySmall, + maxLines = 1, + ) } } } diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt index a838a99524a3..ac5004e16a3b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/HorizontalVolumePanelContent.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier @@ -35,7 +37,7 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( val spacing = 20.dp Row(modifier = modifier, horizontalArrangement = Arrangement.spacedBy(space = spacing)) { Column( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(spacing) ) { for (component in layout.contentComponents) { @@ -46,7 +48,7 @@ fun VolumePanelComposeScope.HorizontalVolumePanelContent( } Column( - modifier = Modifier.weight(1f), + modifier = Modifier.weight(1f).verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(space = spacing, alignment = Alignment.Top) ) { for (component in layout.headerComponents) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt index 4d073798c70c..dd767817a5ae 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VerticalVolumePanelContent.kt @@ -22,6 +22,8 @@ import androidx.compose.foundation.layout.Column import androidx.compose.foundation.layout.Row import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.wrapContentHeight +import androidx.compose.foundation.rememberScrollState +import androidx.compose.foundation.verticalScroll import androidx.compose.runtime.Composable import androidx.compose.ui.Modifier import androidx.compose.ui.unit.dp @@ -33,7 +35,7 @@ fun VolumePanelComposeScope.VerticalVolumePanelContent( modifier: Modifier = Modifier, ) { Column( - modifier = modifier, + modifier = modifier.verticalScroll(rememberScrollState()), verticalArrangement = Arrangement.spacedBy(20.dp), ) { for (component in layout.headerComponents) { diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt index 8a9ebc918be6..910cd5ec107b 100644 --- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt +++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt @@ -21,6 +21,8 @@ import androidx.compose.foundation.isSystemInDarkTheme import androidx.compose.foundation.layout.Arrangement import androidx.compose.foundation.layout.Box import androidx.compose.foundation.layout.Column +import androidx.compose.foundation.layout.WindowInsets +import androidx.compose.foundation.layout.displayCutout import androidx.compose.foundation.layout.fillMaxSize import androidx.compose.foundation.layout.fillMaxWidth import androidx.compose.foundation.layout.heightIn @@ -36,13 +38,17 @@ import androidx.compose.runtime.collectAsState import androidx.compose.runtime.getValue import androidx.compose.ui.Alignment import androidx.compose.ui.Modifier +import androidx.compose.ui.platform.LocalDensity +import androidx.compose.ui.platform.LocalLayoutDirection import androidx.compose.ui.res.dimensionResource import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.max import com.android.compose.theme.PlatformTheme import com.android.systemui.res.R import com.android.systemui.volume.panel.ui.layout.ComponentsLayout import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelViewModel +import kotlin.math.max private val padding = 24.dp @@ -65,12 +71,12 @@ fun VolumePanelRoot( val components by viewModel.componentsLayout.collectAsState(null) with(VolumePanelComposeScope(state)) { - var boxModifier = modifier.fillMaxSize().clickable(onClick = onDismiss) - if (!isPortrait) { - boxModifier = boxModifier.padding(horizontal = 48.dp) - } Box( - modifier = boxModifier, + modifier = + modifier + .fillMaxSize() + .clickable(onClick = onDismiss) + .volumePanelPaddings(isPortrait = isPortrait), contentAlignment = Alignment.BottomCenter, ) { val radius = dimensionResource(R.dimen.volume_panel_corner_radius) @@ -80,8 +86,8 @@ fun VolumePanelRoot( interactionSource = null, indication = null, onClick = { - // prevent windowCloseOnTouchOutside from dismissing when tapped on - // the panel itself. + // prevent windowCloseOnTouchOutside from dismissing when tapped + // on the panel itself. }, ), shape = RoundedCornerShape(topStart = radius, topEnd = radius), @@ -110,7 +116,7 @@ private fun VolumePanelComposeScope.Components( layout: ComponentsLayout, modifier: Modifier = Modifier ) { - val arrangement = + val arrangement: Arrangement.Vertical = if (isLargeScreen) { Arrangement.spacedBy(20.dp) } else { @@ -120,16 +126,21 @@ private fun VolumePanelComposeScope.Components( modifier = modifier.widthIn(max = 800.dp), verticalArrangement = arrangement, ) { - val contentModifier = Modifier if (isPortrait || isLargeScreen) { - VerticalVolumePanelContent(modifier = contentModifier, layout = layout) + VerticalVolumePanelContent( + modifier = Modifier.weight(weight = 1f, fill = false), + layout = layout + ) } else { HorizontalVolumePanelContent( - modifier = contentModifier.heightIn(max = 212.dp), - layout = layout + modifier = Modifier.weight(weight = 1f, fill = false).heightIn(max = 212.dp), + layout = layout, ) } - BottomBar(layout = layout, modifier = Modifier) + BottomBar( + modifier = Modifier, + layout = layout, + ) } } @@ -149,3 +160,28 @@ private fun VolumePanelComposeScope.BottomBar( } } } + +/** + * Makes sure volume panel stays symmetrically in the middle of the screen while still avoiding + * being under the cutouts. + */ +@Composable +private fun Modifier.volumePanelPaddings(isPortrait: Boolean): Modifier { + val cutout = WindowInsets.displayCutout + return with(LocalDensity.current) { + val horizontalCutout = + max( + cutout.getLeft(density = this, layoutDirection = LocalLayoutDirection.current), + cutout.getRight(density = this, layoutDirection = LocalLayoutDirection.current) + ) + val minHorizontalPadding = if (isPortrait) 0.dp else 48.dp + val horizontalPadding = max(horizontalCutout.toDp(), minHorizontalPadding) + + padding( + start = horizontalPadding, + top = cutout.getTop(this).toDp(), + end = horizontalPadding, + bottom = cutout.getBottom(this).toDp(), + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt new file mode 100644 index 000000000000..a932dd6d106d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/media/domain/interactor/SpatializerInteractorTest.kt @@ -0,0 +1,92 @@ +/* + * Copyright (C) 2024 The Android Open Source Project + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.android.systemui.media.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.SysuiTestCase +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.test.runTest +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class SpatializerInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val underTest = SpatializerInteractor(kosmos.spatializerRepository) + + @Test + fun setSpatialAudioEnabledFalse_isEnabled_false() { + with(kosmos) { + testScope.runTest { + underTest.setSpatialAudioEnabled(deviceAttributes, false) + + assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isFalse() + } + } + } + + @Test + fun setSpatialAudioEnabledTrue_isEnabled_true() { + with(kosmos) { + testScope.runTest { + underTest.setSpatialAudioEnabled(deviceAttributes, true) + + assertThat(underTest.isSpatialAudioEnabled(deviceAttributes)).isTrue() + } + } + } + + @Test + fun setHeadTrackingEnabledFalse_isEnabled_false() { + with(kosmos) { + testScope.runTest { + underTest.setHeadTrackingEnabled(deviceAttributes, false) + + assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isFalse() + } + } + } + + @Test + fun setHeadTrackingEnabledTrue_isEnabled_true() { + with(kosmos) { + testScope.runTest { + underTest.setHeadTrackingEnabled(deviceAttributes, true) + + assertThat(underTest.isHeadTrackingEnabled(deviceAttributes)).isTrue() + } + } + } + + private companion object { + val deviceAttributes = + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + "test_address", + ) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt index 4e72843922e1..fff0a316cbf4 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt @@ -40,6 +40,7 @@ import com.android.systemui.bouncer.ui.viewmodel.PinBouncerViewModel import com.android.systemui.bouncer.ui.viewmodel.bouncerViewModel import com.android.systemui.classifier.domain.interactor.falsingInteractor import com.android.systemui.classifier.falsingCollector +import com.android.systemui.classifier.falsingManager import com.android.systemui.communal.domain.interactor.communalInteractor import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository @@ -265,6 +266,7 @@ class SceneFrameworkIntegrationTest : SysuiTestCase() { displayId = displayTracker.defaultDisplayId, sceneLogger = mock(), falsingCollector = kosmos.falsingCollector, + falsingManager = kosmos.falsingManager, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, simBouncerInteractor = dagger.Lazy { kosmos.simBouncerInteractor }, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt new file mode 100644 index 000000000000..9b0adb172e8d --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractorTest.kt @@ -0,0 +1,191 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import android.platform.test.annotations.DisableFlags +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.fakeSceneDataSource +import com.android.systemui.shade.data.repository.fakeShadeRepository +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.panelExpansionInteractor +import com.android.systemui.testKosmos +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest +import org.junit.Before +import org.junit.Test +import org.junit.runner.RunWith + +@SmallTest +@RunWith(AndroidJUnit4::class) +class PanelExpansionInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val testScope = kosmos.testScope + private val deviceEntryRepository = kosmos.fakeDeviceEntryRepository + private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + private val sceneInteractor = kosmos.sceneInteractor + private val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(SceneKey.Lockscreen) + ) + private val fakeSceneDataSource = kosmos.fakeSceneDataSource + private val fakeShadeRepository = kosmos.fakeShadeRepository + + private lateinit var underTest: PanelExpansionInteractor + + @Before + fun setUp() { + sceneInteractor.setTransitionState(transitionState) + } + + @Test + @EnableSceneContainer + fun legacyPanelExpansion_whenIdle_whenLocked() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + setUnlocked(false) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Lockscreen) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Bouncer) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Shade) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.QuickSettings) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Communal) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + } + + @Test + @EnableSceneContainer + fun legacyPanelExpansion_whenIdle_whenUnlocked() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + setUnlocked(true) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Gone) { assertThat(panelExpansion).isEqualTo(0f) } + assertThat(panelExpansion).isEqualTo(0f) + + changeScene(SceneKey.Shade) { progress -> + assertThat(panelExpansion).isEqualTo(progress) + } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.QuickSettings) { + // Shade's already expanded, so moving to QS should also be 1f. + assertThat(panelExpansion).isEqualTo(1f) + } + assertThat(panelExpansion).isEqualTo(1f) + + changeScene(SceneKey.Communal) { assertThat(panelExpansion).isEqualTo(1f) } + assertThat(panelExpansion).isEqualTo(1f) + } + + @Test + @DisableFlags(FLAG_SCENE_CONTAINER) + fun legacyPanelExpansion_whenInLegacyMode() = + testScope.runTest { + underTest = kosmos.panelExpansionInteractor + val leet = 0.1337f + fakeShadeRepository.setLegacyShadeExpansion(leet) + setUnlocked(false) + val panelExpansion by collectLastValue(underTest.legacyPanelExpansion) + + changeScene(SceneKey.Lockscreen) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Bouncer) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Shade) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.QuickSettings) + assertThat(panelExpansion).isEqualTo(leet) + + changeScene(SceneKey.Communal) + assertThat(panelExpansion).isEqualTo(leet) + } + + private fun TestScope.setUnlocked(isUnlocked: Boolean) { + val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked) + deviceEntryRepository.setUnlocked(isUnlocked) + runCurrent() + + assertThat(isDeviceUnlocked).isEqualTo(isUnlocked) + } + + private fun TestScope.changeScene( + toScene: SceneKey, + assertDuringProgress: ((progress: Float) -> Unit) = {}, + ) { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = checkNotNull(currentScene), + toScene = toScene, + progress = progressFlow, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.2f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.6f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 1f + runCurrent() + assertDuringProgress(progressFlow.value) + + transitionState.value = ObservableTransitionState.Idle(toScene) + fakeSceneDataSource.changeScene(toScene) + runCurrent() + assertDuringProgress(progressFlow.value) + + assertThat(currentScene).isEqualTo(toScene) + } +} diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt index f49b4777cf14..4e1623661a58 100644 --- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/domain/startable/SceneContainerStartableTest.kt @@ -32,6 +32,7 @@ import com.android.systemui.authentication.shared.model.AuthenticationMethodMode import com.android.systemui.bouncer.domain.interactor.bouncerInteractor import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor import com.android.systemui.classifier.FalsingCollector +import com.android.systemui.classifier.falsingManager import com.android.systemui.coroutines.collectLastValue import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor @@ -115,6 +116,7 @@ class SceneContainerStartableTest : SysuiTestCase() { displayId = Display.DEFAULT_DISPLAY, sceneLogger = mock(), falsingCollector = falsingCollector, + falsingManager = kosmos.falsingManager, powerInteractor = powerInteractor, bouncerInteractor = bouncerInteractor, simBouncerInteractor = { kosmos.simBouncerInteractor }, @@ -970,6 +972,20 @@ class SceneContainerStartableTest : SysuiTestCase() { ) } + @Test + fun respondToFalsingDetections() = + testScope.runTest { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val transitionStateFlow = prepareState() + underTest.start() + emulateSceneTransition(transitionStateFlow, toScene = SceneKey.Bouncer) + assertThat(currentScene).isNotEqualTo(SceneKey.Lockscreen) + + kosmos.falsingManager.sendFalsingBelief() + + assertThat(currentScene).isEqualTo(SceneKey.Lockscreen) + } + private fun TestScope.emulateSceneTransition( transitionStateFlow: MutableStateFlow<ObservableTransitionState>, toScene: SceneKey, diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.kt new file mode 100644 index 000000000000..737b7f3e0af0 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/SpatialAudioComponentKosmos.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.volume.panel.component.spatial + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerInteractor +import com.android.systemui.volume.mediaOutputInteractor +import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor + +val Kosmos.spatialAudioComponentInteractor by + Kosmos.Fixture { + SpatialAudioComponentInteractor( + mediaOutputInteractor, + spatializerInteractor, + testScope.backgroundScope + ) + } 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 new file mode 100644 index 000000000000..36be90ecbf7e --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteriaTest.kt @@ -0,0 +1,138 @@ +/* + * 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.spatial.domain + +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.testing.TestableLooper.RunWithLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +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 +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) +@RunWithLooper(setAsMainLooper = true) +class SpatialAudioAvailabilityCriteriaTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private val cachedBluetoothDevice: CachedBluetoothDevice = mock { + whenever(address).thenReturn("test_address") + } + private val bluetoothMediaDevice: BluetoothMediaDevice = mock { + whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + } + + private lateinit var underTest: SpatialAudioAvailabilityCriteria + + @Before + fun setup() { + with(kosmos) { + mediaControllerRepository.setActiveLocalMediaController( + mediaController.apply { + whenever(packageName).thenReturn("test.pkg") + whenever(sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(playbackState).thenReturn(PlaybackState.Builder().build()) + } + ) + + underTest = SpatialAudioAvailabilityCriteria(spatialAudioComponentInteractor) + } + } + + @Test + fun noSpatialAudio_noHeadTracking_unavailable() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(false) + spatializerRepository.defaultSpatialAudioAvailable = false + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isFalse() + } + } + } + + @Test + fun spatialAudio_noHeadTracking_available() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(false) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isTrue() + } + } + } + + @Test + fun spatialAudio_headTracking_available() { + with(kosmos) { + testScope.runTest { + localMediaRepository.updateCurrentConnectedDevice(bluetoothMediaDevice) + spatializerRepository.setIsHeadTrackingAvailable(true) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isTrue() + } + } + } + + @Test + fun spatialAudio_headTracking_noDevice_unavailable() { + with(kosmos) { + testScope.runTest { + spatializerRepository.setIsHeadTrackingAvailable(true) + spatializerRepository.defaultSpatialAudioAvailable = true + + val isAvailable by collectLastValue(underTest.isAvailable()) + runCurrent() + + assertThat(isAvailable).isFalse() + } + } + } +} 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 new file mode 100644 index 000000000000..eb6f0b2e32b3 --- /dev/null +++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractorTest.kt @@ -0,0 +1,119 @@ +/* + * 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.spatial.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import android.media.session.MediaSession +import android.media.session.PlaybackState +import android.testing.TestableLooper +import androidx.test.ext.junit.runners.AndroidJUnit4 +import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectValues +import com.android.systemui.kosmos.testScope +import com.android.systemui.media.spatializerInteractor +import com.android.systemui.media.spatializerRepository +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.mock +import com.android.systemui.util.mockito.whenever +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.SpatialAudioEnabledModel +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 SpatialAudioComponentInteractorTest : SysuiTestCase() { + + private val kosmos = testKosmos() + private lateinit var underTest: SpatialAudioComponentInteractor + + @Before + fun setup() { + with(kosmos) { + val cachedBluetoothDevice: CachedBluetoothDevice = mock { + whenever(address).thenReturn("test_address") + } + localMediaRepository.updateCurrentConnectedDevice( + mock<BluetoothMediaDevice> { + whenever(name).thenReturn("test_device") + whenever(cachedDevice).thenReturn(cachedBluetoothDevice) + } + ) + + whenever(mediaController.packageName).thenReturn("test.pkg") + whenever(mediaController.sessionToken).thenReturn(MediaSession.Token(0, mock {})) + whenever(mediaController.playbackState).thenReturn(PlaybackState.Builder().build()) + + mediaControllerRepository.setActiveLocalMediaController(mediaController) + + spatializerRepository.setIsSpatialAudioAvailable( + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + "test_address" + ), + true + ) + spatializerRepository.setIsHeadTrackingAvailable(true) + + underTest = + SpatialAudioComponentInteractor( + mediaOutputInteractor, + spatializerInteractor, + testScope.backgroundScope, + ) + } + } + + @Test + fun setEnabled_changesIsEnabled() { + with(kosmos) { + testScope.runTest { + val values by collectValues(underTest.isEnabled) + + underTest.setEnabled(SpatialAudioEnabledModel.Disabled) + runCurrent() + underTest.setEnabled(SpatialAudioEnabledModel.HeadTrackingEnabled) + runCurrent() + underTest.setEnabled(SpatialAudioEnabledModel.SpatialAudioEnabled) + runCurrent() + + assertThat(values) + .containsExactly( + SpatialAudioEnabledModel.Disabled, + SpatialAudioEnabledModel.HeadTrackingEnabled, + SpatialAudioEnabledModel.SpatialAudioEnabled, + ) + .inOrder() + } + } + } +} diff --git a/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml b/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml new file mode 100644 index 000000000000..324ae0c5c1d4 --- /dev/null +++ b/packages/SystemUI/res/drawable/bg_shutdown_finder_message.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<shape xmlns:android="http://schemas.android.com/apk/res/android"> + <corners android:radius="28dp" /> + <solid android:color="@color/global_actions_lite_button_background" /> +</shape>
\ No newline at end of file diff --git a/packages/SystemUI/res/drawable/ic_finder_active.xml b/packages/SystemUI/res/drawable/ic_finder_active.xml new file mode 100644 index 000000000000..8ca221ab7392 --- /dev/null +++ b/packages/SystemUI/res/drawable/ic_finder_active.xml @@ -0,0 +1,14 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="24dp" + android:height="24dp" + android:viewportWidth="24" + android:viewportHeight="24"> + <path + android:pathData="M12,0L12,0A12,12 0,0 1,24 12L24,12A12,12 0,0 1,12 24L12,24A12,12 0,0 1,0 12L0,12A12,12 0,0 1,12 0z" + android:fillColor="#00677D"/> + <path + android:pathData="M12.797,4.005C11.949,3.936 11.203,4.597 11.203,5.467V6.659C8.855,7.001 6.998,8.856 6.653,11.203H5.467C4.597,11.203 3.936,11.948 4.005,12.796L4.006,12.802L4.006,12.809C4.38,16.605 7.399,19.625 11.195,20C12.051,20.087 12.803,19.404 12.803,18.547V17.355C15.154,17.012 17.013,15.154 17.355,12.803H18.54C19.406,12.803 20.079,12.058 19.992,11.196C19.618,7.4 16.606,4.388 12.812,4.006L12.804,4.006L12.797,4.005ZM11.203,9.344V8.283C9.741,8.591 8.588,9.741 8.278,11.203H9.344C9.585,10.4 10.179,9.754 10.942,9.437C11.027,9.402 11.114,9.371 11.203,9.344ZM11.998,13.171C11.358,13.175 10.828,12.651 10.827,12.004H10.827C10.827,11.959 10.83,11.915 10.835,11.871C10.885,11.427 11.185,11.056 11.59,10.902C11.694,10.863 11.806,10.838 11.921,10.83C11.948,10.833 11.976,10.834 12.003,10.834C12.65,10.834 13.177,11.356 13.179,12.007C13.177,12.622 12.695,13.13 12.091,13.175C12.06,13.172 12.029,13.17 11.998,13.171ZM17.353,11.203H18.383C18.028,8.289 15.72,5.979 12.804,5.616V6.658C15.153,7 17.004,8.852 17.353,11.203ZM14.663,11.203C14.395,10.311 13.692,9.611 12.804,9.344V8.283C14.265,8.59 15.414,9.736 15.727,11.203H14.663ZM5.615,12.803H6.654C7.001,15.15 8.855,17.002 11.203,17.346V18.391C8.287,18.034 5.972,15.719 5.615,12.803ZM11.203,14.666C10.316,14.394 9.613,13.692 9.345,12.803H8.279C8.591,14.264 9.741,15.412 11.203,15.721V14.666ZM14.661,12.811H15.729C15.418,14.272 14.266,15.422 12.804,15.73V14.662C13.689,14.396 14.391,13.699 14.661,12.811Z" + android:fillColor="#ffffff" + android:fillType="evenOdd"/> +</vector> diff --git a/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml b/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml new file mode 100644 index 000000000000..b6db7fc8007f --- /dev/null +++ b/packages/SystemUI/res/layout/shutdown_dialog_finder_active.xml @@ -0,0 +1,72 @@ +<?xml version="1.0" encoding="utf-8"?> +<androidx.constraintlayout.widget.ConstraintLayout + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:app="http://schemas.android.com/apk/res-auto" + android:layout_width="match_parent" + android:layout_height="match_parent"> + + <TextView + android:id="@android:id/text1" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:fontFamily="google-sans" + android:gravity="center" + android:text="@string/shutdown_progress" + android:textAppearance="?android:attr/textAppearanceMedium" + android:textDirection="locale" + android:textSize="18sp" + android:visibility="gone" + app:layout_constraintBottom_toTopOf="@android:id/text2" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toTopOf="parent" + app:layout_constraintVertical_bias="0.375" + app:layout_constraintVertical_chainStyle="packed" /> + + <TextView + android:id="@android:id/text2" + android:layout_width="wrap_content" + android:layout_height="wrap_content" + android:layout_marginBottom="24dp" + android:fontFamily="google-sans" + android:gravity="center" + android:text="@string/shutdown_progress" + android:textAppearance="?android:attr/textAppearanceLarge" + android:textDirection="locale" + android:textSize="24sp" + app:layout_constraintBottom_toTopOf="@android:id/progress" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/text1" /> + + <ProgressBar + android:id="@android:id/progress" + style="?android:attr/progressBarStyleLarge" + android:layout_width="30dp" + android:layout_height="30dp" + android:importantForAccessibility="no" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintEnd_toEndOf="parent" + app:layout_constraintStart_toStartOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/text2" /> + + <TextView + android:id="@+id/finer_hint" + android:layout_width="match_parent" + android:layout_height="wrap_content" + android:layout_margin="32dp" + android:background="@drawable/bg_shutdown_finder_message" + android:drawablePadding="16dp" + android:drawableStart="@drawable/ic_finder_active" + android:fontFamily="google-sans" + android:gravity="start" + android:padding="20dp" + android:text="@string/finder_active" + android:textAppearance="?android:attr/textAppearanceSmall" + android:textColor="@android:color/secondary_text_dark" + android:textDirection="locale" + app:layout_constraintBottom_toBottomOf="parent" + app:layout_constraintTop_toBottomOf="@android:id/progress" + app:layout_constraintVertical_bias="1" /> +</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml index 4be1deb3de1c..34eb1b4c5276 100644 --- a/packages/SystemUI/res/values/dimens.xml +++ b/packages/SystemUI/res/values/dimens.xml @@ -1742,12 +1742,18 @@ <dimen name="communal_grid_height">630dp</dimen> <!-- Number of columns for each communal card --> <integer name="communal_grid_columns_per_card">6</integer> - <!-- Width of area on right edge of screen in which swipes will open the communal hub --> - <dimen name="communal_right_edge_swipe_region_width">16dp</dimen> + + <!-- The width of the swipe target to initiate opening or closing communal hub. --> + <dimen name="communal_gesture_initiation_width">68dp</dimen> + + <!-- TODO(b/322549765): unify with communal_gesture_initiation_width --> + <!-- Width of area on right edge of screen in which swipes will open the communal hub when on + the lockscreen --> + <dimen name="communal_right_edge_swipe_region_width">40dp</dimen> <!-- Height of area at top of communal hub where swipes should open the notification shade --> - <dimen name="communal_top_edge_swipe_region_height">32dp</dimen> + <dimen name="communal_top_edge_swipe_region_height">68dp</dimen> <!-- Height of area at bottom of communal hub where swipes should open the bouncer --> - <dimen name="communal_bottom_edge_swipe_region_height">32dp</dimen> + <dimen name="communal_bottom_edge_swipe_region_height">68dp</dimen> <dimen name="drag_and_drop_icon_size">70dp</dimen> @@ -1819,9 +1825,6 @@ <dimen name="dream_overlay_complication_smartspace_padding">24dp</dimen> <dimen name="dream_overlay_complication_smartspace_max_width">408dp</dimen> - <!-- The width of the swipe target to initiate opening communal hub over dreams. --> - <dimen name="communal_gesture_initiation_width">48dp</dimen> - <!-- The position of the end guide, which dream overlay complications can align their start with if their end is aligned with the parent end. Represented as the percentage over from the start of the parent container. --> diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml index 4263d9402d66..ea0e3092a781 100644 --- a/packages/SystemUI/res/values/strings.xml +++ b/packages/SystemUI/res/values/strings.xml @@ -2236,6 +2236,11 @@ <!-- Tuner string --> <!-- Tuner string --> + <!-- Message shown during shutdown when Find My Device with Dead Battery Finder is active [CHAR LIMIT=300] --> + <string name="finder_active">You can locate this phone with Find My Device even when powered off</string> + <!-- Shutdown Progress Dialog. This is shown if the user chooses to power off the phone. [CHAR LIMIT=60] --> + <string name="shutdown_progress">Shutting down\u2026</string> + <!-- Text help link for care instructions for overheating devices [CHAR LIMIT=40] --> <string name="thermal_shutdown_dialog_help_text">See care steps</string> <!-- URL for care instructions for overheating devices --> diff --git a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java index 6d9994fb2205..ce24259bbc1e 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/FrameworkServicesModule.java @@ -71,6 +71,7 @@ import android.media.MediaRouter2Manager; import android.media.projection.IMediaProjectionManager; import android.media.projection.MediaProjectionManager; import android.media.session.MediaSessionManager; +import android.nearby.NearbyManager; import android.net.ConnectivityManager; import android.net.NetworkScoreManager; import android.net.wifi.WifiManager; @@ -441,6 +442,12 @@ public class FrameworkServicesModule { @Provides @Singleton + static NearbyManager provideNearbyManager(Context context) { + return context.getSystemService(NearbyManager.class); + } + + @Provides + @Singleton static NetworkScoreManager provideNetworkScoreManager(Context context) { return context.getSystemService(NetworkScoreManager.class); } diff --git a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java index a90980fddfb0..a431a59fcef6 100644 --- a/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java +++ b/packages/SystemUI/src/com/android/systemui/dagger/ReferenceSysUIComponent.java @@ -16,7 +16,6 @@ package com.android.systemui.dagger; -import com.android.systemui.globalactions.ShutdownUiModule; import com.android.systemui.keyguard.CustomizationProvider; import com.android.systemui.statusbar.NotificationInsetsModule; import com.android.systemui.statusbar.QsFrameTranslateModule; @@ -32,7 +31,6 @@ import dagger.Subcomponent; DependencyProvider.class, NotificationInsetsModule.class, QsFrameTranslateModule.class, - ShutdownUiModule.class, SystemUIBinder.class, SystemUIModule.class, SystemUICoreStartableModule.class, diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java index 51978ece14db..ccd69ca55f0c 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java +++ b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUi.java @@ -22,7 +22,10 @@ import android.annotation.Nullable; import android.annotation.StringRes; import android.app.Dialog; import android.content.Context; +import android.nearby.NearbyManager; +import android.net.platform.flags.Flags; import android.os.PowerManager; +import android.util.Log; import android.view.View; import android.view.ViewGroup; import android.view.Window; @@ -38,6 +41,8 @@ import com.android.systemui.scrim.ScrimDrawable; import com.android.systemui.statusbar.BlurUtils; import com.android.systemui.statusbar.phone.ScrimController; +import javax.inject.Inject; + /** * Provides the UI shown during system shutdown. */ @@ -45,9 +50,13 @@ public class ShutdownUi { private Context mContext; private BlurUtils mBlurUtils; - public ShutdownUi(Context context, BlurUtils blurUtils) { + private NearbyManager mNearbyManager; + + @Inject + public ShutdownUi(Context context, BlurUtils blurUtils, NearbyManager nearbyManager) { mContext = context; mBlurUtils = blurUtils; + mNearbyManager = nearbyManager; } /** @@ -132,12 +141,28 @@ public class ShutdownUi { /** * Returns the layout resource to use for UI while shutting down. * @param isReboot Whether this is a reboot or a shutdown. - * @return */ - public int getShutdownDialogContent(boolean isReboot) { - return R.layout.shutdown_dialog; + @VisibleForTesting int getShutdownDialogContent(boolean isReboot) { + if (!Flags.poweredOffFindingPlatform()) { + return R.layout.shutdown_dialog; + } + int finderActive = mNearbyManager.getPoweredOffFindingMode(); + if (finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED + || finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_UNSUPPORTED) { + // inactive or unsupported, use regular shutdown dialog + return R.layout.shutdown_dialog; + } else if (finderActive == NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED) { + // active, use dialog with finder info if shutting down + return isReboot ? R.layout.shutdown_dialog : + com.android.systemui.res.R.layout.shutdown_dialog_finder_active; + } else { + // that's weird? default to regular dialog + Log.w("ShutdownUi", "Unexpected value for finder active: " + finderActive); + return R.layout.shutdown_dialog; + } } + @StringRes @VisibleForTesting int getRebootMessage(boolean isReboot, @Nullable String reason) { if (reason != null && reason.startsWith(PowerManager.REBOOT_RECOVERY_UPDATE)) { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt index 1c9be0f105b2..f13ecf3e91b9 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactory.kt @@ -21,6 +21,7 @@ import android.content.Context import android.media.AudioManager import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.flags.Flags import com.android.systemui.res.R private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on @@ -36,6 +37,7 @@ private val actionAccessibilityLabelDisconnect = /** Factories to create different types of Bluetooth device items from CachedBluetoothDevice. */ internal abstract class DeviceItemFactory { abstract fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean @@ -45,6 +47,7 @@ internal abstract class DeviceItemFactory { internal class ActiveMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { @@ -71,6 +74,7 @@ internal class ActiveMediaDeviceItemFactory : DeviceItemFactory() { internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { @@ -99,10 +103,18 @@ internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() { internal class ConnectedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { - return BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { + !BluetoothUtils.isExclusivelyManagedBluetoothDevice( + context, + cachedDevice.getDevice() + ) && BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + } else { + BluetoothUtils.isConnectedBluetoothDevice(cachedDevice, audioManager) + } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { @@ -125,10 +137,18 @@ internal class ConnectedDeviceItemFactory : DeviceItemFactory() { internal class SavedDeviceItemFactory : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ): Boolean { - return cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) { + !BluetoothUtils.isExclusivelyManagedBluetoothDevice( + context, + cachedDevice.getDevice() + ) && cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + } else { + cachedDevice.bondState == BluetoothDevice.BOND_BONDED && !cachedDevice.isConnected + } } override fun create(context: Context, cachedDevice: CachedBluetoothDevice): DeviceItem { diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt index fcd45a6431bb..1df496b17aa5 100644 --- a/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt +++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractor.kt @@ -133,7 +133,7 @@ constructor( bluetoothTileDialogRepository.cachedDevices .mapNotNull { cachedDevice -> deviceItemFactoryList - .firstOrNull { it.isFilterMatched(cachedDevice, audioManager) } + .firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) } ?.create(context, cachedDevice) } .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices) diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt new file mode 100644 index 000000000000..36350f8af455 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/interactor/PanelExpansionInteractor.kt @@ -0,0 +1,104 @@ +/* + * 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. + */ + +@file:OptIn(ExperimentalCoroutinesApi::class) + +package com.android.systemui.scene.domain.interactor + +import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.scene.shared.flag.SceneContainerFlag +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.shade.data.repository.ShadeRepository +import javax.inject.Inject +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.flatMapLatest +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.flow.map + +@SysUISingleton +class PanelExpansionInteractor +@Inject +constructor( + sceneInteractor: SceneInteractor, + shadeRepository: ShadeRepository, +) { + + /** + * The amount by which the "panel" has been expanded (`0` when fully collapsed, `1` when fully + * expanded). + * + * This is a legacy concept from the time when the "panel" included the notification/QS shades + * as well as the keyguard (lockscreen and bouncer). This value is meant only for + * backwards-compatibility and should not be consumed by newer code. + */ + @Deprecated("Use SceneInteractor.currentScene instead.") + val legacyPanelExpansion: Flow<Float> = + if (SceneContainerFlag.isEnabled) { + sceneInteractor.transitionState.flatMapLatest { state -> + when (state) { + is ObservableTransitionState.Idle -> + flowOf( + if (state.scene != SceneKey.Gone) { + // When resting on a non-Gone scene, the panel is fully expanded. + 1f + } else { + // When resting on the Gone scene, the panel is considered fully + // collapsed. + 0f + } + ) + is ObservableTransitionState.Transition -> + when { + state.fromScene == SceneKey.Gone -> + if (state.toScene.isExpandable()) { + // Moving from Gone to a scene that can animate-expand has a + // panel + // expansion + // that tracks with the transition. + state.progress + } else { + // Moving from Gone to a scene that doesn't animate-expand + // immediately makes + // the panel fully expanded. + flowOf(1f) + } + state.toScene == SceneKey.Gone -> + if (state.fromScene.isExpandable()) { + // Moving to Gone from a scene that can animate-expand has a + // panel + // expansion + // that tracks with the transition. + state.progress.map { 1 - it } + } else { + // Moving to Gone from a scene that doesn't animate-expand + // immediately makes + // the panel fully collapsed. + flowOf(0f) + } + else -> flowOf(1f) + } + } + } + } else { + shadeRepository.legacyShadeExpansion + } + + private fun SceneKey.isExpandable(): Boolean { + return this == SceneKey.Shade || this == SceneKey.QuickSettings + } +} diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt index b642d38289fe..034f87f4c72f 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt @@ -26,6 +26,7 @@ import com.android.systemui.bouncer.domain.interactor.BouncerInteractor import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor import com.android.systemui.classifier.FalsingCollector import com.android.systemui.classifier.FalsingCollectorActual +import com.android.systemui.common.coroutine.ConflatedCallbackFlow.conflatedCallbackFlow import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.DisplayId @@ -34,6 +35,8 @@ import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor import com.android.systemui.model.SceneContainerPlugin import com.android.systemui.model.SysUiState import com.android.systemui.model.updateFlags +import com.android.systemui.plugins.FalsingManager +import com.android.systemui.plugins.FalsingManager.FalsingBeliefListener import com.android.systemui.power.domain.interactor.PowerInteractor import com.android.systemui.scene.domain.interactor.SceneInteractor import com.android.systemui.scene.shared.flag.SceneContainerFlags @@ -53,6 +56,7 @@ import java.io.PrintWriter import javax.inject.Inject import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.collectLatest import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.distinctUntilChanged @@ -82,6 +86,7 @@ constructor( @DisplayId private val displayId: Int, private val sceneLogger: SceneLogger, @FalsingCollectorActual private val falsingCollector: FalsingCollector, + private val falsingManager: FalsingManager, private val powerInteractor: PowerInteractor, private val simBouncerInteractor: Lazy<SimBouncerInteractor>, private val authenticationInteractor: Lazy<AuthenticationInteractor>, @@ -98,6 +103,7 @@ constructor( automaticallySwitchScenes() hydrateSystemUiState() collectFalsingSignals() + respondToFalsingDetections() hydrateWindowFocus() hydrateInteractionState() } else { @@ -376,6 +382,18 @@ constructor( } } + /** Switches to the lockscreen when falsing is detected. */ + private fun respondToFalsingDetections() { + applicationScope.launch { + conflatedCallbackFlow { + val listener = FalsingBeliefListener { trySend(Unit) } + falsingManager.addFalsingBeliefListener(listener) + awaitClose { falsingManager.removeFalsingBeliefListener(listener) } + } + .collect { switchToScene(SceneKey.Lockscreen, "Falsing detected.") } + } + } + /** Keeps the focus state of the window view up-to-date. */ private fun hydrateWindowFocus() { applicationScope.launch { diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt index 45b6f65d5d67..ee76c0582b9d 100644 --- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt +++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt @@ -16,11 +16,9 @@ package com.android.systemui.scene.ui.view -import android.view.Gravity import android.view.View import android.view.ViewGroup import android.view.WindowInsets -import android.widget.FrameLayout import androidx.activity.OnBackPressedDispatcher import androidx.activity.OnBackPressedDispatcherOwner import androidx.activity.setViewTreeOnBackPressedDispatcherOwner @@ -39,7 +37,6 @@ import com.android.systemui.scene.shared.model.SceneKey import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel import com.android.systemui.statusbar.notification.stack.shared.flexiNotifsEnabled import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer -import java.time.Instant import kotlinx.coroutines.flow.StateFlow import kotlinx.coroutines.launch @@ -98,7 +95,7 @@ object SceneWindowRootViewBinder { ) val legacyView = view.requireViewById<View>(R.id.legacy_window_root) - view.addView(createVisibilityToggleView(legacyView)) + legacyView.isVisible = false // This moves the SharedNotificationContainer to the WindowRootView just after // the SceneContainerView. This SharedNotificationContainer should contain NSSL @@ -123,29 +120,4 @@ object SceneWindowRootViewBinder { } } } - - private var clickCount = 0 - private var lastClick = Instant.now() - - /** - * A temporary UI to toggle on/off the visibility of the given [otherView]. It is toggled by - * tapping 5 times in quick succession on the device camera (top center). - */ - // TODO(b/291321285): Remove this when the Flexiglass UI is mature enough to turn off legacy - // SysUI altogether. - private fun createVisibilityToggleView(otherView: View): View { - val toggleView = View(otherView.context) - otherView.isVisible = false - toggleView.layoutParams = FrameLayout.LayoutParams(200, 200, Gravity.CENTER_HORIZONTAL) - toggleView.setOnClickListener { - val now = Instant.now() - clickCount = if (now.minusSeconds(2) > lastClick) 1 else clickCount + 1 - if (clickCount == 5) { - otherView.isVisible = !otherView.isVisible - clickCount = 0 - } - lastClick = now - } - return toggleView - } } diff --git a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java index 7068f5fcc32b..d5bbaa5be53c 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java +++ b/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java @@ -1539,6 +1539,7 @@ public final class NotificationPanelViewController implements ShadeSurface, Dump @Override public void setOpenCloseListener(OpenCloseListener openCloseListener) { + SceneContainerFlag.assertInLegacyMode(); mOpenCloseListener = openCloseListener; } diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java index ea419127d7c1..e6555f2e0993 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java +++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeControllerImpl.java @@ -32,6 +32,7 @@ import com.android.systemui.log.LogBuffer; import com.android.systemui.log.dagger.ShadeTouchLog; import com.android.systemui.plugins.statusbar.StatusBarStateController; import com.android.systemui.scene.domain.interactor.WindowRootViewVisibilityInteractor; +import com.android.systemui.scene.shared.flag.SceneContainerFlag; import com.android.systemui.statusbar.CommandQueue; import com.android.systemui.statusbar.NotificationShadeWindowController; import com.android.systemui.statusbar.StatusBarState; @@ -98,6 +99,7 @@ public final class ShadeControllerImpl extends BaseShadeControllerImpl { statusBarKeyguardViewManager, notificationShadeWindowController, assistManagerLazy); + SceneContainerFlag.assertInLegacyMode(); mCommandQueue = commandQueue; mMainExecutor = mainExecutor; mWindowRootViewVisibilityInteractor = windowRootViewVisibilityInteractor; diff --git a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt index 971507055873..84afbed51faa 100644 --- a/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt +++ b/packages/SystemUI/src/com/android/systemui/shade/transition/ShadeTransitionController.kt @@ -19,8 +19,11 @@ package com.android.systemui.shade.transition import android.content.Context import android.content.res.Configuration import com.android.systemui.dagger.SysUISingleton +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dump.DumpManager import com.android.systemui.plugins.qs.QS +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.shared.flag.SceneContainerFlag import com.android.systemui.shade.PanelState import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionStateManager @@ -31,21 +34,26 @@ import com.android.systemui.statusbar.SysuiStatusBarStateController import com.android.systemui.statusbar.notification.stack.NotificationStackScrollLayoutController import com.android.systemui.statusbar.policy.ConfigurationController import com.android.systemui.statusbar.policy.SplitShadeStateController +import dagger.Lazy import java.io.PrintWriter import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.launch /** Controls the shade expansion transition on non-lockscreen. */ @SysUISingleton class ShadeTransitionController @Inject constructor( + @Application private val applicationScope: CoroutineScope, configurationController: ConfigurationController, shadeExpansionStateManager: ShadeExpansionStateManager, dumpManager: DumpManager, private val context: Context, private val scrimShadeTransitionController: ScrimShadeTransitionController, private val statusBarStateController: SysuiStatusBarStateController, - private val splitShadeStateController: SplitShadeStateController + private val splitShadeStateController: SplitShadeStateController, + private val panelExpansionInteractor: Lazy<PanelExpansionInteractor>, ) { lateinit var shadeViewController: ShadeViewController @@ -63,11 +71,27 @@ constructor( override fun onConfigChanged(newConfig: Configuration?) { updateResources() } - }) - val currentState = - shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) - onPanelExpansionChanged(currentState) - shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) + } + ) + if (SceneContainerFlag.isEnabled) { + applicationScope.launch { + panelExpansionInteractor.get().legacyPanelExpansion.collect { panelExpansion -> + onPanelExpansionChanged( + ShadeExpansionChangeEvent( + fraction = panelExpansion, + expanded = panelExpansion > 0f, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + } + } + } else { + val currentState = + shadeExpansionStateManager.addExpansionListener(this::onPanelExpansionChanged) + onPanelExpansionChanged(currentState) + shadeExpansionStateManager.addStateListener(this::onPanelStateChanged) + } dumpManager.registerCriticalDumpable("ShadeTransitionController") { printWriter, _ -> dump(printWriter) } @@ -98,7 +122,9 @@ constructor( qs.isInitialized: ${this::qs.isInitialized} npvc.isInitialized: ${this::shadeViewController.isInitialized} nssl.isInitialized: ${this::notificationStackScrollLayoutController.isInitialized} - """.trimIndent()) + """ + .trimIndent() + ) } private fun isScreenUnlocked() = diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt index 599600d61976..0fd05550dbe3 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt +++ b/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt @@ -39,7 +39,6 @@ import android.view.ViewGroup import com.android.keyguard.KeyguardUpdateMonitor import com.android.settingslib.Utils import com.android.systemui.Dumpable -import com.android.systemui.res.R import com.android.systemui.dagger.SysUISingleton import com.android.systemui.dagger.qualifiers.Background import com.android.systemui.dagger.qualifiers.Main @@ -54,6 +53,7 @@ import com.android.systemui.plugins.BcSmartspaceDataPlugin.SmartspaceView import com.android.systemui.plugins.FalsingManager import com.android.systemui.plugins.clocks.WeatherData import com.android.systemui.plugins.statusbar.StatusBarStateController +import com.android.systemui.res.R import com.android.systemui.settings.UserTracker import com.android.systemui.shared.regionsampling.RegionSampler import com.android.systemui.smartspace.dagger.SmartspaceModule.Companion.DATE_SMARTSPACE_DATA_PLUGIN @@ -68,11 +68,14 @@ import com.android.systemui.util.settings.SecureSettings import com.android.systemui.util.time.SystemClock import java.io.PrintWriter import java.time.Instant +import java.util.Deque +import java.util.LinkedList import java.util.Optional import java.util.concurrent.Executor import javax.inject.Inject import javax.inject.Named + /** Controller for managing the smartspace view on the lockscreen */ @SysUISingleton class LockscreenSmartspaceController @@ -106,6 +109,8 @@ constructor( ) : Dumpable { companion object { private const val TAG = "LockscreenSmartspaceController" + + private const val MAX_RECENT_SMARTSPACE_DATA_FOR_DUMP = 5 } private var session: SmartspaceSession? = null @@ -114,6 +119,9 @@ constructor( private val plugin: BcSmartspaceDataPlugin? = optionalPlugin.orElse(null) private val configPlugin: BcSmartspaceConfigPlugin? = optionalConfigPlugin.orElse(null) + // This stores recently received Smartspace pushes to be included in dumpsys. + private val recentSmartspaceData: Deque<List<SmartspaceTarget>> = LinkedList() + // Smartspace can be used on multiple displays, such as when the user casts their screen private var smartspaceViews = mutableSetOf<SmartspaceView>() private var regionSamplers = @@ -173,6 +181,7 @@ constructor( // The weather data plugin takes unfiltered targets and performs the filtering internally. weatherPlugin?.onTargetsAvailable(targets) + val now = Instant.ofEpochMilli(systemClock.currentTimeMillis()) val weatherTarget = targets.find { t -> t.featureType == SmartspaceTarget.FEATURE_WEATHER && @@ -201,6 +210,14 @@ constructor( } val filteredTargets = targets.filter(::filterSmartspaceTarget) + + synchronized(recentSmartspaceData) { + recentSmartspaceData.offerLast(filteredTargets) + if (recentSmartspaceData.size > MAX_RECENT_SMARTSPACE_DATA_FOR_DUMP) { + recentSmartspaceData.pollFirst() + } + } + plugin?.onTargetsAvailable(filteredTargets) } @@ -588,9 +605,26 @@ constructor( return null } - override fun dump(pw: PrintWriter, args: Array<out String>) = pw.asIndenting().run { - printCollection("Region Samplers", regionSamplers.values) { - it.dump(this) + override fun dump(pw: PrintWriter, args: Array<out String>) { + pw.asIndenting().run { + printCollection("Region Samplers", regionSamplers.values) { + it.dump(this) + } + } + + pw.println("Recent BC Smartspace Targets (most recent first)") + synchronized(recentSmartspaceData) { + if (recentSmartspaceData.size === 0) { + pw.println(" No data\n") + return + } + recentSmartspaceData.descendingIterator().forEachRemaining { smartspaceTargets -> + pw.println(" Number of targets: ${smartspaceTargets.size}") + for (target in smartspaceTargets) { + pw.println(" $target") + } + pw.println() + } } } } diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java index 510086d4892b..dc9eeb35565a 100644 --- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java +++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/interruption/NotificationInterruptStateProviderImpl.java @@ -191,11 +191,11 @@ public class NotificationInterruptStateProviderImpl implements NotificationInter public boolean shouldBubbleUp(NotificationEntry entry) { final StatusBarNotification sbn = entry.getSbn(); - if (!canAlertCommon(entry, true)) { + if (!canAlertCommon(entry, false)) { return false; } - if (!canAlertAwakeCommon(entry, true)) { + if (!canAlertAwakeCommon(entry, false)) { return false; } diff --git a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt index 18a9161ac0e3..593b90aa3c68 100644 --- a/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt +++ b/packages/SystemUI/src/com/android/systemui/volume/dagger/SpatializerModule.kt @@ -21,15 +21,19 @@ import android.media.Spatializer import com.android.settingslib.media.data.repository.SpatializerRepository import com.android.settingslib.media.data.repository.SpatializerRepositoryImpl import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.dagger.qualifiers.Application import com.android.systemui.dagger.qualifiers.Background import dagger.Module import dagger.Provides import kotlin.coroutines.CoroutineContext +import kotlinx.coroutines.CoroutineScope /** Spatializer module. */ @Module interface SpatializerModule { + companion object { + @Provides fun provideSpatializer( audioManager: AudioManager, @@ -38,8 +42,9 @@ interface SpatializerModule { @Provides fun provdieSpatializerRepository( spatializer: Spatializer, + @Application scope: CoroutineScope, @Background backgroundContext: CoroutineContext, - ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, backgroundContext) + ): SpatializerRepository = SpatializerRepositoryImpl(spatializer, scope, backgroundContext) @Provides fun provideSpatializerInetractor(repository: SpatializerRepository): SpatializerInteractor = diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.kt new file mode 100644 index 000000000000..71bce5e470f4 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/SpatialAudioAvailabilityCriteria.kt @@ -0,0 +1,35 @@ +/* + * 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.spatial.domain + +import com.android.systemui.volume.panel.component.spatial.domain.interactor.SpatialAudioComponentInteractor +import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria +import javax.inject.Inject +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.map + +@VolumePanelScope +class SpatialAudioAvailabilityCriteria +@Inject +constructor(private val interactor: SpatialAudioComponentInteractor) : + ComponentAvailabilityCriteria { + + override fun isAvailable(): Flow<Boolean> = + interactor.isAvailable.map { it is SpatialAudioAvailabilityModel.SpatialAudio } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt new file mode 100644 index 000000000000..4358611694b2 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/interactor/SpatialAudioComponentInteractor.kt @@ -0,0 +1,170 @@ +/* + * 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.spatial.domain.interactor + +import android.media.AudioDeviceAttributes +import android.media.AudioDeviceInfo +import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.media.BluetoothMediaDevice +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.volume.panel.component.mediaoutput.domain.interactor.MediaOutputInteractor +import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioAvailabilityModel +import com.android.systemui.volume.panel.component.spatial.domain.model.SpatialAudioEnabledModel +import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope +import javax.inject.Inject +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.combine +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.onStart +import kotlinx.coroutines.flow.stateIn + +/** + * Provides an ability to access and update spatial audio and head tracking state. + * + * Head tracking is a sub-feature of spatial audio. This means that it requires spatial audio to be + * available for it to be available. And spatial audio to be enabled for it to be enabled. + */ +@VolumePanelScope +class SpatialAudioComponentInteractor +@Inject +constructor( + mediaOutputInteractor: MediaOutputInteractor, + private val spatializerInteractor: SpatializerInteractor, + @VolumePanelScope private val coroutineScope: CoroutineScope, +) { + + private val changes = MutableSharedFlow<Unit>() + private val currentAudioDeviceAttributes: StateFlow<AudioDeviceAttributes?> = + mediaOutputInteractor.currentConnectedDevice + .map { mediaDevice -> + mediaDevice ?: return@map null + val btDevice: CachedBluetoothDevice = + (mediaDevice as? BluetoothMediaDevice)?.cachedDevice ?: return@map null + btDevice.getAudioDeviceAttributes() + } + .stateIn(coroutineScope, SharingStarted.WhileSubscribed(), null) + + /** + * Returns spatial audio availability model. It can be: + * - unavailable + * - only spatial audio is available + * - spatial audio and head tracking are available + */ + val isAvailable: StateFlow<SpatialAudioAvailabilityModel> = + combine( + currentAudioDeviceAttributes, + changes.onStart { emit(Unit) }, + spatializerInteractor.isHeadTrackingAvailable, + ) { attributes, _, isHeadTrackingAvailable -> + attributes ?: return@combine SpatialAudioAvailabilityModel.Unavailable + if (isHeadTrackingAvailable) { + return@combine SpatialAudioAvailabilityModel.HeadTracking + } + if (spatializerInteractor.isSpatialAudioAvailable(attributes)) { + return@combine SpatialAudioAvailabilityModel.SpatialAudio + } + SpatialAudioAvailabilityModel.Unavailable + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + SpatialAudioAvailabilityModel.Unavailable, + ) + + /** + * Returns spatial audio enabled/disabled model. It can be + * - disabled + * - only spatial audio is enabled + * - spatial audio and head tracking are enabled + */ + val isEnabled: StateFlow<SpatialAudioEnabledModel> = + combine( + changes.onStart { emit(Unit) }, + currentAudioDeviceAttributes, + isAvailable, + ) { _, attributes, isAvailable -> + if (isAvailable is SpatialAudioAvailabilityModel.Unavailable) { + return@combine SpatialAudioEnabledModel.Disabled + } + attributes ?: return@combine SpatialAudioEnabledModel.Disabled + if (spatializerInteractor.isHeadTrackingEnabled(attributes)) { + return@combine SpatialAudioEnabledModel.HeadTrackingEnabled + } + if (spatializerInteractor.isSpatialAudioEnabled(attributes)) { + return@combine SpatialAudioEnabledModel.SpatialAudioEnabled + } + SpatialAudioEnabledModel.Disabled + } + .stateIn( + coroutineScope, + SharingStarted.Eagerly, + SpatialAudioEnabledModel.Disabled, + ) + + /** + * Sets current [isEnabled] to a specific [SpatialAudioEnabledModel]. It + * - disables both spatial audio and head tracking + * - enables only spatial audio + * - enables both spatial audio and head tracking + */ + suspend fun setEnabled(model: SpatialAudioEnabledModel) { + val attributes = currentAudioDeviceAttributes.value ?: return + spatializerInteractor.setSpatialAudioEnabled( + attributes, + model is SpatialAudioEnabledModel.SpatialAudioEnabled, + ) + spatializerInteractor.setHeadTrackingEnabled( + attributes, + model is SpatialAudioEnabledModel.HeadTrackingEnabled, + ) + changes.emit(Unit) + } + + private suspend fun CachedBluetoothDevice.getAudioDeviceAttributes(): AudioDeviceAttributes? { + return listOf( + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_HEADSET, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_SPEAKER, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLE_BROADCAST, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_BLUETOOTH_A2DP, + address + ), + AudioDeviceAttributes( + AudioDeviceAttributes.ROLE_OUTPUT, + AudioDeviceInfo.TYPE_HEARING_AID, + address + ) + ) + .firstOrNull { spatializerInteractor.isSpatialAudioAvailable(it) } + } +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.kt new file mode 100644 index 000000000000..cf1454618367 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioAvailabilityModel.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.volume.panel.component.spatial.domain.model + +/** Models spatial audio and head tracking availability. */ +interface SpatialAudioAvailabilityModel { + + /** Spatial audio is unavailable. */ + data object Unavailable : SpatialAudioAvailabilityModel + + /** Spatial audio is available. */ + interface SpatialAudio : SpatialAudioAvailabilityModel { + companion object : SpatialAudio + } + + /** Head tracking is available. This also means that [SpatialAudio] is available. */ + data object HeadTracking : SpatialAudio +} diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.kt new file mode 100644 index 000000000000..4e65f60aa0e1 --- /dev/null +++ b/packages/SystemUI/src/com/android/systemui/volume/panel/component/spatial/domain/model/SpatialAudioEnabledModel.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.volume.panel.component.spatial.domain.model + +/** Models spatial audio and head tracking enabled/disabled state. */ +interface SpatialAudioEnabledModel { + + /** Spatial audio is disabled. */ + data object Disabled : SpatialAudioEnabledModel + + /** Spatial audio is enabled. */ + interface SpatialAudioEnabled : SpatialAudioEnabledModel { + companion object : SpatialAudioEnabled + } + + /** Head tracking is enabled. This also means that [SpatialAudioEnabled]. */ + data object HeadTrackingEnabled : SpatialAudioEnabled +} diff --git a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java index 9d9b263c5df5..2d3ca6095835 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/globalactions/ShutdownUiTest.java @@ -19,7 +19,13 @@ package com.android.systemui.globalactions; import static junit.framework.Assert.assertEquals; import static junit.framework.Assert.assertNull; +import static org.mockito.Mockito.when; + +import android.nearby.NearbyManager; +import android.net.platform.flags.Flags; import android.os.PowerManager; +import android.platform.test.annotations.DisableFlags; +import android.platform.test.annotations.EnableFlags; import android.testing.AndroidTestingRunner; import androidx.test.filters.SmallTest; @@ -32,6 +38,7 @@ import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; +import org.mockito.MockitoAnnotations; @SmallTest @@ -41,10 +48,13 @@ public class ShutdownUiTest extends SysuiTestCase { ShutdownUi mShutdownUi; @Mock BlurUtils mBlurUtils; + @Mock + NearbyManager mNearbyManager; @Before public void setUp() throws Exception { - mShutdownUi = new ShutdownUi(getContext(), mBlurUtils); + MockitoAnnotations.initMocks(this); + mShutdownUi = new ShutdownUi(getContext(), mBlurUtils, mNearbyManager); } @Test @@ -82,4 +92,53 @@ public class ShutdownUiTest extends SysuiTestCase { String message = mShutdownUi.getReasonMessage("anything-else"); assertNull(message); } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabled_returnsFinderDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = com.android.systemui.res.R.layout.shutdown_dialog_finder_active; + assertEquals(actualLayout, expectedLayout); + } + + @DisableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabledFlagDisabled_returnsFinderDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeDisabled_returnsDefaultDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_DISABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(false); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + + @EnableFlags(Flags.FLAG_POWERED_OFF_FINDING_PLATFORM) + @Test + public void getDialog_whenPowerOffFindingModeEnabledAndIsReboot_returnsDefaultDialog() { + when(mNearbyManager.getPoweredOffFindingMode()).thenReturn( + NearbyManager.POWERED_OFF_FINDING_MODE_ENABLED); + + int actualLayout = mShutdownUi.getShutdownDialogContent(true); + + int expectedLayout = R.layout.shutdown_dialog; + assertEquals(actualLayout, expectedLayout); + } + } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt index 92c73261a95e..a8cd8c801a95 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemFactoryTest.kt @@ -16,10 +16,18 @@ package com.android.systemui.qs.tiles.dialog.bluetooth +import android.bluetooth.BluetoothDevice +import android.content.pm.PackageInfo +import android.content.pm.PackageManager +import android.media.AudioManager +import android.platform.test.annotations.DisableFlags +import android.platform.test.annotations.EnableFlags import android.testing.AndroidTestingRunner import android.testing.TestableLooper import androidx.test.filters.SmallTest +import com.android.settingslib.bluetooth.BluetoothUtils import com.android.settingslib.bluetooth.CachedBluetoothDevice +import com.android.settingslib.flags.Flags import com.android.systemui.SysuiTestCase import com.google.common.truth.Truth.assertThat import org.junit.Before @@ -35,19 +43,26 @@ import org.mockito.junit.MockitoRule @RunWith(AndroidTestingRunner::class) @TestableLooper.RunWithLooper(setAsMainLooper = true) class DeviceItemFactoryTest : SysuiTestCase() { - @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule() @Mock private lateinit var cachedDevice: CachedBluetoothDevice + @Mock private lateinit var bluetoothDevice: BluetoothDevice + @Mock private lateinit var packageManager: PackageManager private val availableMediaDeviceItemFactory = AvailableMediaDeviceItemFactory() private val connectedDeviceItemFactory = ConnectedDeviceItemFactory() private val savedDeviceItemFactory = SavedDeviceItemFactory() + private val audioManager = context.getSystemService(AudioManager::class.java)!! + @Before fun setup() { `when`(cachedDevice.name).thenReturn(DEVICE_NAME) + `when`(cachedDevice.address).thenReturn(DEVICE_ADDRESS) + `when`(cachedDevice.device).thenReturn(bluetoothDevice) `when`(cachedDevice.connectionSummary).thenReturn(CONNECTION_SUMMARY) + + context.setMockPackageManager(packageManager) } @Test @@ -72,6 +87,225 @@ class DeviceItemFactoryTest : SysuiTestCase() { assertThat(deviceItem.background).isNotNull() } + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_bondedAndNotConnected_returnsTrue() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_connected_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(true) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notBonded_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo()) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray()) + `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0)) + .thenReturn(PackageInfo()) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)) + .thenThrow(PackageManager.NameNotFoundException("Test!")) + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(cachedDevice.isConnected).thenReturn(false) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testSavedFactory_isFilterMatched_notExclusivelyManaged_connected_returnsFalse() { + `when`(cachedDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(cachedDevice.isConnected).thenReturn(true) + + assertThat(savedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_bondedAndConnected_returnsTrue() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notConnected_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(false) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @DisableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notBonded_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_exclusivelyManaged_returnsFalse() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)).thenReturn(PackageInfo()) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_noExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notAllowedExclusiveManager_returnsTrue() { + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(FAKE_EXCLUSIVE_MANAGER_NAME.toByteArray()) + `when`(packageManager.getPackageInfo(FAKE_EXCLUSIVE_MANAGER_NAME, 0)) + .thenReturn(PackageInfo()) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_uninstalledExclusiveManager_returnsTrue() { + val exclusiveManagerName = + BluetoothUtils.getExclusiveManagers().firstOrNull() ?: FAKE_EXCLUSIVE_MANAGER_NAME + `when`(bluetoothDevice.getMetadata(BluetoothDevice.METADATA_EXCLUSIVE_MANAGER)) + .thenReturn(exclusiveManagerName.toByteArray()) + `when`(packageManager.getPackageInfo(exclusiveManagerName, 0)) + .thenThrow(PackageManager.NameNotFoundException("Test!")) + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isTrue() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notBonded_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_NONE) + `when`(bluetoothDevice.isConnected).thenReturn(true) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + + @Test + @EnableFlags(Flags.FLAG_ENABLE_HIDE_EXCLUSIVELY_MANAGED_BLUETOOTH_DEVICE) + fun testConnectedFactory_isFilterMatched_notExclusivelyManaged_notConnected_returnsFalse() { + `when`(bluetoothDevice.bondState).thenReturn(BluetoothDevice.BOND_BONDED) + `when`(bluetoothDevice.isConnected).thenReturn(false) + audioManager.setMode(AudioManager.MODE_NORMAL) + + assertThat(connectedDeviceItemFactory.isFilterMatched(context, cachedDevice, audioManager)) + .isFalse() + } + private fun assertDeviceItem(deviceItem: DeviceItem?, deviceItemType: DeviceItemType) { assertThat(deviceItem).isNotNull() assertThat(deviceItem!!.type).isEqualTo(deviceItemType) @@ -83,5 +317,7 @@ class DeviceItemFactoryTest : SysuiTestCase() { companion object { const val DEVICE_NAME = "DeviceName" const val CONNECTION_SUMMARY = "ConnectionSummary" + private const val FAKE_EXCLUSIVE_MANAGER_NAME = "com.fake.name" + private const val DEVICE_ADDRESS = "04:52:C7:0B:D8:3C" } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt index e236f4a7730f..ddf0b9a78165 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/dialog/bluetooth/DeviceItemInteractorTest.kt @@ -279,6 +279,7 @@ class DeviceItemInteractorTest : SysuiTestCase() { ): DeviceItemFactory { return object : DeviceItemFactory() { override fun isFilterMatched( + context: Context, cachedDevice: CachedBluetoothDevice, audioManager: AudioManager? ) = isFilterMatchFunc(cachedDevice) diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt index 7737b4313e3c..651006dfc953 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt +++ b/packages/SystemUI/tests/src/com/android/systemui/shade/transition/ShadeTransitionControllerTest.kt @@ -1,15 +1,46 @@ +@file:OptIn(ExperimentalCoroutinesApi::class) + package com.android.systemui.shade.transition +import android.platform.test.annotations.DisableFlags import android.testing.AndroidTestingRunner import androidx.test.filters.SmallTest +import com.android.systemui.Flags.FLAG_SCENE_CONTAINER import com.android.systemui.SysuiTestCase +import com.android.systemui.coroutines.collectLastValue +import com.android.systemui.deviceentry.data.repository.FakeDeviceEntryRepository +import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository +import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor +import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor import com.android.systemui.dump.DumpManager +import com.android.systemui.flags.EnableSceneContainer +import com.android.systemui.kosmos.applicationCoroutineScope +import com.android.systemui.kosmos.testScope +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.domain.interactor.SceneInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.scene.shared.model.FakeSceneDataSource +import com.android.systemui.scene.shared.model.ObservableTransitionState +import com.android.systemui.scene.shared.model.SceneKey +import com.android.systemui.scene.shared.model.fakeSceneDataSource import com.android.systemui.shade.STATE_OPENING import com.android.systemui.shade.ShadeExpansionChangeEvent import com.android.systemui.shade.ShadeExpansionStateManager import com.android.systemui.statusbar.SysuiStatusBarStateController +import com.android.systemui.statusbar.notification.stack.ui.viewmodel.panelExpansionInteractor import com.android.systemui.statusbar.policy.FakeConfigurationController import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController +import com.android.systemui.testKosmos +import com.android.systemui.util.mockito.any +import com.android.systemui.util.mockito.whenever +import com.google.common.truth.Truth.assertThat +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.ExperimentalCoroutinesApi +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.flowOf +import kotlinx.coroutines.test.TestScope +import kotlinx.coroutines.test.runCurrent +import kotlinx.coroutines.test.runTest import org.junit.Before import org.junit.Test import org.junit.runner.RunWith @@ -29,32 +60,95 @@ class ShadeTransitionControllerTest : SysuiTestCase() { private val configurationController = FakeConfigurationController() private val shadeExpansionStateManager = ShadeExpansionStateManager() + private val kosmos = testKosmos() + private lateinit var testScope: TestScope + private lateinit var applicationScope: CoroutineScope + private lateinit var panelExpansionInteractor: PanelExpansionInteractor + private lateinit var deviceEntryRepository: FakeDeviceEntryRepository + private lateinit var deviceUnlockedInteractor: DeviceUnlockedInteractor + private lateinit var sceneInteractor: SceneInteractor + private lateinit var fakeSceneDataSource: FakeSceneDataSource @Before fun setUp() { MockitoAnnotations.initMocks(this) + testScope = kosmos.testScope + applicationScope = kosmos.applicationCoroutineScope + panelExpansionInteractor = kosmos.panelExpansionInteractor + deviceEntryRepository = kosmos.fakeDeviceEntryRepository + deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor + sceneInteractor = kosmos.sceneInteractor + fakeSceneDataSource = kosmos.fakeSceneDataSource + controller = ShadeTransitionController( + applicationScope, configurationController, shadeExpansionStateManager, dumpManager, context, scrimShadeTransitionController, statusBarStateController, - ResourcesSplitShadeStateController() - ) + ResourcesSplitShadeStateController(), + ) { + panelExpansionInteractor + } } @Test + @DisableFlags(FLAG_SCENE_CONTAINER) fun onPanelStateChanged_forwardsToScrimTransitionController() { - startPanelExpansion() + startLegacyPanelExpansion() verify(scrimShadeTransitionController).onPanelStateChanged(STATE_OPENING) verify(scrimShadeTransitionController).onPanelExpansionChanged(DEFAULT_EXPANSION_EVENT) } - private fun startPanelExpansion() { + @Test + @EnableSceneContainer + fun sceneChanges_forwardsToScrimTransitionController() = + testScope.runTest { + var latestChangeEvent: ShadeExpansionChangeEvent? = null + whenever(scrimShadeTransitionController.onPanelExpansionChanged(any())).thenAnswer { + latestChangeEvent = it.arguments[0] as ShadeExpansionChangeEvent + Unit + } + setUnlocked(true) + val transitionState = + MutableStateFlow<ObservableTransitionState>( + ObservableTransitionState.Idle(SceneKey.Gone) + ) + sceneInteractor.setTransitionState(transitionState) + + changeScene(SceneKey.Gone, transitionState) + val currentScene by collectLastValue(sceneInteractor.currentScene) + assertThat(currentScene).isEqualTo(SceneKey.Gone) + + assertThat(latestChangeEvent) + .isEqualTo( + ShadeExpansionChangeEvent( + fraction = 0f, + expanded = false, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + + changeScene(SceneKey.Shade, transitionState) { progress -> + assertThat(latestChangeEvent) + .isEqualTo( + ShadeExpansionChangeEvent( + fraction = progress, + expanded = progress > 0, + tracking = true, + dragDownPxAmount = 0f, + ) + ) + } + } + + private fun startLegacyPanelExpansion() { shadeExpansionStateManager.onPanelExpansionChanged( DEFAULT_EXPANSION_EVENT.fraction, DEFAULT_EXPANSION_EVENT.expanded, @@ -63,6 +157,52 @@ class ShadeTransitionControllerTest : SysuiTestCase() { ) } + private fun TestScope.setUnlocked(isUnlocked: Boolean) { + val isDeviceUnlocked by collectLastValue(deviceUnlockedInteractor.isDeviceUnlocked) + deviceEntryRepository.setUnlocked(isUnlocked) + runCurrent() + + assertThat(isDeviceUnlocked).isEqualTo(isUnlocked) + } + + private fun TestScope.changeScene( + toScene: SceneKey, + transitionState: MutableStateFlow<ObservableTransitionState>, + assertDuringProgress: ((progress: Float) -> Unit) = {}, + ) { + val currentScene by collectLastValue(sceneInteractor.currentScene) + val progressFlow = MutableStateFlow(0f) + transitionState.value = + ObservableTransitionState.Transition( + fromScene = checkNotNull(currentScene), + toScene = toScene, + progress = progressFlow, + isInitiatedByUserInput = true, + isUserInputOngoing = flowOf(true), + ) + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.2f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 0.6f + runCurrent() + assertDuringProgress(progressFlow.value) + + progressFlow.value = 1f + runCurrent() + assertDuringProgress(progressFlow.value) + + transitionState.value = ObservableTransitionState.Idle(toScene) + fakeSceneDataSource.changeScene(toScene) + runCurrent() + assertDuringProgress(progressFlow.value) + + assertThat(currentScene).isEqualTo(toScene) + } + companion object { private const val DEFAULT_DRAG_DOWN_AMOUNT = 123f private val DEFAULT_EXPANSION_EVENT = @@ -70,6 +210,7 @@ class ShadeTransitionControllerTest : SysuiTestCase() { fraction = 0.5f, expanded = true, tracking = true, - dragDownPxAmount = DEFAULT_DRAG_DOWN_AMOUNT) + dragDownPxAmount = DEFAULT_DRAG_DOWN_AMOUNT + ) } } diff --git a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java index d2e03861b022..3a6324d3de53 100644 --- a/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java +++ b/packages/SystemUI/tests/src/com/android/systemui/volume/VolumeDialogImplTest.java @@ -61,7 +61,6 @@ import android.widget.ImageButton; import android.widget.SeekBar; import androidx.test.core.view.MotionEventBuilder; -import androidx.test.filters.FlakyTest; import androidx.test.filters.SmallTest; import com.android.internal.jank.InteractionJankMonitor; @@ -502,7 +501,6 @@ public class VolumeDialogImplTest extends SysuiTestCase { } @Test - @FlakyTest(bugId = 326204750) public void dialogDestroy_removesPostureControllerCallback() { verify(mPostureController, never()).removeCallback(any()); mDialog.destroy(); diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java index 5038285aef00..974a11cfaf77 100644 --- a/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/classifier/FalsingManagerFake.java @@ -201,4 +201,13 @@ public class FalsingManagerFake implements FalsingManager { public List<FalsingTapListener> getTapListeners() { return mTapListeners; } + + /** + * Calls every registered {@link FalsingBeliefListener} as if false touch occurred. + */ + public void sendFalsingBelief() { + for (FalsingBeliefListener listener : mFalsingBeliefListeners) { + listener.onFalse(); + } + } } diff --git a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt index b7285da49bb7..7001ea8b6427 100644 --- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt @@ -1,5 +1,5 @@ /* - * Copyright (C) 2023 The Android Open Source Project + * 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. @@ -13,19 +13,12 @@ * See the License for the specific language governing permissions and * limitations under the License. */ -package com.android.systemui.globalactions -import android.content.Context -import com.android.systemui.statusbar.BlurUtils -import dagger.Module -import dagger.Provides +package com.android.systemui.media -/** Provides the UI shown during system shutdown. */ -@Module -class ShutdownUiModule { - /** Shutdown UI provider. */ - @Provides - fun provideShutdownUi(context: Context?, blurUtils: BlurUtils?): ShutdownUi { - return ShutdownUi(context, blurUtils) - } -} +import com.android.settingslib.media.domain.interactor.SpatializerInteractor +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.media.data.repository.FakeSpatializerRepository + +val Kosmos.spatializerRepository by Kosmos.Fixture { FakeSpatializerRepository() } +val Kosmos.spatializerInteractor by Kosmos.Fixture { SpatializerInteractor(spatializerRepository) } diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt new file mode 100644 index 000000000000..0183b97090dc --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/data/repository/FakeSpatializerRepository.kt @@ -0,0 +1,83 @@ +/* + * 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.media.data.repository + +import android.media.AudioDeviceAttributes +import com.android.settingslib.media.data.repository.SpatializerRepository +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.asStateFlow + +class FakeSpatializerRepository : SpatializerRepository { + + var defaultSpatialAudioAvailable: Boolean = false + + private val spatialAudioAvailabilityByDevice: MutableMap<AudioDeviceAttributes, Boolean> = + mutableMapOf() + private val spatialAudioCompatibleDevices: MutableList<AudioDeviceAttributes> = mutableListOf() + + private val mutableHeadTrackingAvailable = MutableStateFlow(false) + private val headTrackingEnabledByDevice = mutableMapOf<AudioDeviceAttributes, Boolean>() + + override val isHeadTrackingAvailable: StateFlow<Boolean> = + mutableHeadTrackingAvailable.asStateFlow() + + override suspend fun isSpatialAudioAvailableForDevice( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = + spatialAudioAvailabilityByDevice.getOrDefault( + audioDeviceAttributes, + defaultSpatialAudioAvailable + ) + + override suspend fun getSpatialAudioCompatibleDevices(): Collection<AudioDeviceAttributes> = + spatialAudioCompatibleDevices + + override suspend fun addSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { + spatialAudioCompatibleDevices.add(audioDeviceAttributes) + } + + override suspend fun removeSpatialAudioCompatibleDevice( + audioDeviceAttributes: AudioDeviceAttributes + ) { + spatialAudioCompatibleDevices.remove(audioDeviceAttributes) + } + + override suspend fun isHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes + ): Boolean = headTrackingEnabledByDevice.getOrDefault(audioDeviceAttributes, false) + + override suspend fun setHeadTrackingEnabled( + audioDeviceAttributes: AudioDeviceAttributes, + isEnabled: Boolean + ) { + headTrackingEnabledByDevice[audioDeviceAttributes] = isEnabled + } + + fun setIsSpatialAudioAvailable( + audioDeviceAttributes: AudioDeviceAttributes, + isAvailable: Boolean, + ) { + spatialAudioAvailabilityByDevice[audioDeviceAttributes] = isAvailable + } + + fun setIsHeadTrackingAvailable(isAvailable: Boolean) { + mutableHeadTrackingAvailable.value = isAvailable + } +} diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt new file mode 100644 index 000000000000..a025846f74a3 --- /dev/null +++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/PanelExpansionInteractorKosmos.kt @@ -0,0 +1,30 @@ +/* + * 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.ui.viewmodel + +import com.android.systemui.kosmos.Kosmos +import com.android.systemui.kosmos.Kosmos.Fixture +import com.android.systemui.scene.domain.interactor.PanelExpansionInteractor +import com.android.systemui.scene.domain.interactor.sceneInteractor +import com.android.systemui.shade.data.repository.shadeRepository + +val Kosmos.panelExpansionInteractor by Fixture { + PanelExpansionInteractor( + sceneInteractor = sceneInteractor, + shadeRepository = shadeRepository, + ) +} diff --git a/ravenwood/framework-minus-apex-ravenwood-policies.txt b/ravenwood/framework-minus-apex-ravenwood-policies.txt index 6b6736476210..371c3acab144 100644 --- a/ravenwood/framework-minus-apex-ravenwood-policies.txt +++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt @@ -55,3 +55,5 @@ class android.content.Context stub method getSystemService (Ljava/lang/Class;)Ljava/lang/Object; stub class android.content.pm.PackageManager stub method <init> ()V stub +class android.text.ClipboardManager stub + method <init> ()V stub diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java index 3668b03e58d3..c17d0903f856 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodContext.java @@ -16,18 +16,28 @@ package android.platform.test.ravenwood; +import android.content.ClipboardManager; import android.content.Context; import android.hardware.ISerialManager; import android.hardware.SerialManager; +import android.os.Handler; +import android.os.HandlerThread; +import android.os.Looper; import android.os.PermissionEnforcer; import android.os.ServiceManager; +import android.os.UserHandle; import android.test.mock.MockContext; import android.util.ArrayMap; import android.util.Singleton; +import java.util.Objects; +import java.util.concurrent.Executor; import java.util.function.Supplier; public class RavenwoodContext extends MockContext { + private final String mPackageName; + private final HandlerThread mMainThread; + private final RavenwoodPermissionEnforcer mEnforcer = new RavenwoodPermissionEnforcer(); private final ArrayMap<Class<?>, String> mClassToName = new ArrayMap<>(); @@ -39,7 +49,13 @@ public class RavenwoodContext extends MockContext { mNameToFactory.put(serviceName, serviceSupplier); } - public RavenwoodContext() { + public RavenwoodContext(String packageName, HandlerThread mainThread) { + mPackageName = packageName; + mMainThread = mainThread; + + registerService(ClipboardManager.class, + Context.CLIPBOARD_SERVICE, asSingleton(() -> + new ClipboardManager(this, getMainThreadHandler()))); registerService(PermissionEnforcer.class, Context.PERMISSION_ENFORCER_SERVICE, () -> mEnforcer); registerService(SerialManager.class, @@ -73,18 +89,79 @@ public class RavenwoodContext extends MockContext { } } + @Override + public Looper getMainLooper() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getLooper(); + } + + @Override + public Handler getMainThreadHandler() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getThreadHandler(); + } + + @Override + public Executor getMainExecutor() { + Objects.requireNonNull(mMainThread, + "Test must request setProvideMainThread() via RavenwoodRule"); + return mMainThread.getThreadExecutor(); + } + + @Override + public String getPackageName() { + return Objects.requireNonNull(mPackageName, + "Test must request setPackageName() via RavenwoodRule"); + } + + @Override + public String getOpPackageName() { + return Objects.requireNonNull(mPackageName, + "Test must request setPackageName() via RavenwoodRule"); + } + + @Override + public String getAttributionTag() { + return null; + } + + @Override + public UserHandle getUser() { + return android.os.UserHandle.of(android.os.UserHandle.myUserId()); + } + + @Override + public int getUserId() { + return android.os.UserHandle.myUserId(); + } + + @Override + public int getDeviceId() { + return Context.DEVICE_ID_DEFAULT; + } + /** * Wrap the given {@link Supplier} to become a memoized singleton. */ - private static <T> Supplier<T> asSingleton(Supplier<T> supplier) { + private static <T> Supplier<T> asSingleton(ThrowingSupplier<T> supplier) { final Singleton<T> singleton = new Singleton<>() { @Override protected T create() { - return supplier.get(); + try { + return supplier.get(); + } catch (Exception e) { + throw new RuntimeException(e); + } } }; return () -> { return singleton.get(); }; } + + public interface ThrowingSupplier<T> { + T get() throws Exception; + } } diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java index 231cce95f353..56a3c64a5750 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodRuleImpl.java @@ -110,13 +110,16 @@ public class RavenwoodRuleImpl { ActivityManager.init$ravenwood(rule.mCurrentUser); + final HandlerThread main; if (rule.mProvideMainThread) { - final HandlerThread main = new HandlerThread(MAIN_THREAD_NAME); + main = new HandlerThread(MAIN_THREAD_NAME); main.start(); Looper.setMainLooperForTest(main.getLooper()); + } else { + main = null; } - rule.mContext = new RavenwoodContext(); + rule.mContext = new RavenwoodContext(rule.mPackageName, main); rule.mInstrumentation = new Instrumentation(); rule.mInstrumentation.basicInit(rule.mContext); InstrumentationRegistry.registerInstance(rule.mInstrumentation, Bundle.EMPTY); diff --git a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java index bb280f47ccd9..3de96c0990ea 100644 --- a/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java +++ b/ravenwood/junit-impl-src/android/platform/test/ravenwood/RavenwoodSystemServer.java @@ -16,6 +16,7 @@ package android.platform.test.ravenwood; +import android.content.ClipboardManager; import android.hardware.SerialManager; import android.os.SystemClock; import android.util.ArrayMap; @@ -40,7 +41,10 @@ public class RavenwoodSystemServer { // authors to exhaustively declare all transitive services static { - sKnownServices.put(SerialManager.class, "com.android.server.SerialService$Lifecycle"); + sKnownServices.put(ClipboardManager.class, + "com.android.server.FakeClipboardService$Lifecycle"); + sKnownServices.put(SerialManager.class, + "com.android.server.SerialService$Lifecycle"); } private static TimingsTraceAndSlog sTimings; diff --git a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java index a8c24fcbd7e0..a520d4ccafa1 100644 --- a/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java +++ b/ravenwood/junit-src/android/platform/test/ravenwood/RavenwoodRule.java @@ -121,6 +121,8 @@ public class RavenwoodRule implements TestRule { int mUid = NOBODY_UID; int mPid = sNextPid.getAndIncrement(); + String mPackageName; + boolean mProvideMainThread = false; final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties(); @@ -158,6 +160,15 @@ public class RavenwoodRule implements TestRule { } /** + * Configure the identity of this process to be the given package name for the duration + * of the test. Has no effect on non-Ravenwood environments. + */ + public Builder setPackageName(/* @NonNull */ String packageName) { + mRule.mPackageName = Objects.requireNonNull(packageName); + return this; + } + + /** * Configure a "main" thread to be available for the duration of the test, as defined * by {@code Looper.getMainLooper()}. Has no effect on non-Ravenwood environments. */ diff --git a/ravenwood/ravenwood-annotation-allowed-classes.txt b/ravenwood/ravenwood-annotation-allowed-classes.txt index eb3c55cb4ff6..9b4d378cc7b7 100644 --- a/ravenwood/ravenwood-annotation-allowed-classes.txt +++ b/ravenwood/ravenwood-annotation-allowed-classes.txt @@ -186,6 +186,7 @@ android.os.WorkSource android.content.ClipData android.content.ClipData$Item android.content.ClipDescription +android.content.ClipboardManager android.content.ComponentName android.content.ContentUris android.content.ContentValues diff --git a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java index 5f12ce1e4163..9f31f375dafe 100644 --- a/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java +++ b/services/core/java/com/android/server/ambientcontext/AmbientContextManagerService.java @@ -73,8 +73,7 @@ public class AmbientContextManagerService extends private static final Set<Integer> DEFAULT_EVENT_SET = Sets.newHashSet( AmbientContextEvent.EVENT_COUGH, AmbientContextEvent.EVENT_SNORE, - AmbientContextEvent.EVENT_BACK_DOUBLE_TAP, - AmbientContextEvent.EVENT_HEART_RATE); + AmbientContextEvent.EVENT_BACK_DOUBLE_TAP); /** Default value in absence of {@link DeviceConfig} override. */ private static final boolean DEFAULT_SERVICE_ENABLED = true; diff --git a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java index 70993ca3e21b..1e90ab279d32 100644 --- a/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java +++ b/services/core/java/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystem.java @@ -317,6 +317,10 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { if ((systemAudioOnPowerOnProp == ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON) || ((systemAudioOnPowerOnProp == USE_LAST_STATE_SYSTEM_AUDIO_CONTROL_ON_POWER_ON) && lastSystemAudioControlStatus && isSystemAudioControlFeatureEnabled())) { + if (hasAction(SystemAudioInitiationActionFromAvr.class)) { + Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting."); + removeAction(SystemAudioInitiationActionFromAvr.class); + } addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); } } @@ -1032,6 +1036,10 @@ public class HdmiCecLocalDeviceAudioSystem extends HdmiCecLocalDeviceSource { void onSystemAudioControlFeatureSupportChanged(boolean enabled) { setSystemAudioControlFeatureEnabled(enabled); if (enabled) { + if (hasAction(SystemAudioInitiationActionFromAvr.class)) { + Slog.i(TAG, "SystemAudioInitiationActionFromAvr is in progress. Restarting."); + removeAction(SystemAudioInitiationActionFromAvr.class); + } addAndStartAction(new SystemAudioInitiationActionFromAvr(this)); } } diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java index 582058d21256..31bfc6954416 100644 --- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java +++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java @@ -340,8 +340,6 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { static final String TAG = NetworkPolicyLogger.TAG; private static final boolean LOGD = NetworkPolicyLogger.LOGD; private static final boolean LOGV = NetworkPolicyLogger.LOGV; - // TODO: b/304347838 - Remove once the feature is in staging. - private static final boolean ALWAYS_RESTRICT_BACKGROUND_NETWORK = false; /** * No opportunistic quota could be calculated from user data plan or data settings. @@ -1070,8 +1068,7 @@ public class NetworkPolicyManagerService extends INetworkPolicyManager.Stub { } // The flag is boot-stable. - mBackgroundNetworkRestricted = ALWAYS_RESTRICT_BACKGROUND_NETWORK - && Flags.networkBlockedForTopSleepingAndAbove(); + mBackgroundNetworkRestricted = Flags.networkBlockedForTopSleepingAndAbove(); if (mBackgroundNetworkRestricted) { // Firewall rules and UidBlockedState will get updated in // updateRulesForGlobalChangeAL below. diff --git a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java index b0dcf95b3f5d..881583ad8c91 100644 --- a/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java +++ b/services/core/java/com/android/server/stats/pull/AggregatedMobileDataStatsPuller.java @@ -282,10 +282,7 @@ class AggregatedMobileDataStatsPuller { Slog.d(TAG, "pullDataBytesTransferLocked() done. results count " + pulledData.size()); } - if (!pulledData.isEmpty()) { - return StatsManager.PULL_SUCCESS; - } - return StatsManager.PULL_SKIP; + return StatsManager.PULL_SUCCESS; } private static boolean isEmpty(NetworkStats stats) { diff --git a/services/core/java/com/android/server/wm/ActivityAssistInfo.java b/services/core/java/com/android/server/wm/ActivityAssistInfo.java index 3b91780431cb..2dc00460eeb9 100644 --- a/services/core/java/com/android/server/wm/ActivityAssistInfo.java +++ b/services/core/java/com/android/server/wm/ActivityAssistInfo.java @@ -30,12 +30,14 @@ public class ActivityAssistInfo { private final IBinder mAssistToken; private final int mTaskId; private final ComponentName mComponentName; + private final int mUserId; public ActivityAssistInfo(ActivityRecord activityRecord) { this.mActivityToken = activityRecord.token; this.mAssistToken = activityRecord.assistToken; this.mTaskId = activityRecord.getTask().mTaskId; this.mComponentName = activityRecord.mActivityComponent; + this.mUserId = activityRecord.mUserId; } /** @hide */ @@ -57,4 +59,9 @@ public class ActivityAssistInfo { public ComponentName getComponentName() { return mComponentName; } + + /** @hide */ + public int getUserId() { + return mUserId; + } } diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java index 5036fc646327..90cff3950047 100644 --- a/services/core/java/com/android/server/wm/ActivityRecord.java +++ b/services/core/java/com/android/server/wm/ActivityRecord.java @@ -1456,7 +1456,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A mSizeConfigurations = sizeConfigurations; } - private void scheduleActivityMovedToDisplay(int displayId, Configuration config) { + private void scheduleActivityMovedToDisplay(int displayId, @NonNull Configuration config, + @NonNull ActivityWindowInfo activityWindowInfo) { if (!attachedToProcess()) { ProtoLog.w(WM_DEBUG_SWITCH, "Can't report activity moved " + "to display - client not running, activityRecord=%s, displayId=%d", @@ -1469,7 +1470,7 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A config); mAtmService.getLifecycleManager().scheduleTransactionItem(app.getThread(), - MoveToDisplayItem.obtain(token, displayId, config)); + MoveToDisplayItem.obtain(token, displayId, config, activityWindowInfo)); } catch (RemoteException e) { // If process died, whatever. } @@ -9799,8 +9800,10 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // Update last reported values. final Configuration newMergedOverrideConfig = getMergedOverrideConfiguration(); + final ActivityWindowInfo newActivityWindowInfo = getActivityWindowInfo(); setLastReportedConfiguration(getProcessGlobalConfiguration(), newMergedOverrideConfig); + setLastReportedActivityWindowInfo(newActivityWindowInfo); if (mState == INITIALIZING) { // No need to relaunch or schedule new config for activity that hasn't been launched @@ -9817,7 +9820,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // There are no significant differences, so we won't relaunch but should still deliver // the new configuration to the client process. if (displayChanged) { - scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig); + scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig, + newActivityWindowInfo); } else { scheduleConfigurationChanged(newMergedOverrideConfig); } @@ -9884,7 +9888,8 @@ final class ActivityRecord extends WindowToken implements WindowManagerService.A // changes is always sent to all processes when they happen so it can just use whatever // system level configuration it last got. if (displayChanged) { - scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig); + scheduleActivityMovedToDisplay(newDisplayId, newMergedOverrideConfig, + newActivityWindowInfo); } else { scheduleConfigurationChanged(newMergedOverrideConfig); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java index 4a5b2211800c..c0881180af94 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerInternal.java @@ -822,4 +822,7 @@ public abstract class ActivityTaskManagerInternal { */ public abstract void unregisterCompatScaleProvider( @CompatScaleProvider.CompatScaleModeOrderId int id); + + /** Returns whether assist data is allowed. */ + public abstract boolean isAssistDataAllowed(); } diff --git a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java index 514a3d87ecf0..218b7512b861 100644 --- a/services/core/java/com/android/server/wm/ActivityTaskManagerService.java +++ b/services/core/java/com/android/server/wm/ActivityTaskManagerService.java @@ -6023,6 +6023,10 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { public int startActivityWithScreenshot(@NonNull Intent intent, @NonNull String callingPackage, int callingUid, int callingPid, @Nullable IBinder resultTo, @Nullable Bundle options, int userId) { + userId = getActivityStartController().checkTargetUser(userId, + false /* validateIncomingUser */, Binder.getCallingPid(), + Binder.getCallingUid(), "startActivityWithScreenshot"); + return getActivityStartController() .obtainStarter(intent, "startActivityWithScreenshot") .setCallingUid(callingUid) @@ -7347,6 +7351,11 @@ public class ActivityTaskManagerService extends IActivityTaskManager.Stub { @CompatScaleProvider.CompatScaleModeOrderId int id) { ActivityTaskManagerService.this.unregisterCompatScaleProvider(id); } + + @Override + public boolean isAssistDataAllowed() { + return ActivityTaskManagerService.this.isAssistDataAllowed(); + } } static boolean isPip2ExperimentEnabled() { diff --git a/services/core/java/com/android/server/wm/OWNERS b/services/core/java/com/android/server/wm/OWNERS index e06f2158dc14..79eb0dc620a5 100644 --- a/services/core/java/com/android/server/wm/OWNERS +++ b/services/core/java/com/android/server/wm/OWNERS @@ -24,3 +24,5 @@ per-file Background*Start* = set noparent per-file Background*Start* = file:/BAL_OWNERS per-file Background*Start* = ogunwale@google.com, louischang@google.com +# File related to activity callers +per-file ActivityCallerState.java = file:/core/java/android/app/COMPONENT_CALLER_OWNERS diff --git a/services/core/java/com/android/server/wm/WindowManagerInternal.java b/services/core/java/com/android/server/wm/WindowManagerInternal.java index 4698b6b2925c..5df2edc808f6 100644 --- a/services/core/java/com/android/server/wm/WindowManagerInternal.java +++ b/services/core/java/com/android/server/wm/WindowManagerInternal.java @@ -1079,4 +1079,10 @@ public abstract class WindowManagerInternal { * Moves the current focus to the top activity window if the top activity is embedded. */ public abstract boolean moveFocusToTopEmbeddedWindowIfNeeded(); + + /** + * Returns an instance of {@link ScreenCapture.ScreenshotHardwareBuffer} containing the current + * screenshot. + */ + public abstract ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot(); } diff --git a/services/core/java/com/android/server/wm/WindowManagerService.java b/services/core/java/com/android/server/wm/WindowManagerService.java index 08d43ae6f4c9..c93cc074aa3d 100644 --- a/services/core/java/com/android/server/wm/WindowManagerService.java +++ b/services/core/java/com/android/server/wm/WindowManagerService.java @@ -4081,13 +4081,8 @@ public class WindowManagerService extends IWindowManager.Stub } } - /** - * Takes a snapshot of the screen. In landscape mode this grabs the whole screen. - * In portrait mode, it grabs the upper region of the screen based on the vertical dimension - * of the target image. - */ - @Override - public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) { + @Nullable + private ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot() { if (!checkCallingPermission(READ_FRAME_BUFFER, "requestAssistScreenshot()")) { throw new SecurityException("Requires READ_FRAME_BUFFER permission"); } @@ -4106,24 +4101,34 @@ public class WindowManagerService extends IWindowManager.Stub } } - final Bitmap bm; + final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer; if (captureArgs != null) { ScreenCapture.SynchronousScreenCaptureListener syncScreenCapture = ScreenCapture.createSyncCaptureListener(); ScreenCapture.captureLayers(captureArgs, syncScreenCapture); - final ScreenCapture.ScreenshotHardwareBuffer screenshotBuffer = - syncScreenCapture.getBuffer(); - bm = screenshotBuffer == null ? null : screenshotBuffer.asBitmap(); + screenshotBuffer = syncScreenCapture.getBuffer(); } else { - bm = null; + screenshotBuffer = null; } - if (bm == null) { + if (screenshotBuffer == null) { Slog.w(TAG_WM, "Failed to take screenshot"); } + return screenshotBuffer; + } + + /** + * Takes a snapshot of the screen. In landscape mode this grabs the whole screen. + * In portrait mode, it grabs the upper region of the screen based on the vertical dimension + * of the target image. + */ + @Override + public boolean requestAssistScreenshot(final IAssistDataReceiver receiver) { + final ScreenCapture.ScreenshotHardwareBuffer shb = takeAssistScreenshot(); + final Bitmap bm = shb != null ? shb.asBitmap() : null; FgThread.getHandler().post(() -> { try { receiver.onHandleAssistScreenshot(bm); @@ -8688,6 +8693,12 @@ public class WindowManagerService extends IWindowManager.Stub return false; } } + + @Override + public ScreenCapture.ScreenshotHardwareBuffer takeAssistScreenshot() { + // WMS.takeAssistScreenshot takes care of the locking. + return WindowManagerService.this.takeAssistScreenshot(); + } } private final class ImeTargetVisibilityPolicyImpl extends ImeTargetVisibilityPolicy { diff --git a/services/fakes/Android.bp b/services/fakes/Android.bp new file mode 100644 index 000000000000..148054b31e89 --- /dev/null +++ b/services/fakes/Android.bp @@ -0,0 +1,20 @@ +package { + // See: http://go/android-license-faq + // A large-scale-change added 'default_applicable_licenses' to import + // all of the 'license_kinds' from "frameworks_base_license" + // to get the below license kinds: + // SPDX-license-identifier-Apache-2.0 + default_applicable_licenses: ["frameworks_base_license"], +} + +// NOTE: These "fake" services are intended for use under the Ravenwood +// deviceless test environment, and should *not* be included in the build +// artifacts for physical devices, as they already supply "real" services +filegroup { + name: "services.fakes-sources", + srcs: [ + "java/**/*.java", + ], + path: "java", + visibility: ["//frameworks/base"], +} diff --git a/services/fakes/java/com/android/server/FakeClipboardService.java b/services/fakes/java/com/android/server/FakeClipboardService.java new file mode 100644 index 000000000000..01016219e73d --- /dev/null +++ b/services/fakes/java/com/android/server/FakeClipboardService.java @@ -0,0 +1,165 @@ +/* + * 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; + +import android.content.ClipData; +import android.content.ClipDescription; +import android.content.Context; +import android.content.IClipboard; +import android.content.IOnPrimaryClipChangedListener; +import android.os.PermissionEnforcer; +import android.os.RemoteCallbackList; +import android.os.RemoteException; +import android.os.UserHandle; + +import com.android.internal.util.Preconditions; + +/** + * Fake implementation of {@code ClipboardManager} since the real implementation is tightly + * coupled with many other internal services. + */ +public class FakeClipboardService extends IClipboard.Stub { + private final RemoteCallbackList<IOnPrimaryClipChangedListener> mListeners = + new RemoteCallbackList<>(); + + private ClipData mPrimaryClip; + private String mPrimaryClipSource; + + public FakeClipboardService(Context context) { + super(PermissionEnforcer.fromContext(context)); + } + + public static class Lifecycle extends SystemService { + private FakeClipboardService mService; + + public Lifecycle(Context context) { + super(context); + } + + @Override + public void onStart() { + mService = new FakeClipboardService(getContext()); + publishBinderService(Context.CLIPBOARD_SERVICE, mService); + } + } + + private static void checkArguments(int userId, int deviceId) { + Preconditions.checkArgument(userId == UserHandle.USER_SYSTEM, + "Fake only supports USER_SYSTEM user"); + Preconditions.checkArgument(deviceId == Context.DEVICE_ID_DEFAULT, + "Fake only supports DEVICE_ID_DEFAULT device"); + } + + private void dispatchPrimaryClipChanged() { + mListeners.broadcast((listener) -> { + try { + listener.dispatchPrimaryClipChanged(); + } catch (RemoteException ignored) { + } + }); + } + + @Override + public void setPrimaryClip(ClipData clip, String callingPackage, String attributionTag, + int userId, int deviceId) { + checkArguments(userId, deviceId); + mPrimaryClip = clip; + mPrimaryClipSource = callingPackage; + dispatchPrimaryClipChanged(); + } + + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.SET_CLIP_SOURCE) + public void setPrimaryClipAsPackage(ClipData clip, String callingPackage, String attributionTag, + int userId, int deviceId, String sourcePackage) { + setPrimaryClipAsPackage_enforcePermission(); + checkArguments(userId, deviceId); + mPrimaryClip = clip; + mPrimaryClipSource = sourcePackage; + dispatchPrimaryClipChanged(); + } + + @Override + public void clearPrimaryClip(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + mPrimaryClip = null; + mPrimaryClipSource = null; + dispatchPrimaryClipChanged(); + } + + @Override + public ClipData getPrimaryClip(String pkg, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + return mPrimaryClip; + } + + @Override + public ClipDescription getPrimaryClipDescription(String callingPackage, String attributionTag, + int userId, int deviceId) { + checkArguments(userId, deviceId); + return (mPrimaryClip != null) ? mPrimaryClip.getDescription() : null; + } + + @Override + public boolean hasPrimaryClip(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + return mPrimaryClip != null; + } + + @Override + public void addPrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + mListeners.register(listener); + } + + @Override + public void removePrimaryClipChangedListener(IOnPrimaryClipChangedListener listener, + String callingPackage, String attributionTag, int userId, int deviceId) { + checkArguments(userId, deviceId); + mListeners.unregister(listener); + } + + @Override + public boolean hasClipboardText(String callingPackage, String attributionTag, int userId, + int deviceId) { + checkArguments(userId, deviceId); + return (mPrimaryClip != null) && (mPrimaryClip.getItemCount() > 0) + && (mPrimaryClip.getItemAt(0).getText() != null); + } + + @Override + @android.annotation.EnforcePermission(android.Manifest.permission.SET_CLIP_SOURCE) + public String getPrimaryClipSource(String callingPackage, String attributionTag, int userId, + int deviceId) { + getPrimaryClipSource_enforcePermission(); + checkArguments(userId, deviceId); + return mPrimaryClipSource; + } + + @Override + public boolean areClipboardAccessNotificationsEnabledForUser(int userId) { + throw new UnsupportedOperationException(); + } + + @Override + public void setClipboardAccessNotificationsEnabledForUser(boolean enable, int userId) { + throw new UnsupportedOperationException(); + } +} diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp index 3743483377b5..37967fa86b0f 100644 --- a/services/tests/servicestests/Android.bp +++ b/services/tests/servicestests/Android.bp @@ -85,6 +85,7 @@ android_test { "ravenwood-junit", "net_flags_lib", "CtsVirtualDeviceCommonLib", + "com_android_server_accessibility_flags_lib", ], libs: [ diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java index 89d146da1b75..8717a0500e57 100644 --- a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerTest.java @@ -47,6 +47,9 @@ import android.os.IBinder; import android.os.LocaleList; import android.os.RemoteException; import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsDisabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; import android.util.SparseArray; import android.view.Display; import android.view.IWindow; @@ -67,6 +70,7 @@ import org.hamcrest.Description; import org.hamcrest.TypeSafeMatcher; import org.junit.After; import org.junit.Before; +import org.junit.Rule; import org.junit.Test; import org.mockito.ArgumentCaptor; import org.mockito.Mock; @@ -77,9 +81,16 @@ import java.util.ArrayList; import java.util.Arrays; import java.util.List; +// This test verifies deprecated codepath. Probably changing this file means +// AccessibilityWindowManagerWithAccessibilityWindowTest also needs to be updated. +// LINT.IfChange + /** - * Tests for the AccessibilityWindowManager + * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y enabled. + * TODO(b/322444245): Merge with AccessibilityWindowManagerWithAccessibilityWindowTest + * after completing the flag migration. */ +@RequiresFlagsDisabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y) public class AccessibilityWindowManagerTest { private static final String PACKAGE_NAME = "com.android.server.accessibility"; private static final boolean FORCE_SEND = true; @@ -132,6 +143,9 @@ public class AccessibilityWindowManagerTest { @Mock private IBinder mMockEmbeddedToken; @Mock private IBinder mMockInvalidToken; + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + @Before public void setUp() throws RemoteException { MockitoAnnotations.initMocks(this); @@ -1227,3 +1241,4 @@ public class AccessibilityWindowManagerTest { } } } +// LINT.ThenChange(/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java) diff --git a/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java new file mode 100644 index 000000000000..4db27d272f8e --- /dev/null +++ b/services/tests/servicestests/src/com/android/server/accessibility/AccessibilityWindowManagerWithAccessibilityWindowTest.java @@ -0,0 +1,1247 @@ +/* + * Copyright (C) 2019 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.accessibility; + +import static com.android.server.accessibility.AbstractAccessibilityServiceConnection.DISPLAY_TYPE_DEFAULT; +import static com.android.server.accessibility.AccessibilityWindowManagerTest.DisplayIdMatcher.displayId; +import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowChangesMatcher.a11yWindowChanges; +import static com.android.server.accessibility.AccessibilityWindowManagerTest.WindowIdMatcher.a11yWindowId; + +import static junit.framework.Assert.assertFalse; +import static junit.framework.Assert.assertNotNull; +import static junit.framework.Assert.assertNull; +import static junit.framework.Assert.assertTrue; + +import static org.hamcrest.Matchers.allOf; +import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.not; +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertNotEquals; +import static org.junit.Assert.assertThat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.ArgumentMatchers.isNull; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import android.annotation.Nullable; +import android.graphics.Region; +import android.os.IBinder; +import android.os.LocaleList; +import android.os.RemoteException; +import android.os.UserHandle; +import android.platform.test.annotations.RequiresFlagsEnabled; +import android.platform.test.flag.junit.CheckFlagsRule; +import android.platform.test.flag.junit.DeviceFlagsValueProvider; +import android.util.SparseArray; +import android.view.Display; +import android.view.IWindow; +import android.view.WindowInfo; +import android.view.WindowManager; +import android.view.accessibility.AccessibilityEvent; +import android.view.accessibility.AccessibilityNodeInfo; +import android.view.accessibility.AccessibilityWindowAttributes; +import android.view.accessibility.AccessibilityWindowInfo; +import android.view.accessibility.IAccessibilityInteractionConnection; + +import com.android.server.accessibility.AccessibilityWindowManager.RemoteAccessibilityConnection; +import com.android.server.accessibility.test.MessageCapturingHandler; +import com.android.server.wm.WindowManagerInternal; +import com.android.server.wm.WindowManagerInternal.WindowsForAccessibilityCallback; + +import org.hamcrest.Description; +import org.hamcrest.TypeSafeMatcher; +import org.junit.After; +import org.junit.Before; +import org.junit.Rule; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoAnnotations; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; + +/** + * Tests for the AccessibilityWindowManager with Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y + * TODO(b/322444245): Merge with AccessibilityWindowManagerTest + * after completing the flag migration. + */ +@RequiresFlagsEnabled(Flags.FLAG_COMPUTE_WINDOW_CHANGES_ON_A11Y) +public class AccessibilityWindowManagerWithAccessibilityWindowTest { + private static final String PACKAGE_NAME = "com.android.server.accessibility"; + private static final boolean FORCE_SEND = true; + private static final boolean SEND_ON_WINDOW_CHANGES = false; + private static final int USER_SYSTEM_ID = UserHandle.USER_SYSTEM; + private static final int USER_PROFILE = 11; + private static final int USER_PROFILE_PARENT = 1; + private static final int SECONDARY_DISPLAY_ID = Display.DEFAULT_DISPLAY + 1; + private static final int NUM_GLOBAL_WINDOWS = 4; + private static final int NUM_APP_WINDOWS = 4; + private static final int NUM_OF_WINDOWS = (NUM_GLOBAL_WINDOWS + NUM_APP_WINDOWS); + private static final int DEFAULT_FOCUSED_INDEX = 1; + private static final int SCREEN_WIDTH = 1080; + private static final int SCREEN_HEIGHT = 1920; + private static final int INVALID_ID = AccessibilityWindowInfo.UNDEFINED_WINDOW_ID; + private static final int HOST_WINDOW_ID = 10; + private static final int EMBEDDED_WINDOW_ID = 11; + private static final int OTHER_WINDOW_ID = 12; + + private AccessibilityWindowManager mA11yWindowManager; + // Window manager will support multiple focused window if config_perDisplayFocusEnabled is true, + // i.e., each display would have its current focused window, and one of all focused windows + // would be top focused window. Otherwise, window manager only supports one focused window + // at all displays, and that focused window would be top focused window. + private boolean mSupportPerDisplayFocus = false; + private int mTopFocusedDisplayId = Display.INVALID_DISPLAY; + private IBinder mTopFocusedWindowToken = null; + + // List of window token, mapping from windowId -> window token. + private final SparseArray<IWindow> mA11yWindowTokens = new SparseArray<>(); + // List of window info lists, mapping from displayId -> window info lists. + private final SparseArray<ArrayList<WindowInfo>> mWindowInfos = + new SparseArray<>(); + // List of callback, mapping from displayId -> callback. + private final SparseArray<WindowsForAccessibilityCallback> mCallbackOfWindows = + new SparseArray<>(); + // List of display ID. + private final ArrayList<Integer> mExpectedDisplayList = new ArrayList<>(Arrays.asList( + Display.DEFAULT_DISPLAY, SECONDARY_DISPLAY_ID)); + + private final MessageCapturingHandler mHandler = new MessageCapturingHandler(null); + + @Mock + private WindowManagerInternal mMockWindowManagerInternal; + @Mock + private AccessibilityWindowManager.AccessibilityEventSender mMockA11yEventSender; + @Mock + private AccessibilitySecurityPolicy mMockA11ySecurityPolicy; + @Mock + private AccessibilitySecurityPolicy.AccessibilityUserManager mMockA11yUserManager; + @Mock + private AccessibilityTraceManager mMockA11yTraceManager; + + @Mock + private IBinder mMockHostToken; + @Mock + private IBinder mMockEmbeddedToken; + @Mock + private IBinder mMockInvalidToken; + + @Rule + public final CheckFlagsRule mCheckFlagsRule = DeviceFlagsValueProvider.createCheckFlagsRule(); + + @Before + public void setUp() throws RemoteException { + MockitoAnnotations.initMocks(this); + when(mMockA11yUserManager.getCurrentUserIdLocked()).thenReturn(USER_SYSTEM_ID); + when(mMockA11ySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked( + USER_PROFILE)).thenReturn(USER_PROFILE_PARENT); + when(mMockA11ySecurityPolicy.resolveCallingUserIdEnforcingPermissionsLocked( + USER_SYSTEM_ID)).thenReturn(USER_SYSTEM_ID); + when(mMockA11ySecurityPolicy.resolveValidReportedPackageLocked( + anyString(), anyInt(), anyInt(), anyInt())).thenReturn(PACKAGE_NAME); + + doAnswer((invocation) -> { + onWindowsForAccessibilityChanged(invocation.getArgument(0), false); + return null; + }).when(mMockWindowManagerInternal).computeWindowsForAccessibility(anyInt()); + + mA11yWindowManager = new AccessibilityWindowManager(new Object(), mHandler, + mMockWindowManagerInternal, + mMockA11yEventSender, + mMockA11ySecurityPolicy, + mMockA11yUserManager, + mMockA11yTraceManager); + // Starts tracking window of default display and sets the default display + // as top focused display before each testing starts. + startTrackingPerDisplay(Display.DEFAULT_DISPLAY); + + // AccessibilityEventSender is invoked during onWindowsForAccessibilityChanged. + // Resets it for mockito verify of further test case. + Mockito.reset(mMockA11yEventSender); + + registerLeashedTokenAndWindowId(); + } + + @After + public void tearDown() { + mHandler.removeAllMessages(); + } + + @Test + public void startTrackingWindows_shouldEnableWindowManagerCallback() { + // AccessibilityWindowManager#startTrackingWindows already invoked in setup. + assertTrue(mA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY)); + final WindowsForAccessibilityCallback callbacks = + mCallbackOfWindows.get(Display.DEFAULT_DISPLAY); + verify(mMockWindowManagerInternal).setWindowsForAccessibilityCallback( + eq(Display.DEFAULT_DISPLAY), eq(callbacks)); + } + + @Test + public void stopTrackingWindows_shouldDisableWindowManagerCallback() { + assertTrue(mA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY)); + Mockito.reset(mMockWindowManagerInternal); + + mA11yWindowManager.stopTrackingWindows(Display.DEFAULT_DISPLAY); + assertFalse(mA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY)); + verify(mMockWindowManagerInternal).setWindowsForAccessibilityCallback( + eq(Display.DEFAULT_DISPLAY), isNull()); + + } + + @Test + public void stopTrackingWindows_shouldClearWindows() { + assertTrue(mA11yWindowManager.isTrackingWindowsLocked(Display.DEFAULT_DISPLAY)); + final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + + mA11yWindowManager.stopTrackingWindows(Display.DEFAULT_DISPLAY); + assertNull(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY)); + assertEquals(mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT), + AccessibilityWindowInfo.UNDEFINED_WINDOW_ID); + assertEquals(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), + activeWindowId); + } + + @Test + public void stopTrackingWindows_onNonTopFocusedDisplay_shouldNotResetTopFocusWindow() + throws RemoteException { + // At setup, the default display sets be the top focused display and + // its current focused window sets be the top focused window. + // Starts tracking window of second display. + startTrackingPerDisplay(SECONDARY_DISPLAY_ID); + assertTrue(mA11yWindowManager.isTrackingWindowsLocked(SECONDARY_DISPLAY_ID)); + // Stops tracking windows of second display. + mA11yWindowManager.stopTrackingWindows(SECONDARY_DISPLAY_ID); + assertNotEquals(mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT), + AccessibilityWindowInfo.UNDEFINED_WINDOW_ID); + } + + @Test + public void onWindowsChanged_duringTouchInteractAndFocusChange_shouldChangeActiveWindow() { + final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + WindowInfo focusedWindowInfo = + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX); + assertEquals(activeWindowId, mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, focusedWindowInfo.token)); + + focusedWindowInfo.focused = false; + focusedWindowInfo = + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1); + focusedWindowInfo.focused = true; + + mA11yWindowManager.onTouchInteractionStart(); + setTopFocusedWindowAndDisplay(Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX + 1); + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); + } + + @Test + public void + onWindowsChanged_focusChangeOnNonTopFocusedDisplay_perDisplayFocusOn_notChangeWindow() + throws RemoteException { + // At setup, the default display sets be the top focused display and + // its current focused window sets be the top focused window. + // Sets supporting multiple focused window, i.e., config_perDisplayFocusEnabled is true. + mSupportPerDisplayFocus = true; + // Starts tracking window of second display. + startTrackingPerDisplay(SECONDARY_DISPLAY_ID); + // Gets the active window. + final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + // Gets the top focused window. + final int topFocusedWindowId = + mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT); + // Changes the current focused window at second display. + changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID, + DEFAULT_FOCUSED_INDEX + 1, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX); + + onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); + // The active window should not be changed. + assertEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); + // The top focused window should not be changed. + assertEquals(topFocusedWindowId, + mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT)); + } + + @Test + public void + onWindowChange_focusChangeToNonTopFocusedDisplay_perDisplayFocusOff_shouldChangeWindow() + throws RemoteException { + // At setup, the default display sets be the top focused display and + // its current focused window sets be the top focused window. + // Sets not supporting multiple focused window, i.e., config_perDisplayFocusEnabled is + // false. + mSupportPerDisplayFocus = false; + // Starts tracking window of second display. + startTrackingPerDisplay(SECONDARY_DISPLAY_ID); + // Gets the active window. + final int activeWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + // Gets the top focused window. + final int topFocusedWindowId = + mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT); + // Changes the current focused window from default display to second display. + changeFocusedWindowOnDisplayPerDisplayFocusConfig(SECONDARY_DISPLAY_ID, + DEFAULT_FOCUSED_INDEX, Display.DEFAULT_DISPLAY, DEFAULT_FOCUSED_INDEX); + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + onWindowsForAccessibilityChanged(SECONDARY_DISPLAY_ID, SEND_ON_WINDOW_CHANGES); + // The active window should be changed. + assertNotEquals(activeWindowId, mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID)); + // The top focused window should be changed. + assertNotEquals(topFocusedWindowId, + mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT)); + } + + @Test + public void onWindowsChanged_shouldReportCorrectLayer() { + // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup. + List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + for (int i = 0; i < a11yWindows.size(); i++) { + final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i); + final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i); + assertThat(mWindowInfos.get(Display.DEFAULT_DISPLAY).size() - windowInfo.layer - 1, + is(a11yWindow.getLayer())); + } + } + + @Test + public void onWindowsChanged_shouldReportCorrectOrder() { + // AccessibilityWindowManager#onWindowsForAccessibilityChanged already invoked in setup. + List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + for (int i = 0; i < a11yWindows.size(); i++) { + final AccessibilityWindowInfo a11yWindow = a11yWindows.get(i); + final IBinder windowToken = mA11yWindowManager + .getWindowTokenForUserAndWindowIdLocked(USER_SYSTEM_ID, a11yWindow.getId()); + final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(i); + assertThat(windowToken, is(windowInfo.token)); + } + } + + @Test + public void onWindowsChangedAndForceSend_shouldUpdateWindows() { + final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + final int correctLayer = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer(); + windowInfo.layer += 1; + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + assertNotEquals(correctLayer, + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer()); + } + + @Test + public void onWindowsChangedNoForceSend_layerChanged_shouldNotUpdateWindows() { + final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + final int correctLayer = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer(); + windowInfo.layer += 1; + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + assertEquals(correctLayer, + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0).getLayer()); + } + + @Test + public void onWindowsChangedNoForceSend_windowChanged_shouldUpdateWindows() + throws RemoteException { + final AccessibilityWindowInfo oldWindow = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0); + final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + true, USER_SYSTEM_ID); + final WindowInfo windowInfo = WindowInfo.obtain(); + windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION; + windowInfo.token = token.asBinder(); + windowInfo.layer = 0; + windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + mWindowInfos.get(Display.DEFAULT_DISPLAY).set(0, windowInfo); + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + assertNotEquals(oldWindow, + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0)); + } + + @Test + public void onWindowsChangedNoForceSend_focusChanged_shouldUpdateWindows() { + final WindowInfo focusedWindowInfo = + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX); + final WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + focusedWindowInfo.focused = false; + windowInfo.focused = true; + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + assertTrue(mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY).get(0) + .isFocused()); + } + + @Test + public void removeAccessibilityInteractionConnection_byWindowToken_shouldRemoved() { + for (int i = 0; i < NUM_OF_WINDOWS; i++) { + final int windowId = mA11yWindowTokens.keyAt(i); + final IWindow windowToken = mA11yWindowTokens.valueAt(i); + assertNotNull(mA11yWindowManager.getConnectionLocked(USER_SYSTEM_ID, windowId)); + + mA11yWindowManager.removeAccessibilityInteractionConnection(windowToken); + assertNull(mA11yWindowManager.getConnectionLocked(USER_SYSTEM_ID, windowId)); + } + } + + @Test + public void remoteAccessibilityConnection_binderDied_shouldRemoveConnection() { + for (int i = 0; i < NUM_OF_WINDOWS; i++) { + final int windowId = mA11yWindowTokens.keyAt(i); + final RemoteAccessibilityConnection remoteA11yConnection = + mA11yWindowManager.getConnectionLocked(USER_SYSTEM_ID, windowId); + assertNotNull(remoteA11yConnection); + + remoteA11yConnection.binderDied(); + assertNull(mA11yWindowManager.getConnectionLocked(USER_SYSTEM_ID, windowId)); + } + } + + @Test + public void getWindowTokenForUserAndWindowId_shouldNotNull() { + final List<AccessibilityWindowInfo> windows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + for (int i = 0; i < windows.size(); i++) { + final int windowId = windows.get(i).getId(); + + assertNotNull(mA11yWindowManager.getWindowTokenForUserAndWindowIdLocked( + USER_SYSTEM_ID, windowId)); + } + } + + @Test + public void findWindowId() { + final List<AccessibilityWindowInfo> windows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + for (int i = 0; i < windows.size(); i++) { + final int windowId = windows.get(i).getId(); + final IBinder windowToken = mA11yWindowManager.getWindowTokenForUserAndWindowIdLocked( + USER_SYSTEM_ID, windowId); + + assertEquals(mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, windowToken), windowId); + } + } + + @Test + public void resolveParentWindowId_windowIsNotEmbedded_shouldReturnGivenId() + throws RemoteException { + final int windowId = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, false, + Mockito.mock(IBinder.class), USER_SYSTEM_ID); + assertEquals(windowId, mA11yWindowManager.resolveParentWindowIdLocked(windowId)); + } + + @Test + public void resolveParentWindowId_windowIsNotRegistered_shouldReturnGivenId() { + final int windowId = -1; + assertEquals(windowId, mA11yWindowManager.resolveParentWindowIdLocked(windowId)); + } + + @Test + public void resolveParentWindowId_windowIsAssociated_shouldReturnParentWindowId() + throws RemoteException { + final IBinder mockHostToken = Mockito.mock(IBinder.class); + final IBinder mockEmbeddedToken = Mockito.mock(IBinder.class); + final int hostWindowId = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, mockHostToken, USER_SYSTEM_ID); + final int embeddedWindowId = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, mockEmbeddedToken, USER_SYSTEM_ID); + + mA11yWindowManager.associateEmbeddedHierarchyLocked(mockHostToken, mockEmbeddedToken); + + final int resolvedWindowId = mA11yWindowManager.resolveParentWindowIdLocked( + embeddedWindowId); + assertEquals(hostWindowId, resolvedWindowId); + } + + @Test + public void resolveParentWindowId_windowIsDisassociated_shouldReturnGivenId() + throws RemoteException { + final IBinder mockHostToken = Mockito.mock(IBinder.class); + final IBinder mockEmbeddedToken = Mockito.mock(IBinder.class); + final int hostWindowId = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, mockHostToken, USER_SYSTEM_ID); + final int embeddedWindowId = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, mockEmbeddedToken, USER_SYSTEM_ID); + + mA11yWindowManager.associateEmbeddedHierarchyLocked(mockHostToken, mockEmbeddedToken); + mA11yWindowManager.disassociateEmbeddedHierarchyLocked(mockEmbeddedToken); + + final int resolvedWindowId = mA11yWindowManager.resolveParentWindowIdLocked( + embeddedWindowId); + assertNotEquals(hostWindowId, resolvedWindowId); + assertEquals(embeddedWindowId, resolvedWindowId); + } + + @Test + public void computePartialInteractiveRegionForWindow_wholeVisible_returnWholeRegion() { + // Updates top 2 z-order WindowInfo are whole visible. + WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); + windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1); + windowInfo.regionInScreen.set(0, SCREEN_HEIGHT / 2, + SCREEN_WIDTH, SCREEN_HEIGHT); + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + final Region outBounds = new Region(); + int windowId = a11yWindows.get(0).getId(); + + mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); + assertThat(outBounds.getBounds().width(), is(SCREEN_WIDTH)); + assertThat(outBounds.getBounds().height(), is(SCREEN_HEIGHT / 2)); + + windowId = a11yWindows.get(1).getId(); + + mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); + assertThat(outBounds.getBounds().width(), is(SCREEN_WIDTH)); + assertThat(outBounds.getBounds().height(), is(SCREEN_HEIGHT / 2)); + } + + @Test + public void computePartialInteractiveRegionForWindow_halfVisible_returnHalfRegion() { + // Updates z-order #1 WindowInfo is half visible. + WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT / 2); + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + final Region outBounds = new Region(); + int windowId = a11yWindows.get(1).getId(); + + mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); + assertThat(outBounds.getBounds().width(), is(SCREEN_WIDTH)); + assertThat(outBounds.getBounds().height(), is(SCREEN_HEIGHT / 2)); + } + + @Test + public void computePartialInteractiveRegionForWindow_notVisible_returnEmptyRegion() { + // Since z-order #0 WindowInfo is full screen, z-order #1 WindowInfo should be invisible. + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + final Region outBounds = new Region(); + int windowId = a11yWindows.get(1).getId(); + + mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); + assertTrue(outBounds.getBounds().isEmpty()); + } + + @Test + public void computePartialInteractiveRegionForWindow_partialVisible_returnVisibleRegion() { + // Updates z-order #0 WindowInfo to have two interact-able areas. + Region region = new Region(0, 0, SCREEN_WIDTH, 200); + region.op(0, SCREEN_HEIGHT - 200, SCREEN_WIDTH, SCREEN_HEIGHT, Region.Op.UNION); + WindowInfo windowInfo = mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0); + windowInfo.regionInScreen.set(region); + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + final List<AccessibilityWindowInfo> a11yWindows = + mA11yWindowManager.getWindowListLocked(Display.DEFAULT_DISPLAY); + final Region outBounds = new Region(); + int windowId = a11yWindows.get(1).getId(); + + mA11yWindowManager.computePartialInteractiveRegionForWindowLocked(windowId, outBounds); + assertFalse(outBounds.getBounds().isEmpty()); + assertThat(outBounds.getBounds().width(), is(SCREEN_WIDTH)); + assertThat(outBounds.getBounds().height(), is(SCREEN_HEIGHT - 400)); + } + + @Test + public void updateActiveAndA11yFocusedWindow_windowStateChangedEvent_noTracking_shouldUpdate() { + final IBinder eventWindowToken = + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX + 1).token; + final int eventWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, eventWindowToken); + when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates()) + .thenReturn(eventWindowToken); + + final int noUse = 0; + mA11yWindowManager.stopTrackingWindows(Display.DEFAULT_DISPLAY); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + noUse, + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + noUse); + assertThat(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), is(eventWindowId)); + assertThat(mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT), + is(eventWindowId)); + } + + @Test + public void updateActiveAndA11yFocusedWindow_hoverEvent_touchInteract_shouldSetActiveWindow() { + final int eventWindowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, + DEFAULT_FOCUSED_INDEX + 1); + final int currentActiveWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + assertThat(currentActiveWindowId, is(not(eventWindowId))); + + final int noUse = 0; + mA11yWindowManager.onTouchInteractionStart(); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + noUse, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, + noUse); + assertThat(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), is(eventWindowId)); + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(2)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(currentActiveWindowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); + assertThat(captor.getAllValues().get(1), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(eventWindowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); + } + + @Test + public void updateActiveAndA11yFocusedWindow_a11yFocusEvent_shouldUpdateA11yFocus() { + final int eventWindowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, + DEFAULT_FOCUSED_INDEX); + final int currentA11yFocusedWindowId = mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); + assertThat(currentA11yFocusedWindowId, is(AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)); + + final int noUse = 0; + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, + noUse); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), is(eventWindowId)); + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(1)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(eventWindowId), + a11yWindowChanges( + AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); + } + + @Test + public void updateActiveAndA11yFocusedWindow_a11yFocusEvent_multiDisplay_defaultToSecondary() + throws RemoteException { + runUpdateActiveAndA11yFocusedWindow_MultiDisplayTest( + Display.DEFAULT_DISPLAY, SECONDARY_DISPLAY_ID); + } + + @Test + public void updateActiveAndA11yFocusedWindow_a11yFocusEvent_multiDisplay_SecondaryToDefault() + throws RemoteException { + runUpdateActiveAndA11yFocusedWindow_MultiDisplayTest( + SECONDARY_DISPLAY_ID, Display.DEFAULT_DISPLAY); + } + + private void runUpdateActiveAndA11yFocusedWindow_MultiDisplayTest( + int initialDisplayId, int eventDisplayId) throws RemoteException { + startTrackingPerDisplay(SECONDARY_DISPLAY_ID); + final int initialWindowId = getWindowIdFromWindowInfosForDisplay( + initialDisplayId, DEFAULT_FOCUSED_INDEX); + final int noUse = 0; + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + initialWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, + noUse); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), is(initialWindowId)); + Mockito.reset(mMockA11yEventSender); + + final int eventWindowId = getWindowIdFromWindowInfosForDisplay( + eventDisplayId, DEFAULT_FOCUSED_INDEX); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, + noUse); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), is(eventWindowId)); + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(2)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(initialDisplayId), + a11yWindowId(initialWindowId), + a11yWindowChanges( + AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); + assertThat(captor.getAllValues().get(1), + allOf(displayId(eventDisplayId), + a11yWindowId(eventWindowId), + a11yWindowChanges( + AccessibilityEvent.WINDOWS_CHANGE_ACCESSIBILITY_FOCUSED))); + } + + @Test + public void updateActiveAndA11yFocusedWindow_clearA11yFocusEvent_shouldClearA11yFocus() { + final int eventWindowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, + DEFAULT_FOCUSED_INDEX); + final int currentA11yFocusedWindowId = mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY); + assertThat(currentA11yFocusedWindowId, is(not(eventWindowId))); + + final int noUse = 0; + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, + noUse); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), is(eventWindowId)); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUS_CLEARED, + noUse); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), + is(AccessibilityWindowInfo.UNDEFINED_WINDOW_ID)); + } + + @Test + public void onTouchInteractionEnd_shouldRollbackActiveWindow() { + final int eventWindowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, + DEFAULT_FOCUSED_INDEX + 1); + final int currentActiveWindowId = mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID); + assertThat(currentActiveWindowId, is(not(eventWindowId))); + + final int noUse = 0; + mA11yWindowManager.onTouchInteractionStart(); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + eventWindowId, + noUse, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, + noUse); + assertThat(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), is(eventWindowId)); + // AccessibilityEventSender is invoked after active window changed. Reset it. + Mockito.reset(mMockA11yEventSender); + + mA11yWindowManager.onTouchInteractionEnd(); + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(2)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(eventWindowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); + assertThat(captor.getAllValues().get(1), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(currentActiveWindowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ACTIVE))); + } + + @Test + public void onTouchInteractionEnd_noServiceInteractiveWindow_shouldClearA11yFocus() + throws RemoteException { + final IBinder defaultFocusWinToken = + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(DEFAULT_FOCUSED_INDEX).token; + final int defaultFocusWindowId = mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, defaultFocusWinToken); + when(mMockWindowManagerInternal.getFocusedWindowTokenFromWindowStates()) + .thenReturn(defaultFocusWinToken); + final int newFocusWindowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, + DEFAULT_FOCUSED_INDEX + 1); + final IAccessibilityInteractionConnection mockNewFocusConnection = + mA11yWindowManager.getConnectionLocked( + USER_SYSTEM_ID, newFocusWindowId).getRemote(); + + mA11yWindowManager.stopTrackingWindows(Display.DEFAULT_DISPLAY); + final int noUse = 0; + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + defaultFocusWindowId, + noUse, + AccessibilityEvent.TYPE_WINDOW_STATE_CHANGED, + noUse); + assertThat(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), is(defaultFocusWindowId)); + assertThat(mA11yWindowManager.getFocusedWindowId(AccessibilityNodeInfo.FOCUS_INPUT), + is(defaultFocusWindowId)); + + mA11yWindowManager.onTouchInteractionStart(); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + newFocusWindowId, + noUse, + AccessibilityEvent.TYPE_VIEW_HOVER_ENTER, + noUse); + mA11yWindowManager.updateActiveAndAccessibilityFocusedWindowLocked(USER_SYSTEM_ID, + newFocusWindowId, + AccessibilityNodeInfo.ROOT_NODE_ID, + AccessibilityEvent.TYPE_VIEW_ACCESSIBILITY_FOCUSED, + noUse); + assertThat(mA11yWindowManager.getActiveWindowId(USER_SYSTEM_ID), is(newFocusWindowId)); + assertThat(mA11yWindowManager.getFocusedWindowId( + AccessibilityNodeInfo.FOCUS_ACCESSIBILITY), is(newFocusWindowId)); + + mA11yWindowManager.onTouchInteractionEnd(); + mHandler.sendLastMessage(); + verify(mockNewFocusConnection).clearAccessibilityFocus(); + } + + @Test + public void getPictureInPictureWindow_shouldNotNull() { + assertNull(mA11yWindowManager.getPictureInPictureWindowLocked()); + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(1).inPictureInPicture = true; + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + assertNotNull(mA11yWindowManager.getPictureInPictureWindowLocked()); + } + + @Test + public void notifyOutsideTouch() throws RemoteException { + final int targetWindowId = + getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 1); + final int outsideWindowId = + getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); + final IAccessibilityInteractionConnection mockRemoteConnection = + mA11yWindowManager.getConnectionLocked( + USER_SYSTEM_ID, outsideWindowId).getRemote(); + mWindowInfos.get(Display.DEFAULT_DISPLAY).get(0).hasFlagWatchOutsideTouch = true; + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, SEND_ON_WINDOW_CHANGES); + + mA11yWindowManager.notifyOutsideTouch(USER_SYSTEM_ID, targetWindowId); + verify(mockRemoteConnection).notifyOutsideTouch(); + } + + @Test + public void addAccessibilityInteractionConnection_profileUser_findInParentUser() + throws RemoteException { + final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, USER_PROFILE); + final int windowId = mA11yWindowManager.findWindowIdLocked( + USER_PROFILE_PARENT, token.asBinder()); + assertTrue(windowId >= 0); + } + + @Test + public void getDisplayList() throws RemoteException { + // Starts tracking window of second display. + startTrackingPerDisplay(SECONDARY_DISPLAY_ID); + + final ArrayList<Integer> displayList = mA11yWindowManager.getDisplayListLocked( + DISPLAY_TYPE_DEFAULT); + assertTrue(displayList.equals(mExpectedDisplayList)); + } + + @Test + public void setAccessibilityWindowIdToSurfaceMetadata() + throws RemoteException { + final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + true, USER_SYSTEM_ID); + int windowId = -1; + for (int i = 0; i < mA11yWindowTokens.size(); i++) { + if (mA11yWindowTokens.valueAt(i).equals(token)) { + windowId = mA11yWindowTokens.keyAt(i); + } + } + assertNotEquals("Returned token is not found in mA11yWindowTokens", -1, windowId); + verify(mMockWindowManagerInternal, times(1)).setAccessibilityIdToSurfaceMetadata( + token.asBinder(), windowId); + + mA11yWindowManager.removeAccessibilityInteractionConnection(token); + verify(mMockWindowManagerInternal, times(1)).setAccessibilityIdToSurfaceMetadata( + token.asBinder(), -1); + } + + @Test + public void getHostTokenLocked_hierarchiesAreAssociated_shouldReturnHostToken() { + mA11yWindowManager.associateLocked(mMockEmbeddedToken, mMockHostToken); + final IBinder hostToken = mA11yWindowManager.getHostTokenLocked(mMockEmbeddedToken); + assertEquals(hostToken, mMockHostToken); + } + + @Test + public void getHostTokenLocked_hierarchiesAreNotAssociated_shouldReturnNull() { + final IBinder hostToken = mA11yWindowManager.getHostTokenLocked(mMockEmbeddedToken); + assertNull(hostToken); + } + + @Test + public void getHostTokenLocked_embeddedHierarchiesAreDisassociated_shouldReturnNull() { + mA11yWindowManager.associateLocked(mMockEmbeddedToken, mMockHostToken); + mA11yWindowManager.disassociateLocked(mMockEmbeddedToken); + final IBinder hostToken = mA11yWindowManager.getHostTokenLocked(mMockEmbeddedToken); + assertNull(hostToken); + } + + @Test + public void getHostTokenLocked_hostHierarchiesAreDisassociated_shouldReturnNull() { + mA11yWindowManager.associateLocked(mMockEmbeddedToken, mMockHostToken); + mA11yWindowManager.disassociateLocked(mMockHostToken); + final IBinder hostToken = mA11yWindowManager.getHostTokenLocked(mMockHostToken); + assertNull(hostToken); + } + + @Test + public void getWindowIdLocked_windowIsRegistered_shouldReturnWindowId() { + final int windowId = mA11yWindowManager.getWindowIdLocked(mMockHostToken); + assertEquals(windowId, HOST_WINDOW_ID); + } + + @Test + public void getWindowIdLocked_windowIsNotRegistered_shouldReturnInvalidWindowId() { + final int windowId = mA11yWindowManager.getWindowIdLocked(mMockInvalidToken); + assertEquals(windowId, INVALID_ID); + } + + @Test + public void getTokenLocked_windowIsRegistered_shouldReturnToken() { + final IBinder token = mA11yWindowManager.getLeashTokenLocked(HOST_WINDOW_ID); + assertEquals(token, mMockHostToken); + } + + @Test + public void getTokenLocked_windowIsNotRegistered_shouldReturnNull() { + final IBinder token = mA11yWindowManager.getLeashTokenLocked(OTHER_WINDOW_ID); + assertNull(token); + } + + @Test + public void setAccessibilityWindowAttributes_windowIsNotRegistered_titleIsChanged() { + final int windowId = + getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); + final WindowManager.LayoutParams layoutParams = new WindowManager.LayoutParams(); + layoutParams.accessibilityTitle = "accessibility window title"; + final AccessibilityWindowAttributes attributes = new AccessibilityWindowAttributes( + layoutParams, new LocaleList()); + + mA11yWindowManager.setAccessibilityWindowAttributes(Display.DEFAULT_DISPLAY, windowId, + USER_SYSTEM_ID, attributes); + + final AccessibilityWindowInfo a11yWindow = mA11yWindowManager.findA11yWindowInfoByIdLocked( + windowId); + assertEquals(toString(layoutParams.accessibilityTitle), toString(a11yWindow.getTitle())); + } + + @Test + public void sendAccessibilityEventOnWindowRemoval() { + final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); + + // Removing index 0 because it's not focused, and avoids unnecessary layer change. + final int windowId = + getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); + infos.remove(0); + for (WindowInfo info : infos) { + // Adjust layer number because it should start from 0. + info.layer--; + } + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(1)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(windowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_REMOVED))); + } + + @Test + public void sendAccessibilityEventOnWindowAddition() throws RemoteException { + final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); + + for (WindowInfo info : infos) { + // Adjust layer number because new window will have 0 so that layer number in + // A11yWindowInfo in window won't be changed. + info.layer++; + } + + final IWindow token = addAccessibilityInteractionConnection(Display.DEFAULT_DISPLAY, + false, USER_SYSTEM_ID); + addWindowInfo(infos, token, 0); + final int windowId = + getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, infos.size() - 1); + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(1)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(windowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_ADDED))); + } + + @Test + public void sendAccessibilityEventOnWindowChange() { + final ArrayList<WindowInfo> infos = mWindowInfos.get(Display.DEFAULT_DISPLAY); + infos.get(0).title = "new title"; + final int windowId = getWindowIdFromWindowInfosForDisplay(Display.DEFAULT_DISPLAY, 0); + + onWindowsForAccessibilityChanged(Display.DEFAULT_DISPLAY, FORCE_SEND); + + final ArgumentCaptor<AccessibilityEvent> captor = + ArgumentCaptor.forClass(AccessibilityEvent.class); + verify(mMockA11yEventSender, times(1)) + .sendAccessibilityEventForCurrentUserLocked(captor.capture()); + assertThat(captor.getAllValues().get(0), + allOf(displayId(Display.DEFAULT_DISPLAY), + a11yWindowId(windowId), + a11yWindowChanges(AccessibilityEvent.WINDOWS_CHANGE_TITLE))); + } + + private void registerLeashedTokenAndWindowId() { + mA11yWindowManager.registerIdLocked(mMockHostToken, HOST_WINDOW_ID); + mA11yWindowManager.registerIdLocked(mMockEmbeddedToken, EMBEDDED_WINDOW_ID); + } + + private void startTrackingPerDisplay(int displayId) throws RemoteException { + ArrayList<WindowInfo> windowInfosForDisplay = new ArrayList<>(); + // Adds RemoteAccessibilityConnection into AccessibilityWindowManager, and copy + // mock window token into mA11yWindowTokens. Also, preparing WindowInfo mWindowInfos + // for the test. + int layer = 0; + for (int i = 0; i < NUM_GLOBAL_WINDOWS; i++) { + final IWindow token = addAccessibilityInteractionConnection(displayId, + true, USER_SYSTEM_ID); + addWindowInfo(windowInfosForDisplay, token, layer++); + + } + for (int i = 0; i < NUM_APP_WINDOWS; i++) { + final IWindow token = addAccessibilityInteractionConnection(displayId, + false, USER_SYSTEM_ID); + addWindowInfo(windowInfosForDisplay, token, layer++); + } + // Sets up current focused window of display. + // Each display has its own current focused window if config_perDisplayFocusEnabled is true. + // Otherwise only default display needs to current focused window. + if (mSupportPerDisplayFocus || displayId == Display.DEFAULT_DISPLAY) { + windowInfosForDisplay.get(DEFAULT_FOCUSED_INDEX).focused = true; + } + // Turns on windows tracking, and update window info. + mA11yWindowManager.startTrackingWindows(displayId, false); + // Puts window lists into array. + mWindowInfos.put(displayId, windowInfosForDisplay); + // Sets the default display is the top focused display and + // its current focused window is the top focused window. + if (displayId == Display.DEFAULT_DISPLAY) { + setTopFocusedWindowAndDisplay(displayId, DEFAULT_FOCUSED_INDEX); + } + // Invokes callback for sending window lists to A11y framework. + onWindowsForAccessibilityChanged(displayId, FORCE_SEND); + + assertEquals(mA11yWindowManager.getWindowListLocked(displayId).size(), + windowInfosForDisplay.size()); + } + + private WindowsForAccessibilityCallback getWindowsForAccessibilityCallbacks(int displayId) { + ArgumentCaptor<WindowsForAccessibilityCallback> windowsForAccessibilityCallbacksCaptor = + ArgumentCaptor.forClass( + WindowsForAccessibilityCallback.class); + verify(mMockWindowManagerInternal) + .setWindowsForAccessibilityCallback(eq(displayId), + windowsForAccessibilityCallbacksCaptor.capture()); + return windowsForAccessibilityCallbacksCaptor.getValue(); + } + + private IWindow addAccessibilityInteractionConnection(int displayId, boolean bGlobal, + int userId) throws RemoteException { + final IWindow mockWindowToken = Mockito.mock(IWindow.class); + final IAccessibilityInteractionConnection mockA11yConnection = Mockito.mock( + IAccessibilityInteractionConnection.class); + final IBinder mockConnectionBinder = Mockito.mock(IBinder.class); + final IBinder mockWindowBinder = Mockito.mock(IBinder.class); + final IBinder mockLeashToken = Mockito.mock(IBinder.class); + when(mockA11yConnection.asBinder()).thenReturn(mockConnectionBinder); + when(mockWindowToken.asBinder()).thenReturn(mockWindowBinder); + when(mMockA11ySecurityPolicy.isCallerInteractingAcrossUsers(userId)) + .thenReturn(bGlobal); + when(mMockWindowManagerInternal.getDisplayIdForWindow(mockWindowBinder)) + .thenReturn(displayId); + + int windowId = mA11yWindowManager.addAccessibilityInteractionConnection( + mockWindowToken, mockLeashToken, mockA11yConnection, PACKAGE_NAME, userId); + mA11yWindowTokens.put(windowId, mockWindowToken); + return mockWindowToken; + } + + private int addAccessibilityInteractionConnection(int displayId, boolean bGlobal, + IBinder leashToken, int userId) throws RemoteException { + final IWindow mockWindowToken = Mockito.mock(IWindow.class); + final IAccessibilityInteractionConnection mockA11yConnection = Mockito.mock( + IAccessibilityInteractionConnection.class); + final IBinder mockConnectionBinder = Mockito.mock(IBinder.class); + final IBinder mockWindowBinder = Mockito.mock(IBinder.class); + when(mockA11yConnection.asBinder()).thenReturn(mockConnectionBinder); + when(mockWindowToken.asBinder()).thenReturn(mockWindowBinder); + when(mMockA11ySecurityPolicy.isCallerInteractingAcrossUsers(userId)) + .thenReturn(bGlobal); + when(mMockWindowManagerInternal.getDisplayIdForWindow(mockWindowBinder)) + .thenReturn(displayId); + + int windowId = mA11yWindowManager.addAccessibilityInteractionConnection( + mockWindowToken, leashToken, mockA11yConnection, PACKAGE_NAME, userId); + mA11yWindowTokens.put(windowId, mockWindowToken); + return windowId; + } + + private void addWindowInfo(ArrayList<WindowInfo> windowInfos, IWindow windowToken, int layer) { + final WindowInfo windowInfo = WindowInfo.obtain(); + windowInfo.type = AccessibilityWindowInfo.TYPE_APPLICATION; + windowInfo.token = windowToken.asBinder(); + windowInfo.layer = layer; + windowInfo.regionInScreen.set(0, 0, SCREEN_WIDTH, SCREEN_HEIGHT); + windowInfos.add(windowInfo); + } + + private int getWindowIdFromWindowInfosForDisplay(int displayId, int index) { + final IBinder windowToken = mWindowInfos.get(displayId).get(index).token; + return mA11yWindowManager.findWindowIdLocked( + USER_SYSTEM_ID, windowToken); + } + + private void setTopFocusedWindowAndDisplay(int displayId, int index) { + // Sets the top focus window. + mTopFocusedWindowToken = mWindowInfos.get(displayId).get(index).token; + // Sets the top focused display. + mTopFocusedDisplayId = displayId; + } + + private void onWindowsForAccessibilityChanged(int displayId, boolean forceSend) { + WindowsForAccessibilityCallback callbacks = mCallbackOfWindows.get(displayId); + if (callbacks == null) { + callbacks = getWindowsForAccessibilityCallbacks(displayId); + mCallbackOfWindows.put(displayId, callbacks); + } + callbacks.onWindowsForAccessibilityChanged(forceSend, mTopFocusedDisplayId, + mTopFocusedWindowToken, mWindowInfos.get(displayId)); + } + + private void changeFocusedWindowOnDisplayPerDisplayFocusConfig( + int changeFocusedDisplayId, int newFocusedWindowIndex, int oldTopFocusedDisplayId, + int oldFocusedWindowIndex) { + if (mSupportPerDisplayFocus) { + // Gets the old focused window of display which wants to change focused window. + WindowInfo focusedWindowInfo = + mWindowInfos.get(changeFocusedDisplayId).get(oldFocusedWindowIndex); + // Resets the focus of old focused window. + focusedWindowInfo.focused = false; + // Gets the new window of display which wants to change focused window. + focusedWindowInfo = + mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex); + // Sets the focus of new focused window. + focusedWindowInfo.focused = true; + } else { + // Gets the window of display which wants to change focused window. + WindowInfo focusedWindowInfo = + mWindowInfos.get(changeFocusedDisplayId).get(newFocusedWindowIndex); + // Sets the focus of new focused window. + focusedWindowInfo.focused = true; + // Gets the old focused window of old top focused display. + focusedWindowInfo = + mWindowInfos.get(oldTopFocusedDisplayId).get(oldFocusedWindowIndex); + // Resets the focus of old focused window. + focusedWindowInfo.focused = false; + // Changes the top focused display and window. + setTopFocusedWindowAndDisplay(changeFocusedDisplayId, newFocusedWindowIndex); + } + } + + @Nullable + private static String toString(@Nullable CharSequence cs) { + return cs == null ? null : cs.toString(); + } + + static class DisplayIdMatcher extends TypeSafeMatcher<AccessibilityEvent> { + private final int mDisplayId; + + DisplayIdMatcher(int displayId) { + super(); + mDisplayId = displayId; + } + + static DisplayIdMatcher displayId(int displayId) { + return new DisplayIdMatcher(displayId); + } + + @Override + protected boolean matchesSafely(AccessibilityEvent event) { + return event.getDisplayId() == mDisplayId; + } + + @Override + public void describeTo(Description description) { + description.appendText("Matching to displayId " + mDisplayId); + } + } + + static class WindowIdMatcher extends TypeSafeMatcher<AccessibilityEvent> { + private int mWindowId; + + WindowIdMatcher(int windowId) { + super(); + mWindowId = windowId; + } + + static WindowIdMatcher a11yWindowId(int windowId) { + return new WindowIdMatcher(windowId); + } + + @Override + protected boolean matchesSafely(AccessibilityEvent event) { + return event.getWindowId() == mWindowId; + } + + @Override + public void describeTo(Description description) { + description.appendText("Matching to windowId " + mWindowId); + } + } + + static class WindowChangesMatcher extends TypeSafeMatcher<AccessibilityEvent> { + private int mWindowChanges; + + WindowChangesMatcher(int windowChanges) { + super(); + mWindowChanges = windowChanges; + } + + static WindowChangesMatcher a11yWindowChanges(int windowChanges) { + return new WindowChangesMatcher(windowChanges); + } + + @Override + protected boolean matchesSafely(AccessibilityEvent event) { + return event.getWindowChanges() == mWindowChanges; + } + + @Override + public void describeTo(Description description) { + description.appendText("Matching to window changes " + mWindowChanges); + } + } +} diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java index a6e05ddc792c..55208972895d 100644 --- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java +++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecLocalDeviceAudioSystemTest.java @@ -364,6 +364,30 @@ public class HdmiCecLocalDeviceAudioSystemTest { } @Test + public void systemAudioControlOnPowerOn_singleActionStarted() throws Exception { + mHdmiCecLocalDeviceAudioSystem.removeAction(SystemAudioInitiationActionFromAvr.class); + mHdmiCecLocalDeviceAudioSystem.systemAudioControlOnPowerOn( + Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON, true); + mHdmiCecLocalDeviceAudioSystem.systemAudioControlOnPowerOn( + Constants.ALWAYS_SYSTEM_AUDIO_CONTROL_ON_POWER_ON, true); + assertThat( + mHdmiCecLocalDeviceAudioSystem.getActions( + SystemAudioInitiationActionFromAvr.class)) + .hasSize(1); + } + + @Test + public void onSystemAudioControlFeatureSupportChanged_singleActionStarted() throws Exception { + mHdmiCecLocalDeviceAudioSystem.removeAction(SystemAudioInitiationActionFromAvr.class); + mHdmiCecLocalDeviceAudioSystem.onSystemAudioControlFeatureSupportChanged(true); + mHdmiCecLocalDeviceAudioSystem.onSystemAudioControlFeatureSupportChanged(true); + assertThat( + mHdmiCecLocalDeviceAudioSystem.getActions( + SystemAudioInitiationActionFromAvr.class)) + .hasSize(1); + } + + @Test public void handleActiveSource_updateActiveSource() throws Exception { HdmiCecMessage message = HdmiCecMessageBuilder.buildActiveSource(ADDR_TV, 0x0000); ActiveSource expectedActiveSource = new ActiveSource(ADDR_TV, 0x0000); diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java index b1e62f9c9a82..15cd5115a49e 100644 --- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java +++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java @@ -206,7 +206,6 @@ import libcore.io.Streams; import org.junit.After; import org.junit.Assume; import org.junit.Before; -import org.junit.Ignore; import org.junit.Rule; import org.junit.Test; import org.junit.rules.MethodRule; @@ -2151,14 +2150,12 @@ public class NetworkPolicyManagerServiceTest { assertFalse(mService.isUidNetworkingBlocked(UID_E, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainEnabled() throws Exception { verify(mNetworkManager).setFirewallChainEnabled(FIREWALL_CHAIN_BACKGROUND, true); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnProcStateChange() throws Exception { @@ -2188,7 +2185,6 @@ public class NetworkPolicyManagerServiceTest { assertTrue(mService.isUidNetworkingBlocked(UID_A, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnAllowlistChange() throws Exception { @@ -2227,7 +2223,6 @@ public class NetworkPolicyManagerServiceTest { assertFalse(mService.isUidNetworkingBlocked(UID_B, false)); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testBackgroundChainOnTempAllowlistChange() throws Exception { @@ -2266,7 +2261,6 @@ public class NetworkPolicyManagerServiceTest { && uidState.procState == procState && uidState.capability == capability; } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersProcStateChanges() throws Exception { @@ -2329,7 +2323,6 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersStaleChanges() throws Exception { @@ -2350,7 +2343,6 @@ public class NetworkPolicyManagerServiceTest { waitForUidEventHandlerIdle(); } - @Ignore("Temporarily disabled until the feature is enabled") @Test @RequiresFlagsEnabled(Flags.FLAG_NETWORK_BLOCKED_FOR_TOP_SLEEPING_AND_ABOVE) public void testUidObserverFiltersCapabilityChanges() throws Exception { diff --git a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java index c217780d90d6..aef7158fd613 100644 --- a/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java +++ b/services/voiceinteraction/java/com/android/server/voiceinteraction/VoiceInteractionManagerService.java @@ -16,15 +16,21 @@ package com.android.server.voiceinteraction; +import static android.content.Intent.FLAG_ACTIVITY_NEW_TASK; +import static android.content.Intent.FLAG_ACTIVITY_NO_ANIMATION; +import static android.content.Intent.FLAG_ACTIVITY_NO_USER_ACTION; import android.Manifest; import android.annotation.CallbackExecutor; import android.annotation.NonNull; import android.annotation.Nullable; +import android.annotation.RequiresPermission; import android.annotation.UserIdInt; import android.app.ActivityManager; import android.app.ActivityManagerInternal; +import android.app.ActivityOptions; import android.app.AppGlobals; +import android.app.admin.DevicePolicyManagerInternal; import android.app.role.OnRoleHoldersChangedListener; import android.app.role.RoleManager; import android.content.ComponentName; @@ -41,6 +47,7 @@ import android.content.pm.ShortcutServiceInternal; import android.content.pm.UserInfo; import android.content.res.Resources; import android.database.ContentObserver; +import android.graphics.Bitmap; import android.hardware.soundtrigger.IRecognitionStatusCallback; import android.hardware.soundtrigger.KeyphraseMetadata; import android.hardware.soundtrigger.ModelParams; @@ -84,6 +91,7 @@ import android.util.ArrayMap; import android.util.ArraySet; import android.util.Log; import android.util.Slog; +import android.window.ScreenCapture; import com.android.internal.R; import com.android.internal.annotations.GuardedBy; @@ -110,7 +118,9 @@ import com.android.server.pm.permission.LegacyPermissionManagerInternal; import com.android.server.policy.AppOpsPolicy; import com.android.server.utils.Slogf; import com.android.server.utils.TimingsTraceAndSlog; +import com.android.server.wm.ActivityAssistInfo; import com.android.server.wm.ActivityTaskManagerInternal; +import com.android.server.wm.WindowManagerInternal; import java.io.FileDescriptor; import java.io.PrintWriter; @@ -127,6 +137,19 @@ public class VoiceInteractionManagerService extends SystemService { static final String TAG = "VoiceInteractionManager"; static final boolean DEBUG = false; + /** Static constants used by Contextual Search helper. */ + private static final String CS_KEY_FLAG_SECURE_FOUND = + "com.android.contextualsearch.flag_secure_found"; + private static final String CS_KEY_FLAG_SCREENSHOT = + "com.android.contextualsearch.screenshot"; + private static final String CS_KEY_FLAG_IS_MANAGED_PROFILE_VISIBLE = + "com.android.contextualsearch.is_managed_profile_visible"; + private static final String CS_KEY_FLAG_VISIBLE_PACKAGE_NAMES = + "com.android.contextualsearch.visible_package_names"; + private static final String CS_INTENT_FILTER = + "com.android.contextualsearch.LAUNCH"; + + final Context mContext; final ContentResolver mResolver; // Can be overridden for testing purposes @@ -135,6 +158,8 @@ public class VoiceInteractionManagerService extends SystemService { final ActivityManagerInternal mAmInternal; final ActivityTaskManagerInternal mAtmInternal; final UserManagerInternal mUserManagerInternal; + final WindowManagerInternal mWmInternal; + final DevicePolicyManagerInternal mDpmInternal; final ArrayMap<Integer, VoiceInteractionManagerServiceStub.SoundTriggerSession> mLoadedKeyphraseIds = new ArrayMap<>(); ShortcutServiceInternal mShortcutServiceInternal; @@ -156,7 +181,10 @@ public class VoiceInteractionManagerService extends SystemService { LocalServices.getService(ActivityManagerInternal.class)); mAtmInternal = Objects.requireNonNull( LocalServices.getService(ActivityTaskManagerInternal.class)); - + mWmInternal = Objects.requireNonNull( + LocalServices.getService(WindowManagerInternal.class)); + mDpmInternal = Objects.requireNonNull( + LocalServices.getService(DevicePolicyManagerInternal.class)); LegacyPermissionManagerInternal permissionManagerInternal = LocalServices.getService( LegacyPermissionManagerInternal.class); permissionManagerInternal.setVoiceInteractionPackagesProvider( @@ -1019,6 +1047,56 @@ public class VoiceInteractionManagerService extends SystemService { public boolean showSessionFromSession(@NonNull IBinder token, @Nullable Bundle sessionArgs, int flags, @Nullable String attributionTag) { synchronized (this) { + final String csKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchKey); + final String csEnabledKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchEnabled); + + // If the request is for Contextual Search, process it differently + if (sessionArgs != null && sessionArgs.containsKey(csKey)) { + if (sessionArgs.getBoolean(csEnabledKey, true)) { + // If Contextual Search is enabled, try to follow that path. + Intent launchIntent = getContextualSearchIntent(sessionArgs); + if (launchIntent != null) { + // Hand over to contextual search helper. + Slog.d(TAG, "Handed over to contextual search helper."); + final long caller = Binder.clearCallingIdentity(); + try { + return startContextualSearch(launchIntent); + } finally { + Binder.restoreCallingIdentity(caller); + } + } + } + + // Since we are here, Contextual Search helper couldn't handle the request. + final String visEnabledKey = mContext.getResources() + .getString(R.string.config_defaultContextualSearchLegacyEnabled); + if (sessionArgs.getBoolean(visEnabledKey, true)) { + // If visEnabledKey is set to true (or absent), we try following VIS path. + String csPkgName = mContext.getResources() + .getString(R.string.config_defaultContextualSearchPackageName); + if (!csPkgName.equals(getCurInteractor( + Binder.getCallingUserHandle().getIdentifier()).getPackageName())) { + // Check if the interactor can handle Contextual Search. + // If not, return failure. + Slog.w(TAG, "Contextual Search not supported yet. Returning failure."); + return false; + } + } else { + // If visEnabledKey is set to false AND the request was for Contextual + // Search, return false. + return false; + } + // Given that we haven't returned yet, we can say that + // - Contextual Search Helper couldn't handle the request + // - VIS path for Contextual Search is enabled + // - The current interactor supports Contextual Search. + // Hence, we will proceed with the VIS path. + Slog.d(TAG, "Contextual search not supported yet. Proceeding with VIS."); + + } + if (mImpl == null) { Slog.w(TAG, "showSessionFromSession without running voice interaction service"); return false; @@ -2644,6 +2722,70 @@ public class VoiceInteractionManagerService extends SystemService { } } }; + + private Intent getContextualSearchIntent(Bundle args) { + String csPkgName = mContext.getResources() + .getString(R.string.config_defaultContextualSearchPackageName); + if (csPkgName.isEmpty()) { + // Return null if csPackageName is not specified. + return null; + } + Intent launchIntent = new Intent(CS_INTENT_FILTER); + launchIntent.setPackage(csPkgName); + ResolveInfo resolveInfo = mContext.getPackageManager().resolveActivity( + launchIntent, PackageManager.MATCH_FACTORY_ONLY); + if (resolveInfo == null) { + return null; + } + launchIntent.setComponent(resolveInfo.getComponentInfo().getComponentName()); + launchIntent.addFlags(FLAG_ACTIVITY_NEW_TASK | FLAG_ACTIVITY_NO_ANIMATION + | FLAG_ACTIVITY_NO_USER_ACTION); + launchIntent.putExtras(args); + boolean isAssistDataAllowed = mAtmInternal.isAssistDataAllowed(); + final List<ActivityAssistInfo> records = mAtmInternal.getTopVisibleActivities(); + ArrayList<String> visiblePackageNames = new ArrayList<>(); + boolean isManagedProfileVisible = false; + for (ActivityAssistInfo record: records) { + // Add the package name to the list only if assist data is allowed. + if (isAssistDataAllowed) { + visiblePackageNames.add(record.getComponentName().getPackageName()); + } + if (mDpmInternal.isUserOrganizationManaged(record.getUserId())) { + isManagedProfileVisible = true; + } + } + final ScreenCapture.ScreenshotHardwareBuffer shb = mWmInternal.takeAssistScreenshot(); + final Bitmap bm = shb != null ? shb.asBitmap() : null; + // Now that everything is fetched, putting it in the launchIntent. + if (bm != null) { + launchIntent.putExtra(CS_KEY_FLAG_SECURE_FOUND, shb.containsSecureLayers()); + // Only put the screenshot if assist data is allowed + if (isAssistDataAllowed) { + launchIntent.putExtra(CS_KEY_FLAG_SCREENSHOT, bm.asShared()); + } + } + launchIntent.putExtra(CS_KEY_FLAG_IS_MANAGED_PROFILE_VISIBLE, isManagedProfileVisible); + // Only put the list of visible package names if assist data is allowed + if (isAssistDataAllowed) { + launchIntent.putExtra(CS_KEY_FLAG_VISIBLE_PACKAGE_NAMES, visiblePackageNames); + } + + return launchIntent; + } + + @RequiresPermission(android.Manifest.permission.START_TASKS_FROM_RECENTS) + private boolean startContextualSearch(Intent launchIntent) { + // Contextual search starts with a frozen screen - so we launch without + // any system animations or starting window. + final ActivityOptions opts = ActivityOptions.makeCustomTaskAnimation(mContext, + /* enterResId= */ 0, /* exitResId= */ 0, null, null, null); + opts.setDisableStartingWindow(true); + int resultCode = mAtmInternal.startActivityWithScreenshot(launchIntent, + mContext.getPackageName(), Binder.getCallingUid(), Binder.getCallingPid(), null, + opts.toBundle(), Binder.getCallingUserHandle().getIdentifier()); + return resultCode == ActivityManager.START_SUCCESS; + } + } /** diff --git a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java index 217659ee4345..700856c50bae 100644 --- a/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java +++ b/tests/CtsSurfaceControlTestsStaging/src/main/java/android/view/surfacecontroltests/GraphicsActivity.java @@ -726,7 +726,7 @@ public class GraphicsActivity extends Activity { .map(Object::toString) .collect(Collectors.joining(", "))); int initialNumEvents = mModeChangedEvents.size(); - surface.setFrameRate(30.f, compatibility); + surface.setFrameRate(70.f, compatibility); verifyFrameRates(expectedFrameRates, surface); verifyModeSwitchesDontChangeResolution(initialNumEvents, mModeChangedEvents.size()); }); @@ -824,7 +824,7 @@ public class GraphicsActivity extends Activity { Display display = getDisplay(); List<Float> expectedFrameRates = getRefreshRates(display.getMode(), display) .stream() - .filter(rate -> rate >= 30.f) + .filter(rate -> rate >= 70.f) .collect(Collectors.toList()); assumeTrue("**** testSurfaceControlFrameRateCompatibility SKIPPED because no refresh rate " |