Merge "Fix crash in RowAlertTimeCoordinator when a group has no children" into main
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 4e36075..c73e048 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -145,6 +145,16 @@
}
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 @@
"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 fc24b30..e4cb5ad 100644
--- a/apex/jobscheduler/service/aconfig/device_idle.aconfig
+++ b/apex/jobscheduler/service/aconfig/device_idle.aconfig
@@ -4,5 +4,5 @@
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 ef9ac73..5e6d377 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -4,7 +4,7 @@
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 bc89f12..ac98a94 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -3350,7 +3350,6 @@
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 @@
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 @@
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 @@
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 2fec0ad..67ccd9d 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -1647,14 +1647,12 @@
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 @@
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 @@
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 42c3272..d70fa19 100644
--- a/core/java/android/accessibilityservice/AccessibilityService.java
+++ b/core/java/android/accessibilityservice/AccessibilityService.java
@@ -81,7 +81,6 @@
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 @@
private final SparseArray<AccessibilityButtonController> mAccessibilityButtonControllers =
new SparseArray<>(0);
private BrailleDisplayController mBrailleDisplayController;
- private BrailleDisplayController mTestBrailleDisplayController;
private int mGestureStatusCallbackSequence;
@@ -3650,46 +3648,10 @@
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 074f7e9..41151c0 100644
--- a/core/java/android/app/ActivityThread.java
+++ b/core/java/android/app/ActivityThread.java
@@ -717,7 +717,7 @@
}
activity.mMainThread.handleActivityConfigurationChanged(
ActivityClientRecord.this, overrideConfig, newDisplayId,
- false /* alwaysReportChange */);
+ mActivityWindowInfo, false /* alwaysReportChange */);
}
@Override
@@ -6659,11 +6659,12 @@
/**
* 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 @@
}
@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 @@
* @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 @@
// 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 4c92dee..b5b3669 100644
--- a/core/java/android/app/ClientTransactionHandler.java
+++ b/core/java/android/app/ClientTransactionHandler.java
@@ -167,11 +167,12 @@
/** 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 7e06735..d1e517b 100644
--- a/core/java/android/app/ForegroundServiceTypePolicy.java
+++ b/core/java/android/app/ForegroundServiceTypePolicy.java
@@ -62,7 +62,6 @@
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 @@
* 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 3f73da2..0000000
--- 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 5ab7991..13d959c 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 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 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 @@
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 @@
//
// 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 @@
EVENT_COUGH,
EVENT_SNORE,
EVENT_BACK_DOUBLE_TAP,
- EVENT_HEART_RATE,
EVENT_VENDOR_WEARABLE_START
})
@Retention(RetentionPolicy.SOURCE)
@@ -239,8 +209,6 @@
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 @@
@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 @@
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 @@
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 @@
"endTime = " + mEndTime + ", " +
"confidenceLevel = " + mConfidenceLevel + ", " +
"densityLevel = " + mDensityLevel + ", " +
- "vendorData = " + mVendorData + ", " +
- "ratePerMinute = " + mRatePerMinute +
+ "vendorData = " + mVendorData +
" }";
}
@@ -429,7 +380,6 @@
dest.writeInt(mConfidenceLevel);
dest.writeInt(mDensityLevel);
dest.writeTypedObject(mVendorData, flags);
- dest.writeInt(mRatePerMinute);
}
@Override
@@ -449,7 +399,6 @@
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 @@
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 @@
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 @@
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 @@
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 @@
}
@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 bc8fac5..48ea846 100644
--- a/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java
+++ b/core/java/android/app/servertransaction/ActivityConfigurationChangeItem.java
@@ -29,6 +29,7 @@
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
+import android.window.ActivityWindowInfo;
import java.util.Objects;
@@ -49,11 +50,13 @@
}
@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 1353d16..0702c45 100644
--- a/core/java/android/app/servertransaction/MoveToDisplayItem.java
+++ b/core/java/android/app/servertransaction/MoveToDisplayItem.java
@@ -28,6 +28,7 @@
import android.os.IBinder;
import android.os.Parcel;
import android.os.Trace;
+import android.window.ActivityWindowInfo;
import java.util.Objects;
@@ -39,6 +40,7 @@
private int mTargetDisplayId;
private Configuration mConfiguration;
+ private ActivityWindowInfo mActivityWindowInfo;
@Override
public void preExecute(@NonNull ClientTransactionHandler client) {
@@ -52,7 +54,8 @@
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 @@
/** 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 @@
instance.setActivityToken(activityToken);
instance.mTargetDisplayId = targetDisplayId;
instance.mConfiguration = new Configuration(configuration);
+ instance.mActivityWindowInfo = new ActivityWindowInfo(activityWindowInfo);
return instance;
}
@@ -86,6 +90,7 @@
super.recycle();
mTargetDisplayId = 0;
mConfiguration = null;
+ mActivityWindowInfo = null;
ObjectPool.recycle(this);
}
@@ -97,6 +102,7 @@
super.writeToParcel(dest, flags);
dest.writeInt(mTargetDisplayId);
dest.writeTypedObject(mConfiguration, flags);
+ dest.writeTypedObject(mActivityWindowInfo, flags);
}
/** Read from Parcel. */
@@ -104,6 +110,7 @@
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 @@
}
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 @@
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 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 728c350..b421339 100644
--- a/core/java/android/content/ClipData.java
+++ b/core/java/android/content/ClipData.java
@@ -169,6 +169,8 @@
*/
@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 @@
* @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 @@
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 @@
// 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 @@
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 @@
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 @@
}
} 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 @@
} 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 @@
*
* @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 @@
*
* @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 @@
}
}
+ /** @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 107f107..2fabcba 100644
--- a/core/java/android/content/ClipboardManager.java
+++ b/core/java/android/content/ClipboardManager.java
@@ -50,6 +50,7 @@
* </div>
*/
@SystemService(Context.CLIPBOARD_SERVICE)
+@android.ravenwood.annotation.RavenwoodKeepWholeClass
public class ClipboardManager extends android.text.ClipboardManager {
/**
@@ -143,6 +144,7 @@
*/
@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 @@
*/
@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 9c6aab4..5b0cee7 100644
--- a/core/java/android/content/pm/ServiceInfo.java
+++ b/core/java/android/content/pm/ServiceInfo.java
@@ -163,25 +163,12 @@
* 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 36730cb..f852d3c 100644
--- a/core/java/android/os/HandlerThread.java
+++ b/core/java/android/os/HandlerThread.java
@@ -19,6 +19,8 @@
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 @@
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 @@
}
/**
+ * @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 7631454..5e7edda 100644
--- a/core/java/android/security/flags.aconfig
+++ b/core/java/android/security/flags.aconfig
@@ -31,6 +31,13 @@
}
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 b5f3b9a..333cbb3 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -95,6 +95,7 @@
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 @@
* 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 @@
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 @@
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 9d613bc..05cabd5 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 @@
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 13dc4ef..0e5747d 100644
--- a/core/java/android/widget/RemoteViews.java
+++ b/core/java/android/widget/RemoteViews.java
@@ -3874,7 +3874,7 @@
}
}
- private static class SetDrawInstructionAction extends Action {
+ private class SetDrawInstructionAction extends Action {
@Nullable
private final DrawInstructions mInstructions;
@@ -3909,6 +3909,15 @@
}
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 @@
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 82067de..254f4f7 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 @@
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 b2e0be7c..c882938 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 1f06b0b..6134e78 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 b36b1d6..2f5183f 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 d95834f..64c17bd 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 @@
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 @@
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 @@
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 @@
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 @@
/**
* 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 @@
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 @@
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 30545f9..85a1b4e 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 @@
@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 a8466bb..906558f 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 @@
@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 9743e84..dbb090f 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 @@
@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 11b8271..bd9abec 100644
--- a/keystore/java/android/security/KeyStore.java
+++ b/keystore/java/android/security/KeyStore.java
@@ -21,12 +21,14 @@
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 @@
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 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 f50efd2..5cffe46 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 @@
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 @@
@KeyProperties.AuthEnum int userAuthenticationType,
boolean userAuthenticationRequirementEnforcedBySecureHardware,
boolean userAuthenticationValidWhileOnBody,
+ boolean unlockedDeviceRequired,
boolean trustedUserPresenceRequired,
boolean invalidatedByBiometricEnrollment,
boolean userConfirmationRequired,
@@ -132,6 +135,7 @@
mUserAuthenticationRequirementEnforcedBySecureHardware =
userAuthenticationRequirementEnforcedBySecureHardware;
mUserAuthenticationValidWhileOnBody = userAuthenticationValidWhileOnBody;
+ mUnlockedDeviceRequired = unlockedDeviceRequired;
mTrustedUserPresenceRequired = trustedUserPresenceRequired;
mInvalidatedByBiometricEnrollment = invalidatedByBiometricEnrollment;
mUserConfirmationRequired = userConfirmationRequired;
@@ -275,6 +279,20 @@
}
/**
+ * 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 97592b4..2682eb6 100644
--- a/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java
+++ b/keystore/java/android/security/keystore2/AndroidKeyStoreSecretKeyFactorySpi.java
@@ -93,6 +93,7 @@
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 @@
+ 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 @@
: 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 d319e4c..a7b5c36 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.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 @@
/** 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 bdfe399..c68ae8b 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 @@
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 @@
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 a6253b8..8ff17e0 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.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 @@
}
/**
- * 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 @@
}
}
-@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 4ed84b9..7277550 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 @@
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 baebfeb..75a0dcc 100644
--- a/packages/CredentialManager/tests/robotests/Android.bp
+++ b/packages/CredentialManager/tests/robotests/Android.bp
@@ -37,7 +37,7 @@
":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 @@
"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
new file mode 100644
index 0000000..81860e5
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_landscape_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..8c1fff7
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/dark_portrait_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..4eb025f
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_landscape_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..c709f93
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/phone/light_portrait_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..278c13f
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_landscape_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..cb85df3
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/dark_portrait_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..2eca707
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_landscape_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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
new file mode 100644
index 0000000..7ee91b3
--- /dev/null
+++ b/packages/CredentialManager/tests/robotests/customization/assets/tablet/light_portrait_singleCredentialScreen_newM3BottomSheet.png
Binary files differ
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 a0e1fed..e609d0c 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 @@
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 @@
}
}
+ @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 988afd7..a395266 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.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 @@
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 @@
}
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 efd53a4..c60ce41 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.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.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 @@
}
@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 6a1ee3a..54c5a14 100644
--- a/packages/SettingsLib/aconfig/settingslib.aconfig
+++ b/packages/SettingsLib/aconfig/settingslib.aconfig
@@ -43,4 +43,11 @@
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 6ee403d..bd27c89 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 @@
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.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.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 @@
* 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 @@
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 @@
Log.d(TAG, "onBroadcastMetadataChanged(), broadcastId = " + broadcastId);
}
setLatestBluetoothLeBroadcastMetadata(metadata);
+ notifyBroadcastStateChange(BROADCAST_STATE_ON);
}
@Override
@@ -212,7 +228,7 @@
+ ", broadcastId = "
+ broadcastId);
}
-
+ notifyBroadcastStateChange(BROADCAST_STATE_OFF);
stopLocalSourceReceivers();
resetCacheInfo();
}
@@ -1005,10 +1021,6 @@
/** 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 @@
"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 2a4658b..a5c63be 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 @@
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 [getCompatibleDevices] list. */
- suspend fun addCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
+ /** Adds a [audioDeviceAttributes] to [getSpatialAudioCompatibleDevices] list. */
+ suspend fun addSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
- /** Removes a [audioDeviceAttributes] to [getCompatibleDevices] list. */
- suspend fun removeCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
+ /** Removes a [audioDeviceAttributes] from [getSpatialAudioCompatibleDevices] list. */
+ suspend fun removeSpatialAudioCompatibleDevice(audioDeviceAttributes: AudioDeviceAttributes)
+
+ /** Checks if the head tracking is enabled for the [audioDeviceAttributes]. */
+ suspend fun isHeadTrackingEnabled(audioDeviceAttributes: AudioDeviceAttributes): Boolean
+
+ /** 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 @@
}
}
- 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 c3cc340..0347403 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 @@
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 3f52f24..0000000
--- 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 a44baeb..0000000
--- 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 3705267..70ba415 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.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 @@
}
@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 ea46c0c..ee81813 100644
--- a/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
+++ b/packages/SoundPicker/src/com/android/soundpicker/RingtonePickerActivity.java
@@ -300,6 +300,8 @@
}
};
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 62dd4ac..ef15c84 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 @@
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 be5aa8a..7535a51 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.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.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 @@
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 @@
)
}
}
-
-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 66cef86..6875bc5 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.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.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.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 @@
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 2e0ce42..8484b7f 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.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.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 @@
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 d401261..c08eb94 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 @@
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 @@
@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 @@
) {
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 d49fed5..b3fcc30 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.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.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 @@
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 @@
@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 ddc9252..4d810df 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.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 @@
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 0d94bb0..18a62dc 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.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 @@
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 @@
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 a838a99..ac5004e 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.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 @@
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 @@
}
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 4d07379..dd76781 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.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 @@
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 8a9ebc9..910cd5e 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.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.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 @@
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 @@
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 @@
layout: ComponentsLayout,
modifier: Modifier = Modifier
) {
- val arrangement =
+ val arrangement: Arrangement.Vertical =
if (isLargeScreen) {
Arrangement.spacedBy(20.dp)
} else {
@@ -120,16 +126,21 @@
modifier = modifier.widthIn(max = 800.dp),
verticalArrangement = arrangement,
) {
- val contentModifier = Modifier
if (isPortrait || isLargeScreen) {
- VerticalVolumePanelContent(modifier = contentModifier, layout = layout)
- } else {
- HorizontalVolumePanelContent(
- modifier = contentModifier.heightIn(max = 212.dp),
+ VerticalVolumePanelContent(
+ modifier = Modifier.weight(weight = 1f, fill = false),
layout = layout
)
+ } else {
+ HorizontalVolumePanelContent(
+ 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 @@
}
}
}
+
+/**
+ * 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 0000000..a932dd6
--- /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 4e72843..fff0a31 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.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 @@
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 0000000..9b0adb1
--- /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 f49b477..4e16236 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.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 @@
displayId = Display.DEFAULT_DISPLAY,
sceneLogger = mock(),
falsingCollector = falsingCollector,
+ falsingManager = kosmos.falsingManager,
powerInteractor = powerInteractor,
bouncerInteractor = bouncerInteractor,
simBouncerInteractor = { kosmos.simBouncerInteractor },
@@ -970,6 +972,20 @@
)
}
+ @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 0000000..737b7f3
--- /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 0000000..36be90e
--- /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 0000000..eb6f0b2
--- /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 0000000..324ae0c
--- /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 0000000..8ca221a
--- /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 0000000..b6db7fc
--- /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 4be1deb..34eb1b4 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 4263d94..ea0e309 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 6d9994f..ce24259 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.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 @@
@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 a90980f..a431a59 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 @@
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 51978ec..ccd69ca 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.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.statusbar.BlurUtils;
import com.android.systemui.statusbar.phone.ScrimController;
+import javax.inject.Inject;
+
/**
* Provides the UI shown during system shutdown.
*/
@@ -45,9 +50,13 @@
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 @@
/**
* 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/globalactions/ShutdownUiModule.kt b/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt
deleted file mode 100644
index b7285da..0000000
--- a/packages/SystemUI/src/com/android/systemui/globalactions/ShutdownUiModule.kt
+++ /dev/null
@@ -1,31 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-package com.android.systemui.globalactions
-
-import android.content.Context
-import com.android.systemui.statusbar.BlurUtils
-import dagger.Module
-import dagger.Provides
-
-/** 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)
- }
-}
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 1c9be0f..f13ecf3 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.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 @@
/** 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 class ActiveMediaDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
+ context: Context,
cachedDevice: CachedBluetoothDevice,
audioManager: AudioManager?
): Boolean {
@@ -71,6 +74,7 @@
internal class AvailableMediaDeviceItemFactory : DeviceItemFactory() {
override fun isFilterMatched(
+ context: Context,
cachedDevice: CachedBluetoothDevice,
audioManager: AudioManager?
): Boolean {
@@ -99,10 +103,18 @@
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 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 fcd45a6..1df496b 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 @@
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 0000000..36350f8
--- /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 b642d38..034f87f 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.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.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 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 @@
@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 @@
automaticallySwitchScenes()
hydrateSystemUiState()
collectFalsingSignals()
+ respondToFalsingDetections()
hydrateWindowFocus()
hydrateInteractionState()
} else {
@@ -376,6 +382,18 @@
}
}
+ /** 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 45b6f65..ee76c05 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.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 @@
)
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 @@
}
}
}
-
- 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 7068f5f..d5bbaa5 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 @@
@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 ea41912..e6555f2e 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.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 @@
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 9715070..84afbed 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 @@
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.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 @@
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 @@
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 599600d6..0fd0555 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 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.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.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 @@
) : 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 @@
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 @@
// 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 @@
}
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 @@
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 510086d..dc9eeb3 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 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 18a9161..593b90a 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 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 @@
@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 0000000..71bce5e
--- /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 0000000..4358611
--- /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 0000000..cf14546
--- /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 0000000..4e65f60
--- /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 9d9b263..2d3ca60 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 @@
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.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
@SmallTest
@@ -41,10 +48,13 @@
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 @@
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 92c7326..a8cd8c8 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 @@
@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 @@
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 @@
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 e236f4a..ddf0b9a 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 @@
): 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 7737b43..651006d 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 @@
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 @@
)
}
+ 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 @@
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 d2e0386..3a6324d 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.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 @@
}
@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 5038285..974a11c 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 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/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt
new file mode 100644
index 0000000..7001ea8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/media/SpatializerKosmos.kt
@@ -0,0 +1,24 @@
+/*
+ * 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
+
+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 0000000..0183b97
--- /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 0000000..a025846
--- /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 6b67364..371c3ac 100644
--- a/ravenwood/framework-minus-apex-ravenwood-policies.txt
+++ b/ravenwood/framework-minus-apex-ravenwood-policies.txt
@@ -55,3 +55,5 @@
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 3668b03..c17d090 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 @@
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 @@
}
}
+ @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 231cce9..56a3c64 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 @@
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 bb280f4..3de96c0 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 @@
// 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 a8c24fc..a520d4c 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 @@
int mUid = NOBODY_UID;
int mPid = sNextPid.getAndIncrement();
+ String mPackageName;
+
boolean mProvideMainThread = false;
final RavenwoodSystemProperties mSystemProperties = new RavenwoodSystemProperties();
@@ -158,6 +160,15 @@
}
/**
+ * 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 eb3c55c..9b4d378 100644
--- a/ravenwood/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/ravenwood-annotation-allowed-classes.txt
@@ -186,6 +186,7 @@
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 5f12ce1..9f31f37 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 @@
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 70993ca..1e90ab2 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 @@
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 @@
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 582058d..31bfc695 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 @@
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 @@
}
// 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 b0dcf95..881583a 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 @@
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 3b91780..2dc0046 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 @@
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 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 5036fc6..90cff39 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 @@
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 @@
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 @@
// 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 @@
// 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 @@
// 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 4a5b221..c088118 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 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 514a3d8..218b751 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 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 @@
@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 e06f215..79eb0dc 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* = 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 4698b6b..5df2edc 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 @@
* 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 08d43ae..c93cc07 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 @@
}
}
- /**
- * 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 @@
}
}
- 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 @@
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 0000000..148054b
--- /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 0000000..0101621
--- /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 3743483..37967fa 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -85,6 +85,7 @@
"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 89d146d..8717a05 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.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.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.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 @@
@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 @@
}
}
}
+// 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 0000000..4db27d2
--- /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 a6e05dd..5520897 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 @@
}
@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 b1e62f9..15cd511 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 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 @@
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 @@
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 @@
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 @@
&& 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 @@
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 @@
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 c217780..aef7158 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.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.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.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 @@
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 @@
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 @@
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 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 @@
}
}
};
+
+ 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 217659e..700856c 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 @@
.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 @@
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 "