Merge "[RONs] Bind ProgressStyle values to NotificationProgressBar" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index d582cb7..c6ce799 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -65,6 +65,7 @@
"android.server.app.flags-aconfig-java",
"android.service.autofill.flags-aconfig-java",
"android.service.chooser.flags-aconfig-java",
+ "android.service.compat.flags-aconfig-java",
"android.service.controls.flags-aconfig-java",
"android.service.dreams.flags-aconfig-java",
"android.service.notification.flags-aconfig-java",
@@ -863,6 +864,21 @@
defaults: ["framework-minus-apex-aconfig-java-defaults"],
}
+aconfig_declarations {
+ name: "android.service.compat.flags-aconfig",
+ package: "com.android.server.compat",
+ container: "system",
+ srcs: [
+ "services/core/java/com/android/server/compat/*.aconfig",
+ ],
+}
+
+java_aconfig_library {
+ name: "android.service.compat.flags-aconfig-java",
+ aconfig_declarations: "android.service.compat.flags-aconfig",
+ defaults: ["framework-minus-apex-aconfig-java-defaults"],
+}
+
// Multi user
aconfig_declarations {
name: "android.multiuser.flags-aconfig",
diff --git a/Android.bp b/Android.bp
index 811755d..d4776f5 100644
--- a/Android.bp
+++ b/Android.bp
@@ -170,12 +170,6 @@
//same purpose.
"//external/robolectric:__subpackages__",
"//frameworks/layoutlib:__subpackages__",
-
- // This is for the same purpose as robolectric -- to build "framework.jar" for host-side
- // testing.
- // TODO: Once Ravenwood is stable, move the host side jar targets to this directory,
- // and remove this line.
- "//frameworks/base/tools/hoststubgen:__subpackages__",
],
}
diff --git a/Ravenwood.bp b/Ravenwood.bp
index ec58210..2e038e0 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -12,256 +12,19 @@
// See the License for the specific language governing permissions and
// limitations under the License.
-// We need this "trampoline" rule to force soong to give a host-side jar to
-// framework-minus-apex.ravenwood-base. Otherwise, soong would mix up the arch (?) and we'd get
-// a dex jar.
-java_library {
- name: "framework-minus-apex-for-hoststubgen",
- installable: false, // host only jar.
- static_libs: [
- "framework-minus-apex",
- ],
- sdk_version: "core_platform",
- visibility: ["//visibility:private"],
-}
-
-// Process framework-all with hoststubgen for Ravenwood.
-// This step takes several tens of seconds, so we manually shard it to multiple modules.
-// All the copies have to be kept in sync.
-// TODO: Do the sharding better, either by making hostsubgen support sharding natively, or
-// making a better build rule.
-
-genrule_defaults {
- name: "framework-minus-apex.ravenwood-base_defaults",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- tools: ["hoststubgen"],
- srcs: [
- ":framework-minus-apex-for-hoststubgen",
- ":ravenwood-framework-policies",
- ":ravenwood-standard-options",
- ":ravenwood-annotation-allowed-classes",
- ],
- out: [
- "ravenwood.jar",
- "hoststubgen_framework-minus-apex.log",
- ],
-}
-
-framework_minus_apex_cmd = "$(location hoststubgen) " +
- "@$(location :ravenwood-standard-options) " +
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--out-jar $(location ravenwood.jar) " +
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) "
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X0",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 0",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X1",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 1",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X2",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 2",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X3",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 3",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X4",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X5",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X6",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X7",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X8",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8",
-}
-
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_X9",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 9",
-}
-
-// Build framework-minus-apex.ravenwood-base without sharding.
-// We extract the various dump files from this one, rather than the sharded ones, because
-// some dumps use the output from other classes (e.g. base classes) which may not be in the
-// same shard. Also some of the dump files ("apis") may be slow even when sharded, because
-// the output contains the information from all the input classes, rather than the output classes.
-// Not using sharding is fine for this module because it's only used for collecting the
-// dump / stats files, which don't have to happen regularly.
-java_genrule {
- name: "framework-minus-apex.ravenwood-base_all",
- defaults: ["framework-minus-apex.ravenwood-base_defaults"],
- cmd: framework_minus_apex_cmd +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
-
- out: [
- "hoststubgen_framework-minus-apex_keep_all.txt",
- "hoststubgen_framework-minus-apex_dump.txt",
- "hoststubgen_framework-minus-apex_stats.csv",
- "hoststubgen_framework-minus-apex_apis.csv",
- ],
-}
-
-// Marge all the sharded jars
-java_genrule {
- name: "framework-minus-apex.ravenwood",
- defaults: ["ravenwood-internal-only-visibility-java"],
- cmd: "$(location merge_zips) $(out) $(in)",
- tools: ["merge_zips"],
- srcs: [
- ":framework-minus-apex.ravenwood-base_X0{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X1{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X2{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X3{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X4{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X5{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X6{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X7{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X8{ravenwood.jar}",
- ":framework-minus-apex.ravenwood-base_X9{ravenwood.jar}",
- ],
- out: [
- "framework-minus-apex.ravenwood.jar",
- ],
-}
+// "framework-minus-apex" and "all-updatable-modules-system-stubs" are not
+// visible publicly. We re-export them to Ravenwood in this file.
java_library {
- name: "services.core-for-hoststubgen",
- installable: false, // host only jar.
- static_libs: [
- "services.core",
- ],
- sdk_version: "core_platform",
- visibility: ["//visibility:private"],
-}
-
-java_genrule {
- name: "services.core.ravenwood-base",
- tools: ["hoststubgen"],
- cmd: "$(location hoststubgen) " +
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_services.core.log) " +
- "--stats-file $(location hoststubgen_services.core_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_services.core_apis.csv) " +
-
- "--out-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_services.core_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_services.core_dump.txt) " +
-
- "--in-jar $(location :services.core-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-services-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
- srcs: [
- ":services.core-for-hoststubgen",
- ":ravenwood-services-policies",
- ":ravenwood-standard-options",
- ":ravenwood-annotation-allowed-classes",
- ],
- out: [
- "ravenwood.jar",
-
- // Following files are created just as FYI.
- "hoststubgen_services.core_keep_all.txt",
- "hoststubgen_services.core_dump.txt",
-
- "hoststubgen_services.core.log",
- "hoststubgen_services.core_stats.csv",
- "hoststubgen_services.core_apis.csv",
- ],
- defaults: ["ravenwood-internal-only-visibility-genrule"],
-}
-
-java_genrule {
- name: "services.core.ravenwood",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cp $(in) $(out)",
- srcs: [
- ":services.core.ravenwood-base{ravenwood.jar}",
- ],
- out: [
- "services.core.ravenwood.jar",
- ],
-}
-
-// TODO(b/313930116) This jarjar is a bit slow. We should use hoststubgen for renaming,
-// but services.core.ravenwood has complex dependencies, so it'll take more than
-// just using hoststubgen "rename"s.
-java_library {
- name: "services.core.ravenwood-jarjar",
- defaults: ["ravenwood-internal-only-visibility-java"],
+ name: "framework-minus-apex-for-host",
installable: false,
- static_libs: [
- "services.core.ravenwood",
- ],
- jarjar_rules: ":ravenwood-services-jarjar-rules",
+ static_libs: ["framework-minus-apex"],
+ visibility: ["//frameworks/base/ravenwood"],
}
-// Jars in "ravenwood-runtime" are set to the classpath, sorted alphabetically.
-// Rename some of the dependencies to make sure they're included in the intended order.
java_library {
- name: "100-framework-minus-apex.ravenwood",
- defaults: ["ravenwood-internal-only-visibility-java"],
- static_libs: [
- "framework-minus-apex.ravenwood",
- ],
- sdk_version: "core_platform",
- // See b/313930116. Jarjar is too slow on this jar. We use HostStubGen to do the rename.
- // jarjar_rules: ":ravenwood-framework-jarjar-rules",
-}
-
-java_genrule {
- // Use 200 to make sure it comes before the mainline stub ("all-updatable...").
- name: "200-kxml2-android",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cp $(in) $(out)",
- srcs: [":kxml2-android"],
- out: ["200-kxml2-android.jar"],
-}
-
-java_genrule {
- name: "z00-all-updatable-modules-system-stubs",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cp $(in) $(out)",
- srcs: [":all-updatable-modules-system-stubs"],
- out: ["z00-all-updatable-modules-system-stubs.jar"],
+ name: "all-updatable-modules-system-stubs-for-host",
+ installable: false,
+ static_libs: ["all-updatable-modules-system-stubs"],
+ visibility: ["//frameworks/base/ravenwood"],
}
diff --git a/apex/jobscheduler/service/aconfig/job.aconfig b/apex/jobscheduler/service/aconfig/job.aconfig
index e5389b4..11c5b51 100644
--- a/apex/jobscheduler/service/aconfig/job.aconfig
+++ b/apex/jobscheduler/service/aconfig/job.aconfig
@@ -75,3 +75,10 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "enforce_quota_policy_to_fgs_jobs"
+ namespace: "backstage_power"
+ description: "Applies the normal quota policy to FGS jobs"
+ bug: "341201311"
+}
diff --git a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
index a1c72fb..03a3a0d 100644
--- a/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
+++ b/apex/jobscheduler/service/java/com/android/server/job/controllers/QuotaController.java
@@ -99,10 +99,10 @@
* the number of jobs or sessions that can run within the window. Regardless of bucket, apps will
* not be allowed to run more than 20 jobs within the past 10 minutes.
*
- * Jobs are throttled while an app is not in a foreground state. All jobs are allowed to run
- * freely when an app enters the foreground state and are restricted when the app leaves the
- * foreground state. However, jobs that are started while the app is in the TOP state do not count
- * towards any quota and are not restricted regardless of the app's state change.
+ * Jobs are throttled while an app is not in a TOP or BOUND_TOP state. All jobs are allowed to run
+ * freely when an app enters the TOP or BOUND_TOP state and are restricted when the app leaves those
+ * states. However, jobs that are started while the app is in the TOP state do not count towards any
+ * quota and are not restricted regardless of the app's state change.
*
* Jobs will not be throttled when the device is charging. The device is considered to be charging
* once the {@link BatteryManager#ACTION_CHARGING} intent has been broadcast.
@@ -567,6 +567,11 @@
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE, null);
+ if (Flags.enforceQuotaPolicyToFgsJobs()) {
+ ActivityManager.getService().registerUidObserver(new QcUidObserver(),
+ ActivityManager.UID_OBSERVER_PROCSTATE,
+ ActivityManager.PROCESS_STATE_BOUND_TOP, null);
+ }
ActivityManager.getService().registerUidObserver(new QcUidObserver(),
ActivityManager.UID_OBSERVER_PROCSTATE,
ActivityManager.PROCESS_STATE_TOP, null);
@@ -2706,6 +2711,12 @@
}
}
+ @VisibleForTesting
+ int getProcessStateQuotaFreeThreshold() {
+ return Flags.enforceQuotaPolicyToFgsJobs() ? ActivityManager.PROCESS_STATE_BOUND_TOP :
+ ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE;
+ }
+
private class QcHandler extends Handler {
QcHandler(Looper looper) {
@@ -2832,15 +2843,15 @@
mTopAppCache.put(uid, true);
mTopAppGraceCache.delete(uid);
if (mForegroundUids.get(uid)) {
- // Went from FGS to TOP. We don't need to reprocess timers or
- // jobs.
+ // Went from a process state with quota free to TOP. We don't
+ // need to reprocess timers or jobs.
break;
}
mForegroundUids.put(uid, true);
isQuotaFree = true;
} else {
final boolean reprocess;
- if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ if (procState <= getProcessStateQuotaFreeThreshold()) {
reprocess = !mForegroundUids.get(uid);
mForegroundUids.put(uid, true);
isQuotaFree = true;
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 7781f88..10206f2 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -3489,41 +3489,41 @@
public static class VirtualDeviceManager.VirtualDevice implements java.lang.AutoCloseable {
method public void addActivityListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
- method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
- method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
+ method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void addActivityPolicyExemption(@NonNull android.content.ComponentName);
+ method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void addActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
method public void addSoundEffectListener(@NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+ method public void close();
method @NonNull public android.content.Context createContext();
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
- method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
- method @Deprecated @Nullable @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
- method @Nullable @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.input.VirtualKeyboardConfig);
- method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
- method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
- method @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull android.hardware.input.VirtualRotaryEncoderConfig);
- method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
- method @Deprecated @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method @NonNull public android.companion.virtual.audio.VirtualAudioDevice createVirtualAudioDevice(@NonNull android.hardware.display.VirtualDisplay, @Nullable java.util.concurrent.Executor, @Nullable android.companion.virtual.audio.VirtualAudioDevice.AudioConfigurationChangeCallback);
+ method @FlaggedApi("android.companion.virtual.flags.virtual_camera") @NonNull public android.companion.virtual.camera.VirtualCamera createVirtualCamera(@NonNull android.companion.virtual.camera.VirtualCameraConfig);
+ method @Deprecated @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@IntRange(from=1) int, @IntRange(from=1) int, @IntRange(from=1) int, @Nullable android.view.Surface, int, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
+ method @Nullable public android.hardware.display.VirtualDisplay createVirtualDisplay(@NonNull android.hardware.display.VirtualDisplayConfig, @Nullable java.util.concurrent.Executor, @Nullable android.hardware.display.VirtualDisplay.Callback);
+ method @NonNull public android.hardware.input.VirtualDpad createVirtualDpad(@NonNull android.hardware.input.VirtualDpadConfig);
+ method @NonNull public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.input.VirtualKeyboardConfig);
+ method @Deprecated @NonNull public android.hardware.input.VirtualKeyboard createVirtualKeyboard(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method @NonNull public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.input.VirtualMouseConfig);
+ method @Deprecated @NonNull public android.hardware.input.VirtualMouse createVirtualMouse(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
+ method @NonNull public android.hardware.input.VirtualNavigationTouchpad createVirtualNavigationTouchpad(@NonNull android.hardware.input.VirtualNavigationTouchpadConfig);
+ method @FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") @NonNull public android.hardware.input.VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull android.hardware.input.VirtualRotaryEncoderConfig);
+ method @FlaggedApi("android.companion.virtual.flags.virtual_stylus") @NonNull public android.hardware.input.VirtualStylus createVirtualStylus(@NonNull android.hardware.input.VirtualStylusConfig);
+ method @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.input.VirtualTouchscreenConfig);
+ method @Deprecated @NonNull public android.hardware.input.VirtualTouchscreen createVirtualTouchscreen(@NonNull android.hardware.display.VirtualDisplay, @NonNull String, int, int);
method public int getDeviceId();
method @FlaggedApi("android.companion.virtual.flags.vdm_public_apis") @Nullable public String getPersistentDeviceId();
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public java.util.List<android.companion.virtual.sensor.VirtualSensor> getVirtualSensorList();
- method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void goToSleep();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
+ method @NonNull public java.util.List<android.companion.virtual.sensor.VirtualSensor> getVirtualSensorList();
+ method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") public void goToSleep();
+ method public void launchPendingIntent(int, @NonNull android.app.PendingIntent, @NonNull java.util.concurrent.Executor, @NonNull java.util.function.IntConsumer);
+ method public void registerIntentInterceptor(@NonNull android.content.IntentFilter, @NonNull java.util.concurrent.Executor, @NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
method public void removeActivityListener(@NonNull android.companion.virtual.VirtualDeviceManager.ActivityListener);
- method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
- method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
+ method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void removeActivityPolicyExemption(@NonNull android.content.ComponentName);
+ method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void removeActivityPolicyExemption(@NonNull android.companion.virtual.ActivityPolicyExemption);
method public void removeSoundEffectListener(@NonNull android.companion.virtual.VirtualDeviceManager.SoundEffectListener);
- method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int);
- method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDevicePolicy(int, int, int);
- method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setDisplayImePolicy(int, int);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void setShowPointerIcon(boolean);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
- method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void wakeUp();
+ method @FlaggedApi("android.companion.virtual.flags.dynamic_policy") public void setDevicePolicy(int, int);
+ method @FlaggedApi("android.companion.virtualdevice.flags.activity_control_api") public void setDevicePolicy(int, int, int);
+ method @FlaggedApi("android.companion.virtual.flags.vdm_custom_ime") public void setDisplayImePolicy(int, int);
+ method public void setShowPointerIcon(boolean);
+ method public void unregisterIntentInterceptor(@NonNull android.companion.virtual.VirtualDeviceManager.IntentInterceptorCallback);
+ method @FlaggedApi("android.companion.virtualdevice.flags.device_aware_display_power") public void wakeUp();
}
public final class VirtualDeviceParams implements android.os.Parcelable {
@@ -3632,7 +3632,7 @@
package android.companion.virtual.camera {
@FlaggedApi("android.companion.virtual.flags.virtual_camera") public final class VirtualCamera implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
+ method public void close();
method @NonNull public android.companion.virtual.camera.VirtualCameraConfig getConfig();
}
@@ -3684,7 +3684,7 @@
method public int getDeviceId();
method @NonNull public String getName();
method public int getType();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
+ method public void sendEvent(@NonNull android.companion.virtual.sensor.VirtualSensorEvent);
method public void writeToParcel(@NonNull android.os.Parcel, int);
field @NonNull public static final android.os.Parcelable.Creator<android.companion.virtual.sensor.VirtualSensor> CREATOR;
}
@@ -5695,8 +5695,8 @@
package android.hardware.input {
public class VirtualDpad implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
+ method public void close();
+ method public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
}
public final class VirtualDpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5747,8 +5747,8 @@
}
public class VirtualKeyboard implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
+ method public void close();
+ method public void sendKeyEvent(@NonNull android.hardware.input.VirtualKeyEvent);
}
public final class VirtualKeyboardConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5769,11 +5769,11 @@
}
public class VirtualMouse implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @NonNull @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public android.graphics.PointF getCursorPosition();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent);
+ method public void close();
+ method @NonNull public android.graphics.PointF getCursorPosition();
+ method public void sendButtonEvent(@NonNull android.hardware.input.VirtualMouseButtonEvent);
+ method public void sendRelativeEvent(@NonNull android.hardware.input.VirtualMouseRelativeEvent);
+ method public void sendScrollEvent(@NonNull android.hardware.input.VirtualMouseScrollEvent);
}
public final class VirtualMouseButtonEvent implements android.os.Parcelable {
@@ -5846,8 +5846,8 @@
}
public class VirtualNavigationTouchpad implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+ method public void close();
+ method public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
}
public final class VirtualNavigationTouchpadConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5864,8 +5864,8 @@
}
@FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") public class VirtualRotaryEncoder implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendScrollEvent(@NonNull android.hardware.input.VirtualRotaryEncoderScrollEvent);
+ method public void close();
+ method public void sendScrollEvent(@NonNull android.hardware.input.VirtualRotaryEncoderScrollEvent);
}
@FlaggedApi("android.companion.virtualdevice.flags.virtual_rotary") public final class VirtualRotaryEncoderConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
@@ -5895,9 +5895,9 @@
}
@FlaggedApi("android.companion.virtual.flags.virtual_stylus") public class VirtualStylus implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
+ method public void close();
+ method public void sendButtonEvent(@NonNull android.hardware.input.VirtualStylusButtonEvent);
+ method public void sendMotionEvent(@NonNull android.hardware.input.VirtualStylusMotionEvent);
}
@FlaggedApi("android.companion.virtual.flags.virtual_stylus") public final class VirtualStylusButtonEvent implements android.os.Parcelable {
@@ -6000,8 +6000,8 @@
}
public class VirtualTouchscreen implements java.io.Closeable {
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void close();
- method @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE) public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
+ method public void close();
+ method public void sendTouchEvent(@NonNull android.hardware.input.VirtualTouchEvent);
}
public final class VirtualTouchscreenConfig extends android.hardware.input.VirtualInputDeviceConfig implements android.os.Parcelable {
diff --git a/core/java/android/app/AppOps.md b/core/java/android/app/AppOps.md
index 7b11a03..535d62c 100644
--- a/core/java/android/app/AppOps.md
+++ b/core/java/android/app/AppOps.md
@@ -119,20 +119,20 @@
In addition to proc state, the `AppOpsService` also receives process capability update from the
`ActivityManagerService`. Proc capability specifies what while-in-use(`MODE_FOREGROUND`) operations
the proc is allowed to perform in its current proc state. There are three proc capabilities
- defined so far:
+ defined so far:
`PROCESS_CAPABILITY_FOREGROUND_LOCATION`, `PROCESS_CAPABILITY_FOREGROUND_CAMERA` and
`PROCESS_CAPABILITY_FOREGROUND_MICROPHONE`, they correspond to the while-in-use operation of
location, camera and microphone (microphone is `RECORD_AUDIO`).
In `ActivityManagerService`, `PROCESS_STATE_TOP` and `PROCESS_STATE_PERSISTENT` have all
three capabilities, `PROCESS_STATE_FOREGROUND_SERVICE` has capabilities defined by
- `foregroundServiceType` that is specified in foreground service's manifest file. A client process
+ `foregroundServiceType` that is specified in foreground service's manifest file. A client process
can pass its capabilities to service using `BIND_INCLUDE_CAPABILITIES` flag.
The proc state and capability are used for two use cases: Firstly, Tracking remembers the proc state
for each tracked event. Secondly, `noteOp`/`checkOp` calls for app-op that are set to
`MODE_FOREGROUND` are translated using the `AppOpsService.UidState.evalMode` method into
- `MODE_ALLOWED` when the app has the capability and `MODE_IGNORED` when the app does not have the
+ `MODE_ALLOWED` when the app has the capability and `MODE_IGNORED` when the app does not have the
capability. `checkOpRaw` calls are not affected.
The current proc state and capability for an app can be read from `dumpsys appops`.
@@ -284,7 +284,7 @@
##### Self data accesses
This is similar to the [synchronous data access](#synchronous-data-accesses) case only that the data
-provider and client are in the same process. In this case Android's RPC code is no involved and
+provider and client are in the same process. In this case Android's RPC code is not involved and
`AppOpsManager.noteOp` directly triggers `OnOpNotedCallback.onSelfNoted`. This should be a uncommon
case as it is uncommon for an app to provide data, esp. to itself.
diff --git a/core/java/android/companion/virtual/IVirtualDevice.aidl b/core/java/android/companion/virtual/IVirtualDevice.aidl
index 40debe8..d3a1c25 100644
--- a/core/java/android/companion/virtual/IVirtualDevice.aidl
+++ b/core/java/android/companion/virtual/IVirtualDevice.aidl
@@ -98,191 +98,160 @@
/*
* Turns off all trusted non-mirror displays of the virtual device.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void goToSleep();
/**
* Turns on all trusted non-mirror displays of the virtual device.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void wakeUp();
/**
* Closes the virtual device and frees all associated resources.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void close();
/**
* Specifies a policy for this virtual device.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setDevicePolicy(int policyType, int devicePolicy);
/**
* Adds an exemption to the default activity launch policy.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void addActivityPolicyExemption(in ActivityPolicyExemption exemption);
/**
* Removes an exemption to the default activity launch policy.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void removeActivityPolicyExemption(in ActivityPolicyExemption exemption);
/**
* Specifies a policy for this virtual device on the given display.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setDevicePolicyForDisplay(int displayId, int policyType, int devicePolicy);
/**
* Notifies that an audio session being started.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionStarting(int displayId, IAudioRoutingCallback routingCallback,
IAudioConfigChangedCallback configChangedCallback);
/**
* Notifies that an audio session has ended.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void onAudioSessionEnded();
/**
* Creates a virtual display and registers it with the display framework.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
int createVirtualDisplay(in VirtualDisplayConfig virtualDisplayConfig,
in IVirtualDisplayCallback callback);
/**
* Creates a new dpad and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualDpad(in VirtualDpadConfig config, IBinder token);
/**
* Creates a new keyboard and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualKeyboard(in VirtualKeyboardConfig config, IBinder token);
/**
* Creates a new mouse and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualMouse(in VirtualMouseConfig config, IBinder token);
/**
* Creates a new touchscreen and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualTouchscreen(in VirtualTouchscreenConfig config, IBinder token);
/**
* Creates a new navigation touchpad and registers it with the input framework with the given
* token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualNavigationTouchpad(in VirtualNavigationTouchpadConfig config, IBinder token);
/**
* Creates a new stylus and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualStylus(in VirtualStylusConfig config, IBinder token);
/**
* Creates a new rotary encoder and registers it with the input framework with the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void createVirtualRotaryEncoder(in VirtualRotaryEncoderConfig config, IBinder token);
/**
* Removes the input device corresponding to the given token from the framework.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterInputDevice(IBinder token);
/**
* Returns the ID of the device corresponding to the given token, as registered with the input
* framework.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
int getInputDeviceId(IBinder token);
/**
* Injects a key event to the virtual dpad corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendDpadKeyEvent(IBinder token, in VirtualKeyEvent event);
/**
* Injects a key event to the virtual keyboard corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendKeyEvent(IBinder token, in VirtualKeyEvent event);
/**
* Injects a button event to the virtual mouse corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendButtonEvent(IBinder token, in VirtualMouseButtonEvent event);
/**
* Injects a relative event to the virtual mouse corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendRelativeEvent(IBinder token, in VirtualMouseRelativeEvent event);
/**
* Injects a scroll event to the virtual mouse corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendScrollEvent(IBinder token, in VirtualMouseScrollEvent event);
/**
* Injects a touch event to the virtual touch input device corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendTouchEvent(IBinder token, in VirtualTouchEvent event);
/**
* Injects a motion event from the virtual stylus input device corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendStylusMotionEvent(IBinder token, in VirtualStylusMotionEvent event);
/**
* Injects a button event from the virtual stylus input device corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendStylusButtonEvent(IBinder token, in VirtualStylusButtonEvent event);
/**
* Injects a scroll event from the virtual rotary encoder corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendRotaryEncoderScrollEvent(IBinder token, in VirtualRotaryEncoderScrollEvent event);
/**
* Returns all virtual sensors created for this device.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
List<VirtualSensor> getVirtualSensorList();
/**
* Sends an event to the virtual sensor corresponding to the given token.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
boolean sendSensorEvent(IBinder token, in VirtualSensorEvent event);
/**
* Launches a pending intent on the given display that is owned by this virtual device.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void launchPendingIntent(int displayId, in PendingIntent pendingIntent,
in ResultReceiver resultReceiver);
@@ -290,15 +259,12 @@
* Returns the current cursor position of the mouse corresponding to the given token, in x and y
* coordinates.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
PointF getCursorPosition(IBinder token);
/** Sets whether to show or hide the cursor while this virtual device is active. */
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setShowPointerIcon(boolean showPointerIcon);
/** Sets an IME policy for the given display. */
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setDisplayImePolicy(int displayId, int policy);
/**
@@ -306,33 +272,28 @@
* when matching the provided IntentFilter and calls the callback with the intercepted
* intent.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void registerIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor,
in IntentFilter filter);
/**
* Unregisters a previously registered intent interceptor.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterIntentInterceptor(in IVirtualDeviceIntentInterceptor intentInterceptor);
/**
* Creates a new virtual camera and registers it with the virtual camera service.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void registerVirtualCamera(in VirtualCameraConfig camera);
/**
* Destroys the virtual camera with given config and unregisters it from the virtual camera
* service.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void unregisterVirtualCamera(in VirtualCameraConfig camera);
/**
* Returns the id of the virtual camera with given config.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
String getVirtualCameraId(in VirtualCameraConfig camera);
/**
@@ -342,7 +303,6 @@
* This is needed for virtual devices that are created by the system, as the VirtualDeviceImpl
* object is created before the returned VirtualDeviceInternal one.
*/
- @EnforcePermission("CREATE_VIRTUAL_DEVICE")
void setListeners(in IVirtualDeviceActivityListener activityListener,
in IVirtualDeviceSoundEffectListener soundEffectListener);
}
diff --git a/core/java/android/companion/virtual/VirtualDeviceInternal.java b/core/java/android/companion/virtual/VirtualDeviceInternal.java
index 6708cce..d63a443 100644
--- a/core/java/android/companion/virtual/VirtualDeviceInternal.java
+++ b/core/java/android/companion/virtual/VirtualDeviceInternal.java
@@ -26,7 +26,6 @@
import android.annotation.CallbackExecutor;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.annotation.RequiresPermission;
import android.annotation.UserIdInt;
import android.app.PendingIntent;
import android.companion.virtual.audio.VirtualAudioDevice;
@@ -242,7 +241,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
List<VirtualSensor> getVirtualSensorList() {
try {
@@ -252,7 +250,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void goToSleep() {
try {
mVirtualDevice.goToSleep();
@@ -261,7 +258,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void wakeUp() {
try {
mVirtualDevice.wakeUp();
@@ -270,7 +266,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void launchPendingIntent(
int displayId,
@NonNull PendingIntent pendingIntent,
@@ -292,7 +287,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Nullable
VirtualDisplay createVirtualDisplay(
@NonNull VirtualDisplayConfig config,
@@ -310,7 +304,6 @@
return displayManager.createVirtualDisplayWrapper(config, callbackWrapper, displayId);
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void close() {
try {
// This also takes care of unregistering all virtual sensors.
@@ -324,7 +317,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
switch (policyType) {
@@ -344,7 +336,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
try {
mVirtualDevice.addActivityPolicyExemption(exemption);
@@ -353,7 +344,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
try {
mVirtualDevice.removeActivityPolicyExemption(exemption);
@@ -362,7 +352,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void setDevicePolicyForDisplay(int displayId,
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
@@ -382,7 +371,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
try {
@@ -395,7 +383,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
try {
@@ -408,7 +395,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
try {
@@ -421,7 +407,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualTouchscreen createVirtualTouchscreen(
@NonNull VirtualTouchscreenConfig config) {
@@ -435,7 +420,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualStylus createVirtualStylus(@NonNull VirtualStylusConfig config) {
try {
@@ -448,7 +432,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualRotaryEncoder createVirtualRotaryEncoder(@NonNull VirtualRotaryEncoderConfig config) {
try {
@@ -461,7 +444,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualNavigationTouchpad createVirtualNavigationTouchpad(
@NonNull VirtualNavigationTouchpadConfig config) {
@@ -476,7 +458,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualAudioDevice createVirtualAudioDevice(
@NonNull VirtualDisplay display,
@@ -501,7 +482,6 @@
return mVirtualAudioDevice;
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
try {
@@ -513,7 +493,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void setShowPointerIcon(boolean showPointerIcon) {
try {
mVirtualDevice.setShowPointerIcon(showPointerIcon);
@@ -522,7 +501,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
try {
mVirtualDevice.setDisplayImePolicy(displayId, policy);
@@ -564,7 +542,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void registerIntentInterceptor(
@NonNull IntentFilter interceptorFilter,
@CallbackExecutor @NonNull Executor executor,
@@ -584,7 +561,6 @@
}
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void unregisterIntentInterceptor(
@NonNull VirtualDeviceManager.IntentInterceptorCallback interceptorCallback) {
Objects.requireNonNull(interceptorCallback);
diff --git a/core/java/android/companion/virtual/VirtualDeviceManager.java b/core/java/android/companion/virtual/VirtualDeviceManager.java
index 96700a9..6ea7834 100644
--- a/core/java/android/companion/virtual/VirtualDeviceManager.java
+++ b/core/java/android/companion/virtual/VirtualDeviceManager.java
@@ -614,7 +614,6 @@
*
* @return A list of all sensors for this device, or an empty list if no sensors exist.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public List<VirtualSensor> getVirtualSensorList() {
return mVirtualDeviceInternal.getVirtualSensorList();
@@ -635,7 +634,6 @@
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void goToSleep() {
mVirtualDeviceInternal.goToSleep();
}
@@ -654,7 +652,6 @@
* @see DisplayManager#VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_DEVICE_AWARE_DISPLAY_POWER)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void wakeUp() {
mVirtualDeviceInternal.wakeUp();
}
@@ -677,7 +674,6 @@
* on the virtual display, or one of the {@code LAUNCH_FAILED} status explaining why it
* failed.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void launchPendingIntent(
int displayId,
@NonNull PendingIntent pendingIntent,
@@ -718,7 +714,6 @@
* VirtualDisplay.Callback)}
*/
@Deprecated
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Nullable
public VirtualDisplay createVirtualDisplay(
@IntRange(from = 1) int width,
@@ -756,7 +751,6 @@
*
* @see DisplayManager#createVirtualDisplay
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Nullable
public VirtualDisplay createVirtualDisplay(
@NonNull VirtualDisplayConfig config,
@@ -770,7 +764,6 @@
* Closes the virtual device, stopping and tearing down any virtual displays, associated
* virtual audio device, and event injection that's currently in progress.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
mVirtualDeviceInternal.close();
}
@@ -789,7 +782,6 @@
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
*/
@FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
mVirtualDeviceInternal.setDevicePolicy(policyType, devicePolicy);
@@ -812,7 +804,6 @@
* @see #setDevicePolicy
*/
@FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void addActivityPolicyExemption(@NonNull ComponentName componentName) {
addActivityPolicyExemption(new ActivityPolicyExemption.Builder()
.setComponentName(componentName)
@@ -836,7 +827,6 @@
* @see #setDevicePolicy
*/
@FlaggedApi(Flags.FLAG_DYNAMIC_POLICY)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void removeActivityPolicyExemption(@NonNull ComponentName componentName) {
removeActivityPolicyExemption(new ActivityPolicyExemption.Builder()
.setComponentName(componentName)
@@ -861,7 +851,6 @@
* @see #setDevicePolicy
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.addActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -877,7 +866,6 @@
* @see #setDevicePolicy
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
mVirtualDeviceInternal.removeActivityPolicyExemption(Objects.requireNonNull(exemption));
}
@@ -900,7 +888,6 @@
* @see VirtualDeviceParams#POLICY_TYPE_ACTIVITY
*/
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_ACTIVITY_CONTROL_API)
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setDevicePolicy(
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy,
@@ -913,7 +900,6 @@
*
* @param config the configurations of the virtual dpad.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualDpad createVirtualDpad(@NonNull VirtualDpadConfig config) {
Objects.requireNonNull(config, "config must not be null");
@@ -925,7 +911,6 @@
*
* @param config the configurations of the virtual keyboard.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualKeyboardConfig config) {
Objects.requireNonNull(config, "config must not be null");
@@ -943,7 +928,6 @@
* @deprecated Use {@link #createVirtualKeyboard(VirtualKeyboardConfig config)} instead
*/
@Deprecated
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualKeyboard createVirtualKeyboard(@NonNull VirtualDisplay display,
@NonNull String inputDeviceName, int vendorId, int productId) {
@@ -962,7 +946,6 @@
*
* @param config the configurations of the virtual mouse.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualMouse createVirtualMouse(@NonNull VirtualMouseConfig config) {
Objects.requireNonNull(config, "config must not be null");
@@ -980,7 +963,6 @@
* @deprecated Use {@link #createVirtualMouse(VirtualMouseConfig config)} instead
*/
@Deprecated
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualMouse createVirtualMouse(@NonNull VirtualDisplay display,
@NonNull String inputDeviceName, int vendorId, int productId) {
@@ -999,7 +981,6 @@
*
* @param config the configurations of the virtual touchscreen.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualTouchscreen createVirtualTouchscreen(
@NonNull VirtualTouchscreenConfig config) {
@@ -1019,7 +1000,6 @@
* instead
*/
@Deprecated
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualTouchscreen createVirtualTouchscreen(@NonNull VirtualDisplay display,
@NonNull String inputDeviceName, int vendorId, int productId) {
@@ -1046,7 +1026,6 @@
* @param config the configurations of the virtual navigation touchpad.
* @see android.view.InputDevice#SOURCE_TOUCH_NAVIGATION
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualNavigationTouchpad createVirtualNavigationTouchpad(
@NonNull VirtualNavigationTouchpadConfig config) {
@@ -1058,7 +1037,6 @@
*
* @param config the touchscreen configurations for the virtual stylus.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
@FlaggedApi(Flags.FLAG_VIRTUAL_STYLUS)
public VirtualStylus createVirtualStylus(
@@ -1072,7 +1050,6 @@
* @param config the configuration for the virtual rotary encoder.
* @see android.view.InputDevice#SOURCE_ROTARY_ENCODER
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
@FlaggedApi(android.companion.virtualdevice.flags.Flags.FLAG_VIRTUAL_ROTARY)
public VirtualRotaryEncoder createVirtualRotaryEncoder(
@@ -1100,7 +1077,6 @@
* applications running on virtual display is changed.
* @return A {@link VirtualAudioDevice} instance.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
public VirtualAudioDevice createVirtualAudioDevice(
@NonNull VirtualDisplay display,
@@ -1121,7 +1097,6 @@
* @throws UnsupportedOperationException if virtual camera isn't supported on this device.
* @see VirtualDeviceParams#POLICY_TYPE_CAMERA
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@NonNull
@FlaggedApi(Flags.FLAG_VIRTUAL_CAMERA)
public VirtualCamera createVirtualCamera(@NonNull VirtualCameraConfig config) {
@@ -1135,10 +1110,12 @@
/**
* Sets the visibility of the pointer icon for this VirtualDevice's associated displays.
*
+ * <p>Only applicable to trusted displays.</p>
+ *
* @param showPointerIcon True if the pointer should be shown; false otherwise. The default
* visibility is true.
+ * @see DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
mVirtualDeviceInternal.setShowPointerIcon(showPointerIcon);
}
@@ -1153,7 +1130,6 @@
* @throws SecurityException if the display is not owned by this device or is not
* {@link DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED trusted}
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@FlaggedApi(Flags.FLAG_VDM_CUSTOM_IME)
public void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
if (Flags.vdmCustomIme()) {
@@ -1217,7 +1193,6 @@
* is intercepted.
* @see #unregisterIntentInterceptor(IntentInterceptorCallback)
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerIntentInterceptor(
@NonNull IntentFilter interceptorFilter,
@CallbackExecutor @NonNull Executor executor,
@@ -1230,7 +1205,6 @@
* Unregisters the intent interceptor previously registered with
* {@link #registerIntentInterceptor}.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterIntentInterceptor(
@NonNull IntentInterceptorCallback interceptorCallback) {
mVirtualDeviceInternal.unregisterIntentInterceptor(interceptorCallback);
diff --git a/core/java/android/companion/virtual/VirtualDeviceParams.java b/core/java/android/companion/virtual/VirtualDeviceParams.java
index 03b72bd..65f9cbe 100644
--- a/core/java/android/companion/virtual/VirtualDeviceParams.java
+++ b/core/java/android/companion/virtual/VirtualDeviceParams.java
@@ -159,7 +159,7 @@
* @hide
*/
@IntDef(prefix = "POLICY_TYPE_", value = {POLICY_TYPE_SENSORS, POLICY_TYPE_AUDIO,
- POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CAMERA,
+ POLICY_TYPE_RECENTS, POLICY_TYPE_ACTIVITY, POLICY_TYPE_CLIPBOARD, POLICY_TYPE_CAMERA,
POLICY_TYPE_BLOCKED_ACTIVITY})
@Retention(RetentionPolicy.SOURCE)
@Target({ElementType.TYPE_PARAMETER, ElementType.TYPE_USE})
@@ -220,11 +220,16 @@
* Tells the activity manager how to handle recents entries for activities run on this device.
*
* <ul>
- * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on VirtualDisplays owned by this
+ * <li>{@link #DEVICE_POLICY_DEFAULT}: Activities launched on trusted displays owned by this
* device will appear in the host device recents.
- * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on VirtualDisplays owned by this
+ * <li>{@link #DEVICE_POLICY_CUSTOM}: Activities launched on trusted displays owned by this
* device will not appear in recents.
* </ul>
+ *
+ * <p>Activities launched on untrusted displays will always show in the host device recents,
+ * regardless of the policy.</p>
+ *
+ * @see android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
public static final int POLICY_TYPE_RECENTS = 2;
@@ -254,8 +259,10 @@
* not shared with other devices' clipboards, including the clipboard of the default device.
* <li>{@link #DEVICE_POLICY_CUSTOM}: The device's clipboard is shared with the default
* device's clipboard. Any clipboard operation on the virtual device is as if it was done on
- * the default device.
+ * the default device. Requires all displays of the virtual device to be trusted.
* </ul>
+ *
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
*/
@FlaggedApi(Flags.FLAG_CROSS_DEVICE_CLIPBOARD)
public static final int POLICY_TYPE_CLIPBOARD = 4;
@@ -821,8 +828,8 @@
}
/**
- * Specifies a component to be used as input method on all displays owned by this virtual
- * device.
+ * Specifies a component to be used as input method on all trusted displays owned by this
+ * virtual device.
*
* @param inputMethodComponent The component name to be used as input method. Must comply to
* all general input method requirements described in the guide to
@@ -831,6 +838,7 @@
* may interact with the virtual device, then there will effectively be no IME on this
* device's displays for that user.
*
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
* @see android.inputmethodservice.InputMethodService
* @attr ref android.R.styleable#InputMethod_isVirtualDeviceOnly
* @attr ref android.R.styleable#InputMethod_showInInputMethodPicker
diff --git a/core/java/android/companion/virtual/camera/VirtualCamera.java b/core/java/android/companion/virtual/camera/VirtualCamera.java
index f727589..ece048d 100644
--- a/core/java/android/companion/virtual/camera/VirtualCamera.java
+++ b/core/java/android/companion/virtual/camera/VirtualCamera.java
@@ -17,7 +17,6 @@
package android.companion.virtual.camera;
import android.annotation.FlaggedApi;
-import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -94,7 +93,6 @@
}
@Override
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
try {
mVirtualDevice.unregisterVirtualCamera(mConfig);
diff --git a/core/java/android/companion/virtual/sensor/VirtualSensor.java b/core/java/android/companion/virtual/sensor/VirtualSensor.java
index 37e494b..934a1a8 100644
--- a/core/java/android/companion/virtual/sensor/VirtualSensor.java
+++ b/core/java/android/companion/virtual/sensor/VirtualSensor.java
@@ -17,7 +17,6 @@
package android.companion.virtual.sensor;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -136,7 +135,6 @@
/**
* Send a sensor event to the system.
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendEvent(@NonNull VirtualSensorEvent event) {
try {
mVirtualDevice.sendSensorEvent(mToken, event);
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index dcf82bf..ff0a3dd 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -47,13 +47,6 @@
}
flag {
- name: "start_user_before_scheduled_alarms"
- namespace: "multiuser"
- description: "Persist list of users with alarms scheduled and wakeup stopped users before alarms are due"
- bug: "314907186"
-}
-
-flag {
name: "add_ui_for_sounds_from_background_users"
namespace: "multiuser"
description: "Allow foreground user to dismiss sounds that are coming from background users"
diff --git a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
index 1845827..739dbe1 100644
--- a/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
+++ b/core/java/android/hardware/devicestate/DeviceStateManagerGlobal.java
@@ -32,6 +32,7 @@
import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.annotations.VisibleForTesting.Visibility;
+import com.android.window.flags.Flags;
import java.util.ArrayList;
import java.util.List;
@@ -47,6 +48,7 @@
@VisibleForTesting(visibility = Visibility.PACKAGE)
public final class DeviceStateManagerGlobal {
@Nullable
+ @GuardedBy("DeviceStateManagerGlobal.class")
private static DeviceStateManagerGlobal sInstance;
private static final String TAG = "DeviceStateManagerGlobal";
private static final boolean DEBUG = Build.IS_DEBUGGABLE;
@@ -84,10 +86,12 @@
@GuardedBy("mLock")
private DeviceStateInfo mLastReceivedInfo;
+ // Constructor should be called while holding the lock.
+ // @GuardedBy("DeviceStateManagerGlobal.class") can't be used on constructors.
@VisibleForTesting
public DeviceStateManagerGlobal(@NonNull IDeviceStateManager deviceStateManager) {
mDeviceStateManager = deviceStateManager;
- registerCallbackIfNeededLocked();
+ registerCallbackLocked();
}
/**
@@ -279,14 +283,17 @@
}
}
- private void registerCallbackIfNeededLocked() {
- if (mCallback != null) {
- return;
- }
-
+ @GuardedBy("DeviceStateManagerGlobal.class")
+ private void registerCallbackLocked() {
mCallback = new DeviceStateManagerCallback();
try {
- mDeviceStateManager.registerCallback(mCallback);
+ if (Flags.wlinfoOncreate()) {
+ synchronized (mLock) {
+ mLastReceivedInfo = mDeviceStateManager.registerCallback(mCallback);
+ }
+ } else {
+ mDeviceStateManager.registerCallback(mCallback);
+ }
} catch (RemoteException ex) {
mCallback = null;
throw ex.rethrowFromSystemServer();
diff --git a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
index ea4fe26..50d0623 100644
--- a/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
+++ b/core/java/android/hardware/devicestate/IDeviceStateManager.aidl
@@ -32,16 +32,24 @@
DeviceStateInfo getDeviceStateInfo();
/**
- * Registers a callback to receive notifications from the device state manager. Only one
- * callback can be registered per-process.
+ * Registers a callback to receive notifications from the device state manager and returns the
+ * current {@link DeviceStateInfo}. Only one callback can be registered per-process.
* <p>
* As the callback mechanism is used to alert the caller of changes to request status a callback
* <b>MUST</b> be registered before calling {@link #requestState(IBinder, int, int)} or
* {@link #cancelRequest(IBinder)}, otherwise an exception will be thrown.
+ * <p>
+ * Upon successful registration, this method returns the committed {@link DeviceStateInfo} if
+ * available, ensuring the availability of the device state after the callback is registered.
+ * This guarantees that the client will have access to the latest device state immediately upon
+ * completion of the two-way IPC call.
*
+ * @param callback the callback to register for device state notifications.
+ * @return DeviceStateInfo the current device state information including the committed state
+ * or null if no state has been committed by the {@link DeviceStateProvider} yet.
* @throws SecurityException if a callback is already registered for the calling process.
*/
- void registerCallback(in IDeviceStateManagerCallback callback);
+ @nullable DeviceStateInfo registerCallback(in IDeviceStateManagerCallback callback);
/**
* Requests that the device enter the supplied {@code state}. A callback <b>MUST</b> have been
diff --git a/core/java/android/hardware/display/DisplayManagerInternal.java b/core/java/android/hardware/display/DisplayManagerInternal.java
index 6c1aa90..75ffcc3 100644
--- a/core/java/android/hardware/display/DisplayManagerInternal.java
+++ b/core/java/android/hardware/display/DisplayManagerInternal.java
@@ -461,6 +461,16 @@
public abstract void stylusGestureStarted(long eventTime);
/**
+ * Called by {@link com.android.server.wm.ContentRecorder} to verify whether
+ * the display is allowed to mirror primary display's content.
+ * @param displayId the id of the display where we mirror to.
+ * @return true if the mirroring dialog is confirmed (display is enabled), or
+ * {@link com.android.server.display.ExternalDisplayPolicy#ENABLE_ON_CONNECT}
+ * system property is enabled.
+ */
+ public abstract boolean isDisplayReadyForMirroring(int displayId);
+
+ /**
* Describes the requested power state of the display.
*
* This object is intended to describe the general characteristics of the
diff --git a/core/java/android/hardware/input/InputSettings.java b/core/java/android/hardware/input/InputSettings.java
index 177ee6f..897ce4a 100644
--- a/core/java/android/hardware/input/InputSettings.java
+++ b/core/java/android/hardware/input/InputSettings.java
@@ -24,6 +24,8 @@
import static com.android.hardware.input.Flags.keyboardA11ySlowKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yStickyKeysFlag;
import static com.android.hardware.input.Flags.keyboardA11yMouseKeys;
+import static com.android.hardware.input.Flags.mouseReverseVerticalScrolling;
+import static com.android.hardware.input.Flags.mouseSwapPrimaryButton;
import static com.android.hardware.input.Flags.touchpadTapDragging;
import static com.android.hardware.input.Flags.touchpadVisualizer;
import static com.android.input.flags.Flags.enableInputFilterRustImpl;
@@ -363,6 +365,22 @@
}
/**
+ * Returns true if the feature flag for mouse reverse vertical scrolling is enabled.
+ * @hide
+ */
+ public static boolean isMouseReverseVerticalScrollingFeatureFlagEnabled() {
+ return mouseReverseVerticalScrolling();
+ }
+
+ /**
+ * Returns true if the feature flag for mouse swap primary button is enabled.
+ * @hide
+ */
+ public static boolean isMouseSwapPrimaryButtonFeatureFlagEnabled() {
+ return mouseSwapPrimaryButton();
+ }
+
+ /**
* Returns true if the touchpad visualizer is allowed to appear.
*
* @param context The application context.
@@ -501,6 +519,86 @@
}
/**
+ * Whether mouse vertical scrolling is enabled, this applies only to connected mice.
+ *
+ * @param context The application context.
+ * @return Whether the mouse will have its vertical scrolling reversed
+ * (scroll down to move up).
+ *
+ * @hide
+ */
+ public static boolean isMouseReverseVerticalScrollingEnabled(@NonNull Context context) {
+ if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+ return false;
+ }
+
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, 0, UserHandle.USER_CURRENT)
+ != 0;
+ }
+
+ /**
+ * Sets whether the connected mouse will have its vertical scrolling reversed.
+ *
+ * @param context The application context.
+ * @param reverseScrolling Whether reverse scrolling is enabled.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setMouseReverseVerticalScrolling(@NonNull Context context,
+ boolean reverseScrolling) {
+ if (!isMouseReverseVerticalScrollingFeatureFlagEnabled()) {
+ return;
+ }
+
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING, reverseScrolling ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
+ * Whether the primary mouse button is swapped on connected mice.
+ *
+ * @param context The application context.
+ * @return Whether mice will have their primary buttons swapped, so that left clicking will
+ * perform the secondary action (e.g. show menu) and right clicking will perform the primary
+ * action.
+ *
+ * @hide
+ */
+ public static boolean isMouseSwapPrimaryButtonEnabled(@NonNull Context context) {
+ if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+ return false;
+ }
+
+ return Settings.System.getIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, 0, UserHandle.USER_CURRENT)
+ != 0;
+ }
+
+ /**
+ * Sets whether mice will have their primary buttons swapped between left and right
+ * clicks.
+ *
+ * @param context The application context.
+ * @param swapPrimaryButton Whether swapping the primary button is enabled.
+ *
+ * @hide
+ */
+ @RequiresPermission(Manifest.permission.WRITE_SETTINGS)
+ public static void setMouseSwapPrimaryButton(@NonNull Context context,
+ boolean swapPrimaryButton) {
+ if (!isMouseSwapPrimaryButtonFeatureFlagEnabled()) {
+ return;
+ }
+
+ Settings.System.putIntForUser(context.getContentResolver(),
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON, swapPrimaryButton ? 1 : 0,
+ UserHandle.USER_CURRENT);
+ }
+
+ /**
* Whether Accessibility bounce keys feature is enabled.
*
* <p>
diff --git a/core/java/android/hardware/input/VirtualDpad.java b/core/java/android/hardware/input/VirtualDpad.java
index 5985c39..5b08a0e 100644
--- a/core/java/android/hardware/input/VirtualDpad.java
+++ b/core/java/android/hardware/input/VirtualDpad.java
@@ -17,7 +17,6 @@
package android.hardware.input;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
@@ -72,7 +71,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendKeyEvent(@NonNull VirtualKeyEvent event) {
try {
if (!mSupportedKeyCodes.contains(event.getKeyCode())) {
diff --git a/core/java/android/hardware/input/VirtualInputDevice.java b/core/java/android/hardware/input/VirtualInputDevice.java
index affa4ed..8e4e097 100644
--- a/core/java/android/hardware/input/VirtualInputDevice.java
+++ b/core/java/android/hardware/input/VirtualInputDevice.java
@@ -16,7 +16,6 @@
package android.hardware.input;
-import android.annotation.RequiresPermission;
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
import android.os.RemoteException;
@@ -68,7 +67,6 @@
}
@Override
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
Log.d(TAG, "Closing virtual input device " + mConfig.getInputDeviceName());
try {
diff --git a/core/java/android/hardware/input/VirtualInputDeviceConfig.java b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
index e8ef8cd..3b74d7f 100644
--- a/core/java/android/hardware/input/VirtualInputDeviceConfig.java
+++ b/core/java/android/hardware/input/VirtualInputDeviceConfig.java
@@ -163,7 +163,6 @@
return self();
}
-
/**
* Sets the product id of the device, uniquely identifying the device within the address
* space of a given vendor, identified by the device's vendor id.
@@ -179,6 +178,10 @@
*
* <p>The input device is restricted to the display with the given ID and may not send
* events to any other display.</p>
+ * <p>The corresponding display must be trusted or mirror display.</p>
+ *
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_TRUSTED
+ * @see android.hardware.display.DisplayManager#VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR
*/
@NonNull
public T setAssociatedDisplayId(int displayId) {
diff --git a/core/java/android/hardware/input/VirtualKeyboard.java b/core/java/android/hardware/input/VirtualKeyboard.java
index 6a7d195..9664004 100644
--- a/core/java/android/hardware/input/VirtualKeyboard.java
+++ b/core/java/android/hardware/input/VirtualKeyboard.java
@@ -17,7 +17,6 @@
package android.hardware.input;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SuppressLint;
import android.annotation.SystemApi;
import android.annotation.TestApi;
@@ -31,8 +30,8 @@
* A virtual keyboard representing a key input mechanism on a remote device, such as a built-in
* keyboard on a laptop, a software keyboard on a tablet, or a keypad on a TV remote control.
*
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
*
* @hide
*/
@@ -52,7 +51,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendKeyEvent(@NonNull VirtualKeyEvent event) {
try {
if (mUnsupportedKeyCode == event.getKeyCode()) {
diff --git a/core/java/android/hardware/input/VirtualMouse.java b/core/java/android/hardware/input/VirtualMouse.java
index fb0f700..f2d113c 100644
--- a/core/java/android/hardware/input/VirtualMouse.java
+++ b/core/java/android/hardware/input/VirtualMouse.java
@@ -17,7 +17,6 @@
package android.hardware.input;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.graphics.PointF;
@@ -30,8 +29,8 @@
* A virtual mouse representing a relative input mechanism on a remote device, such as a mouse or
* trackpad.
*
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
*
* @hide
*/
@@ -50,7 +49,6 @@
* @throws IllegalStateException if the display this mouse is associated with is not currently
* targeted
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendButtonEvent(@NonNull VirtualMouseButtonEvent event) {
try {
if (!mVirtualDevice.sendButtonEvent(mToken, event)) {
@@ -70,7 +68,6 @@
* @throws IllegalStateException if the display this mouse is associated with is not currently
* targeted
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendScrollEvent(@NonNull VirtualMouseScrollEvent event) {
try {
if (!mVirtualDevice.sendScrollEvent(mToken, event)) {
@@ -89,7 +86,6 @@
* @throws IllegalStateException if the display this mouse is associated with is not currently
* targeted
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendRelativeEvent(@NonNull VirtualMouseRelativeEvent event) {
try {
if (!mVirtualDevice.sendRelativeEvent(mToken, event)) {
@@ -108,7 +104,6 @@
* @throws IllegalStateException if the display this mouse is associated with is not currently
* targeted
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public @NonNull PointF getCursorPosition() {
try {
return mVirtualDevice.getCursorPosition(mToken);
diff --git a/core/java/android/hardware/input/VirtualNavigationTouchpad.java b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
index 3dbb385..94e2151 100644
--- a/core/java/android/hardware/input/VirtualNavigationTouchpad.java
+++ b/core/java/android/hardware/input/VirtualNavigationTouchpad.java
@@ -17,7 +17,6 @@
package android.hardware.input;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
@@ -51,7 +50,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
try {
if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualRotaryEncoder.java b/core/java/android/hardware/input/VirtualRotaryEncoder.java
index bc131df..47c92c8 100644
--- a/core/java/android/hardware/input/VirtualRotaryEncoder.java
+++ b/core/java/android/hardware/input/VirtualRotaryEncoder.java
@@ -18,7 +18,6 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtualdevice.flags.Flags;
@@ -48,7 +47,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendScrollEvent(@NonNull VirtualRotaryEncoderScrollEvent event) {
try {
if (!mVirtualDevice.sendRotaryEncoderScrollEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualStylus.java b/core/java/android/hardware/input/VirtualStylus.java
index c763f740..4b79bc4 100644
--- a/core/java/android/hardware/input/VirtualStylus.java
+++ b/core/java/android/hardware/input/VirtualStylus.java
@@ -18,7 +18,6 @@
import android.annotation.FlaggedApi;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.companion.virtual.flags.Flags;
@@ -30,8 +29,8 @@
* A virtual stylus which can be used to inject input into the framework that represents a stylus
* on a remote device.
*
- * This registers an {@link android.view.InputDevice} that is interpreted like a
- * physically-connected device and dispatches received events to it.
+ * <p>This registers an {@link android.view.InputDevice} that is interpreted like a
+ * physically-connected device and dispatches received events to it.</p>
*
* @hide
*/
@@ -49,7 +48,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendMotionEvent(@NonNull VirtualStylusMotionEvent event) {
try {
if (!mVirtualDevice.sendStylusMotionEvent(mToken, event)) {
@@ -66,7 +64,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendButtonEvent(@NonNull VirtualStylusButtonEvent event) {
try {
if (!mVirtualDevice.sendStylusButtonEvent(mToken, event)) {
diff --git a/core/java/android/hardware/input/VirtualTouchscreen.java b/core/java/android/hardware/input/VirtualTouchscreen.java
index 2c800aa..d0537f0 100644
--- a/core/java/android/hardware/input/VirtualTouchscreen.java
+++ b/core/java/android/hardware/input/VirtualTouchscreen.java
@@ -17,7 +17,6 @@
package android.hardware.input;
import android.annotation.NonNull;
-import android.annotation.RequiresPermission;
import android.annotation.SystemApi;
import android.companion.virtual.IVirtualDevice;
import android.os.IBinder;
@@ -27,8 +26,8 @@
/**
* A virtual touchscreen representing a touch-based display input mechanism on a remote device.
*
- * This registers an InputDevice that is interpreted like a physically-connected device and
- * dispatches received events to it.
+ * <p>This registers an InputDevice that is interpreted like a physically-connected device and
+ * dispatches received events to it.</p>
*
* @hide
*/
@@ -45,7 +44,6 @@
*
* @param event the event to send
*/
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void sendTouchEvent(@NonNull VirtualTouchEvent event) {
try {
if (!mVirtualDevice.sendTouchEvent(mToken, event)) {
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 3ae9511..f1964e7 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -6368,8 +6368,12 @@
Settings.Global.DEVICE_DEMO_MODE, 0) > 0;
}
- /** @hide */
- public static final void invalidateUserSerialNumberCache() {
+
+ /**
+ * This method is used to invalidate caches, when user was added or removed.
+ * @hide
+ */
+ public static final void invalidateCacheOnUserListChange() {
UserManagerCache.invalidateUserSerialNumber();
}
@@ -6382,7 +6386,7 @@
* @hide
*/
@UnsupportedAppUsage
- @CachedProperty(modsFlagOnOrNone = {})
+ @CachedProperty(modsFlagOnOrNone = {}, api = "user_manager_users")
public int getUserSerialNumber(@UserIdInt int userId) {
// Read only flag should is to fix early access to this API
// cacheUserSerialNumber to be removed after the
diff --git a/core/java/android/os/flags.aconfig b/core/java/android/os/flags.aconfig
index a1bfe39..81987907 100644
--- a/core/java/android/os/flags.aconfig
+++ b/core/java/android/os/flags.aconfig
@@ -232,3 +232,19 @@
bug: "361329788"
is_exported: true
}
+
+flag {
+ name: "enable_angle_allow_list"
+ namespace: "gpu"
+ description: "Whether to read from angle allowlist to determine if app should use ANGLE"
+ is_fixed_read_only: true
+ bug: "370845648"
+}
+
+flag {
+ name: "api_for_backported_fixes"
+ namespace: "media_reliability"
+ description: "Public API app developers use to check if a known issue is fixed on a device."
+ bug: "308461809"
+ is_exported: true
+}
diff --git a/core/java/android/permission/flags.aconfig b/core/java/android/permission/flags.aconfig
index 271970b..1d8fcec 100644
--- a/core/java/android/permission/flags.aconfig
+++ b/core/java/android/permission/flags.aconfig
@@ -261,7 +261,7 @@
is_fixed_read_only: true
namespace: "permissions"
description: "If proc state is decreasing over the restriction threshold and capability is changed, delay if no new capabilities are added"
- bug: "308573169"
+ bug: "347891382"
metadata {
purpose: PURPOSE_BUGFIX
}
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 1a15d09..594005c 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2351,6 +2351,11 @@
/**
* Activity Action: Show the permission screen for allowing apps to post promoted notifications.
+ * Properly formatted priority notifications are elevated in appearance. For example they may be
+ * able to use colors, have richer progress bars, show as chips in the status bar, and/or
+ * permanently appear on always-on-displays. This functionality is intended to be reserved for
+ * user initiated ongoing activities like navigation, phone calls, and ride sharing.
+ *
* <p>
* Input: {@link #EXTRA_APP_PACKAGE}, the package to display.
* <p>
@@ -6205,6 +6210,25 @@
public static final String TOUCHPAD_RIGHT_CLICK_ZONE = "touchpad_right_click_zone";
/**
+ * Whether to enable reversed vertical scrolling for connected mice.
+ *
+ * When enabled, scrolling down on the mouse wheel will move the screen up and vice versa.
+ * @hide
+ */
+ public static final String MOUSE_REVERSE_VERTICAL_SCROLLING =
+ "mouse_reverse_vertical_scrolling";
+
+ /**
+ * Whether to enable swapping the primary button for connected mice.
+ *
+ * When enabled, right clicking will be the primary button and left clicking will be the
+ * secondary button (e.g. show menu).
+ * @hide
+ */
+ public static final String MOUSE_SWAP_PRIMARY_BUTTON =
+ "mouse_swap_primary_button";
+
+ /**
* Pointer fill style, specified by
* {@link android.view.PointerIcon.PointerIconVectorStyleFill} constants.
*
@@ -6442,6 +6466,8 @@
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION);
PRIVATE_SETTINGS.add(SCREEN_FLASH_NOTIFICATION_COLOR);
PRIVATE_SETTINGS.add(DEFAULT_DEVICE_FONT_SCALE);
+ PRIVATE_SETTINGS.add(MOUSE_REVERSE_VERTICAL_SCROLLING);
+ PRIVATE_SETTINGS.add(MOUSE_SWAP_PRIMARY_BUTTON);
}
/**
diff --git a/core/java/android/window/DesktopModeFlags.java b/core/java/android/window/DesktopModeFlags.java
index 6fb82af..8e35843e 100644
--- a/core/java/android/window/DesktopModeFlags.java
+++ b/core/java/android/window/DesktopModeFlags.java
@@ -65,7 +65,9 @@
ENABLE_DESKTOP_WINDOWING_TASKBAR_RUNNING_APPS(
Flags::enableDesktopWindowingTaskbarRunningApps, true),
ENABLE_DESKTOP_WINDOWING_TRANSITIONS(Flags::enableDesktopWindowingTransitions, false),
- ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false);
+ ENABLE_DESKTOP_WINDOWING_EXIT_TRANSITIONS(Flags::enableDesktopWindowingExitTransitions, false),
+ ENABLE_WINDOWING_TRANSITION_HANDLERS_OBSERVERS(
+ Flags::enableWindowingTransitionHandlersObservers, false);
private static final String TAG = "DesktopModeFlagsUtil";
// Function called to obtain aconfig flag value.
diff --git a/core/java/android/window/flags/lse_desktop_experience.aconfig b/core/java/android/window/flags/lse_desktop_experience.aconfig
index 31bb3a6..155494f 100644
--- a/core/java/android/window/flags/lse_desktop_experience.aconfig
+++ b/core/java/android/window/flags/lse_desktop_experience.aconfig
@@ -228,7 +228,7 @@
name: "enable_desktop_windowing_app_handle_education"
namespace: "lse_desktop_experience"
description: "Enables desktop windowing app handle education"
- bug: "348208342"
+ bug: "316006079"
}
flag {
diff --git a/core/java/com/android/internal/util/MemInfoReader.java b/core/java/com/android/internal/util/MemInfoReader.java
index 0c5c853..d34bca6 100644
--- a/core/java/com/android/internal/util/MemInfoReader.java
+++ b/core/java/com/android/internal/util/MemInfoReader.java
@@ -88,6 +88,13 @@
}
/**
+ * Amount of RAM that used by shared memory (shmem) and tmpfs
+ */
+ public long getShmemSizeKb() {
+ return mInfos[Debug.MEMINFO_SHMEM];
+ }
+
+ /**
* Amount of RAM that the kernel is being used for caches, not counting caches
* that are mapped in to processes.
*/
diff --git a/core/proto/android/providers/settings/system.proto b/core/proto/android/providers/settings/system.proto
index e795e809..9779dc0e 100644
--- a/core/proto/android/providers/settings/system.proto
+++ b/core/proto/android/providers/settings/system.proto
@@ -220,6 +220,15 @@
}
optional Touchpad touchpad = 36;
+ message Mouse {
+ option (android.msg_privacy).dest = DEST_EXPLICIT;
+
+ optional SettingProto reverse_vertical_scrolling = 1 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ optional SettingProto swap_primary_button = 2 [ (android.privacy).dest = DEST_AUTOMATIC ];
+ }
+
+ optional Mouse mouse = 38;
+
optional SettingProto tty_mode = 31 [ (android.privacy).dest = DEST_AUTOMATIC ];
message Vibrate {
@@ -277,5 +286,5 @@
// Please insert fields in alphabetical order and group them into messages
// if possible (to avoid reaching the method limit).
- // Next tag = 38;
+ // Next tag = 39;
}
diff --git a/core/res/AndroidManifest.xml b/core/res/AndroidManifest.xml
index 549f8df..5693d66 100644
--- a/core/res/AndroidManifest.xml
+++ b/core/res/AndroidManifest.xml
@@ -155,6 +155,7 @@
<protected-broadcast android:name="android.bluetooth.intent.DISCOVERABLE_TIMEOUT" />
<protected-broadcast android:name="android.bluetooth.action.AUTO_ON_STATE_CHANGED" />
+ <protected-broadcast android:name="android.bluetooth.action.CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.adapter.action.STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.adapter.action.SCAN_MODE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.adapter.action.DISCOVERY_STARTED" />
@@ -240,6 +241,8 @@
<protected-broadcast
android:name="android.bluetooth.avrcp-controller.profile.action.FOLDER_LIST" />
<protected-broadcast
+ android:name="android.bluetooth.avrcp-controller.profile.action.PLAYER_SETTING" />
+ <protected-broadcast
android:name="android.bluetooth.avrcp-controller.profile.action.TRACK_EVENT" />
<protected-broadcast
android:name="android.bluetooth.input.profile.action.CONNECTION_STATE_CHANGED" />
@@ -266,6 +269,7 @@
<protected-broadcast
android:name="android.bluetooth.pan.profile.action.CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.HAP_CONNECTION_STATE_CHANGED" />
+ <protected-broadcast android:name="android.bluetooth.action.HAP_DEVICE_AVAILABLE" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONNECTION_STATE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_ACTIVE_DEVICE_CHANGED" />
<protected-broadcast android:name="android.bluetooth.action.LE_AUDIO_CONF_CHANGED" />
diff --git a/core/tests/coretests/src/android/app/ActivityManagerTest.java b/core/tests/coretests/src/android/app/ActivityManagerTest.java
index d850f86..85ff846 100644
--- a/core/tests/coretests/src/android/app/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/ActivityManagerTest.java
@@ -60,7 +60,6 @@
public void testProcState() throws Exception {
// For the moment mostly want to confirm we don't crash
assertNotNull(ActivityManager.procStateToString(PROCESS_STATE_SERVICE));
- assertNotNull(ActivityManager.processStateAmToProto(PROCESS_STATE_SERVICE));
assertTrue(ActivityManager.isProcStateBackground(PROCESS_STATE_SERVICE));
assertFalse(ActivityManager.isProcStateCached(PROCESS_STATE_SERVICE));
assertFalse(ActivityManager.isForegroundService(PROCESS_STATE_SERVICE));
diff --git a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
index b972882..cd52421 100644
--- a/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
+++ b/core/tests/coretests/src/android/app/activity/ActivityManagerTest.java
@@ -111,12 +111,6 @@
assertEquals(config.reqKeyboardType, vconfig.keyboard);
assertEquals(config.reqTouchScreen, vconfig.touchscreen);
assertEquals(config.reqNavigation, vconfig.navigation);
- if (vconfig.navigation == Configuration.NAVIGATION_NONAV) {
- assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_FIVE_WAY_NAV);
- }
- if (vconfig.keyboard != Configuration.KEYBOARD_UNDEFINED) {
- assertNotNull(config.reqInputFeatures & ConfigurationInfo.INPUT_FEATURE_HARD_KEYBOARD);
- }
}
@SmallTest
diff --git a/core/tests/devicestatetests/Android.bp b/core/tests/devicestatetests/Android.bp
index a2aa62d..e573a51 100644
--- a/core/tests/devicestatetests/Android.bp
+++ b/core/tests/devicestatetests/Android.bp
@@ -28,8 +28,10 @@
static_libs: [
"androidx.test.ext.junit",
"androidx.test.rules",
+ "flag-junit",
"frameworks-base-testutils",
"mockito-target-minus-junit4",
+ "platform-parametric-runner-lib",
"platform-test-annotations",
"truth",
],
diff --git a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
index 7c01ecc..e640ce5 100644
--- a/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
+++ b/core/tests/devicestatetests/src/android/hardware/devicestate/DeviceStateManagerGlobalTest.java
@@ -18,8 +18,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.verifyZeroInteractions;
@@ -29,15 +31,20 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.os.test.FakePermissionEnforcer;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
import com.android.internal.util.ConcurrentUtils;
+import com.android.window.flags.Flags;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -46,6 +53,9 @@
import java.util.List;
import java.util.Set;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
/**
* Unit tests for {@link DeviceStateManagerGlobal}.
*
@@ -53,18 +63,30 @@
* atest FrameworksCoreDeviceStateManagerTests:DeviceStateManagerGlobalTest
*/
@SmallTest
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
public final class DeviceStateManagerGlobalTest {
private static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(
new DeviceState.Configuration.Builder(0 /* identifier */, "" /* name */).build());
private static final DeviceState OTHER_DEVICE_STATE = new DeviceState(
new DeviceState.Configuration.Builder(1 /* identifier */, "" /* name */).build());
+ @Rule
+ public final SetFlagsRule mSetFlagsRule;
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(Flags.FLAG_WLINFO_ONCREATE);
+ }
+
@NonNull
private TestDeviceStateManagerService mService;
@NonNull
private DeviceStateManagerGlobal mDeviceStateManagerGlobal;
+ public DeviceStateManagerGlobalTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(flags);
+ }
+
@Before
public void setUp() {
final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
@@ -74,6 +96,36 @@
}
@Test
+ @DisableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ public void create_whenWlinfoOncreateIsDisabled_receivesDeviceStateInfoFromCallback() {
+ final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+ final TestDeviceStateManagerService service = new TestDeviceStateManagerService(
+ permissionEnforcer, true /* simulatePostCallback */);
+ final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
+ final DeviceStateCallback callback = mock(DeviceStateCallback.class);
+ dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
+
+ verify(callback, never()).onDeviceStateChanged(any());
+
+ // Simulate DeviceStateManagerService#registerProcess by notifying clients of current device
+ // state via callback.
+ service.notifyDeviceStateInfoChanged();
+ verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_WLINFO_ONCREATE)
+ public void create_whenWlinfoOncreateIsEnabled_returnsDeviceStateInfoFromRegistration() {
+ final FakePermissionEnforcer permissionEnforcer = new FakePermissionEnforcer();
+ final IDeviceStateManager service = new TestDeviceStateManagerService(permissionEnforcer);
+ final DeviceStateManagerGlobal dsmGlobal = new DeviceStateManagerGlobal(service);
+ final DeviceStateCallback callback = mock(DeviceStateCallback.class);
+ dsmGlobal.registerDeviceStateCallback(callback, ConcurrentUtils.DIRECT_EXECUTOR);
+
+ verify(callback).onDeviceStateChanged(eq(DEFAULT_DEVICE_STATE));
+ }
+
+ @Test
public void registerCallback() {
final DeviceStateCallback callback1 = mock(DeviceStateCallback.class);
final DeviceStateCallback callback2 = mock(DeviceStateCallback.class);
@@ -267,10 +319,17 @@
@Nullable
private Request mBaseStateRequest;
+ private final boolean mSimulatePostCallback;
private final Set<IDeviceStateManagerCallback> mCallbacks = new HashSet<>();
TestDeviceStateManagerService(@NonNull FakePermissionEnforcer enforcer) {
+ this(enforcer, false /* simulatePostCallback */);
+ }
+
+ TestDeviceStateManagerService(@NonNull FakePermissionEnforcer enforcer,
+ boolean simulatePostCallback) {
super(enforcer);
+ mSimulatePostCallback = simulatePostCallback;
}
@NonNull
@@ -304,18 +363,26 @@
return getInfo();
}
+ @Nullable
@Override
- public void registerCallback(IDeviceStateManagerCallback callback) {
+ public DeviceStateInfo registerCallback(IDeviceStateManagerCallback callback) {
if (mCallbacks.contains(callback)) {
throw new SecurityException("Callback is already registered.");
}
mCallbacks.add(callback);
- try {
- callback.onDeviceStateInfoChanged(getInfo());
- } catch (RemoteException e) {
- e.rethrowFromSystemServer();
+ if (Flags.wlinfoOncreate()) {
+ return getInfo();
}
+
+ if (!mSimulatePostCallback) {
+ try {
+ callback.onDeviceStateInfoChanged(getInfo());
+ } catch (RemoteException e) {
+ e.rethrowFromSystemServer();
+ }
+ }
+ return null;
}
@Override
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
index 5a277316f..379e052 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeEventLogger.kt
@@ -156,6 +156,21 @@
)
}
+ fun logTaskInfoStateInit() {
+ logTaskUpdate(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD,
+ /* session_id */ 0,
+ TaskUpdate(
+ visibleTaskCount = 0,
+ instanceId = 0,
+ uid = 0,
+ taskHeight = 0,
+ taskWidth = 0,
+ taskX = 0,
+ taskY = 0)
+ )
+ }
+
private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) {
FrameworkStatsLog.write(
DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
index b8507e3..f847aa89 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserver.kt
@@ -102,6 +102,7 @@
SystemProperties.set(
VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY,
VISIBLE_TASKS_COUNTER_SYSTEM_PROPERTY_DEFAULT_VALUE)
+ desktopModeEventLogger.logTaskInfoStateInit()
}
override fun onTransitionReady(
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
index d7a132d..dde9fda 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeEventLoggerTest.kt
@@ -16,7 +16,6 @@
package com.android.wm.shell.desktopmode
-import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
import android.platform.test.flag.junit.SetFlagsRule
import com.android.dx.mockito.inline.extended.ExtendedMockito.verify
@@ -397,6 +396,37 @@
}
}
+ @Test
+ fun logTaskInfoStateInit_logsTaskInfoChangedStateInit() {
+ desktopModeEventLogger.logTaskInfoStateInit()
+ verify {
+ FrameworkStatsLog.write(eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE),
+ /* task_event */
+ eq(FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INIT_STATSD),
+ /* instance_id */
+ eq(0),
+ /* uid */
+ eq(0),
+ /* task_height */
+ eq(0),
+ /* task_width */
+ eq(0),
+ /* task_x */
+ eq(0),
+ /* task_y */
+ eq(0),
+ /* session_id */
+ eq(0),
+ /* minimize_reason */
+ eq(UNSET_MINIMIZE_REASON),
+ /* unminimize_reason */
+ eq(UNSET_UNMINIMIZE_REASON),
+ /* visible_task_count */
+ eq(0)
+ )
+ }
+ }
+
private companion object {
private const val SESSION_ID = 1
private const val TASK_ID = 1
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
index daf7e7d..e7593b5 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/DesktopModeLoggerTransitionObserverTest.kt
@@ -115,6 +115,9 @@
val initRunnableCaptor = ArgumentCaptor.forClass(Runnable::class.java)
verify(mockShellInit).addInitCallback(initRunnableCaptor.capture(), same(transitionObserver))
initRunnableCaptor.value.run()
+ // verify this initialisation interaction to leave the desktopmodeEventLogger mock in a
+ // consistent state with no outstanding interactions when test cases start executing.
+ verify(desktopModeEventLogger).logTaskInfoStateInit()
}
@Test
diff --git a/nfc/api/system-current.txt b/nfc/api/system-current.txt
index 4428ade..24e14e6 100644
--- a/nfc/api/system-current.txt
+++ b/nfc/api/system-current.txt
@@ -63,7 +63,7 @@
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isAutoChangeEnabled();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public boolean isTagPresent();
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void maybeTriggerFirmwareUpdate();
- method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int);
+ method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void overwriteRoutingTable(int, int, int, int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void pausePolling(int);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void registerCallback(@NonNull java.util.concurrent.Executor, @NonNull android.nfc.NfcOemExtension.Callback);
method @FlaggedApi("android.nfc.nfc_oem_extension") @RequiresPermission(android.Manifest.permission.WRITE_SECURE_SETTINGS) public void resumePolling();
diff --git a/nfc/java/android/nfc/INfcCardEmulation.aidl b/nfc/java/android/nfc/INfcCardEmulation.aidl
index 1eae3c6..8535e4a 100644
--- a/nfc/java/android/nfc/INfcCardEmulation.aidl
+++ b/nfc/java/android/nfc/INfcCardEmulation.aidl
@@ -54,5 +54,5 @@
void setAutoChangeStatus(boolean state);
boolean isAutoChangeEnabled();
List<String> getRoutingStatus();
- void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech);
+ void overwriteRoutingTable(int userHandle, String emptyAid, String protocol, String tech, String sc);
}
diff --git a/nfc/java/android/nfc/NfcOemExtension.java b/nfc/java/android/nfc/NfcOemExtension.java
index fb63b5c..bc410c7 100644
--- a/nfc/java/android/nfc/NfcOemExtension.java
+++ b/nfc/java/android/nfc/NfcOemExtension.java
@@ -647,24 +647,29 @@
* {@link ProtocolAndTechnologyRoute}
* @param emptyAid Zero-length AID route destination, where the possible inputs are defined in
* {@link ProtocolAndTechnologyRoute}
+ * @param systemCode System Code route destination, where the possible inputs are defined in
+ * {@link ProtocolAndTechnologyRoute}
*/
@RequiresPermission(Manifest.permission.WRITE_SECURE_SETTINGS)
@FlaggedApi(Flags.FLAG_NFC_OEM_EXTENSION)
public void overwriteRoutingTable(
@CardEmulation.ProtocolAndTechnologyRoute int protocol,
@CardEmulation.ProtocolAndTechnologyRoute int technology,
- @CardEmulation.ProtocolAndTechnologyRoute int emptyAid) {
+ @CardEmulation.ProtocolAndTechnologyRoute int emptyAid,
+ @CardEmulation.ProtocolAndTechnologyRoute int systemCode) {
String protocolRoute = routeIntToString(protocol);
String technologyRoute = routeIntToString(technology);
String emptyAidRoute = routeIntToString(emptyAid);
+ String systemCodeRoute = routeIntToString(systemCode);
NfcAdapter.callService(() ->
NfcAdapter.sCardEmulationService.overwriteRoutingTable(
mContext.getUser().getIdentifier(),
emptyAidRoute,
protocolRoute,
- technologyRoute
+ technologyRoute,
+ systemCodeRoute
));
}
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
index 3d41337..5f1f8df 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/KeyValueStore.kt
@@ -25,6 +25,7 @@
fun contains(key: String): Boolean
/** Gets default value of given key. */
+ @Suppress("UNCHECKED_CAST")
fun <T : Any> getDefaultValue(key: String, valueType: Class<T>): T? =
when (valueType) {
Boolean::class.javaObjectType -> false
@@ -56,6 +57,7 @@
override fun contains(key: String): Boolean = sharedPreferences.contains(key)
+ @Suppress("IMPLICIT_CAST_TO_ANY", "UNCHECKED_CAST")
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
when (valueType) {
Boolean::class.javaObjectType -> sharedPreferences.getBoolean(key, false)
@@ -68,6 +70,7 @@
}
as T?
+ @Suppress("UNCHECKED_CAST")
override fun <T : Any> setValue(key: String, valueType: Class<T>, value: T?) {
if (value == null) {
sharedPreferences.edit().remove(key).apply()
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
index 4aef0fc..fb93559 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsGlobalStore.kt
@@ -34,6 +34,7 @@
override fun contains(key: String): Boolean = Global.getString(contentResolver, key) != null
+ @Suppress("UNCHECKED_CAST")
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
try {
when (valueType) {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
index 9f41ecb..bc37571 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSecureStore.kt
@@ -34,6 +34,7 @@
override fun contains(key: String): Boolean = Secure.getString(contentResolver, key) != null
+ @Suppress("UNCHECKED_CAST")
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
try {
when (valueType) {
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
index 5981688..fdefa39 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsStore.kt
@@ -27,7 +27,7 @@
import java.util.concurrent.atomic.AtomicInteger
/** Base class of the Settings provider data stores. */
-open abstract class SettingsStore(protected val contentResolver: ContentResolver) :
+abstract class SettingsStore(protected val contentResolver: ContentResolver) :
KeyedDataObservable<String>(), KeyValueStore {
/**
diff --git a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
index 6cca7ed..1c75c7c 100644
--- a/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
+++ b/packages/SettingsLib/DataStore/src/com/android/settingslib/datastore/SettingsSystemStore.kt
@@ -34,6 +34,7 @@
override fun contains(key: String): Boolean = System.getString(contentResolver, key) != null
+ @Suppress("UNCHECKED_CAST")
override fun <T : Any> getValue(key: String, valueType: Class<T>): T? =
try {
when (valueType) {
diff --git a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
index 02acfca..c2728b4 100644
--- a/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
+++ b/packages/SettingsLib/Preference/src/com/android/settingslib/preference/PreferenceDataStoreAdapter.kt
@@ -37,6 +37,7 @@
override fun getString(key: String, defValue: String?): String? =
keyValueStore.getValue(key, String::class.javaObjectType) ?: defValue
+ @Suppress("UNCHECKED_CAST")
override fun getStringSet(key: String, defValues: Set<String>?): Set<String>? =
(keyValueStore.getValue(key, Set::class.javaObjectType) as Set<String>?) ?: defValues
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
index c9f9d1b..a4c5a00d 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastAssistant.java
@@ -42,6 +42,7 @@
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.Executor;
+import java.util.concurrent.Executors;
/**
* LocalBluetoothLeBroadcastAssistant provides an interface between the Settings app and the
@@ -63,6 +64,7 @@
private BluetoothLeBroadcastMetadata mBluetoothLeBroadcastMetadata;
private BluetoothLeBroadcastMetadata.Builder mBuilder;
private boolean mIsProfileReady;
+ private Executor mExecutor;
// Cached assistant callbacks being register before service is connected.
private final Map<BluetoothLeBroadcastAssistant.Callback, Executor> mCachedCallbackExecutorMap =
new ConcurrentHashMap<>();
@@ -98,15 +100,19 @@
}
mProfileManager.callServiceConnectedListeners();
- mIsProfileReady = true;
- if (DEBUG) {
- Log.d(
- TAG,
- "onServiceConnected, register mCachedCallbackExecutorMap = "
- + mCachedCallbackExecutorMap);
+ if (!mIsProfileReady) {
+ mIsProfileReady = true;
+ registerServiceCallBack(mExecutor, mAssistantCallback);
+ if (DEBUG) {
+ Log.d(
+ TAG,
+ "onServiceConnected, register mCachedCallbackExecutorMap = "
+ + mCachedCallbackExecutorMap);
+ }
+ mCachedCallbackExecutorMap.forEach(
+ (callback, executor) -> registerServiceCallBack(executor,
+ callback));
}
- mCachedCallbackExecutorMap.forEach(
- (callback, executor) -> registerServiceCallBack(executor, callback));
}
@Override
@@ -119,17 +125,71 @@
Log.d(TAG, "Bluetooth service disconnected");
}
mProfileManager.callServiceDisconnectedListeners();
- mIsProfileReady = false;
- mCachedCallbackExecutorMap.clear();
+ if (mIsProfileReady) {
+ mIsProfileReady = false;
+ unregisterServiceCallBack(mAssistantCallback);
+ mCachedCallbackExecutorMap.clear();
+ }
}
};
+ private final BluetoothLeBroadcastAssistant.Callback mAssistantCallback =
+ new BluetoothLeBroadcastAssistant.Callback() {
+ @Override
+ public void onSourceAdded(@NonNull BluetoothDevice sink, int sourceId, int reason) {
+ }
+
+ @Override
+ public void onSearchStarted(int reason) {}
+
+ @Override
+ public void onSearchStartFailed(int reason) {}
+
+ @Override
+ public void onSearchStopped(int reason) {}
+
+ @Override
+ public void onSearchStopFailed(int reason) {}
+
+ @Override
+ public void onSourceFound(@NonNull BluetoothLeBroadcastMetadata source) {}
+
+ @Override
+ public void onSourceAddFailed(
+ @NonNull BluetoothDevice sink,
+ @NonNull BluetoothLeBroadcastMetadata source,
+ int reason) {}
+
+ @Override
+ public void onSourceModified(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onSourceModifyFailed(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onSourceRemoved(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onSourceRemoveFailed(
+ @NonNull BluetoothDevice sink, int sourceId, int reason) {}
+
+ @Override
+ public void onReceiveStateChanged(
+ @NonNull BluetoothDevice sink,
+ int sourceId,
+ @NonNull BluetoothLeBroadcastReceiveState state) {}
+ };
+
public LocalBluetoothLeBroadcastAssistant(
Context context,
CachedBluetoothDeviceManager deviceManager,
LocalBluetoothProfileManager profileManager) {
mProfileManager = profileManager;
mDeviceManager = deviceManager;
+ mExecutor = Executors.newSingleThreadExecutor();
BluetoothAdapter.getDefaultAdapter()
.getProfileProxy(
context, mServiceListener, BluetoothProfile.LE_AUDIO_BROADCAST_ASSISTANT);
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
index 0bcf7fe..07abb6b 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcastCallbackExt.kt
@@ -68,3 +68,44 @@
awaitClose { unregisterServiceCallBack(listener) }
}
.buffer(capacity = Channel.CONFLATED)
+
+/** [Flow] for [BluetoothLeBroadcast.Callback] onPlaybackStarted event */
+val LocalBluetoothLeBroadcast.onPlaybackStarted: Flow<Unit>
+ get() =
+ callbackFlow {
+ val listener =
+ object : BluetoothLeBroadcast.Callback {
+ override fun onBroadcastStarted(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastStartFailed(reason: Int) {
+ }
+
+ override fun onBroadcastStopped(reason: Int, broadcastId: Int) {
+ }
+
+ override fun onBroadcastStopFailed(reason: Int) {
+ }
+
+ override fun onPlaybackStarted(reason: Int, broadcastId: Int) {
+ launch { trySend(Unit) }
+ }
+
+ override fun onPlaybackStopped(reason: Int, broadcastId: Int) {
+ }
+
+ override fun onBroadcastUpdated(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastUpdateFailed(reason: Int, broadcastId: Int) {}
+
+ override fun onBroadcastMetadataChanged(
+ broadcastId: Int,
+ metadata: BluetoothLeBroadcastMetadata
+ ) {}
+ }
+ registerServiceCallBack(
+ ConcurrentUtils.DIRECT_EXECUTOR,
+ listener,
+ )
+ awaitClose { unregisterServiceCallBack(listener) }
+ }
+ .buffer(capacity = Channel.CONFLATED)
diff --git a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
index 727662b..4f315a2 100644
--- a/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/media/InputRouteManager.java
@@ -22,6 +22,7 @@
import android.media.AudioDeviceAttributes;
import android.media.AudioDeviceCallback;
import android.media.AudioDeviceInfo;
+import android.media.AudioDeviceInfo.AudioDeviceType;
import android.media.AudioManager;
import android.media.MediaRecorder;
import android.os.Handler;
@@ -63,7 +64,7 @@
@VisibleForTesting final List<MediaDevice> mInputMediaDevices = new CopyOnWriteArrayList<>();
- private MediaDevice mSelectedInputDevice;
+ private @AudioDeviceType int mSelectedInputDeviceType;
private final Collection<InputDeviceCallback> mCallbacks = new CopyOnWriteArrayList<>();
private final Object mCallbackLock = new Object();
@@ -73,12 +74,12 @@
new AudioDeviceCallback() {
@Override
public void onAudioDevicesAdded(@NonNull AudioDeviceInfo[] addedDevices) {
- dispatchInputDeviceListUpdate();
+ applyDefaultSelectedTypeToAllPresets();
}
@Override
public void onAudioDevicesRemoved(@NonNull AudioDeviceInfo[] removedDevices) {
- dispatchInputDeviceListUpdate();
+ applyDefaultSelectedTypeToAllPresets();
}
};
@@ -92,9 +93,12 @@
mAudioManager.addOnPreferredDevicesForCapturePresetChangedListener(
new HandlerExecutor(handler),
this::onPreferredDevicesForCapturePresetChangedListener);
+
+ applyDefaultSelectedTypeToAllPresets();
}
- private void onPreferredDevicesForCapturePresetChangedListener(
+ @VisibleForTesting
+ void onPreferredDevicesForCapturePresetChangedListener(
@MediaRecorder.SystemSource int capturePreset,
@NonNull List<AudioDeviceAttributes> devices) {
if (capturePreset == MediaRecorder.AudioSource.MIC) {
@@ -117,12 +121,30 @@
}
}
+ // TODO(b/355684672): handle edge case where there are two devices with the same type. Only
+ // using a single mSelectedInputDeviceType might not be enough to recognize the correct device.
public @Nullable MediaDevice getSelectedInputDevice() {
- return mSelectedInputDevice;
+ for (MediaDevice device : mInputMediaDevices) {
+ if (((InputMediaDevice) device).getAudioDeviceInfoType() == mSelectedInputDeviceType) {
+ return device;
+ }
+ }
+ return null;
}
- private void dispatchInputDeviceListUpdate() {
- // Get selected input device.
+ private void applyDefaultSelectedTypeToAllPresets() {
+ mSelectedInputDeviceType = retrieveDefaultSelectedDeviceType();
+ AudioDeviceAttributes deviceAttributes =
+ createInputDeviceAttributes(mSelectedInputDeviceType);
+ setPreferredDeviceForAllPresets(deviceAttributes);
+ }
+
+ private AudioDeviceAttributes createInputDeviceAttributes(@AudioDeviceType int type) {
+ // Address is not used.
+ return new AudioDeviceAttributes(AudioDeviceAttributes.ROLE_INPUT, type, /* address= */ "");
+ }
+
+ private @AudioDeviceType int retrieveDefaultSelectedDeviceType() {
List<AudioDeviceAttributes> attributesOfSelectedInputDevices =
mAudioManager.getDevicesForAttributes(INPUT_ATTRIBUTES);
int selectedInputDeviceAttributesType;
@@ -138,7 +160,10 @@
}
selectedInputDeviceAttributesType = attributesOfSelectedInputDevices.get(0).getType();
}
+ return selectedInputDeviceAttributesType;
+ }
+ private void dispatchInputDeviceListUpdate() {
// Get all input devices.
AudioDeviceInfo[] audioDeviceInfos =
mAudioManager.getDevices(AudioManager.GET_DEVICES_INPUTS);
@@ -154,9 +179,8 @@
isInputGainFixed(),
getProductNameFromAudioDeviceInfo(info));
if (mediaDevice != null) {
- if (info.getType() == selectedInputDeviceAttributesType) {
+ if (info.getType() == mSelectedInputDeviceType) {
mediaDevice.setState(STATE_SELECTED);
- mSelectedInputDevice = mediaDevice;
}
mInputMediaDevices.add(mediaDevice);
}
@@ -190,12 +214,12 @@
}
public void selectDevice(@NonNull MediaDevice device) {
- if (!(device instanceof InputMediaDevice)) {
+ if (!(device instanceof InputMediaDevice inputMediaDevice)) {
Slog.w(TAG, "This device is not an InputMediaDevice: " + device.getName());
return;
}
- if (device.equals(mSelectedInputDevice)) {
+ if (inputMediaDevice.getAudioDeviceInfoType() == mSelectedInputDeviceType) {
Slog.w(TAG, "This device is already selected: " + device.getName());
return;
}
@@ -206,12 +230,11 @@
return;
}
- // TODO(b/355684672): apply address for BT devices.
+ // Update mSelectedInputDeviceType directly based on user action.
+ mSelectedInputDeviceType = inputMediaDevice.getAudioDeviceInfoType();
+
AudioDeviceAttributes deviceAttributes =
- new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_INPUT,
- ((InputMediaDevice) device).getAudioDeviceInfoType(),
- /* address= */ "");
+ createInputDeviceAttributes(inputMediaDevice.getAudioDeviceInfoType());
try {
setPreferredDeviceForAllPresets(deviceAttributes);
} catch (IllegalArgumentException e) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
index 2f8105a..b41e970 100644
--- a/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
+++ b/packages/SettingsLib/src/com/android/settingslib/volume/data/repository/AudioSharingRepository.kt
@@ -74,6 +74,8 @@
/** The headset groupId to volume map during audio sharing. */
val volumeMap: StateFlow<GroupIdToVolumes>
+ suspend fun audioSharingAvailable(): Boolean
+
/** Set the volume of secondary headset during audio sharing. */
suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
@@ -216,6 +218,12 @@
}
.stateIn(coroutineScope, SharingStarted.WhileSubscribed(), emptyMap())
+ override suspend fun audioSharingAvailable(): Boolean {
+ return withContext(backgroundCoroutineContext) {
+ BluetoothUtils.isAudioSharingEnabled()
+ }
+ }
+
override suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
volume: Int
@@ -262,6 +270,8 @@
MutableStateFlow(BluetoothCsipSetCoordinator.GROUP_ID_INVALID)
override val volumeMap: StateFlow<GroupIdToVolumes> = MutableStateFlow(emptyMap())
+ override suspend fun audioSharingAvailable(): Boolean = false
+
override suspend fun setSecondaryVolume(
@IntRange(from = AUDIO_SHARING_VOLUME_MIN.toLong(), to = AUDIO_SHARING_VOLUME_MAX.toLong())
volume: Int
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
index f63bfc7..782cee2 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/media/InputRouteManagerTest.java
@@ -21,6 +21,7 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.Mockito.atLeast;
import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
@@ -55,6 +56,7 @@
private static final int INPUT_USB_DEVICE_ID = 3;
private static final int INPUT_USB_HEADSET_ID = 4;
private static final int INPUT_USB_ACCESSORY_ID = 5;
+ private static final int HDMI_ID = 6;
private static final int MAX_VOLUME = 1;
private static final int CURRENT_VOLUME = 0;
private static final boolean VOLUME_FIXED_TRUE = true;
@@ -63,10 +65,86 @@
private static final String PRODUCT_NAME_USB_HEADSET = "My USB Headset";
private static final String PRODUCT_NAME_USB_DEVICE = "My USB Device";
private static final String PRODUCT_NAME_USB_ACCESSORY = "My USB Accessory";
+ private static final String PRODUCT_NAME_HDMI_DEVICE = "HDMI device";
private final Context mContext = spy(RuntimeEnvironment.application);
private InputRouteManager mInputRouteManager;
+ private AudioDeviceInfo mockBuiltinMicInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
+ when(info.getId()).thenReturn(BUILTIN_MIC_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
+ return info;
+ }
+
+ private AudioDeviceInfo mockWiredHeadsetInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
+ when(info.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbDeviceInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
+ when(info.getId()).thenReturn(INPUT_USB_DEVICE_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbHeadsetInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
+ when(info.getId()).thenReturn(INPUT_USB_HEADSET_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
+ return info;
+ }
+
+ private AudioDeviceInfo mockUsbAccessoryInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
+ when(info.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
+ return info;
+ }
+
+ private AudioDeviceInfo mockHdmiInfo() {
+ final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
+ when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
+ when(info.getId()).thenReturn(HDMI_ID);
+ when(info.getAddress()).thenReturn("");
+ when(info.getProductName()).thenReturn(PRODUCT_NAME_HDMI_DEVICE);
+ return info;
+ }
+
+ private AudioDeviceAttributes getBuiltinMicDeviceAttributes() {
+ return new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT,
+ AudioDeviceInfo.TYPE_BUILTIN_MIC,
+ /* address= */ "");
+ }
+
+ private AudioDeviceAttributes getWiredHeadsetDeviceAttributes() {
+ return new AudioDeviceAttributes(
+ AudioDeviceAttributes.ROLE_INPUT,
+ AudioDeviceInfo.TYPE_WIRED_HEADSET,
+ /* address= */ "");
+ }
+
+ private void onPreferredDevicesForCapturePresetChanged(InputRouteManager inputRouteManager) {
+ final List<AudioDeviceAttributes> audioDeviceAttributesList =
+ new ArrayList<AudioDeviceAttributes>();
+ inputRouteManager.onPreferredDevicesForCapturePresetChangedListener(
+ MediaRecorder.AudioSource.MIC, audioDeviceAttributesList);
+ }
+
@Before
public void setUp() {
MockitoAnnotations.initMocks(this);
@@ -77,37 +155,15 @@
@Test
public void onAudioDevicesAdded_shouldUpdateInputMediaDevice() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info1.getId()).thenReturn(BUILTIN_MIC_ID);
- when(info1.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
- when(info2.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
- final AudioDeviceInfo info3 = mock(AudioDeviceInfo.class);
- when(info3.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_DEVICE);
- when(info3.getId()).thenReturn(INPUT_USB_DEVICE_ID);
- when(info3.getProductName()).thenReturn(PRODUCT_NAME_USB_DEVICE);
-
- final AudioDeviceInfo info4 = mock(AudioDeviceInfo.class);
- when(info4.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_HEADSET);
- when(info4.getId()).thenReturn(INPUT_USB_HEADSET_ID);
- when(info4.getProductName()).thenReturn(PRODUCT_NAME_USB_HEADSET);
-
- final AudioDeviceInfo info5 = mock(AudioDeviceInfo.class);
- when(info5.getType()).thenReturn(AudioDeviceInfo.TYPE_USB_ACCESSORY);
- when(info5.getId()).thenReturn(INPUT_USB_ACCESSORY_ID);
- when(info5.getProductName()).thenReturn(PRODUCT_NAME_USB_ACCESSORY);
-
- final AudioDeviceInfo unsupportedInfo = mock(AudioDeviceInfo.class);
- when(unsupportedInfo.getType()).thenReturn(AudioDeviceInfo.TYPE_HDMI);
- when(unsupportedInfo.getProductName()).thenReturn("HDMI device");
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2, info3, info4, info5, unsupportedInfo};
+ AudioDeviceInfo[] devices = {
+ mockBuiltinMicInfo(),
+ mockWiredHeadsetInfo(),
+ mockUsbDeviceInfo(),
+ mockUsbHeadsetInfo(),
+ mockUsbAccessoryInfo(),
+ mockHdmiInfo()
+ };
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
@@ -115,8 +171,9 @@
assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
- // The unsupported info should be filtered out.
+ // The unsupported (hdmi) info should be filtered out.
assertThat(inputRouteManager.mInputMediaDevices).hasSize(devices.length - 1);
assertThat(inputRouteManager.mInputMediaDevices.get(0).getId())
.isEqualTo(String.valueOf(BUILTIN_MIC_ID));
@@ -141,36 +198,25 @@
final MediaDevice device = mock(MediaDevice.class);
inputRouteManager.mInputMediaDevices.add(device);
- final AudioDeviceInfo info = mock(AudioDeviceInfo.class);
- when(info.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(new AudioDeviceInfo[] {info});
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(
+ new AudioDeviceInfo[] {mockWiredHeadsetInfo()});
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
}
@Test
public void getSelectedInputDevice_returnOneFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
- when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
- when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns exactly one audioDeviceAttributes.
- AudioDeviceAttributes audioDeviceAttributes = new AudioDeviceAttributes(info1);
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
- .thenReturn(Collections.singletonList(audioDeviceAttributes));
+ .thenReturn(Collections.singletonList(getWiredHeadsetDeviceAttributes()));
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has the same type as the one returned from AudioManager.
InputMediaDevice selectedInputDevice =
@@ -181,31 +227,19 @@
@Test
public void getSelectedInputDevice_returnMoreThanOneFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
- when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
- when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns more than one audioDeviceAttributes.
- AudioDeviceAttributes audioDeviceAttributes1 = new AudioDeviceAttributes(info1);
- AudioDeviceAttributes audioDeviceAttributes2 = new AudioDeviceAttributes(info2);
List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
- attributesOfSelectedInputDevices.add(audioDeviceAttributes1);
- attributesOfSelectedInputDevices.add(audioDeviceAttributes2);
+ attributesOfSelectedInputDevices.add(getWiredHeadsetDeviceAttributes());
+ attributesOfSelectedInputDevices.add(getBuiltinMicDeviceAttributes());
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
.thenReturn(attributesOfSelectedInputDevices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has the same type as the first one returned from AudioManager.
InputMediaDevice selectedInputDevice =
@@ -216,27 +250,17 @@
@Test
public void getSelectedInputDevice_returnEmptyFromAudioManager() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
- when(info1.getProductName()).thenReturn(PRODUCT_NAME_WIRED_HEADSET);
-
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_BUILTIN_MIC);
- when(info2.getId()).thenReturn(BUILTIN_MIC_ID);
- when(info2.getProductName()).thenReturn(PRODUCT_NAME_BUILTIN_MIC);
-
final AudioManager audioManager = mock(AudioManager.class);
- AudioDeviceInfo[] devices = {info1, info2};
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo(), mockBuiltinMicInfo()};
when(audioManager.getDevices(AudioManager.GET_DEVICES_INPUTS)).thenReturn(devices);
// Mock audioManager.getDevicesForAttributes returns empty list of audioDeviceAttributes.
- List<AudioDeviceAttributes> attributesOfSelectedInputDevices = new ArrayList<>();
+ List<AudioDeviceAttributes> emptyAttributesOfSelectedInputDevices = new ArrayList<>();
when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
- .thenReturn(attributesOfSelectedInputDevices);
+ .thenReturn(emptyAttributesOfSelectedInputDevices);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
// The selected input device has default type AudioDeviceInfo.TYPE_BUILTIN_MIC.
InputMediaDevice selectedInputDevice =
@@ -249,7 +273,7 @@
public void selectDevice() {
final AudioManager audioManager = mock(AudioManager.class);
InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
- final MediaDevice inputMediaDevice =
+ final MediaDevice builtinMicDevice =
InputMediaDevice.create(
mContext,
String.valueOf(BUILTIN_MIC_ID),
@@ -258,16 +282,57 @@
CURRENT_VOLUME,
VOLUME_FIXED_TRUE,
PRODUCT_NAME_BUILTIN_MIC);
- inputRouteManager.selectDevice(inputMediaDevice);
+ inputRouteManager.selectDevice(builtinMicDevice);
- AudioDeviceAttributes deviceAttributes =
- new AudioDeviceAttributes(
- AudioDeviceAttributes.ROLE_INPUT,
- AudioDeviceInfo.TYPE_BUILTIN_MIC,
- /* address= */ "");
for (@MediaRecorder.Source int preset : PRESETS) {
verify(audioManager, atLeastOnce())
- .setPreferredDeviceForCapturePreset(preset, deviceAttributes);
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+ }
+ }
+
+ @Test
+ public void onInitiation_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ new InputRouteManager(mContext, audioManager);
+
+ verify(audioManager, atLeastOnce()).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeastOnce())
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
+ }
+ }
+
+ @Test
+ public void onAudioDevicesAdded_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ AudioDeviceAttributes wiredHeadsetDeviceAttributes = getWiredHeadsetDeviceAttributes();
+ when(audioManager.getDevicesForAttributes(INPUT_ATTRIBUTES))
+ .thenReturn(Collections.singletonList(wiredHeadsetDeviceAttributes));
+
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+
+ // Called twice, one after initiation, the other after onAudioDevicesAdded call.
+ verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeast(2))
+ .setPreferredDeviceForCapturePreset(preset, wiredHeadsetDeviceAttributes);
+ }
+ }
+
+ @Test
+ public void onAudioDevicesRemoved_shouldApplyDefaultSelectedDeviceToAllPresets() {
+ final AudioManager audioManager = mock(AudioManager.class);
+ InputRouteManager inputRouteManager = new InputRouteManager(mContext, audioManager);
+ AudioDeviceInfo[] devices = {mockWiredHeadsetInfo()};
+ inputRouteManager.mAudioDeviceCallback.onAudioDevicesRemoved(devices);
+
+ // Called twice, one after initiation, the other after onAudioDevicesRemoved call.
+ verify(audioManager, atLeast(2)).getDevicesForAttributes(INPUT_ATTRIBUTES);
+ for (@MediaRecorder.Source int preset : PRESETS) {
+ verify(audioManager, atLeast(2))
+ .setPreferredDeviceForCapturePreset(preset, getBuiltinMicDeviceAttributes());
}
}
@@ -288,27 +353,25 @@
@Test
public void onAudioDevicesAdded_shouldSetProductNameCorrectly() {
- final AudioDeviceInfo info1 = mock(AudioDeviceInfo.class);
- when(info1.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info1.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ final AudioDeviceInfo info1 = mockWiredHeadsetInfo();
String firstProductName = "My first headset";
when(info1.getProductName()).thenReturn(firstProductName);
+ InputMediaDevice inputMediaDevice1 = createInputMediaDeviceFromDeviceInfo(info1);
- final AudioDeviceInfo info2 = mock(AudioDeviceInfo.class);
- when(info2.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(info2.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ final AudioDeviceInfo info2 = mockWiredHeadsetInfo();
String secondProductName = "My second headset";
when(info2.getProductName()).thenReturn(secondProductName);
+ InputMediaDevice inputMediaDevice2 = createInputMediaDeviceFromDeviceInfo(info2);
- final AudioDeviceInfo infoWithNullProductName = mock(AudioDeviceInfo.class);
- when(infoWithNullProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(infoWithNullProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ final AudioDeviceInfo infoWithNullProductName = mockWiredHeadsetInfo();
when(infoWithNullProductName.getProductName()).thenReturn(null);
+ InputMediaDevice inputMediaDevice3 =
+ createInputMediaDeviceFromDeviceInfo(infoWithNullProductName);
- final AudioDeviceInfo infoWithBlankProductName = mock(AudioDeviceInfo.class);
- when(infoWithBlankProductName.getType()).thenReturn(AudioDeviceInfo.TYPE_WIRED_HEADSET);
- when(infoWithBlankProductName.getId()).thenReturn(INPUT_WIRED_HEADSET_ID);
+ final AudioDeviceInfo infoWithBlankProductName = mockWiredHeadsetInfo();
when(infoWithBlankProductName.getProductName()).thenReturn("");
+ InputMediaDevice inputMediaDevice4 =
+ createInputMediaDeviceFromDeviceInfo(infoWithBlankProductName);
final AudioManager audioManager = mock(AudioManager.class);
AudioDeviceInfo[] devices = {
@@ -321,15 +384,22 @@
assertThat(inputRouteManager.mInputMediaDevices).isEmpty();
inputRouteManager.mAudioDeviceCallback.onAudioDevicesAdded(devices);
+ onPreferredDevicesForCapturePresetChanged(inputRouteManager);
- assertThat(getProductNameAtIndex(inputRouteManager, 1)).isEqualTo(firstProductName);
- assertThat(getProductNameAtIndex(inputRouteManager, 2)).isEqualTo(secondProductName);
- assertThat(getProductNameAtIndex(inputRouteManager, 3)).isNull();
- assertThat(getProductNameAtIndex(inputRouteManager, 4)).isNull();
+ assertThat(inputRouteManager.mInputMediaDevices)
+ .containsExactly(
+ inputMediaDevice1, inputMediaDevice2, inputMediaDevice3, inputMediaDevice4)
+ .inOrder();
}
- private String getProductNameAtIndex(InputRouteManager inputRouteManager, int index) {
- return ((InputMediaDevice) inputRouteManager.mInputMediaDevices.get(index))
- .getProductName();
+ private InputMediaDevice createInputMediaDeviceFromDeviceInfo(AudioDeviceInfo info) {
+ return InputMediaDevice.create(
+ mContext,
+ String.valueOf(info.getId()),
+ info.getType(),
+ MAX_VOLUME,
+ CURRENT_VOLUME,
+ VOLUME_FIXED_TRUE,
+ info.getProductName() == null ? null : info.getProductName().toString());
}
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
index 5293011..d8b6707 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/wifi/WifiUtilsTest.java
@@ -207,20 +207,6 @@
}
@Test
- public void getHotspotIconResource_deviceTypeExists_shouldNotNull() {
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_PHONE))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_TABLET))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_LAPTOP))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_WATCH))
- .isNotNull();
- assertThat(WifiUtils.getHotspotIconResource(NetworkProviderInfo.DEVICE_TYPE_AUTO))
- .isNotNull();
- }
-
- @Test
public void testInternetIconInjector_getIcon_returnsCorrectValues() {
WifiUtils.InternetIconInjector iconInjector = new WifiUtils.InternetIconInjector(mContext);
diff --git a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
index 2cdd0ae..3530e0f 100644
--- a/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
+++ b/packages/SettingsProvider/src/android/provider/settings/backup/SystemSettings.java
@@ -106,6 +106,8 @@
Settings.System.UNREAD_NOTIFICATION_DOT_INDICATOR,
Settings.System.AUTO_LAUNCH_MEDIA_CONTROLS,
Settings.System.LOCALE_PREFERENCES,
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING,
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON,
Settings.System.TOUCHPAD_POINTER_SPEED,
Settings.System.TOUCHPAD_NATURAL_SCROLLING,
Settings.System.TOUCHPAD_TAP_TO_CLICK,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
index 2823277..509b88b 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/SystemSettingsValidators.java
@@ -221,6 +221,8 @@
POINTER_ICON_VECTOR_STYLE_STROKE_END));
VALIDATORS.put(System.POINTER_SCALE,
new InclusiveFloatRangeValidator(DEFAULT_POINTER_SCALE, LARGE_POINTER_SCALE));
+ VALIDATORS.put(System.MOUSE_REVERSE_VERTICAL_SCROLLING, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(System.MOUSE_SWAP_PRIMARY_BUTTON, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_POINTER_SPEED, new InclusiveIntegerRangeValidator(-7, 7));
VALIDATORS.put(System.TOUCHPAD_NATURAL_SCROLLING, BOOLEAN_VALIDATOR);
VALIDATORS.put(System.TOUCHPAD_TAP_TO_CLICK, BOOLEAN_VALIDATOR);
diff --git a/packages/SystemUI/Android.bp b/packages/SystemUI/Android.bp
index 1f10ead..c4a45d0 100644
--- a/packages/SystemUI/Android.bp
+++ b/packages/SystemUI/Android.bp
@@ -425,8 +425,8 @@
"tests/src/**/systemui/media/controls/domain/pipeline/LegacyMediaDataManagerImplTest.kt",
"tests/src/**/systemui/shared/system/RemoteTransitionTest.java",
"tests/src/**/systemui/navigationbar/NavigationBarControllerImplTest.java",
- "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt",
- "tests/src/**/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt",
+ "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt",
+ "tests/src/**/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt",
"tests/src/**/systemui/notetask/quickaffordance/NoteTaskQuickAffordanceConfigTest.kt",
"tests/src/**/systemui/notetask/LaunchNotesRoleSettingsTrampolineActivityTest.kt",
"tests/src/**/systemui/notetask/shortcut/LaunchNoteTaskActivityTest.kt",
diff --git a/packages/SystemUI/TEST_MAPPING b/packages/SystemUI/TEST_MAPPING
index 07a1e63..380344a 100644
--- a/packages/SystemUI/TEST_MAPPING
+++ b/packages/SystemUI/TEST_MAPPING
@@ -148,5 +148,10 @@
{
"name": "SystemUIGoogleRobo2RNGTests"
}
+ ],
+ "imports": [
+ {
+ "path": "cts/tests/tests/multiuser"
+ }
]
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
new file mode 100644
index 0000000..9b94c91
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/ComposedDigitalLayerController.kt
@@ -0,0 +1,203 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import com.android.systemui.shared.clocks.view.FlexClockView
+import java.util.Locale
+import java.util.TimeZone
+
+class ComposedDigitalLayerController(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ private val layer: ComposedDigitalHandLayer,
+ private val isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+) : SimpleClockLayerController {
+ private val logger = Logger(messageBuffer, ComposedDigitalLayerController::class.simpleName!!)
+
+ val layerControllers = mutableListOf<SimpleClockLayerController>()
+ val dozeState = DefaultClockController.AnimationState(1F)
+ var isRegionDark = true
+
+ override var view: DigitalClockFaceView =
+ when (layer.customizedView) {
+ "FlexClockView" -> FlexClockView(ctx, assets, messageBuffer)
+ else -> {
+ throw IllegalStateException("CustomizedView string is not valid")
+ }
+ }
+
+ // Matches LayerControllerConstructor
+ internal constructor(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+ ) : this(ctx, assets, layer as ComposedDigitalHandLayer, isLargeClock, messageBuffer)
+
+ init {
+ layer.digitalLayers.forEach {
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ it,
+ isLargeClock,
+ messageBuffer,
+ )
+ view.addView(controller.view)
+ layerControllers.add(controller)
+ }
+ }
+
+ private fun refreshTime() {
+ layerControllers.forEach { it.faceEvents.onTimeTick() }
+ view.refreshTime()
+ }
+
+ override val events =
+ object : ClockEvents {
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ layerControllers.forEach { it.events.onTimeZoneChanged(timeZone) }
+ refreshTime()
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ layerControllers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+ refreshTime()
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ layerControllers.forEach { it.events.onLocaleChanged(locale) }
+ view.onLocaleChanged(locale)
+ refreshTime()
+ }
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ view.onWeatherDataChanged(data)
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ view.onAlarmDataChanged(data)
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ view.onZenDataChanged(data)
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {}
+
+ override fun onSeedColorChanged(seedColor: Int?) {}
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+ override var isReactiveTouchInteractionEnabled
+ get() = view.isReactiveTouchInteractionEnabled
+ set(value) {
+ view.isReactiveTouchInteractionEnabled = value
+ }
+ }
+
+ override fun updateColors() {
+ view.updateColors(assets, isRegionDark)
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ refreshTime()
+ }
+
+ override fun doze(fraction: Float) {
+ val (hasChanged, hasJumped) = dozeState.update(fraction)
+ if (hasChanged) view.animateDoze(dozeState.isActive, !hasJumped)
+ view.dozeFraction = fraction
+ view.invalidate()
+ }
+
+ override fun fold(fraction: Float) {
+ refreshTime()
+ }
+
+ override fun charge() {
+ view.animateCharge()
+ }
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+ view.onPositionUpdated(fromLeft, direction, fraction)
+ }
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {
+ view.onPickerCarouselSwiping(swipingFraction)
+ }
+ }
+
+ override val faceEvents =
+ object : ClockFaceEvents {
+ override fun onTimeTick() {
+ refreshTime()
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ this@ComposedDigitalLayerController.isRegionDark = isRegionDark
+ updateColors()
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ view.onFontSettingChanged(fontSizePx)
+ }
+
+ override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+ }
+
+ override val config =
+ ClockFaceConfig(
+ hasCustomWeatherDataDisplay = view.hasCustomWeatherDataDisplay,
+ hasCustomPositionUpdatedAnimation = view.hasCustomPositionUpdatedAnimation,
+ useCustomClockScene = view.useCustomClockScene,
+ )
+
+ @VisibleForTesting
+ override var fakeTimeMills: Long? = null
+ get() = field
+ set(timeInMills) {
+ field = timeInMills
+ for (layerController in layerControllers) {
+ layerController.fakeTimeMills = timeInMills
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
index 07191c6..ac26842 100644
--- a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/DefaultClockProvider.kt
@@ -24,6 +24,8 @@
import com.android.systemui.plugins.clocks.ClockPickerConfig
import com.android.systemui.plugins.clocks.ClockProvider
import com.android.systemui.plugins.clocks.ClockSettings
+import com.android.systemui.shared.clocks.view.HorizontalAlignment
+import com.android.systemui.shared.clocks.view.VerticalAlignment
private val TAG = DefaultClockProvider::class.simpleName
const val DEFAULT_CLOCK_ID = "DEFAULT"
@@ -33,8 +35,9 @@
val ctx: Context,
val layoutInflater: LayoutInflater,
val resources: Resources,
- val hasStepClockAnimation: Boolean = false,
- val migratedClocks: Boolean = false,
+ private val hasStepClockAnimation: Boolean = false,
+ private val migratedClocks: Boolean = false,
+ private val clockReactiveVariants: Boolean = false,
) : ClockProvider {
private var messageBuffers: ClockMessageBuffers? = null
@@ -49,15 +52,23 @@
throw IllegalArgumentException("${settings.clockId} is unsupported by $TAG")
}
- return DefaultClockController(
- ctx,
- layoutInflater,
- resources,
- settings,
- hasStepClockAnimation,
- migratedClocks,
- messageBuffers,
- )
+ return if (clockReactiveVariants) {
+ // TODO handle the case here where only the smallClock message buffer is added
+ val assetLoader =
+ AssetLoader(ctx, ctx, "clocks/", messageBuffers?.smallClockMessageBuffer!!)
+
+ SimpleClockController(ctx, assetLoader, FLEX_DESIGN, messageBuffers)
+ } else {
+ DefaultClockController(
+ ctx,
+ layoutInflater,
+ resources,
+ settings,
+ hasStepClockAnimation,
+ migratedClocks,
+ messageBuffers,
+ )
+ }
}
override fun getClockPickerConfig(id: ClockId): ClockPickerConfig {
@@ -73,4 +84,163 @@
resources.getDrawable(R.drawable.clock_default_thumbnail, null),
)
}
+
+ companion object {
+ val FLEX_DESIGN = run {
+ val largeLayer =
+ listOf(
+ ComposedDigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ customizedView = "FlexClockView",
+ digitalLayers =
+ listOf(
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.FIRST_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "hh"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.SECOND_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "hh"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.FIRST_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "mm"
+ ),
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.SECOND_DIGIT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ lineHeight = 147.25f,
+ fontVariation =
+ "'wght' 603, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontVariation =
+ "'wght' 74, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fontFamily = "google_sans_flex.ttf",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ transitionInterpolator = InterpolatorEnum.EMPHASIZED,
+ transitionDuration = 750,
+ ),
+ alignment =
+ DigitalAlignment(
+ HorizontalAlignment.CENTER,
+ VerticalAlignment.CENTER
+ ),
+ dateTimeFormat = "mm"
+ )
+ )
+ )
+ )
+
+ val smallLayer =
+ listOf(
+ DigitalHandLayer(
+ layerBounds = LayerBounds.FIT,
+ timespec = DigitalTimespec.TIME_FULL_FORMAT,
+ style =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ fontVariation = "'wght' 600, 'wdth' 100, 'opsz' 144, 'ROND' 100",
+ fontSizeScale = 0.98f,
+ ),
+ aodStyle =
+ FontTextStyle(
+ fontFamily = "google_sans_flex.ttf",
+ fontVariation = "'wght' 133, 'wdth' 43, 'opsz' 144, 'ROND' 100",
+ fillColorLight = "#FFFFFFFF",
+ outlineColor = "#00000000",
+ renderType = RenderType.CHANGE_WEIGHT,
+ ),
+ alignment = DigitalAlignment(HorizontalAlignment.LEFT, null),
+ dateTimeFormat = "h:mm"
+ )
+ )
+
+ ClockDesign(
+ id = DEFAULT_CLOCK_ID,
+ name = "@string/clock_default_name",
+ description = "@string/clock_default_description",
+ large = ClockFace(layers = largeLayer),
+ small = ClockFace(layers = smallLayer)
+ )
+ }
+ }
}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.kt
new file mode 100644
index 0000000..ef8bee0
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/LayoutUtils.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.shared.clocks
+
+import android.graphics.Rect
+import android.view.View
+
+fun computeLayoutDiff(
+ view: View,
+ targetRegion: Rect,
+ isLargeClock: Boolean,
+): Pair<Float, Float> {
+ val parent = view.parent
+ if (parent is View && parent.isLaidOut() && isLargeClock) {
+ return Pair(
+ targetRegion.centerX() - parent.width / 2f,
+ targetRegion.centerY() - parent.height / 2f
+ )
+ }
+ return Pair(0f, 0f)
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt
new file mode 100644
index 0000000..ec77798
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockController.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import com.android.systemui.monet.Style as MonetStyle
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockConfig
+import com.android.systemui.plugins.clocks.ClockController
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockMessageBuffers
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import java.io.PrintWriter
+import java.util.Locale
+import java.util.TimeZone
+
+/** Controller for a simple json specified clock */
+class SimpleClockController(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ val design: ClockDesign,
+ val messageBuffers: ClockMessageBuffers?,
+) : ClockController {
+ override val smallClock = run {
+ val buffer = messageBuffers?.smallClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+ SimpleClockFaceController(
+ ctx,
+ assets.copy(messageBuffer = buffer),
+ design.small ?: design.large!!,
+ false,
+ buffer,
+ )
+ }
+
+ override val largeClock = run {
+ val buffer = messageBuffers?.largeClockMessageBuffer ?: LogUtil.DEFAULT_MESSAGE_BUFFER
+ SimpleClockFaceController(
+ ctx,
+ assets.copy(messageBuffer = buffer),
+ design.large ?: design.small!!,
+ true,
+ buffer,
+ )
+ }
+
+ override val config: ClockConfig by lazy {
+ ClockConfig(
+ design.id,
+ design.name?.let { assets.tryReadString(it) ?: it } ?: "",
+ design.description?.let { assets.tryReadString(it) ?: it } ?: "",
+ isReactiveToTone =
+ design.colorPalette == null || design.colorPalette == MonetStyle.CLOCK,
+ useAlternateSmartspaceAODTransition =
+ smallClock.config.hasCustomWeatherDataDisplay ||
+ largeClock.config.hasCustomWeatherDataDisplay,
+ useCustomClockScene =
+ smallClock.config.useCustomClockScene || largeClock.config.useCustomClockScene,
+ )
+ }
+
+ override val events =
+ object : ClockEvents {
+ override var isReactiveTouchInteractionEnabled = false
+ set(value) {
+ field = value
+ smallClock.events.isReactiveTouchInteractionEnabled = value
+ largeClock.events.isReactiveTouchInteractionEnabled = value
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ smallClock.events.onTimeZoneChanged(timeZone)
+ largeClock.events.onTimeZoneChanged(timeZone)
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ smallClock.events.onTimeFormatChanged(is24Hr)
+ largeClock.events.onTimeFormatChanged(is24Hr)
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ smallClock.events.onLocaleChanged(locale)
+ largeClock.events.onLocaleChanged(locale)
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {
+ assets.refreshColorPalette(design.colorPalette)
+ smallClock.assets.refreshColorPalette(design.colorPalette)
+ largeClock.assets.refreshColorPalette(design.colorPalette)
+
+ smallClock.events.onColorPaletteChanged(resources)
+ largeClock.events.onColorPaletteChanged(resources)
+ }
+
+ override fun onSeedColorChanged(seedColor: Int?) {
+ assets.setSeedColor(seedColor, design.colorPalette)
+ smallClock.assets.setSeedColor(seedColor, design.colorPalette)
+ largeClock.assets.setSeedColor(seedColor, design.colorPalette)
+
+ smallClock.events.onSeedColorChanged(seedColor)
+ largeClock.events.onSeedColorChanged(seedColor)
+ }
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ smallClock.events.onWeatherDataChanged(data)
+ largeClock.events.onWeatherDataChanged(data)
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ smallClock.events.onAlarmDataChanged(data)
+ largeClock.events.onAlarmDataChanged(data)
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ smallClock.events.onZenDataChanged(data)
+ largeClock.events.onZenDataChanged(data)
+ }
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {
+ smallClock.events.onReactiveAxesChanged(axes)
+ largeClock.events.onReactiveAxesChanged(axes)
+ }
+ }
+
+ override fun initialize(resources: Resources, dozeFraction: Float, foldFraction: Float) {
+ events.onColorPaletteChanged(resources)
+ smallClock.animations.doze(dozeFraction)
+ largeClock.animations.doze(dozeFraction)
+ smallClock.animations.fold(foldFraction)
+ largeClock.animations.fold(foldFraction)
+ smallClock.events.onTimeTick()
+ largeClock.events.onTimeTick()
+ }
+
+ override fun dump(pw: PrintWriter) {}
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
new file mode 100644
index 0000000..ef398d1
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockFaceController.kt
@@ -0,0 +1,314 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.Gravity
+import android.view.View
+import android.view.ViewGroup.LayoutParams.MATCH_PARENT
+import android.widget.FrameLayout
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceController
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockFaceLayout
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.ClockTickRate
+import com.android.systemui.plugins.clocks.DefaultClockFaceLayout
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.DigitalClockFaceView
+import java.util.Locale
+import java.util.TimeZone
+import kotlin.math.max
+
+interface ClockEventUnion : ClockEvents, ClockFaceEvents
+
+class SimpleClockFaceController(
+ ctx: Context,
+ val assets: AssetLoader,
+ face: ClockFace,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+) : ClockFaceController {
+ override val view: View
+ override val config: ClockFaceConfig by lazy {
+ ClockFaceConfig(
+ hasCustomWeatherDataDisplay = layers.any { it.config.hasCustomWeatherDataDisplay },
+ hasCustomPositionUpdatedAnimation =
+ layers.any { it.config.hasCustomPositionUpdatedAnimation },
+ tickRate = getTickRate(),
+ useCustomClockScene = layers.any { it.config.useCustomClockScene },
+ )
+ }
+
+ val layers = mutableListOf<SimpleClockLayerController>()
+
+ val timespecHandler = DigitalTimespecHandler(DigitalTimespec.TIME_FULL_FORMAT, "hh:mm")
+
+ init {
+ val lp = FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ lp.gravity = Gravity.CENTER
+ view =
+ if (face.layers.size == 1) {
+ // Optimize a clocks with a single layer by excluding the face level view group. We
+ // expect the view container from the host process to always be a FrameLayout.
+ val layer = face.layers[0]
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ layer,
+ isLargeClock,
+ messageBuffer,
+ )
+ layers.add(controller)
+ controller.view.layoutParams = lp
+ controller.view
+ } else {
+ // For multiple views, we use an intermediate RelativeLayout so that we can do some
+ // intelligent laying out between the children views.
+ val group = SimpleClockRelativeLayout(ctx, face.faceLayout)
+ group.layoutParams = lp
+ group.gravity = Gravity.CENTER
+ group.clipChildren = false
+ for (layer in face.layers) {
+ face.faceLayout?.let {
+ if (layer is DigitalHandLayer) {
+ layer.faceLayout = it
+ }
+ }
+ val controller =
+ SimpleClockLayerController.Factory.create(
+ ctx,
+ assets,
+ layer,
+ isLargeClock,
+ messageBuffer,
+ )
+ group.addView(controller.view)
+ layers.add(controller)
+ }
+ group
+ }
+ }
+
+ override val layout: ClockFaceLayout =
+ DefaultClockFaceLayout(view).apply {
+ views[0].id =
+ if (isLargeClock) {
+ assets.getResourcesId("lockscreen_clock_view_large")
+ } else {
+ assets.getResourcesId("lockscreen_clock_view")
+ }
+ }
+
+ override val events =
+ object : ClockEventUnion {
+ override var isReactiveTouchInteractionEnabled = false
+ get() = field
+ set(value) {
+ field = value
+ layers.forEach { it.events.isReactiveTouchInteractionEnabled = value }
+ }
+
+ override fun onTimeTick() {
+ timespecHandler.updateTime()
+ if (
+ config.tickRate == ClockTickRate.PER_MINUTE ||
+ view.contentDescription != timespecHandler.getContentDescription()
+ ) {
+ view.contentDescription = timespecHandler.getContentDescription()
+ }
+ layers.forEach { it.faceEvents.onTimeTick() }
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ timespecHandler.timeZone = timeZone
+ layers.forEach { it.events.onTimeZoneChanged(timeZone) }
+ }
+
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ timespecHandler.is24Hr = is24Hr
+ layers.forEach { it.events.onTimeFormatChanged(is24Hr) }
+ }
+
+ override fun onLocaleChanged(locale: Locale) {
+ timespecHandler.updateLocale(locale)
+ layers.forEach { it.events.onLocaleChanged(locale) }
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ layers.forEach { it.faceEvents.onFontSettingChanged(fontSizePx) }
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {
+ layers.forEach {
+ it.events.onColorPaletteChanged(resources)
+ it.updateColors()
+ }
+ }
+
+ override fun onSeedColorChanged(seedColor: Int?) {
+ layers.forEach {
+ it.events.onSeedColorChanged(seedColor)
+ it.updateColors()
+ }
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ layers.forEach { it.faceEvents.onRegionDarknessChanged(isRegionDark) }
+ }
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+
+ /**
+ * targetRegion passed to all customized clock applies counter translationY of
+ * KeyguardStatusView and keyguard_large_clock_top_margin from default clock
+ */
+ override fun onTargetRegionChanged(targetRegion: Rect?) {
+ // When a clock needs to be aligned with screen, like weather clock
+ // it needs to offset back the translation of keyguard_large_clock_top_margin
+ if (view is DigitalClockFaceView && view.isAlignedWithScreen()) {
+ val topMargin = getKeyguardLargeClockTopMargin(assets)
+ targetRegion?.let {
+ val (_, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+ // In LS, we use yDiff to counter translate
+ // the translation of KeyguardLargeClockTopMargin
+ // With the targetRegion passed from picker,
+ // we will have yDiff = 0, no translation is needed for weather clock
+ if (yDiff.toInt() != 0) view.translationY = yDiff - topMargin / 2
+ }
+ return
+ }
+
+ var maxWidth = 0f
+ var maxHeight = 0f
+
+ for (layer in layers) {
+ layer.faceEvents.onTargetRegionChanged(targetRegion)
+ maxWidth = max(maxWidth, layer.view.layoutParams.width.toFloat())
+ maxHeight = max(maxHeight, layer.view.layoutParams.height.toFloat())
+ }
+
+ val lp =
+ if (maxHeight <= 0 || maxWidth <= 0 || targetRegion == null) {
+ // No specified width/height. Just match parent size.
+ FrameLayout.LayoutParams(MATCH_PARENT, MATCH_PARENT)
+ } else {
+ // Scale to fit in targetRegion based on largest child elements.
+ val ratio = maxWidth / maxHeight
+ val targetRatio = targetRegion.width() / targetRegion.height().toFloat()
+ val scale =
+ if (ratio > targetRatio) targetRegion.width() / maxWidth
+ else targetRegion.height() / maxHeight
+
+ FrameLayout.LayoutParams(
+ (maxWidth * scale).toInt(),
+ (maxHeight * scale).toInt(),
+ )
+ }
+
+ lp.gravity = Gravity.CENTER
+ view.layoutParams = lp
+ targetRegion?.let {
+ val (xDiff, yDiff) = computeLayoutDiff(view, it, isLargeClock)
+ view.translationX = xDiff
+ view.translationY = yDiff
+ }
+ }
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+
+ override fun onWeatherDataChanged(data: WeatherData) {
+ layers.forEach { it.events.onWeatherDataChanged(data) }
+ }
+
+ override fun onAlarmDataChanged(data: AlarmData) {
+ layers.forEach { it.events.onAlarmDataChanged(data) }
+ }
+
+ override fun onZenDataChanged(data: ZenData) {
+ layers.forEach { it.events.onZenDataChanged(data) }
+ }
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ layers.forEach { it.animations.enter() }
+ }
+
+ override fun doze(fraction: Float) {
+ layers.forEach { it.animations.doze(fraction) }
+ }
+
+ override fun fold(fraction: Float) {
+ layers.forEach { it.animations.fold(fraction) }
+ }
+
+ override fun charge() {
+ layers.forEach { it.animations.charge() }
+ }
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {
+ face.pickerScale?.let {
+ view.scaleX = swipingFraction * (1 - it.scaleX) + it.scaleX
+ view.scaleY = swipingFraction * (1 - it.scaleY) + it.scaleY
+ }
+ if (!(view is DigitalClockFaceView && view.isAlignedWithScreen())) {
+ val topMargin = getKeyguardLargeClockTopMargin(assets)
+ view.translationY = topMargin / 2F * swipingFraction
+ }
+ layers.forEach { it.animations.onPickerCarouselSwiping(swipingFraction) }
+ view.invalidate()
+ }
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {
+ layers.forEach { it.animations.onPositionUpdated(fromLeft, direction, fraction) }
+ }
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {
+ layers.forEach { it.animations.onPositionUpdated(distance, fraction) }
+ }
+ }
+
+ private fun getTickRate(): ClockTickRate {
+ var tickRate = ClockTickRate.PER_MINUTE
+ for (layer in layers) {
+ if (layer.config.tickRate.value < tickRate.value) {
+ tickRate = layer.config.tickRate
+ }
+ }
+ return tickRate
+ }
+
+ private fun getKeyguardLargeClockTopMargin(assets: AssetLoader): Int {
+ val topMarginRes =
+ assets.resolveResourceId(null, "dimen", "keyguard_large_clock_top_margin")
+ if (topMarginRes != null) {
+ val (res, id) = topMarginRes
+ return res.getDimensionPixelSize(id)
+ }
+ return 0
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
new file mode 100644
index 0000000..f71543e
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockLayerController.kt
@@ -0,0 +1,102 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.view.View
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockTextView
+import kotlin.reflect.KClass
+
+typealias LayerControllerConstructor =
+ (
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer,
+ ) -> SimpleClockLayerController
+
+interface SimpleClockLayerController {
+ val view: View
+ val events: ClockEvents
+ val animations: ClockAnimations
+ val faceEvents: ClockFaceEvents
+ val config: ClockFaceConfig
+
+ @VisibleForTesting var fakeTimeMills: Long?
+
+ // Called immediately after either onColorPaletteChanged or onSeedColorChanged is called.
+ // Provided for convience to not duplicate color update logic after state updated.
+ fun updateColors() {}
+
+ companion object Factory {
+ val constructorMap = mutableMapOf<Pair<KClass<*>, KClass<*>?>, LayerControllerConstructor>()
+
+ internal inline fun <reified TLayer> registerConstructor(
+ noinline constructor: LayerControllerConstructor,
+ ) where TLayer : ClockLayer {
+ constructorMap[Pair(TLayer::class, null)] = constructor
+ }
+
+ inline fun <reified TLayer, reified TStyle> registerTextConstructor(
+ noinline constructor: LayerControllerConstructor,
+ ) where TLayer : ClockLayer, TStyle : TextStyle {
+ constructorMap[Pair(TLayer::class, TStyle::class)] = constructor
+ }
+
+ init {
+ registerConstructor<ComposedDigitalHandLayer>(::ComposedDigitalLayerController)
+ registerTextConstructor<DigitalHandLayer, FontTextStyle>(::createSimpleDigitalLayer)
+ }
+
+ private fun createSimpleDigitalLayer(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer
+ ): SimpleClockLayerController {
+ val view = SimpleDigitalClockTextView(ctx, messageBuffer)
+ return SimpleDigitalHandLayerController(
+ ctx,
+ assets,
+ layer as DigitalHandLayer,
+ view,
+ messageBuffer
+ )
+ }
+
+ fun create(
+ ctx: Context,
+ assets: AssetLoader,
+ layer: ClockLayer,
+ isLargeClock: Boolean,
+ messageBuffer: MessageBuffer
+ ): SimpleClockLayerController {
+ val styleClass = if (layer is DigitalHandLayer) layer.style::class else null
+ val key = Pair(layer::class, styleClass)
+ return constructorMap[key]?.invoke(ctx, assets, layer, isLargeClock, messageBuffer)
+ ?: throw IllegalArgumentException("Unrecognized ClockLayer type: $key")
+ }
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
new file mode 100644
index 0000000..6e1b9aa
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleClockRelativeLayout.kt
@@ -0,0 +1,47 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.view.View.MeasureSpec.EXACTLY
+import android.widget.RelativeLayout
+import androidx.core.view.children
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+
+class SimpleClockRelativeLayout(context: Context, val faceLayout: DigitalFaceLayout?) :
+ RelativeLayout(context) {
+ override fun onMeasure(widthMeasureSpec: Int, heightMeasureSpec: Int) {
+ // For migrate_clocks_to_blueprint, mode is EXACTLY
+ // when the flag is turned off, we won't execute this codes
+ if (MeasureSpec.getMode(heightMeasureSpec) == EXACTLY) {
+ if (
+ faceLayout == DigitalFaceLayout.TWO_PAIRS_VERTICAL ||
+ faceLayout == DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER
+ ) {
+ val constrainedHeight = MeasureSpec.getSize(heightMeasureSpec) / 2F
+ children.forEach {
+ // The assumption here is the height of text view is linear to font size
+ (it as SimpleDigitalClockView).applyTextSize(
+ constrainedHeight,
+ constrainedByHeight = true,
+ )
+ }
+ }
+ }
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
new file mode 100644
index 0000000..a3240f8
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/SimpleDigitalHandLayerController.kt
@@ -0,0 +1,328 @@
+/*
+ * 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.shared.clocks
+
+import android.content.Context
+import android.content.res.Resources
+import android.graphics.Rect
+import android.view.View
+import android.view.ViewGroup
+import android.widget.RelativeLayout
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.customization.R
+import com.android.systemui.log.core.Logger
+import com.android.systemui.log.core.MessageBuffer
+import com.android.systemui.plugins.clocks.AlarmData
+import com.android.systemui.plugins.clocks.ClockAnimations
+import com.android.systemui.plugins.clocks.ClockEvents
+import com.android.systemui.plugins.clocks.ClockFaceConfig
+import com.android.systemui.plugins.clocks.ClockFaceEvents
+import com.android.systemui.plugins.clocks.ClockReactiveSetting
+import com.android.systemui.plugins.clocks.WeatherData
+import com.android.systemui.plugins.clocks.ZenData
+import com.android.systemui.shared.clocks.view.SimpleDigitalClockView
+import java.util.Locale
+import java.util.TimeZone
+
+private val TAG = SimpleDigitalHandLayerController::class.simpleName!!
+
+open class SimpleDigitalHandLayerController<T>(
+ private val ctx: Context,
+ private val assets: AssetLoader,
+ private val layer: DigitalHandLayer,
+ override val view: T,
+ messageBuffer: MessageBuffer,
+) : SimpleClockLayerController where T : View, T : SimpleDigitalClockView {
+ private val logger = Logger(messageBuffer, TAG)
+ val timespec = DigitalTimespecHandler(layer.timespec, layer.dateTimeFormat)
+
+ @VisibleForTesting
+ fun hasLeadingZero() = layer.dateTimeFormat.startsWith("hh") || timespec.is24Hr
+
+ @VisibleForTesting
+ override var fakeTimeMills: Long?
+ get() = timespec.fakeTimeMills
+ set(value) {
+ timespec.fakeTimeMills = value
+ }
+
+ override val config = ClockFaceConfig()
+ var dozeState: DefaultClockController.AnimationState? = null
+ var isRegionDark: Boolean = true
+
+ init {
+ view.layoutParams =
+ RelativeLayout.LayoutParams(
+ ViewGroup.LayoutParams.WRAP_CONTENT,
+ ViewGroup.LayoutParams.WRAP_CONTENT
+ )
+ if (layer.alignment != null) {
+ layer.alignment.verticalAlignment?.let { view.verticalAlignment = it }
+ layer.alignment.horizontalAlignment?.let { view.horizontalAlignment = it }
+ }
+ view.applyStyles(assets, layer.style, layer.aodStyle)
+ view.id =
+ ctx.resources.getIdentifier(
+ generateDigitalLayerIdString(layer),
+ "id",
+ ctx.getPackageName(),
+ )
+ }
+
+ fun applyLayout(layout: DigitalFaceLayout?) {
+ when (layout) {
+ DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER,
+ DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> applyFourDigitsLayout(layout)
+ DigitalFaceLayout.TWO_PAIRS_HORIZONTAL,
+ DigitalFaceLayout.TWO_PAIRS_VERTICAL -> applyTwoPairsLayout(layout)
+ else -> {
+ // one view always use FrameLayout
+ // no need to change here
+ }
+ }
+ applyMargin()
+ }
+
+ private fun applyMargin() {
+ if (view.layoutParams is RelativeLayout.LayoutParams) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ layer.marginRatio?.let {
+ lp.setMargins(
+ /* left = */ (it.left * view.measuredWidth).toInt(),
+ /* top = */ (it.top * view.measuredHeight).toInt(),
+ /* right = */ (it.right * view.measuredWidth).toInt(),
+ /* bottom = */ (it.bottom * view.measuredHeight).toInt(),
+ )
+ }
+ view.layoutParams = lp
+ }
+ }
+
+ private fun applyTwoPairsLayout(twoPairsLayout: DigitalFaceLayout) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ lp.addRule(RelativeLayout.TEXT_ALIGNMENT_CENTER)
+ if (twoPairsLayout == DigitalFaceLayout.TWO_PAIRS_HORIZONTAL) {
+ when (view.id) {
+ R.id.HOUR_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_DIGIT_PAIR)
+ }
+ else -> {
+ throw Exception("cannot apply two pairs layout to view ${view.id}")
+ }
+ }
+ } else {
+ when (view.id) {
+ R.id.HOUR_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+ }
+ R.id.MINUTE_DIGIT_PAIR -> {
+ lp.addRule(RelativeLayout.CENTER_HORIZONTAL)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_DIGIT_PAIR)
+ }
+ else -> {
+ throw Exception("cannot apply two pairs layout to view ${view.id}")
+ }
+ }
+ }
+ view.layoutParams = lp
+ }
+
+ private fun applyFourDigitsLayout(fourDigitsfaceLayout: DigitalFaceLayout) {
+ val lp = view.layoutParams as RelativeLayout.LayoutParams
+ when (fourDigitsfaceLayout) {
+ DigitalFaceLayout.FOUR_DIGITS_ALIGN_CENTER -> {
+ when (view.id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_TOP)
+ }
+ R.id.HOUR_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+ lp.addRule(RelativeLayout.ALIGN_TOP, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_FIRST_DIGIT)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.ALIGN_START, R.id.HOUR_SECOND_DIGIT)
+ lp.addRule(RelativeLayout.BELOW, R.id.HOUR_SECOND_DIGIT)
+ }
+ else -> {
+ throw Exception("cannot apply four digits layout to view ${view.id}")
+ }
+ }
+ }
+ DigitalFaceLayout.FOUR_DIGITS_HORIZONTAL -> {
+ when (view.id) {
+ R.id.HOUR_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.ALIGN_PARENT_START)
+ }
+ R.id.HOUR_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_FIRST_DIGIT)
+ }
+ R.id.MINUTE_FIRST_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.HOUR_SECOND_DIGIT)
+ }
+ R.id.MINUTE_SECOND_DIGIT -> {
+ lp.addRule(RelativeLayout.CENTER_VERTICAL)
+ lp.addRule(RelativeLayout.END_OF, R.id.MINUTE_FIRST_DIGIT)
+ }
+ else -> {
+ throw Exception("cannot apply FOUR_DIGITS_HORIZONTAL to view ${view.id}")
+ }
+ }
+ }
+ else -> {
+ throw IllegalArgumentException(
+ "applyFourDigitsLayout function should not " +
+ "have parameters as ${layer.faceLayout}"
+ )
+ }
+ }
+ if (lp == view.layoutParams) {
+ return
+ }
+ view.layoutParams = lp
+ }
+
+ fun refreshTime() {
+ timespec.updateTime()
+ val text = timespec.getDigitString()
+ if (view.text != text) {
+ view.text = text
+ view.refreshTime()
+ logger.d({ "refreshTime: new text=$str1" }) { str1 = text }
+ }
+ }
+
+ override val events =
+ object : ClockEvents {
+ override var isReactiveTouchInteractionEnabled = false
+
+ override fun onLocaleChanged(locale: Locale) {
+ timespec.updateLocale(locale)
+ refreshTime()
+ }
+
+ /** Call whenever the text time format changes (12hr vs 24hr) */
+ override fun onTimeFormatChanged(is24Hr: Boolean) {
+ timespec.is24Hr = is24Hr
+ refreshTime()
+ }
+
+ override fun onTimeZoneChanged(timeZone: TimeZone) {
+ timespec.timeZone = timeZone
+ refreshTime()
+ }
+
+ override fun onColorPaletteChanged(resources: Resources) {}
+
+ override fun onSeedColorChanged(seedColor: Int?) {}
+
+ override fun onWeatherDataChanged(data: WeatherData) {}
+
+ override fun onAlarmDataChanged(data: AlarmData) {}
+
+ override fun onZenDataChanged(data: ZenData) {}
+
+ override fun onReactiveAxesChanged(axes: List<ClockReactiveSetting>) {}
+ }
+
+ override fun updateColors() {
+ view.updateColors(assets, isRegionDark)
+ refreshTime()
+ }
+
+ override val animations =
+ object : ClockAnimations {
+ override fun enter() {
+ applyLayout(layer.faceLayout)
+ refreshTime()
+ }
+
+ override fun doze(fraction: Float) {
+ if (dozeState == null) {
+ dozeState = DefaultClockController.AnimationState(fraction)
+ view.animateDoze(dozeState!!.isActive, false)
+ } else {
+ val (hasChanged, hasJumped) = dozeState!!.update(fraction)
+ if (hasChanged) view.animateDoze(dozeState!!.isActive, !hasJumped)
+ }
+ view.dozeFraction = fraction
+ }
+
+ override fun fold(fraction: Float) {
+ applyLayout(layer.faceLayout)
+ refreshTime()
+ }
+
+ override fun charge() {
+ view.animateCharge()
+ }
+
+ override fun onPickerCarouselSwiping(swipingFraction: Float) {}
+
+ override fun onPositionUpdated(fromLeft: Int, direction: Int, fraction: Float) {}
+
+ override fun onPositionUpdated(distance: Float, fraction: Float) {}
+ }
+
+ override val faceEvents =
+ object : ClockFaceEvents {
+ override fun onTimeTick() {
+ refreshTime()
+ if (
+ layer.timespec == DigitalTimespec.TIME_FULL_FORMAT ||
+ layer.timespec == DigitalTimespec.DATE_FORMAT
+ ) {
+ view.contentDescription = timespec.getContentDescription()
+ }
+ }
+
+ override fun onFontSettingChanged(fontSizePx: Float) {
+ view.applyTextSize(fontSizePx)
+ applyMargin()
+ }
+
+ override fun onRegionDarknessChanged(isRegionDark: Boolean) {
+ this@SimpleDigitalHandLayerController.isRegionDark = isRegionDark
+ updateColors()
+ }
+
+ override fun onTargetRegionChanged(targetRegion: Rect?) {}
+
+ override fun onSecondaryDisplayChanged(onSecondaryDisplay: Boolean) {}
+ }
+
+ companion object {
+ private val DEFAULT_LIGHT_COLOR = "@android:color/system_accent1_100+0"
+ private val DEFAULT_DARK_COLOR = "@android:color/system_accent2_600+0"
+
+ fun getDefaultColor(assets: AssetLoader, isRegionDark: Boolean) =
+ assets.readColor(if (isRegionDark) DEFAULT_LIGHT_COLOR else DEFAULT_DARK_COLOR)
+ }
+}
diff --git a/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
new file mode 100644
index 0000000..ed6a403
--- /dev/null
+++ b/packages/SystemUI/customization/src/com/android/systemui/shared/clocks/TimespecHandler.kt
@@ -0,0 +1,161 @@
+/*
+ * 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.shared.clocks
+
+import android.icu.text.DateFormat
+import android.icu.text.SimpleDateFormat
+import android.icu.util.TimeZone as IcuTimeZone
+import android.icu.util.ULocale
+import androidx.annotation.VisibleForTesting
+import java.util.Calendar
+import java.util.Locale
+import java.util.TimeZone
+
+open class TimespecHandler(
+ val cal: Calendar,
+) {
+ var timeZone: TimeZone
+ get() = cal.timeZone
+ set(value) {
+ cal.timeZone = value
+ onTimeZoneChanged()
+ }
+
+ @VisibleForTesting var fakeTimeMills: Long? = null
+
+ fun updateTime() {
+ var timeMs = fakeTimeMills ?: System.currentTimeMillis()
+ cal.timeInMillis = (timeMs * TIME_TRAVEL_SCALE).toLong()
+ }
+
+ protected open fun onTimeZoneChanged() {}
+
+ companion object {
+ // Modifying this will cause the clock to run faster or slower. This is a useful way of
+ // manually checking that clocks are correctly animating through time.
+ private const val TIME_TRAVEL_SCALE = 1.0
+ }
+}
+
+class DigitalTimespecHandler(
+ val timespec: DigitalTimespec,
+ private val timeFormat: String,
+ cal: Calendar = Calendar.getInstance(),
+) : TimespecHandler(cal) {
+ var is24Hr = false
+ set(value) {
+ field = value
+ applyPattern()
+ }
+
+ private var dateFormat = updateSimpleDateFormat(Locale.getDefault())
+ private var contentDescriptionFormat = getContentDescriptionFormat(Locale.getDefault())
+
+ init {
+ applyPattern()
+ }
+
+ override fun onTimeZoneChanged() {
+ dateFormat.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+ contentDescriptionFormat?.timeZone = IcuTimeZone.getTimeZone(cal.timeZone.id)
+ applyPattern()
+ }
+
+ fun updateLocale(locale: Locale) {
+ dateFormat = updateSimpleDateFormat(locale)
+ contentDescriptionFormat = getContentDescriptionFormat(locale)
+ onTimeZoneChanged()
+ }
+
+ private fun updateSimpleDateFormat(locale: Locale): DateFormat {
+ if (
+ locale.language.equals(Locale.ENGLISH.language) ||
+ timespec != DigitalTimespec.DATE_FORMAT
+ ) {
+ // force date format in English, and time format to use format defined in json
+ return SimpleDateFormat(timeFormat, timeFormat, ULocale.forLocale(locale))
+ } else {
+ return SimpleDateFormat.getInstanceForSkeleton(timeFormat, locale)
+ }
+ }
+
+ private fun getContentDescriptionFormat(locale: Locale): DateFormat? {
+ return when (timespec) {
+ DigitalTimespec.TIME_FULL_FORMAT ->
+ SimpleDateFormat.getInstanceForSkeleton("hh:mm", locale)
+ DigitalTimespec.DATE_FORMAT ->
+ SimpleDateFormat.getInstanceForSkeleton("EEEE MMMM d", locale)
+ else -> {
+ null
+ }
+ }
+ }
+
+ private fun applyPattern() {
+ val timeFormat24Hour = timeFormat.replace("hh", "h").replace("h", "HH")
+ val format = if (is24Hr) timeFormat24Hour else timeFormat
+ if (timespec != DigitalTimespec.DATE_FORMAT) {
+ (dateFormat as SimpleDateFormat).applyPattern(format)
+ (contentDescriptionFormat as? SimpleDateFormat)?.applyPattern(
+ if (is24Hr) CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR
+ else CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR
+ )
+ }
+ }
+
+ private fun getSingleDigit(): String {
+ val isFirstDigit = timespec == DigitalTimespec.FIRST_DIGIT
+ val text = dateFormat.format(cal.time).toString()
+ return text.substring(
+ if (isFirstDigit) 0 else text.length - 1,
+ if (isFirstDigit) text.length - 1 else text.length
+ )
+ }
+
+ fun getDigitString(): String {
+ return when (timespec) {
+ DigitalTimespec.FIRST_DIGIT,
+ DigitalTimespec.SECOND_DIGIT -> getSingleDigit()
+ DigitalTimespec.DIGIT_PAIR -> {
+ dateFormat.format(cal.time).toString()
+ }
+ DigitalTimespec.TIME_FULL_FORMAT -> {
+ dateFormat.format(cal.time).toString()
+ }
+ DigitalTimespec.DATE_FORMAT -> {
+ dateFormat.format(cal.time).toString().uppercase()
+ }
+ }
+ }
+
+ fun getContentDescription(): String? {
+ return when (timespec) {
+ DigitalTimespec.TIME_FULL_FORMAT,
+ DigitalTimespec.DATE_FORMAT -> {
+ contentDescriptionFormat?.format(cal.time).toString()
+ }
+ else -> {
+ return null
+ }
+ }
+ }
+
+ companion object {
+ const val CONTENT_DESCRIPTION_TIME_FORMAT_12_HOUR = "hh:mm"
+ const val CONTENT_DESCRIPTION_TIME_FORMAT_24_HOUR = "HH:mm"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPatternViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityContainerControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSecurityViewFlipperControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPinViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSimPukViewControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/keyguard/KeyguardSliceViewControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/clipboardoverlay/ClipboardOverlaySuppressionControllerImplTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
index 179ba22..cecc11e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalEditModeViewModelTest.kt
@@ -19,7 +19,6 @@
import android.appwidget.AppWidgetProviderInfo
import android.content.ActivityNotFoundException
import android.content.ComponentName
-import android.content.Intent
import android.content.pm.PackageManager
import android.content.pm.UserInfo
import android.provider.Settings
@@ -27,7 +26,6 @@
import android.view.accessibility.AccessibilityManager
import android.view.accessibility.accessibilityManager
import android.widget.RemoteViews
-import androidx.activity.result.ActivityResultLauncher
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.internal.logging.UiEventLogger
@@ -88,7 +86,6 @@
@Mock private lateinit var mediaHost: MediaHost
@Mock private lateinit var uiEventLogger: UiEventLogger
@Mock private lateinit var packageManager: PackageManager
- @Mock private lateinit var activityResultLauncher: ActivityResultLauncher<Intent>
@Mock private lateinit var metricsLogger: CommunalMetricsLogger
private val kosmos = testKosmos()
@@ -117,10 +114,7 @@
communalSceneInteractor = kosmos.communalSceneInteractor
communalInteractor = spy(kosmos.communalInteractor)
kosmos.fakeUserRepository.setUserInfos(listOf(MAIN_USER_INFO))
- kosmos.fakeUserTracker.set(
- userInfos = listOf(MAIN_USER_INFO),
- selectedUserIndex = 0,
- )
+ kosmos.fakeUserTracker.set(userInfos = listOf(MAIN_USER_INFO), selectedUserIndex = 0)
kosmos.fakeFeatureFlagsClassic.set(Flags.COMMUNAL_SERVICE_ENABLED, true)
accessibilityManager = kosmos.accessibilityManager
@@ -257,10 +251,13 @@
@Test
fun onOpenWidgetPicker_launchesWidgetPickerActivity() {
testScope.runTest {
+ var activityStarted = false
val success =
- underTest.onOpenWidgetPicker(testableResources.resources, activityResultLauncher)
+ underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+ run { activityStarted = true }
+ }
- verify(activityResultLauncher).launch(any())
+ assertTrue(activityStarted)
assertTrue(success)
}
}
@@ -268,14 +265,10 @@
@Test
fun onOpenWidgetPicker_activityLaunchThrowsException_failure() {
testScope.runTest {
- whenever(activityResultLauncher.launch(any()))
- .thenThrow(ActivityNotFoundException::class.java)
-
val success =
- underTest.onOpenWidgetPicker(
- testableResources.resources,
- activityResultLauncher,
- )
+ underTest.onOpenWidgetPicker(testableResources.resources) { _ ->
+ run { throw ActivityNotFoundException() }
+ }
assertFalse(success)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/lifecycle/HydratorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/HydratorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/navigationbar/views/NavigationBarButtonTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/RecordIssueTileTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
index 9331c8d..0bbf47c 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenrecord/data/model/ScreenRecordModelTest.kt
@@ -16,13 +16,16 @@
package com.android.systemui.screenrecord.data.model
+import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
import com.android.systemui.screenrecord.data.model.ScreenRecordModel.Starting.Companion.toCountdownSeconds
import com.google.common.truth.Truth.assertThat
+import org.junit.runner.RunWith
import kotlin.test.Test
@SmallTest
+@RunWith(AndroidJUnit4::class)
class ScreenRecordModelTest : SysuiTestCase() {
@Test
fun countdownSeconds_millis0_is0() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDataTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 0e9ef06..0454317 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -22,6 +22,10 @@
import static com.google.common.truth.Truth.assertThat;
+import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
+import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
+import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
+
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyBoolean;
import static org.mockito.ArgumentMatchers.anyFloat;
@@ -36,10 +40,6 @@
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
-import static kotlinx.coroutines.flow.FlowKt.emptyFlow;
-import static kotlinx.coroutines.flow.SharedFlowKt.MutableSharedFlow;
-import static kotlinx.coroutines.flow.StateFlowKt.MutableStateFlow;
-
import android.animation.Animator;
import android.annotation.IdRes;
import android.content.ContentResolver;
@@ -201,6 +201,12 @@
import com.android.systemui.util.time.SystemClock;
import com.android.wm.shell.animation.FlingAnimationUtils;
+import dagger.Lazy;
+
+import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.channels.BufferOverflow;
+import kotlinx.coroutines.test.TestScope;
+
import org.junit.After;
import org.junit.Before;
import org.junit.Rule;
@@ -215,11 +221,6 @@
import java.util.List;
import java.util.Optional;
-import dagger.Lazy;
-import kotlinx.coroutines.CoroutineDispatcher;
-import kotlinx.coroutines.channels.BufferOverflow;
-import kotlinx.coroutines.test.TestScope;
-
public class NotificationPanelViewControllerBaseTest extends SysuiTestCase {
protected static final int SPLIT_SHADE_FULL_TRANSITION_DISTANCE = 400;
@@ -461,7 +462,8 @@
() -> mKosmos.getSceneInteractor(),
() -> mKosmos.getSceneContainerOcclusionInteractor(),
() -> mKosmos.getKeyguardClockInteractor(),
- () -> mKosmos.getSceneBackInteractor());
+ () -> mKosmos.getSceneBackInteractor(),
+ () -> mKosmos.getAlternateBouncerInteractor());
KeyguardStatusView keyguardStatusView = new KeyguardStatusView(mContext);
keyguardStatusView.setId(R.id.keyguard_status_view);
@@ -622,7 +624,8 @@
() -> mKosmos.getSceneInteractor(),
() -> mKosmos.getSceneContainerOcclusionInteractor(),
() -> mKosmos.getKeyguardClockInteractor(),
- () -> mKosmos.getSceneBackInteractor()),
+ () -> mKosmos.getSceneBackInteractor(),
+ () -> mKosmos.getAlternateBouncerInteractor()),
mKeyguardBypassController,
mDozeParameters,
mScreenOffAnimationController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/OperatorNameViewControllerTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
index db274cc..f8720b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/StatusBarStateControllerImplTest.kt
@@ -28,6 +28,8 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.authentication.data.repository.fakeAuthenticationRepository
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.bouncer.domain.interactor.givenCanShowAlternateBouncer
import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.flags.DisableSceneContainer
@@ -83,8 +85,9 @@
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
- private val sceneInteractor = kosmos.sceneInteractor
- private val keyguardTransitionRepository = kosmos.fakeKeyguardTransitionRepository
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
+ private val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
private val mockDarkAnimator = mock<ObjectAnimator>()
private lateinit var underTest: StatusBarStateControllerImpl
@@ -121,6 +124,7 @@
{ kosmos.sceneContainerOcclusionInteractor },
{ kosmos.keyguardClockInteractor },
{ kosmos.sceneBackInteractor },
+ { kosmos.alternateBouncerInteractor },
) {
override fun createDarkAnimator(): ObjectAnimator {
return mockDarkAnimator
@@ -299,6 +303,52 @@
@Test
@EnableSceneContainer
+ @DisableFlags(DualShade.FLAG_NAME)
+ fun start_hydratesStatusBarState_withAlternateBouncer() =
+ testScope.runTest {
+ var statusBarState = underTest.state
+ val listener =
+ object : StatusBarStateController.StateListener {
+ override fun onStateChanged(newState: Int) {
+ statusBarState = newState
+ }
+ }
+ underTest.addCallback(listener)
+
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ val deviceUnlockStatus by
+ collectLastValue(kosmos.deviceUnlockedInteractor.deviceUnlockStatus)
+ val alternateBouncerIsVisible by collectLastValue(alternateBouncerInteractor.isVisible)
+
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Password
+ )
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ assertThat(deviceUnlockStatus!!.isUnlocked).isTrue()
+
+ sceneInteractor.changeScene(toScene = Scenes.Lockscreen, loggingReason = "reason")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+
+ kosmos.givenCanShowAlternateBouncer()
+ alternateBouncerInteractor.forceShow()
+ runCurrent()
+ assertThat(alternateBouncerIsVisible).isTrue()
+
+ // Call start to begin hydrating based on the scene framework:
+ underTest.start()
+
+ sceneInteractor.changeScene(toScene = Scenes.Gone, loggingReason = "reason")
+ runCurrent()
+ assertThat(currentScene).isEqualTo(Scenes.Gone)
+ assertThat(statusBarState).isEqualTo(StatusBarState.KEYGUARD)
+ }
+
+ @Test
+ @EnableSceneContainer
@EnableFlags(DualShade.FLAG_NAME)
fun start_hydratesStatusBarState_dualShade_whileLocked() =
testScope.runTest {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/domain/interactor/CallChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/call/ui/viewmodel/CallChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/domian/interactor/MediaRouterChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndCastScreenToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/view/EndGenericCastToOtherDeviceDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/casttootherdevice/ui/viewmodel/CastToOtherDeviceChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/domain/interactor/MediaProjectionChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/mediaprojection/ui/view/EndMediaProjectionDialogHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ron/demo/ui/viewmodel/DemoRonChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/domain/interactor/ScreenRecordChipInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/view/EndScreenRecordingDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/screenrecord/ui/viewmodel/ScreenRecordChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/view/EndShareToAppDialogDelegateTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/sharetoapp/ui/viewmodel/ShareToAppChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/ChipTransitionHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/chips/ui/viewmodel/OngoingActivityChipsWithRonsViewModelTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/AboveShelfObserverTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/NotificationTransitionAnimatorControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/domain/interactor/HeadsUpNotificationInteractorTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/ExpandableNotificationRowDragControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationMenuRowTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationSnoozeTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/NotificationTestHelper.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationBigPictureTemplateViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationConversationTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationCustomViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationMessagingTemplateViewWrapperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/row/wrapper/NotificationViewWrapperTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationChildrenContainerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/NotificationTargetsHelperTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/HeadsUpAppearanceControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/KeyguardStatusBarViewTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/LightBarTransitionsControllerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/PhoneStatusBarPolicyTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/SystemUIBottomSheetDialogTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/phone/UnlockedScreenOffAnimationControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/pipeline/shared/ui/viewmodel/CollapsedStatusBarViewModelImplTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/CastDeviceTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/policy/ui/dialog/ModesDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
index ba9fa92..cd18925 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,10 +35,12 @@
private var gestureState: GestureState = NotStarted
private val gestureMonitor =
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- )
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun triggersGestureFinishedForThreeFingerGestureRight() {
@@ -82,7 +85,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
index a83ed56..3f1633a 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/EasterEggGestureTest.kt
@@ -36,10 +36,7 @@
private var triggered = false
private val handler =
TouchpadGestureHandler(
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = {},
- ),
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt()),
EasterEggGestureMonitor(callback = { triggered = true }),
)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
index 59cc026..edf0e56 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitorTest.kt
@@ -25,6 +25,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -34,10 +35,12 @@
private var gestureState: GestureState = GestureState.NotStarted
private val gestureMonitor =
- HomeGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- )
+ HomeGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun triggersGestureFinishedForThreeFingerGestureUp() {
@@ -78,7 +81,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
index 7eac6bb..f68e773 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitorTest.kt
@@ -26,6 +26,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.NotStarted
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.kotlin.doReturn
@@ -44,7 +45,7 @@
}
private var gestureState: GestureState = GestureState.NotStarted
- private val velocityTracker =
+ private val velocityTracker1D =
mock<VelocityTracker1D> {
// by default return correct speed for the gesture - as if pointer is slowing down
on { calculateVelocity() } doReturn SLOW
@@ -52,11 +53,15 @@
private val gestureMonitor =
RecentAppsGestureMonitor(
gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
velocityThresholdPxPerMs = THRESHOLD_VELOCITY_PX_PER_MS,
- velocityTracker = velocityTracker,
+ velocityTracker = VerticalVelocityTracker(velocityTracker1D),
)
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
+
@Test
fun triggersGestureFinishedForThreeFingerGestureUp() {
assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = Finished)
@@ -64,7 +69,7 @@
@Test
fun doesntTriggerGestureFinished_onGestureSpeedTooHigh() {
- whenever(velocityTracker.calculateVelocity()).thenReturn(FAST)
+ whenever(velocityTracker1D.calculateVelocity()).thenReturn(FAST)
assertStateAfterEvents(events = ThreeFingerGesture.swipeUp(), expectedState = NotStarted)
}
@@ -102,7 +107,7 @@
}
private fun assertStateAfterEvents(events: List<MotionEvent>, expectedState: GestureState) {
- events.forEach { gestureMonitor.processTouchpadEvent(it) }
+ events.forEach { gestureMonitor.accept(it) }
assertThat(gestureState).isEqualTo(expectedState)
}
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
index 4d26366..9f7ea679 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandlerTest.kt
@@ -27,6 +27,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.touchpad.tutorial.ui.gesture.MultiFingerGesture.Companion.SWIPE_DISTANCE
import com.google.common.truth.Truth.assertThat
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -35,14 +36,14 @@
class TouchpadGestureHandlerTest : SysuiTestCase() {
private var gestureState: GestureState = GestureState.NotStarted
- private val handler =
- TouchpadGestureHandler(
- BackGestureMonitor(
- gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt(),
- gestureStateChangedCallback = { gestureState = it },
- ),
- EasterEggGestureMonitor {},
- )
+ private val gestureMonitor =
+ BackGestureMonitor(gestureDistanceThresholdPx = SWIPE_DISTANCE.toInt())
+ private val handler = TouchpadGestureHandler(gestureMonitor, EasterEggGestureMonitor {})
+
+ @Before
+ fun before() {
+ gestureMonitor.addGestureStateCallback { gestureState = it }
+ }
@Test
fun handlesEventsFromTouchpad() {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
similarity index 100%
rename from packages/SystemUI/tests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/wallpapers/data/repository/WallpaperRepositoryImplTest.kt
diff --git a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
index b1736b1..c09509d 100644
--- a/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
+++ b/packages/SystemUI/plugin/src/com/android/systemui/plugins/VolumeDialogController.java
@@ -14,7 +14,6 @@
package com.android.systemui.plugins;
-import android.annotation.IntegerRes;
import android.content.ComponentName;
import android.media.AudioManager;
import android.media.AudioSystem;
@@ -22,6 +21,8 @@
import android.os.VibrationEffect;
import android.util.SparseArray;
+import androidx.annotation.StringRes;
+
import com.android.systemui.plugins.VolumeDialogController.Callbacks;
import com.android.systemui.plugins.VolumeDialogController.State;
import com.android.systemui.plugins.VolumeDialogController.StreamState;
@@ -90,7 +91,7 @@
public int levelMax;
public boolean muted;
public boolean muteSupported;
- public @IntegerRes int name;
+ public @StringRes int name;
public String remoteLabel;
public boolean routedToBluetooth;
diff --git a/packages/SystemUI/res/drawable/volume_dialog_background.xml b/packages/SystemUI/res/drawable/volume_dialog_background.xml
new file mode 100644
index 0000000..7d7498f
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_background.xml
@@ -0,0 +1,22 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="@dimen/volume_dialog_background_corner_radius" />
+ <solid android:color="?androidprv:attr/materialColorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
new file mode 100644
index 0000000..2694435
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_slider_background.xml
@@ -0,0 +1,21 @@
+<!--
+ ~ 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.
+ -->
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:shape="rectangle">
+ <corners android:radius="20dp" />
+ <solid android:color="?androidprv:attr/colorSurface" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
new file mode 100644
index 0000000..66a205a
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_floating_sliders_spacer.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size
+ android:width="@dimen/volume_dialog_floating_sliders_spacing"
+ android:height="@dimen/volume_dialog_floating_sliders_spacing" />
+ <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_dialog_spacer.xml b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
new file mode 100644
index 0000000..3c60784
--- /dev/null
+++ b/packages/SystemUI/res/drawable/volume_dialog_spacer.xml
@@ -0,0 +1,23 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ Copyright (C) 2024 The Android Open Source Project
+
+ Licensed under the Apache License, Version 2.0 (the "License");
+ you may not use this file except in compliance with the License.
+ You may obtain a copy of the License at
+
+ http://www.apache.org/licenses/LICENSE-2.0
+
+ Unless required by applicable law or agreed to in writing, software
+ distributed under the License is distributed on an "AS IS" BASIS,
+ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ See the License for the specific language governing permissions and
+ limitations under the License.
+-->
+
+<shape xmlns:android="http://schemas.android.com/apk/res/android"
+ android:shape="rectangle">
+ <size
+ android:width="@dimen/volume_dialog_spacing"
+ android:height="@dimen/volume_dialog_spacing" />
+ <solid android:color="@color/transparent" />
+</shape>
diff --git a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
index 21b177b..fa06bd6 100644
--- a/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
+++ b/packages/SystemUI/res/drawable/volume_row_seekbar_progress.xml
@@ -22,7 +22,7 @@
android:autoMirrored="true">
<item android:id="@+id/volume_seekbar_progress_solid">
<shape>
- <size android:height="@dimen/volume_dialog_slider_width" />
+ <size android:height="@dimen/volume_dialog_slider_width_legacy" />
<solid android:color="?android:attr/colorAccent" />
<corners android:radius="@dimen/volume_dialog_slider_corner_radius"/>
</shape>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog.xml b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
index 0fbc519..f77db95 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog.xml
@@ -1,92 +1,67 @@
<!--
- ~ Copyright (C) 2020 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
- -->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:background="@android:color/transparent"
+ android:layout_gravity="right"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <FrameLayout
- android:id="@+id/volume_dialog"
+ <LinearLayout
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
+
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:layout_gravity="right"
- android:background="@android:color/transparent"
- android:padding="@dimen/volume_dialog_panel_transparent_padding"
- android:clipToPadding="false">
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipChildren="false"
- android:clipToPadding="false"
- android:background="@android:color/transparent">
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal"
- android:background="@drawable/tv_volume_dialog_background">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
-
- </LinearLayout>
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
+ android:orientation="vertical"
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginRight="68dp"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:background="@drawable/rounded_bg_full">
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="@color/caption_tint_color_selector"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false"/>
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
- </FrameLayout>
-
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
- android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
- android:layout_gravity="right"/>
-
- </FrameLayout>
-
-</FrameLayout>
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
+ </LinearLayout>
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
new file mode 100644
index 0000000..0fbc519
--- /dev/null
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_legacy.xml
@@ -0,0 +1,92 @@
+<!--
+ ~ Copyright (C) 2020 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
+ -->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@android:color/transparent"
+ android:theme="@style/volume_dialog_theme">
+
+ <FrameLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:background="@android:color/transparent"
+ android:padding="@dimen/volume_dialog_panel_transparent_padding"
+ android:clipToPadding="false">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipChildren="false"
+ android:clipToPadding="false"
+ android:background="@android:color/transparent">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal"
+ android:background="@drawable/tv_volume_dialog_background">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginRight="68dp"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:background="@drawable/rounded_bg_full">
+
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="@color/caption_tint_color_selector"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false"/>
+
+ </FrameLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"
+ android:layout_marginTop="@dimen/volume_tool_tip_top_margin"
+ android:layout_gravity="right"/>
+
+ </FrameLayout>
+
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
index cf301c9..eb89489 100644
--- a/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
+++ b/packages/SystemUI/res/layout-land-television/volume_dialog_row.xml
@@ -63,8 +63,8 @@
android:layout_height="match_parent"
android:layout_gravity="center"
android:layoutDirection="ltr"
- android:maxHeight="@dimen/volume_dialog_slider_width"
- android:minHeight="@dimen/volume_dialog_slider_width"
+ android:maxHeight="@dimen/volume_dialog_slider_width_legacy"
+ android:minHeight="@dimen/volume_dialog_slider_width_legacy"
android:progressDrawable="@drawable/volume_row_seekbar"
android:thumb="@drawable/tv_volume_row_seek_thumb"
android:splitTrack="false"
diff --git a/packages/SystemUI/res/layout-land/volume_dialog.xml b/packages/SystemUI/res/layout-land/volume_dialog.xml
index 08edf59..f77db95 100644
--- a/packages/SystemUI/res/layout-land/volume_dialog.xml
+++ b/packages/SystemUI/res/layout-land/volume_dialog.xml
@@ -1,146 +1,67 @@
<!--
- ~ 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
- -->
-<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ 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.
+-->
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="right"
android:layout_gravity="right"
- android:background="@android:color/transparent"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <!-- right-aligned to be physically near volume button -->
<LinearLayout
- android:id="@+id/volume_dialog"
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
+
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
-
-
- <LinearLayout
- android:id="@+id/volume_dialog_top_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical"
- android:clipChildren="false"
- android:gravity="right">
-
- <include layout="@layout/volume_ringer_drawer" />
-
- <FrameLayout
- android:visibility="gone"
- android:id="@+id/ringer"
- android:layout_width="@dimen/volume_dialog_ringer_size"
- android:layout_height="@dimen/volume_dialog_ringer_size"
- android:layout_marginBottom="@dimen/volume_dialog_spacer"
- android:gravity="right"
- android:layout_gravity="right"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipToPadding="false"
- android:background="@drawable/rounded_bg_full">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/ringer_icon"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitCenter"
- android:padding="@dimen/volume_dialog_ringer_icon_padding"
- android:tint="?android:attr/textColorPrimary"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:clipChildren="false"
- android:clipToPadding="false" >
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
- <FrameLayout
- android:id="@+id/settings_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/volume_background_bottom"
- android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
-
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/settings"
- android:layout_width="@dimen/volume_dialog_tap_target_size"
- android:layout_height="@dimen/volume_dialog_tap_target_size"
- android:layout_gravity="center"
- android:background="@drawable/ripple_drawable_20dp"
- android:contentDescription="@string/accessibility_volume_settings"
- android:scaleType="centerInside"
- android:soundEffectsEnabled="false"
- android:src="@drawable/horizontal_ellipsis"
- android:tint="?androidprv:attr/colorAccent" />
- </FrameLayout>
- </LinearLayout>
-
- </LinearLayout>
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
- android:gravity="right"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:clipToOutline="true"
- android:background="@drawable/volume_row_rounded_background">
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="?android:attr/colorAccent"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
+
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
+
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
</LinearLayout>
-
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom | right"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
-
-</FrameLayout>
\ No newline at end of file
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
new file mode 100644
index 0000000..08edf59
--- /dev/null
+++ b/packages/SystemUI/res/layout-land/volume_dialog_legacy.xml
@@ -0,0 +1,146 @@
+<!--
+ ~ 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
+ -->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:background="@android:color/transparent"
+ android:theme="@style/volume_dialog_theme">
+
+ <!-- right-aligned to be physically near volume button -->
+ <LinearLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_top_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:gravity="right">
+
+ <include layout="@layout/volume_ringer_drawer" />
+
+ <FrameLayout
+ android:visibility="gone"
+ android:id="@+id/ringer"
+ android:layout_width="@dimen/volume_dialog_ringer_size"
+ android:layout_height="@dimen/volume_dialog_ringer_size"
+ android:layout_marginBottom="@dimen/volume_dialog_spacer"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipToPadding="false"
+ android:background="@drawable/rounded_bg_full">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/ringer_icon"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/volume_dialog_ringer_icon_padding"
+ android:tint="?android:attr/textColorPrimary"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false" >
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/settings_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/settings"
+ android:layout_width="@dimen/volume_dialog_tap_target_size"
+ android:layout_height="@dimen/volume_dialog_tap_target_size"
+ android:layout_gravity="center"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:scaleType="centerInside"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/colorAccent" />
+ </FrameLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:clipToOutline="true"
+ android:background="@drawable/volume_row_rounded_background">
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="?android:attr/colorAccent"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+ </LinearLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom | right"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/audio_sharing_dialog.xml b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
new file mode 100644
index 0000000..7534e15
--- /dev/null
+++ b/packages/SystemUI/res/layout/audio_sharing_dialog.xml
@@ -0,0 +1,115 @@
+<?xml version="1.0" encoding="utf-8"?><!--
+ ~ Copyright (C) 2024 The Android Open Source Project
+ ~
+ ~ Licensed under the Apache License, Version 2.0 (the "License");
+ ~ you may not use this file except in compliance with the License.
+ ~ You may obtain a copy of the License at
+ ~
+ ~ http://www.apache.org/licenses/LICENSE-2.0
+ ~
+ ~ Unless required by applicable law or agreed to in writing, software
+ ~ distributed under the License is distributed on an "AS IS" BASIS,
+ ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ ~ See the License for the specific language governing permissions and
+ ~ limitations under the License.
+ -->
+
+<androidx.constraintlayout.widget.ConstraintLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ xmlns:app="http://schemas.android.com/apk/res-auto"
+ android:id="@+id/root"
+ style="@style/Widget.SliceView.Panel"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content">
+
+ <ImageView android:id="@+id/icon"
+ android:layout_width="28dp"
+ android:layout_height="28dp"
+ android:src="@drawable/ic_bt_le_audio_sharing"
+ android:layout_marginTop="5dp"
+ android:layout_marginBottom="20dp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/title"
+ app:layout_constraintTop_toTopOf="parent" />
+
+ <TextView
+ android:id="@+id/title"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:maxLines="1"
+ android:ellipsize="end"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_title"
+ android:textAppearance="@style/TextAppearance.Dialog.Title"
+ android:textSize="24sp"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/subtitle"
+ app:layout_constraintTop_toBottomOf="@id/icon" />
+
+ <TextView
+ android:id="@+id/subtitle"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ android:textFontWeight="500"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/message"
+ app:layout_constraintTop_toBottomOf="@id/title" />
+
+ <TextView
+ android:id="@+id/message"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_marginBottom="20dp"
+ android:gravity="center_vertical|center_horizontal"
+ android:ellipsize="end"
+ android:maxLines="2"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_dialog_message"
+ android:textAppearance="@style/TextAppearance.Dialog.Body.Message"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintBottom_toTopOf="@id/share_audio_button"
+ app:layout_constraintTop_toBottomOf="@id/subtitle" />
+
+ <Button
+ android:id="@+id/share_audio_button"
+ style="@style/SettingsLibActionButton"
+ android:textColor="?androidprv:attr/textColorOnAccent"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:layout_marginBottom="4dp"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="64dp"
+ android:contentDescription="@string/accessibility_bluetooth_device_settings_see_all"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/message"
+ app:layout_constraintBottom_toTopOf="@+id/switch_active_button"
+ android:text="@string/quick_settings_bluetooth_audio_sharing_button"
+ android:maxLines="2" />
+
+ <Button
+ android:id="@+id/switch_active_button"
+ style="@style/SettingsLibActionButton"
+ android:textColor="?androidprv:attr/textColorOnAccent"
+ android:background="@drawable/audio_sharing_rounded_bg_ripple"
+ android:layout_marginBottom="20dp"
+ android:layout_width="0dp"
+ android:layout_height="wrap_content"
+ android:minHeight="64dp"
+ android:contentDescription="@string/accessibility_bluetooth_device_settings_pair_new_device"
+ app:layout_constraintStart_toStartOf="parent"
+ app:layout_constraintEnd_toEndOf="parent"
+ app:layout_constraintTop_toBottomOf="@+id/share_audio_button"
+ app:layout_constraintBottom_toBottomOf="parent"
+ android:maxLines="2" />
+</androidx.constraintlayout.widget.ConstraintLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog.xml b/packages/SystemUI/res/layout/volume_dialog.xml
index 39a1f1f..f77db95 100644
--- a/packages/SystemUI/res/layout/volume_dialog.xml
+++ b/packages/SystemUI/res/layout/volume_dialog.xml
@@ -1,5 +1,5 @@
<!--
- Copyright (C) 2015 The Android Open Source Project
+ Copyright (C) 2024 The Android Open Source Project
Licensed under the Apache License, Version 2.0 (the "License");
you may not use this file except in compliance with the License.
@@ -13,133 +13,55 @@
See the License for the specific language governing permissions and
limitations under the License.
-->
-<FrameLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- xmlns:sysui="http://schemas.android.com/apk/res-auto"
+<LinearLayout xmlns:android="http://schemas.android.com/apk/res/android"
xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
android:id="@+id/volume_dialog_container"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:gravity="right"
android:layout_gravity="right"
- android:clipToPadding="false"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:orientation="horizontal"
+ android:showDividers="middle|end|beginning"
android:theme="@style/volume_dialog_theme">
- <!-- right-aligned to be physically near volume button -->
<LinearLayout
- android:id="@+id/volume_dialog"
+ android:id="@+id/volume_dialog_floating_sliders_container"
android:layout_width="wrap_content"
+ android:layout_height="match_parent"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_floating_sliders_spacer"
+ android:gravity="bottom"
+ android:orientation="horizontal"
+ android:paddingBottom="@dimen/volume_dialog_floating_sliders_bottom_padding"
+ android:showDividers="middle" />
+
+ <LinearLayout
+ android:layout_width="@dimen/volume_dialog_width"
android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:background="@drawable/volume_dialog_background"
+ android:divider="@drawable/volume_dialog_spacer"
+ android:gravity="center_horizontal"
android:orientation="vertical"
- android:clipToPadding="false"
- android:clipChildren="false">
-
- <LinearLayout
- android:id="@+id/volume_dialog_top_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:clipChildren="false"
- android:orientation="vertical"
- android:gravity="right">
-
- <include layout="@layout/volume_ringer_drawer" />
-
- <FrameLayout
- android:visibility="gone"
- android:id="@+id/ringer"
- android:layout_width="@dimen/volume_dialog_ringer_size"
- android:layout_height="@dimen/volume_dialog_ringer_size"
- android:layout_marginBottom="@dimen/volume_dialog_spacer"
- android:gravity="right"
- android:layout_gravity="right"
- android:translationZ="@dimen/volume_dialog_elevation"
- android:clipToPadding="false"
- android:background="@drawable/rounded_bg_full">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/ringer_icon"
- style="@style/VolumeButtons"
- android:background="@drawable/rounded_ripple"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:scaleType="fitCenter"
- android:padding="@dimen/volume_dialog_ringer_icon_padding"
- android:tint="?android:attr/textColorPrimary"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
-
- <LinearLayout
- android:id="@+id/volume_dialog_rows_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="right"
- android:layout_gravity="right"
- android:orientation="vertical"
- android:clipChildren="false"
- android:clipToPadding="false" >
- <LinearLayout
- android:id="@+id/volume_dialog_rows"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:gravity="center"
- android:orientation="horizontal">
- <!-- volume rows added and removed here! :-) -->
- </LinearLayout>
- <FrameLayout
- android:id="@+id/settings_container"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:background="@drawable/volume_background_bottom"
- android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
- android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
- <com.android.keyguard.AlphaOptimizedImageButton
- android:id="@+id/settings"
- android:src="@drawable/horizontal_ellipsis"
- android:layout_width="@dimen/volume_dialog_tap_target_size"
- android:layout_height="@dimen/volume_dialog_tap_target_size"
- android:layout_gravity="center"
- android:contentDescription="@string/accessibility_volume_settings"
- android:background="@drawable/ripple_drawable_20dp"
- android:tint="?androidprv:attr/colorAccent"
- android:soundEffectsEnabled="false" />
- </FrameLayout>
- </LinearLayout>
-
- </LinearLayout>
+ android:paddingVertical="@dimen/volume_dialog_vertical_padding"
+ android:showDividers="middle">
<FrameLayout
- android:id="@+id/odi_captions"
- android:layout_width="@dimen/volume_dialog_caption_size"
- android:layout_height="@dimen/volume_dialog_caption_size"
- android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
- android:gravity="right"
- android:layout_gravity="right"
- android:clipToPadding="false"
- android:clipToOutline="true"
- android:background="@drawable/volume_row_rounded_background">
- <com.android.systemui.volume.CaptionsToggleImageButton
- android:id="@+id/odi_captions_icon"
- android:src="@drawable/ic_volume_odi_captions_disabled"
- style="@style/VolumeButtons"
- android:layout_width="match_parent"
- android:layout_height="match_parent"
- android:tint="?android:attr/colorAccent"
- android:layout_gravity="center"
- android:soundEffectsEnabled="false"/>
- </FrameLayout>
+ android:id="@+id/volume_dialog_ringer_button"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size" />
+
+ <include
+ android:id="@+id/volume_dialog_slider"
+ layout="@layout/volume_dialog_slider" />
+
+ <Button
+ android:id="@+id/volume_dialog_settings"
+ android:layout_width="@dimen/volume_dialog_button_size"
+ android:layout_height="@dimen/volume_dialog_button_size"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:soundEffectsEnabled="false"
+ android:src="@drawable/horizontal_ellipsis"
+ android:tint="?androidprv:attr/materialColorPrimary" />
</LinearLayout>
-
- <ViewStub
- android:id="@+id/odi_captions_tooltip_stub"
- android:inflatedId="@+id/odi_captions_tooltip_view"
- android:layout="@layout/volume_tool_tip_view"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="bottom | right"
- android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
-
-</FrameLayout>
\ No newline at end of file
+</LinearLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_legacy.xml b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
new file mode 100644
index 0000000..39a1f1f
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_legacy.xml
@@ -0,0 +1,145 @@
+<!--
+ Copyright (C) 2015 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.
+-->
+<FrameLayout
+ xmlns:android="http://schemas.android.com/apk/res/android"
+ xmlns:sysui="http://schemas.android.com/apk/res-auto"
+ xmlns:androidprv="http://schemas.android.com/apk/prv/res/android"
+ android:id="@+id/volume_dialog_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:theme="@style/volume_dialog_theme">
+
+ <!-- right-aligned to be physically near volume button -->
+ <LinearLayout
+ android:id="@+id/volume_dialog"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:layout_marginRight="@dimen/volume_dialog_panel_transparent_padding_right"
+ android:orientation="vertical"
+ android:clipToPadding="false"
+ android:clipChildren="false">
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_top_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:clipChildren="false"
+ android:orientation="vertical"
+ android:gravity="right">
+
+ <include layout="@layout/volume_ringer_drawer" />
+
+ <FrameLayout
+ android:visibility="gone"
+ android:id="@+id/ringer"
+ android:layout_width="@dimen/volume_dialog_ringer_size"
+ android:layout_height="@dimen/volume_dialog_ringer_size"
+ android:layout_marginBottom="@dimen/volume_dialog_spacer"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:translationZ="@dimen/volume_dialog_elevation"
+ android:clipToPadding="false"
+ android:background="@drawable/rounded_bg_full">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/ringer_icon"
+ style="@style/VolumeButtons"
+ android:background="@drawable/rounded_ripple"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:scaleType="fitCenter"
+ android:padding="@dimen/volume_dialog_ringer_icon_padding"
+ android:tint="?android:attr/textColorPrimary"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:orientation="vertical"
+ android:clipChildren="false"
+ android:clipToPadding="false" >
+ <LinearLayout
+ android:id="@+id/volume_dialog_rows"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:gravity="center"
+ android:orientation="horizontal">
+ <!-- volume rows added and removed here! :-) -->
+ </LinearLayout>
+ <FrameLayout
+ android:id="@+id/settings_container"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_background_bottom"
+ android:paddingLeft="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingBottom="@dimen/volume_dialog_ringer_rows_padding"
+ android:paddingRight="@dimen/volume_dialog_ringer_rows_padding">
+ <com.android.keyguard.AlphaOptimizedImageButton
+ android:id="@+id/settings"
+ android:src="@drawable/horizontal_ellipsis"
+ android:layout_width="@dimen/volume_dialog_tap_target_size"
+ android:layout_height="@dimen/volume_dialog_tap_target_size"
+ android:layout_gravity="center"
+ android:contentDescription="@string/accessibility_volume_settings"
+ android:background="@drawable/ripple_drawable_20dp"
+ android:tint="?androidprv:attr/colorAccent"
+ android:soundEffectsEnabled="false" />
+ </FrameLayout>
+ </LinearLayout>
+
+ </LinearLayout>
+
+ <FrameLayout
+ android:id="@+id/odi_captions"
+ android:layout_width="@dimen/volume_dialog_caption_size"
+ android:layout_height="@dimen/volume_dialog_caption_size"
+ android:layout_marginTop="@dimen/volume_dialog_row_margin_bottom"
+ android:gravity="right"
+ android:layout_gravity="right"
+ android:clipToPadding="false"
+ android:clipToOutline="true"
+ android:background="@drawable/volume_row_rounded_background">
+ <com.android.systemui.volume.CaptionsToggleImageButton
+ android:id="@+id/odi_captions_icon"
+ android:src="@drawable/ic_volume_odi_captions_disabled"
+ style="@style/VolumeButtons"
+ android:layout_width="match_parent"
+ android:layout_height="match_parent"
+ android:tint="?android:attr/colorAccent"
+ android:layout_gravity="center"
+ android:soundEffectsEnabled="false"/>
+ </FrameLayout>
+ </LinearLayout>
+
+ <ViewStub
+ android:id="@+id/odi_captions_tooltip_stub"
+ android:inflatedId="@+id/odi_captions_tooltip_view"
+ android:layout="@layout/volume_tool_tip_view"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="bottom | right"
+ android:layout_marginRight="@dimen/volume_tool_tip_right_margin"/>
+
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider.xml b/packages/SystemUI/res/layout/volume_dialog_slider.xml
new file mode 100644
index 0000000..8acdd39
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider.xml
@@ -0,0 +1,27 @@
+<!--
+ 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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="@dimen/volume_dialog_slider_width"
+ android:layout_height="@dimen/volume_dialog_slider_height">
+
+ <com.google.android.material.slider.Slider
+ android:id="@+id/volume_dialog_slider"
+ android:layout_width="@dimen/volume_dialog_slider_height"
+ android:layout_height="match_parent"
+ android:layout_gravity="center"
+ android:rotation="270"
+ android:theme="@style/Theme.MaterialComponents.DayNight" />
+</FrameLayout>
diff --git a/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
new file mode 100644
index 0000000..db800aa
--- /dev/null
+++ b/packages/SystemUI/res/layout/volume_dialog_slider_floating.xml
@@ -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.
+-->
+<FrameLayout xmlns:android="http://schemas.android.com/apk/res/android"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:background="@drawable/volume_dialog_floating_slider_background"
+ android:paddingHorizontal="@dimen/volume_dialog_floating_sliders_horizontal_padding"
+ android:paddingVertical="@dimen/volume_dialog_floating_sliders_vertical_padding">
+
+ <include layout="@layout/volume_dialog_slider" />
+</FrameLayout>
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/dimens.xml b/packages/SystemUI/res/values/dimens.xml
index 1727a5f..6c8a740 100644
--- a/packages/SystemUI/res/values/dimens.xml
+++ b/packages/SystemUI/res/values/dimens.xml
@@ -591,7 +591,7 @@
<dimen name="volume_dialog_panel_width_half">28dp</dimen>
- <dimen name="volume_dialog_slider_width">42dp</dimen>
+ <dimen name="volume_dialog_slider_width_legacy">42dp</dimen>
<dimen name="volume_dialog_slider_corner_radius">21dp</dimen>
@@ -622,10 +622,6 @@
<dimen name="volume_tool_tip_arrow_corner_radius">2dp</dimen>
- <!-- Volume panel slices dimensions -->
- <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
- <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
-
<dimen name="bottom_sheet_corner_radius">28dp</dimen>
<!-- Size of each item in the ringer selector drawer. -->
@@ -2050,4 +2046,22 @@
<dimen name="contextual_edu_dialog_bottom_margin">80dp</dimen>
<dimen name="contextual_edu_dialog_elevation">2dp</dimen>
+
+ <!-- Volume start -->
+ <dimen name="volume_dialog_background_corner_radius">30dp</dimen>
+ <dimen name="volume_dialog_width">60dp</dimen>
+ <dimen name="volume_dialog_vertical_padding">6dp</dimen>
+ <dimen name="volume_dialog_components_spacing">8dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_spacing">8dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_vertical_padding">10dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_horizontal_padding">4dp</dimen>
+ <dimen name="volume_dialog_spacing">4dp</dimen>
+ <dimen name="volume_dialog_button_size">48dp</dimen>
+ <dimen name="volume_dialog_floating_sliders_bottom_padding">48dp</dimen>
+ <dimen name="volume_dialog_slider_width">52dp</dimen>
+ <dimen name="volume_dialog_slider_height">254dp</dimen>
+
+ <dimen name="volume_panel_slice_vertical_padding">8dp</dimen>
+ <dimen name="volume_panel_slice_horizontal_padding">24dp</dimen>
+ <!-- Volume end -->
</resources>
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index 7225061..2c5fb56 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -765,6 +765,14 @@
<string name="quick_settings_bluetooth_audio_sharing_button_sharing">Sharing audio</string>
<!-- QuickSettings: Bluetooth dialog audio sharing button text accessibility label. Used as part of the string "Double tap to enter audio sharing settings". [CHAR LIMIT=50]-->
<string name="quick_settings_bluetooth_audio_sharing_button_accessibility">enter audio sharing settings</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog message. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_message">This device\'s music and videos will play on both pairs of headphones</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog title. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_title">Share your audio</string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog subtitle. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_subtitle"><xliff:g id="available_device_name" example="device 1">%1$s</xliff:g> and <xliff:g id="active_device_name" example="device 2">%2$s</xliff:g></string>
+ <!-- QuickSettings: Bluetooth audio sharing dialog button text. [CHAR LIMIT=NONE]-->
+ <string name="quick_settings_bluetooth_audio_sharing_dialog_switch_to_button">Switch to <xliff:g id="available_device_name" example="device 1">%1$s</xliff:g></string>
<!-- QuickSettings: Bluetooth secondary label for the battery level of a connected device [CHAR LIMIT=20]-->
<string name="quick_settings_bluetooth_secondary_label_battery_level"><xliff:g id="battery_level_as_percentage">%s</xliff:g> battery</string>
diff --git a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
index 431f048..83ab524 100644
--- a/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
+++ b/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java
@@ -1875,7 +1875,9 @@
if (posture == DEVICE_POSTURE_OPENED) {
mLogger.d("Posture changed to open - attempting to request active"
+ " unlock and run face auth");
- getFaceAuthInteractor().onDeviceUnfolded();
+ if (getFaceAuthInteractor() != null) {
+ getFaceAuthInteractor().onDeviceUnfolded();
+ }
requestActiveUnlockFromWakeReason(PowerManager.WAKE_REASON_UNFOLD_DEVICE,
false);
}
diff --git a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
index 831543d..ef172a1 100644
--- a/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
+++ b/packages/SystemUI/src/com/android/keyguard/dagger/ClockRegistryModule.java
@@ -69,7 +69,9 @@
layoutInflater,
resources,
featureFlags.isEnabled(Flags.STEP_CLOCK_ANIMATION),
- MigrateClocksToBlueprint.isEnabled()),
+ MigrateClocksToBlueprint.isEnabled(),
+ com.android.systemui.Flags.clockReactiveVariants()
+ ),
context.getString(R.string.lockscreen_clock_id_fallback),
clockBuffers,
/* keepAllLoaded = */ false,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt
new file mode 100644
index 0000000..a6fb150
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModel.kt
@@ -0,0 +1,101 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import androidx.annotation.StringRes
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.lifecycle.ExclusiveActivatable
+import com.android.systemui.res.R
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.combine
+
+sealed class AudioSharingButtonState {
+ object Gone : AudioSharingButtonState()
+
+ data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
+ AudioSharingButtonState()
+}
+
+class AudioSharingButtonViewModel
+@AssistedInject
+constructor(
+ private val localBluetoothManager: LocalBluetoothManager?,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val bluetoothStateInteractor: BluetoothStateInteractor,
+ private val deviceItemInteractor: DeviceItemInteractor,
+) : ExclusiveActivatable() {
+
+ private val mutableButtonState =
+ MutableStateFlow<AudioSharingButtonState>(AudioSharingButtonState.Gone)
+ /** Flow representing the update of AudioSharingButtonState. */
+ val audioSharingButtonStateUpdate: StateFlow<AudioSharingButtonState> =
+ mutableButtonState.asStateFlow()
+
+ override suspend fun onActivated(): Nothing {
+ combine(
+ bluetoothStateInteractor.bluetoothStateUpdate,
+ deviceItemInteractor.deviceItemUpdate,
+ audioSharingInteractor.isAudioSharingOn
+ ) { bluetoothState, deviceItem, audioSharingOn ->
+ getButtonState(bluetoothState, deviceItem, audioSharingOn)
+ }
+ .collect { mutableButtonState.value = it }
+ awaitCancellation()
+ }
+
+ private fun getButtonState(
+ bluetoothState: Boolean,
+ deviceItem: List<DeviceItem>,
+ audioSharingOn: Boolean
+ ): AudioSharingButtonState {
+ return when {
+ // Don't show button when bluetooth is off
+ !bluetoothState -> AudioSharingButtonState.Gone
+ // Show sharing audio when broadcasting
+ audioSharingOn ->
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+ isActive = true
+ )
+ // When not broadcasting, don't show button if there's connected source in any device
+ deviceItem.any {
+ BluetoothUtils.hasConnectedBroadcastSource(
+ it.cachedBluetoothDevice,
+ localBluetoothManager
+ )
+ } -> AudioSharingButtonState.Gone
+ // Show audio sharing when there's a connected LE audio device
+ deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button,
+ isActive = false
+ )
+ else -> AudioSharingButtonState.Gone
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): AudioSharingButtonViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
new file mode 100644
index 0000000..692a78b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorImpl.kt
@@ -0,0 +1,152 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.content.Intent
+import android.os.Bundle
+import android.provider.Settings
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.A2dpProfile
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.HeadsetProfile
+import com.android.settingslib.bluetooth.HearingAidProfile
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
+import com.android.systemui.animation.DialogTransitionAnimator
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.plugins.ActivityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.withContext
+
+@SysUISingleton
+class AudioSharingDeviceItemActionInteractorImpl
+@Inject
+constructor(
+ private val activityStarter: ActivityStarter,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val dialogTransitionAnimator: DialogTransitionAnimator,
+ private val localBluetoothManager: LocalBluetoothManager?,
+ @Main private val mainDispatcher: CoroutineDispatcher,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+ private val logger: BluetoothTileDialogLogger,
+ private val uiEventLogger: UiEventLogger,
+ private val delegateFactory: AudioSharingDialogDelegate.Factory,
+ private val deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl,
+) : DeviceItemActionInteractor {
+
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ withContext(backgroundDispatcher) {
+ if (!audioSharingInteractor.audioSharingAvailable()) {
+ return@withContext deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+ }
+ val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
+ logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
+
+ when {
+ deviceItem.type ==
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ if (audioSharingQsDialogImprovement()) {
+ withContext(mainDispatcher) {
+ delegateFactory
+ .create(deviceItem.cachedBluetoothDevice)
+ .createDialog()
+ .let { dialogTransitionAnimator.showFromDialog(it, dialog) }
+ }
+ } else {
+ launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+ logger.logLaunchSettingsCriteriaMatched(
+ "AvailableAudioSharingDeviceClicked",
+ deviceItem,
+ )
+ }
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
+ )
+ }
+ inSharingAndDeviceNoSource(inAudioSharing, deviceItem) -> {
+ launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
+ logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
+ uiEventLogger.log(
+ if (deviceItem.isLeAudioSupported)
+ BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
+ else
+ BluetoothTileDialogUiEvent
+ .LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
+ )
+ }
+ else -> {
+ deviceItemActionInteractorImpl.onClick(deviceItem, dialog)
+ }
+ }
+ }
+ }
+
+ private fun inSharingAndDeviceNoSource(
+ inAudioSharing: Boolean,
+ deviceItem: DeviceItem,
+ ): Boolean {
+ return inAudioSharing &&
+ deviceItem.isMediaDevice &&
+ !BluetoothUtils.hasConnectedBroadcastSource(
+ deviceItem.cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ }
+
+ private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
+ val intent =
+ Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
+ putExtra(
+ EXTRA_SHOW_FRAGMENT_ARGUMENTS,
+ Bundle().apply {
+ putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+ },
+ )
+ }
+ intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
+ activityStarter.postStartActivityDismissingKeyguard(
+ intent,
+ 0,
+ dialogTransitionAnimator.createActivityTransitionController(dialog),
+ )
+ }
+
+ private companion object {
+ const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
+
+ val DeviceItem.isLeAudioSupported: Boolean
+ get() =
+ cachedBluetoothDevice.profiles.any { profile ->
+ profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
+ }
+
+ val DeviceItem.isMediaDevice: Boolean
+ get() =
+ cachedBluetoothDevice.uiAccessibleProfiles.any {
+ it is A2dpProfile ||
+ it is HearingAidProfile ||
+ it is LeAudioProfile ||
+ it is HeadsetProfile
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
new file mode 100644
index 0000000..3ac942b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegate.kt
@@ -0,0 +1,88 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.os.Bundle
+import android.widget.Button
+import android.widget.TextView
+import com.android.internal.logging.UiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+class AudioSharingDialogDelegate
+@AssistedInject
+constructor(
+ @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+ @Application private val coroutineScope: CoroutineScope,
+ private val viewModelFactory: AudioSharingDialogViewModel.Factory,
+ private val sysuiDialogFactory: SystemUIDialog.Factory,
+ private val uiEventLogger: UiEventLogger,
+) : SystemUIDialog.Delegate {
+
+ override fun createDialog(): SystemUIDialog = sysuiDialogFactory.create(this)
+
+ override fun beforeCreate(dialog: SystemUIDialog, savedInstanceState: Bundle?) {
+ with(dialog.layoutInflater.inflate(R.layout.audio_sharing_dialog, null)) {
+ dialog.setView(this)
+ val subtitleTextView = requireViewById<TextView>(R.id.subtitle)
+ val shareAudioButton = requireViewById<TextView>(R.id.share_audio_button)
+ val switchActiveButton = requireViewById<Button>(R.id.switch_active_button)
+ val job =
+ coroutineScope.launch {
+ val viewModel = viewModelFactory.create(cachedBluetoothDevice, this)
+ viewModel.dialogState.collect {
+ when (it) {
+ is AudioSharingDialogState.Hide -> dialog.dismiss()
+ is AudioSharingDialogState.Show -> {
+ subtitleTextView.text = it.subtitle
+ switchActiveButton.text = it.switchButtonText
+ switchActiveButton.setOnClickListener {
+ viewModel.switchActiveClicked()
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent
+ .AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED
+ )
+ dialog.dismiss()
+ }
+ shareAudioButton.setOnClickListener {
+ viewModel.shareAudioClicked()
+ uiEventLogger.log(
+ BluetoothTileDialogUiEvent
+ .AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED
+ )
+ dialog.dismiss()
+ }
+ }
+ }
+ }
+ }
+ SystemUIDialog.registerDismissListener(dialog) { job.cancel() }
+ }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(cachedBluetoothDevice: CachedBluetoothDevice): AudioSharingDialogDelegate
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
new file mode 100644
index 0000000..dc970aea
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModel.kt
@@ -0,0 +1,109 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.content.Context
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.res.R
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.flowOn
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onStart
+import kotlinx.coroutines.launch
+
+sealed class AudioSharingDialogState {
+ data object Hide : AudioSharingDialogState()
+
+ data class Show(val subtitle: String, val switchButtonText: String) : AudioSharingDialogState()
+}
+
+class AudioSharingDialogViewModel
+@AssistedInject
+constructor(
+ deviceItemInteractor: DeviceItemInteractor,
+ private val audioSharingInteractor: AudioSharingInteractor,
+ private val context: Context,
+ private val localBluetoothManager: LocalBluetoothManager?,
+ @Assisted private val cachedBluetoothDevice: CachedBluetoothDevice,
+ @Assisted private val coroutineScope: CoroutineScope,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) {
+ val dialogState: Flow<AudioSharingDialogState> =
+ deviceItemInteractor.deviceItemUpdateRequest
+ .map {
+ if (
+ audioSharingInteractor.isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice
+ )
+ ) {
+ createShowState(cachedBluetoothDevice)
+ } else {
+ AudioSharingDialogState.Hide
+ }
+ }
+ .onStart { emit(createShowState(cachedBluetoothDevice)) }
+ .flowOn(backgroundDispatcher)
+ .distinctUntilChanged()
+
+ fun switchActiveClicked() {
+ coroutineScope.launch { audioSharingInteractor.switchActive(cachedBluetoothDevice) }
+ }
+
+ fun shareAudioClicked() {
+ coroutineScope.launch { audioSharingInteractor.startAudioSharing() }
+ }
+
+ private fun createShowState(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): AudioSharingDialogState {
+ val activeDeviceName =
+ localBluetoothManager
+ ?.profileManager
+ ?.leAudioProfile
+ ?.activeDevices
+ ?.firstOrNull()
+ ?.let { localBluetoothManager.cachedDeviceManager?.findDevice(it)?.name } ?: ""
+ val availableDeviceName = cachedBluetoothDevice.name
+ return AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ availableDeviceName,
+ activeDeviceName
+ ),
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ availableDeviceName
+ )
+ )
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice,
+ coroutineScope: CoroutineScope
+ ): AudioSharingDialogViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
index 817f2d7..65f1105 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractor.kt
@@ -16,82 +16,148 @@
package com.android.systemui.bluetooth.qsdialog
-import androidx.annotation.StringRes
import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onPlaybackStarted
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.res.R
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.SharingStarted
-import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.flow.firstOrNull
+import kotlinx.coroutines.flow.flatMapLatest
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.flowOn
-import kotlinx.coroutines.flow.stateIn
-
-internal sealed class AudioSharingButtonState {
- object Gone : AudioSharingButtonState()
-
- data class Visible(@StringRes val resId: Int, val isActive: Boolean) :
- AudioSharingButtonState()
-}
+import kotlinx.coroutines.flow.mapNotNull
+import kotlinx.coroutines.withContext
/** Holds business logic for the audio sharing state. */
+interface AudioSharingInteractor {
+ val isAudioSharingOn: Flow<Boolean>
+
+ val audioSourceStateUpdate: Flow<Unit>
+
+ suspend fun handleAudioSourceWhenReady()
+
+ suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): Boolean
+
+ suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice)
+
+ suspend fun startAudioSharing()
+
+ suspend fun audioSharingAvailable(): Boolean
+}
+
@SysUISingleton
-internal class AudioSharingInteractor
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingInteractorImpl
@Inject
constructor(
private val localBluetoothManager: LocalBluetoothManager?,
- bluetoothStateInteractor: BluetoothStateInteractor,
- deviceItemInteractor: DeviceItemInteractor,
- @Application private val coroutineScope: CoroutineScope,
+ private val audioSharingRepository: AudioSharingRepository,
@Background private val backgroundDispatcher: CoroutineDispatcher,
-) {
- /** Flow representing the update of AudioSharingButtonState. */
- internal val audioSharingButtonStateUpdate: Flow<AudioSharingButtonState> =
- combine(
- bluetoothStateInteractor.bluetoothStateUpdate,
- deviceItemInteractor.deviceItemUpdate
- ) { bluetoothState, deviceItem ->
- getButtonState(bluetoothState, deviceItem)
+) : AudioSharingInteractor {
+
+ override val isAudioSharingOn: Flow<Boolean> =
+ flow { emit(audioSharingAvailable()) }
+ .flatMapLatest { isEnabled ->
+ if (isEnabled) {
+ audioSharingRepository.inAudioSharing
+ } else {
+ flowOf(false)
+ }
}
.flowOn(backgroundDispatcher)
- .stateIn(
- coroutineScope,
- SharingStarted.WhileSubscribed(replayExpirationMillis = 0),
- initialValue = AudioSharingButtonState.Gone
- )
- private fun getButtonState(
- bluetoothState: Boolean,
- deviceItem: List<DeviceItem>
- ): AudioSharingButtonState {
- return when {
- // Don't show button when bluetooth is off
- !bluetoothState -> AudioSharingButtonState.Gone
- // Show sharing audio when broadcasting
- BluetoothUtils.isBroadcasting(localBluetoothManager) ->
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
- isActive = true
- )
- // When not broadcasting, don't show button if there's connected source in any device
- deviceItem.any {
- BluetoothUtils.hasConnectedBroadcastSource(
- it.cachedBluetoothDevice,
- localBluetoothManager
- )
- } -> AudioSharingButtonState.Gone
- // Show audio sharing when there's a connected LE audio device
- deviceItem.any { BluetoothUtils.isActiveLeAudioDevice(it.cachedBluetoothDevice) } ->
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button,
- isActive = false
- )
- else -> AudioSharingButtonState.Gone
+ override val audioSourceStateUpdate =
+ isAudioSharingOn
+ .flatMapLatest {
+ if (it) {
+ audioSharingRepository.audioSourceStateUpdate
+ } else {
+ emptyFlow()
+ }
+ }
+ .flowOn(backgroundDispatcher)
+
+ override suspend fun handleAudioSourceWhenReady() {
+ withContext(backgroundDispatcher) {
+ if (audioSharingAvailable()) {
+ audioSharingRepository.leAudioBroadcastProfile?.let { profile ->
+ isAudioSharingOn
+ .mapNotNull { audioSharingOn ->
+ if (audioSharingOn) {
+ // onPlaybackStarted could emit multiple times during one
+ // audio sharing session, we only perform add source on the
+ // first time
+ profile.onPlaybackStarted.firstOrNull()
+ } else {
+ null
+ }
+ }
+ .flowOn(backgroundDispatcher)
+ .collect { audioSharingRepository.addSource() }
+ }
+ }
}
}
+
+ override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): Boolean {
+ return withContext(backgroundDispatcher) {
+ if (audioSharingAvailable()) {
+ BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ } else {
+ false
+ }
+ }
+ }
+
+ override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+ if (!audioSharingAvailable()) {
+ return
+ }
+ audioSharingRepository.setActive(cachedBluetoothDevice)
+ }
+
+ override suspend fun startAudioSharing() {
+ if (!audioSharingAvailable()) {
+ return
+ }
+ audioSharingRepository.startAudioSharing()
+ }
+
+ // TODO(b/367965193): Move this after flags rollout
+ override suspend fun audioSharingAvailable(): Boolean {
+ return audioSharingRepository.audioSharingAvailable()
+ }
+}
+
+@SysUISingleton
+class AudioSharingInteractorEmptyImpl @Inject constructor() : AudioSharingInteractor {
+ override val isAudioSharingOn: Flow<Boolean> = flowOf(false)
+
+ override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+ override suspend fun handleAudioSourceWhenReady() {}
+
+ override suspend fun isAvailableAudioSharingMediaBluetoothDevice(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ) = false
+
+ override suspend fun switchActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+
+ override suspend fun audioSharingAvailable(): Boolean = false
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
new file mode 100644
index 0000000..b9b8d36
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepository.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.onSourceConnectedOrRemoved
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.emptyFlow
+import kotlinx.coroutines.withContext
+
+interface AudioSharingRepository {
+ val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+
+ val audioSourceStateUpdate: Flow<Unit>
+
+ val inAudioSharing: StateFlow<Boolean>
+
+ suspend fun audioSharingAvailable(): Boolean
+
+ suspend fun addSource()
+
+ suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice)
+
+ suspend fun startAudioSharing()
+}
+
+@SysUISingleton
+class AudioSharingRepositoryImpl(
+ private val localBluetoothManager: LocalBluetoothManager,
+ private val settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+ @Background private val backgroundDispatcher: CoroutineDispatcher,
+) : AudioSharingRepository {
+
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+ get() = localBluetoothManager.profileManager?.leAudioBroadcastProfile
+
+ private val leAudioBroadcastAssistantProfile: LocalBluetoothLeBroadcastAssistant?
+ get() = localBluetoothManager.profileManager?.leAudioBroadcastAssistantProfile
+
+ override val audioSourceStateUpdate: Flow<Unit> =
+ leAudioBroadcastAssistantProfile?.onSourceConnectedOrRemoved ?: emptyFlow()
+
+ override val inAudioSharing: StateFlow<Boolean> =
+ settingsLibAudioSharingRepository.inAudioSharing
+
+ override suspend fun audioSharingAvailable(): Boolean {
+ return settingsLibAudioSharingRepository.audioSharingAvailable()
+ }
+
+ override suspend fun addSource() {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ leAudioBroadcastProfile?.latestBluetoothLeBroadcastMetadata?.let { metadata ->
+ leAudioBroadcastAssistantProfile?.let {
+ it.allConnectedDevices.forEach { sink -> it.addSource(sink, metadata, false) }
+ }
+ }
+ }
+ }
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ cachedBluetoothDevice.setActive()
+ }
+ }
+
+ override suspend fun startAudioSharing() {
+ withContext(backgroundDispatcher) {
+ if (!settingsLibAudioSharingRepository.audioSharingAvailable()) {
+ return@withContext
+ }
+ leAudioBroadcastProfile?.startPrivateBroadcast()
+ }
+ }
+}
+
+@SysUISingleton
+class AudioSharingRepositoryEmptyImpl : AudioSharingRepository {
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast? = null
+
+ override val audioSourceStateUpdate: Flow<Unit> = emptyFlow()
+
+ override val inAudioSharing: StateFlow<Boolean> = MutableStateFlow(false)
+
+ override suspend fun audioSharingAvailable(): Boolean = false
+
+ override suspend fun addSource() {}
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
index 17f9e63..55d4d3e 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractor.kt
@@ -39,7 +39,7 @@
/** Holds business logic for the Bluetooth Dialog's bluetooth and device connection state */
@SysUISingleton
-internal class BluetoothStateInteractor
+class BluetoothStateInteractor
@Inject
constructor(
private val localBluetoothManager: LocalBluetoothManager?,
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
index 7deea73..a9c5c69 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogDelegate.kt
@@ -300,7 +300,7 @@
}
private fun getProgressBarBackground(dialog: SystemUIDialog): View {
- return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_animation)
+ return dialog.requireViewById(R.id.bluetooth_tile_dialog_progress_background)
}
private fun getScrollViewContent(dialog: SystemUIDialog): View {
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
index bdd4c16..aad233f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogUiEvent.kt
@@ -42,6 +42,7 @@
LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED(1717),
@UiEvent(doc = "Currently broadcasting and a non-LE audio supported device is clicked")
LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED(1718),
+ @Deprecated("Use case no longer needed")
@UiEvent(
doc = "Not broadcasting, having one connected, another saved LE audio device is clicked"
)
@@ -52,8 +53,13 @@
)
@UiEvent(doc = "Not broadcasting, one of the two connected LE audio devices is clicked")
LAUNCH_SETTINGS_NOT_SHARING_CONNECTED_LE_DEVICE_CLICKED(1720),
+ @Deprecated("Use case no longer needed")
@UiEvent(doc = "Not broadcasting, having two connected, the active LE audio devices is clicked")
- LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881);
+ LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED(1881),
+ @UiEvent(doc = "Clicked on switch active button on audio sharing dialog")
+ AUDIO_SHARING_DIALOG_SWITCH_ACTIVE_CLICKED(1890),
+ @UiEvent(doc = "Clicked on share audio button on audio sharing dialog")
+ AUDIO_SHARING_DIALOG_SHARE_AUDIO_CLICKED(1891);
override fun getId() = metricId
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
index a8f7fc3..5c35c52 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModel.kt
@@ -28,8 +28,8 @@
import androidx.annotation.VisibleForTesting
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.BluetoothUtils
import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.flags.Flags.audioSharingQsDialogImprovement
import com.android.systemui.Prefs
import com.android.systemui.animation.DialogCuj
import com.android.systemui.animation.DialogTransitionAnimator
@@ -51,6 +51,7 @@
import kotlinx.coroutines.channels.awaitClose
import kotlinx.coroutines.channels.produce
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.emptyFlow
import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.merge
@@ -68,10 +69,12 @@
private val bluetoothStateInteractor: BluetoothStateInteractor,
private val bluetoothAutoOnInteractor: BluetoothAutoOnInteractor,
private val audioSharingInteractor: AudioSharingInteractor,
+ private val audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory,
private val bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor,
private val dialogTransitionAnimator: DialogTransitionAnimator,
private val activityStarter: ActivityStarter,
private val uiEventLogger: UiEventLogger,
+ private val logger: BluetoothTileDialogLogger,
@Application private val coroutineScope: CoroutineScope,
@Main private val mainDispatcher: CoroutineDispatcher,
@Background private val backgroundDispatcher: CoroutineDispatcher,
@@ -102,7 +105,7 @@
expandable?.dialogTransitionController(
DialogCuj(
InteractionJankMonitor.CUJ_SHADE_DIALOG_OPEN,
- INTERACTION_JANK_TAG
+ INTERACTION_JANK_TAG,
)
)
controller?.let {
@@ -117,7 +120,7 @@
// stop the progress bar.
combine(
deviceItemInteractor.deviceItemUpdate,
- deviceItemInteractor.showSeeAllUpdate
+ deviceItemInteractor.showSeeAllUpdate,
) { deviceItem, showSeeAll ->
updateDialogUiJob?.cancel()
updateDialogUiJob = launch {
@@ -127,7 +130,7 @@
deviceItem,
showSeeAll,
showPairNewDevice =
- bluetoothStateInteractor.isBluetoothEnabled()
+ bluetoothStateInteractor.isBluetoothEnabled(),
)
animateProgressBar(dialog, false)
}
@@ -139,7 +142,15 @@
// the device item list and animate the progress bar.
merge(
deviceItemInteractor.deviceItemUpdateRequest,
- bluetoothDeviceMetadataInteractor.metadataUpdate
+ bluetoothDeviceMetadataInteractor.metadataUpdate,
+ if (
+ audioSharingInteractor.audioSharingAvailable() &&
+ audioSharingQsDialogImprovement()
+ ) {
+ audioSharingInteractor.audioSourceStateUpdate
+ } else {
+ emptyFlow()
+ },
)
.onEach {
dialogDelegate.animateProgressBar(dialog, true)
@@ -147,35 +158,42 @@
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
context,
- DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED
+ DeviceFetchTrigger.BLUETOOTH_CALLBACK_RECEIVED,
)
}
}
.launchIn(this)
- if (BluetoothUtils.isAudioSharingEnabled()) {
- audioSharingInteractor.audioSharingButtonStateUpdate
- .onEach {
- when (it) {
- is AudioSharingButtonState.Visible -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- VISIBLE,
- context.getString(it.resId),
- it.isActive
- )
- }
- is AudioSharingButtonState.Gone -> {
- dialogDelegate.onAudioSharingButtonUpdated(
- dialog,
- GONE,
- label = null,
- isActive = false
- )
+ if (audioSharingInteractor.audioSharingAvailable()) {
+ if (audioSharingQsDialogImprovement()) {
+ launch { audioSharingInteractor.handleAudioSourceWhenReady() }
+ }
+
+ audioSharingButtonViewModelFactory.create().run {
+ audioSharingButtonStateUpdate
+ .onEach {
+ when (it) {
+ is AudioSharingButtonState.Visible -> {
+ dialogDelegate.onAudioSharingButtonUpdated(
+ dialog,
+ VISIBLE,
+ context.getString(it.resId),
+ it.isActive,
+ )
+ }
+ is AudioSharingButtonState.Gone -> {
+ dialogDelegate.onAudioSharingButtonUpdated(
+ dialog,
+ GONE,
+ label = null,
+ isActive = false,
+ )
+ }
}
}
- }
- .launchIn(this)
+ .launchIn(this@launch)
+ launch { activate() }
+ }
}
// bluetoothStateUpdate is emitted when bluetooth on/off state is changed, re-fetch
@@ -185,13 +203,13 @@
dialogDelegate.onBluetoothStateUpdated(
dialog,
it,
- UiProperties.build(it, isAutoOnToggleFeatureAvailable())
+ UiProperties.build(it, isAutoOnToggleFeatureAvailable()),
)
updateDeviceItemJob?.cancel()
updateDeviceItemJob = launch {
deviceItemInteractor.updateDeviceItems(
context,
- DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED
+ DeviceFetchTrigger.BLUETOOTH_STATE_CHANGE_RECEIVED,
)
}
}
@@ -209,7 +227,10 @@
// deviceItemClick is emitted when user clicked on a device item.
dialogDelegate.deviceItemClick
- .onEach { deviceItemActionInteractor.onClick(it, dialog) }
+ .onEach {
+ deviceItemActionInteractor.onClick(it, dialog)
+ logger.logDeviceClick(it.cachedBluetoothDevice.address, it.type)
+ }
.launchIn(this)
// contentHeight is emitted when the dialog is dismissed.
@@ -230,7 +251,7 @@
dialog,
it,
if (it) R.string.turn_on_bluetooth_auto_info_enabled
- else R.string.turn_on_bluetooth_auto_info_disabled
+ else R.string.turn_on_bluetooth_auto_info_disabled,
)
}
.launchIn(this)
@@ -252,18 +273,18 @@
withContext(backgroundDispatcher) {
sharedPreferences.getInt(
CONTENT_HEIGHT_PREF_KEY,
- ViewGroup.LayoutParams.WRAP_CONTENT
+ ViewGroup.LayoutParams.WRAP_CONTENT,
)
}
return bluetoothDialogDelegateFactory.create(
UiProperties.build(
bluetoothStateInteractor.isBluetoothEnabled(),
- isAutoOnToggleFeatureAvailable()
+ isAutoOnToggleFeatureAvailable(),
),
cachedContentHeight,
this@BluetoothTileDialogViewModel,
- { cancelJob() }
+ { cancelJob() },
)
}
@@ -275,7 +296,7 @@
EXTRA_SHOW_FRAGMENT_ARGUMENTS,
Bundle().apply {
putString("device_address", deviceItem.cachedBluetoothDevice.address)
- }
+ },
)
}
startSettingsActivity(intent, view)
@@ -299,7 +320,7 @@
EXTRA_SHOW_FRAGMENT_ARGUMENTS,
Bundle().apply {
putBoolean(LocalBluetoothLeBroadcast.EXTRA_START_LE_AUDIO_SHARING, true)
- }
+ },
)
}
startSettingsActivity(intent, view)
@@ -345,7 +366,7 @@
companion object {
internal fun build(
isBluetoothEnabled: Boolean,
- isAutoOnToggleFeatureAvailable: Boolean
+ isAutoOnToggleFeatureAvailable: Boolean,
) =
UiProperties(
subTitleResId = getSubtitleResId(isBluetoothEnabled),
@@ -355,7 +376,7 @@
scrollViewMinHeightResId =
if (isAutoOnToggleFeatureAvailable)
R.dimen.bluetooth_dialog_scroll_view_min_height_with_auto_on
- else R.dimen.bluetooth_dialog_scroll_view_min_height
+ else R.dimen.bluetooth_dialog_scroll_view_min_height,
)
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
index f1894d3..cf0f19f 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractor.kt
@@ -16,87 +16,28 @@
package com.android.systemui.bluetooth.qsdialog
-import android.bluetooth.BluetoothDevice
-import android.bluetooth.BluetoothProfile
-import android.content.Intent
-import android.os.Bundle
-import android.provider.Settings
import com.android.internal.logging.UiEventLogger
-import com.android.settingslib.bluetooth.A2dpProfile
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.HeadsetProfile
-import com.android.settingslib.bluetooth.HearingAidProfile
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothManager
-import com.android.systemui.animation.DialogTransitionAnimator
-import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor.LaunchSettingsCriteria.Companion.getCurrentConnectedLeByGroupId
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
-import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.withContext
+interface DeviceItemActionInteractor {
+ suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {}
+}
+
@SysUISingleton
-class DeviceItemActionInteractor
+class DeviceItemActionInteractorImpl
@Inject
constructor(
- private val activityStarter: ActivityStarter,
- private val dialogTransitionAnimator: DialogTransitionAnimator,
- private val localBluetoothManager: LocalBluetoothManager?,
@Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
private val uiEventLogger: UiEventLogger,
-) {
- private val leAudioProfile: LeAudioProfile?
- get() = localBluetoothManager?.profileManager?.leAudioProfile
+) : DeviceItemActionInteractor {
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?
- get() = localBluetoothManager?.profileManager?.leAudioBroadcastAssistantProfile
-
- private val launchSettingsCriteriaList: List<LaunchSettingsCriteria>
- get() =
- listOf(
- InSharingClickedNoSource(localBluetoothManager, backgroundDispatcher, logger),
- NotSharingClickedNonConnect(
- leAudioProfile,
- assistantProfile,
- backgroundDispatcher,
- logger
- ),
- NotSharingClickedActive(
- leAudioProfile,
- assistantProfile,
- backgroundDispatcher,
- logger
- )
- )
-
- suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
+ override suspend fun onClick(deviceItem: DeviceItem, dialog: SystemUIDialog) {
withContext(backgroundDispatcher) {
- logger.logDeviceClick(deviceItem.cachedBluetoothDevice.address, deviceItem.type)
- if (
- BluetoothUtils.isAudioSharingEnabled() &&
- localBluetoothManager != null &&
- leAudioProfile != null &&
- assistantProfile != null
- ) {
- val inAudioSharing = BluetoothUtils.isBroadcasting(localBluetoothManager)
- logger.logDeviceClickInAudioSharingWhenEnabled(inAudioSharing)
-
- val criteriaMatched =
- launchSettingsCriteriaList.firstOrNull {
- it.matched(inAudioSharing, deviceItem)
- }
- if (criteriaMatched != null) {
- uiEventLogger.log(criteriaMatched.getClickUiEvent(deviceItem))
- launchSettings(deviceItem.cachedBluetoothDevice.device, dialog)
- return@withContext
- }
- }
deviceItem.cachedBluetoothDevice.apply {
when (deviceItem.type) {
DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE -> {
@@ -106,12 +47,6 @@
DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
uiEventLogger.log(BluetoothTileDialogUiEvent.AUDIO_SHARING_DEVICE_CLICKED)
}
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
- // TODO(b/360759048): pop up dialog
- uiEventLogger.log(
- BluetoothTileDialogUiEvent.AVAILABLE_AUDIO_SHARING_DEVICE_CLICKED
- )
- }
DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE -> {
setActive()
uiEventLogger.log(BluetoothTileDialogUiEvent.CONNECTED_DEVICE_SET_ACTIVE)
@@ -126,186 +61,12 @@
connect()
uiEventLogger.log(BluetoothTileDialogUiEvent.SAVED_DEVICE_CONNECT)
}
- }
- }
- }
- }
-
- private fun launchSettings(device: BluetoothDevice, dialog: SystemUIDialog) {
- val intent =
- Intent(Settings.ACTION_BLUETOOTH_SETTINGS).apply {
- putExtra(
- EXTRA_SHOW_FRAGMENT_ARGUMENTS,
- Bundle().apply {
- putParcelable(LocalBluetoothLeBroadcast.EXTRA_BLUETOOTH_DEVICE, device)
+ DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE -> {
+ // Do nothing. Should already be handled in
+ // AudioSharingDeviceItemActionInteractor.
}
- )
- }
- intent.flags = Intent.FLAG_ACTIVITY_CLEAR_TASK
- activityStarter.postStartActivityDismissingKeyguard(
- intent,
- 0,
- dialogTransitionAnimator.createActivityTransitionController(dialog)
- )
- }
-
- private interface LaunchSettingsCriteria {
- suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean
-
- suspend fun getClickUiEvent(deviceItem: DeviceItem): BluetoothTileDialogUiEvent
-
- companion object {
- suspend fun getCurrentConnectedLeByGroupId(
- leAudioProfile: LeAudioProfile,
- assistantProfile: LocalBluetoothLeBroadcastAssistant,
- @Background backgroundDispatcher: CoroutineDispatcher,
- logger: BluetoothTileDialogLogger,
- ): Map<Int, List<BluetoothDevice>> {
- return withContext(backgroundDispatcher) {
- assistantProfile
- .getDevicesMatchingConnectionStates(
- intArrayOf(BluetoothProfile.STATE_CONNECTED)
- )
- ?.filterNotNull()
- ?.groupBy { leAudioProfile.getGroupId(it) }
- ?.also { logger.logConnectedLeByGroupId(it) } ?: emptyMap()
}
}
}
}
-
- private class InSharingClickedNoSource(
- private val localBluetoothManager: LocalBluetoothManager?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If currently broadcasting and the clicked device is not connected to the source
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- inAudioSharing &&
- deviceItem.isMediaDevice &&
- !BluetoothUtils.hasConnectedBroadcastSource(
- deviceItem.cachedBluetoothDevice,
- localBluetoothManager
- )
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched("InSharingClickedNoSource", deviceItem)
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- if (deviceItem.isLeAudioSupported)
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_LE_DEVICE_CLICKED
- else BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_IN_SHARING_NON_LE_DEVICE_CLICKED
- }
-
- private class NotSharingClickedNonConnect(
- private val leAudioProfile: LeAudioProfile?,
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If not broadcasting, having one device connected, and clicked on a not yet connected LE
- // audio device
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- leAudioProfile?.let { leAudio ->
- assistantProfile?.let { assistant ->
- !inAudioSharing &&
- getCurrentConnectedLeByGroupId(
- leAudio,
- assistant,
- backgroundDispatcher,
- logger
- )
- .size == 1 &&
- deviceItem.isNotConnectedLeAudioSupported
- }
- } ?: false
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched(
- "NotSharingClickedNonConnect",
- deviceItem
- )
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_SAVED_LE_DEVICE_CLICKED
- }
-
- private class NotSharingClickedActive(
- private val leAudioProfile: LeAudioProfile?,
- private val assistantProfile: LocalBluetoothLeBroadcastAssistant?,
- @Background private val backgroundDispatcher: CoroutineDispatcher,
- private val logger: BluetoothTileDialogLogger,
- ) : LaunchSettingsCriteria {
- // If not broadcasting, having two device connected, clicked on the active LE audio
- // device
- override suspend fun matched(inAudioSharing: Boolean, deviceItem: DeviceItem): Boolean {
- return withContext(backgroundDispatcher) {
- val matched =
- leAudioProfile?.let { leAudio ->
- assistantProfile?.let { assistant ->
- !inAudioSharing &&
- getCurrentConnectedLeByGroupId(
- leAudio,
- assistant,
- backgroundDispatcher,
- logger
- )
- .size == 2 &&
- deviceItem.isActiveLeAudioSupported
- }
- } ?: false
-
- if (matched) {
- logger.logLaunchSettingsCriteriaMatched(
- "NotSharingClickedConnected",
- deviceItem
- )
- }
-
- matched
- }
- }
-
- override suspend fun getClickUiEvent(deviceItem: DeviceItem) =
- BluetoothTileDialogUiEvent.LAUNCH_SETTINGS_NOT_SHARING_ACTIVE_LE_DEVICE_CLICKED
- }
-
- private companion object {
- const val EXTRA_SHOW_FRAGMENT_ARGUMENTS = ":settings:show_fragment_args"
-
- val DeviceItem.isLeAudioSupported: Boolean
- get() =
- cachedBluetoothDevice.profiles.any { profile ->
- profile is LeAudioProfile && profile.isEnabled(cachedBluetoothDevice.device)
- }
-
- val DeviceItem.isNotConnectedLeAudioSupported: Boolean
- get() = type == DeviceItemType.SAVED_BLUETOOTH_DEVICE && isLeAudioSupported
-
- val DeviceItem.isActiveLeAudioSupported: Boolean
- get() = type == DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE && isLeAudioSupported
-
- val DeviceItem.isMediaDevice: Boolean
- get() =
- cachedBluetoothDevice.uiAccessibleProfiles.any {
- it is A2dpProfile ||
- it is HearingAidProfile ||
- it is LeAudioProfile ||
- it is HeadsetProfile
- }
- }
}
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
index 7280489..7ed5629 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactory.kt
@@ -23,7 +23,6 @@
import com.android.settingslib.bluetooth.CachedBluetoothDevice
import com.android.settingslib.bluetooth.LocalBluetoothManager
import com.android.settingslib.flags.Flags
-import com.android.settingslib.flags.Flags.enableLeAudioSharing
import com.android.systemui.res.R
private val backgroundOn = R.drawable.settingslib_switch_bar_bg_on
@@ -56,7 +55,7 @@
connectionSummary: String,
background: Int,
actionAccessibilityLabel: String,
- isActive: Boolean
+ isActive: Boolean,
): DeviceItem {
return DeviceItem(
type = type,
@@ -70,7 +69,7 @@
background = background,
isEnabled = !cachedDevice.isBusy,
actionAccessibilityLabel = actionAccessibilityLabel,
- isActive = isActive
+ isActive = isActive,
)
}
}
@@ -80,7 +79,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -94,20 +93,20 @@
cachedDevice.connectionSummary ?: "",
backgroundOn,
context.getString(actionAccessibilityLabelDisconnect),
- isActive = true
+ isActive = true,
)
}
}
internal class AudioSharingMediaDeviceItemFactory(
- private val localBluetoothManager: LocalBluetoothManager?
+ private val localBluetoothManager: LocalBluetoothManager
) : DeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
- return enableLeAudioSharing() &&
+ return BluetoothUtils.isAudioSharingEnabled() &&
BluetoothUtils.hasConnectedBroadcastSource(cachedDevice, localBluetoothManager)
}
@@ -120,24 +119,24 @@
?: context.getString(audioSharing),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOn,
"",
- isActive = !cachedDevice.isBusy
+ isActive = !cachedDevice.isBusy,
)
}
}
internal class AvailableAudioSharingMediaDeviceItemFactory(
- private val localBluetoothManager: LocalBluetoothManager?
+ private val localBluetoothManager: LocalBluetoothManager
) : AvailableMediaDeviceItemFactory() {
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isAudioSharingEnabled() &&
super.isFilterMatched(context, cachedDevice, audioManager) &&
BluetoothUtils.isAvailableAudioSharingMediaBluetoothDevice(
cachedDevice,
- localBluetoothManager
+ localBluetoothManager,
)
}
@@ -151,7 +150,7 @@
),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
"",
- isActive = false
+ isActive = false,
)
}
}
@@ -160,7 +159,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -171,7 +170,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableMediaBluetoothDevice(cachedDevice, audioManager)
@@ -186,7 +185,7 @@
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelActivate),
- isActive = false
+ isActive = false,
)
}
}
@@ -195,7 +194,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return !BluetoothUtils.isActiveMediaDevice(cachedDevice) &&
BluetoothUtils.isAvailableHearingDevice(cachedDevice)
@@ -206,7 +205,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -225,7 +224,7 @@
?: context.getString(connected),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelDisconnect),
- isActive = false
+ isActive = false,
)
}
}
@@ -234,7 +233,7 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(context, cachedDevice.device) &&
@@ -254,7 +253,7 @@
?: context.getString(saved),
if (cachedDevice.isBusy) backgroundOffBusy else backgroundOff,
context.getString(actionAccessibilityLabelActivate),
- isActive = false
+ isActive = false,
)
}
}
@@ -263,12 +262,12 @@
override fun isFilterMatched(
context: Context,
cachedDevice: CachedBluetoothDevice,
- audioManager: AudioManager
+ audioManager: AudioManager,
): Boolean {
return if (Flags.enableHideExclusivelyManagedBluetoothDevice()) {
!BluetoothUtils.isExclusivelyManagedBluetoothDevice(
context,
- cachedDevice.getDevice()
+ cachedDevice.getDevice(),
) &&
cachedDevice.isHearingAidDevice &&
cachedDevice.bondState == BluetoothDevice.BOND_BONDED &&
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
index 9114eca..01b84da 100644
--- a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractor.kt
@@ -39,6 +39,7 @@
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.asSharedFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.flowOn
import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.isActive
import kotlinx.coroutines.withContext
@@ -54,6 +55,8 @@
private val localBluetoothManager: LocalBluetoothManager?,
private val systemClock: SystemClock,
private val logger: BluetoothTileDialogLogger,
+ private val deviceItemFactoryList: List<@JvmSuppressWildcards DeviceItemFactory>,
+ private val deviceItemDisplayPriority: List<@JvmSuppressWildcards DeviceItemType>,
@Application private val coroutineScope: CoroutineScope,
@Background private val backgroundDispatcher: CoroutineDispatcher,
) {
@@ -67,7 +70,7 @@
internal val showSeeAllUpdate
get() = mutableShowSeeAllUpdate.asStateFlow()
- internal val deviceItemUpdateRequest: SharedFlow<Unit> =
+ val deviceItemUpdateRequest: SharedFlow<Unit> =
conflatedCallbackFlow {
val listener =
object : BluetoothCallback {
@@ -112,28 +115,9 @@
localBluetoothManager?.eventManager?.registerCallback(listener)
awaitClose { localBluetoothManager?.eventManager?.unregisterCallback(listener) }
}
+ .flowOn(backgroundDispatcher)
.shareIn(coroutineScope, SharingStarted.WhileSubscribed(replayExpirationMillis = 0))
- private var deviceItemFactoryList: List<DeviceItemFactory> =
- listOf(
- ActiveMediaDeviceItemFactory(),
- AudioSharingMediaDeviceItemFactory(localBluetoothManager),
- AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager),
- AvailableMediaDeviceItemFactory(),
- ConnectedDeviceItemFactory(),
- SavedDeviceItemFactory()
- )
-
- private var displayPriority: List<DeviceItemType> =
- listOf(
- DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
- DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- )
-
internal suspend fun updateDeviceItems(context: Context, trigger: DeviceFetchTrigger) {
withContext(backgroundDispatcher) {
val start = systemClock.elapsedRealtime()
@@ -144,7 +128,7 @@
.firstOrNull { it.isFilterMatched(context, cachedDevice, audioManager) }
?.create(context, cachedDevice)
}
- .sort(displayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
+ .sort(deviceItemDisplayPriority, bluetoothAdapter?.mostRecentlyConnectedDevices)
// Only emit when the job is not cancelled
if (isActive) {
mutableDeviceItemUpdate.tryEmit(deviceItems.take(MAX_DEVICE_ITEM_ENTRY))
@@ -176,14 +160,6 @@
)
}
- internal fun setDeviceItemFactoryListForTesting(list: List<DeviceItemFactory>) {
- deviceItemFactoryList = list
- }
-
- internal fun setDisplayPriorityForTesting(list: List<DeviceItemType>) {
- displayPriority = list
- }
-
companion object {
private const val TAG = "DeviceItemInteractor"
private const val MAX_DEVICE_ITEM_ENTRY = 3
diff --git a/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
new file mode 100644
index 0000000..50970a5
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/bluetooth/qsdialog/dagger/AudioSharingModule.kt
@@ -0,0 +1,128 @@
+/*
+ * 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.bluetooth.qsdialog.dagger
+
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.flags.Flags
+import com.android.settingslib.volume.data.repository.AudioSharingRepository as SettingsLibAudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.ActiveMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingDeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractor
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepository
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryEmptyImpl
+import com.android.systemui.bluetooth.qsdialog.AudioSharingRepositoryImpl
+import com.android.systemui.bluetooth.qsdialog.AvailableAudioSharingMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.AvailableMediaDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.ConnectedDeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractor
+import com.android.systemui.bluetooth.qsdialog.DeviceItemActionInteractorImpl
+import com.android.systemui.bluetooth.qsdialog.DeviceItemFactory
+import com.android.systemui.bluetooth.qsdialog.DeviceItemType
+import com.android.systemui.bluetooth.qsdialog.SavedDeviceItemFactory
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import dagger.Lazy
+import dagger.Module
+import dagger.Provides
+import kotlinx.coroutines.CoroutineDispatcher
+
+/** Dagger module for audio sharing code for BT QS dialog */
+@Module
+interface AudioSharingModule {
+
+ companion object {
+ @Provides
+ @SysUISingleton
+ fun provideAudioSharingRepository(
+ localBluetoothManager: LocalBluetoothManager?,
+ settingsLibAudioSharingRepository: SettingsLibAudioSharingRepository,
+ @Background backgroundDispatcher: CoroutineDispatcher,
+ ): AudioSharingRepository =
+ if (
+ Flags.enableLeAudioSharing() &&
+ Flags.audioSharingQsDialogImprovement() &&
+ localBluetoothManager != null
+ ) {
+ AudioSharingRepositoryImpl(
+ localBluetoothManager,
+ settingsLibAudioSharingRepository,
+ backgroundDispatcher,
+ )
+ } else {
+ AudioSharingRepositoryEmptyImpl()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideAudioSharingInteractor(
+ localBluetoothManager: LocalBluetoothManager?,
+ impl: Lazy<AudioSharingInteractorImpl>,
+ emptyImpl: Lazy<AudioSharingInteractorEmptyImpl>,
+ ): AudioSharingInteractor =
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ impl.get()
+ } else {
+ emptyImpl.get()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemActionInteractor(
+ localBluetoothManager: LocalBluetoothManager?,
+ audioSharingImpl: Lazy<AudioSharingDeviceItemActionInteractorImpl>,
+ impl: Lazy<DeviceItemActionInteractorImpl>,
+ ): DeviceItemActionInteractor =
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ audioSharingImpl.get()
+ } else {
+ impl.get()
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemFactoryList(
+ localBluetoothManager: LocalBluetoothManager?
+ ): List<DeviceItemFactory> = buildList {
+ add(ActiveMediaDeviceItemFactory())
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ add(AudioSharingMediaDeviceItemFactory(localBluetoothManager))
+ add(AvailableAudioSharingMediaDeviceItemFactory(localBluetoothManager))
+ }
+ add(AvailableMediaDeviceItemFactory())
+ add(ConnectedDeviceItemFactory())
+ add(SavedDeviceItemFactory())
+ }
+
+ @Provides
+ @SysUISingleton
+ fun provideDeviceItemDisplayPriority(
+ localBluetoothManager: LocalBluetoothManager?
+ ): List<DeviceItemType> = buildList {
+ add(DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE)
+ if (Flags.enableLeAudioSharing() && localBluetoothManager != null) {
+ add(DeviceItemType.AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+ add(DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE)
+ }
+ add(DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE)
+ add(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
+ add(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
index 3ae9250..6508e4b5 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/ui/viewmodel/CommunalEditModeViewModel.kt
@@ -27,7 +27,6 @@
import android.util.Log
import android.view.accessibility.AccessibilityEvent
import android.view.accessibility.AccessibilityManager
-import androidx.activity.result.ActivityResultLauncher
import com.android.internal.logging.UiEventLogger
import com.android.systemui.communal.dagger.CommunalModule.Companion.LAUNCHER_PACKAGE
import com.android.systemui.communal.data.model.CommunalWidgetCategories
@@ -184,10 +183,10 @@
val isIdleOnCommunal: StateFlow<Boolean> = communalInteractor.isIdleOnCommunal
- /** Launch the widget picker activity using the given {@link ActivityResultLauncher}. */
+ /** Launch the widget picker activity using the given startActivity method. */
suspend fun onOpenWidgetPicker(
resources: Resources,
- activityLauncher: ActivityResultLauncher<Intent>,
+ startActivity: (intent: Intent) -> Unit,
): Boolean =
withContext(backgroundDispatcher) {
val widgets = communalInteractor.widgetContent.first()
@@ -199,7 +198,7 @@
}
getWidgetPickerActivityIntent(resources, excludeList)?.let {
try {
- activityLauncher.launch(it)
+ startActivity(it)
return@withContext true
} catch (e: Exception) {
Log.e(TAG, "Failed to launch widget picker activity", e)
diff --git a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
index 6228ac5..8c14d63 100644
--- a/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/communal/widgets/EditWidgetsActivity.kt
@@ -27,8 +27,6 @@
import android.view.WindowInsets
import androidx.activity.ComponentActivity
import androidx.activity.compose.setContent
-import androidx.activity.result.ActivityResultLauncher
-import androidx.activity.result.contract.ActivityResultContracts.StartActivityForResult
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
@@ -51,6 +49,7 @@
import com.android.systemui.log.core.Logger
import com.android.systemui.log.dagger.CommunalLog
import com.android.systemui.scene.shared.flag.SceneContainerFlag
+import com.android.systemui.settings.UserTracker
import javax.inject.Inject
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.launch
@@ -64,12 +63,15 @@
private val uiEventLogger: UiEventLogger,
private val widgetConfiguratorFactory: WidgetConfigurationController.Factory,
private val widgetSection: CommunalAppWidgetSection,
+ private val userTracker: UserTracker,
@CommunalLog logBuffer: LogBuffer,
) : ComponentActivity() {
companion object {
private const val TAG = "EditWidgetsActivity"
private const val EXTRA_IS_PENDING_WIDGET_DRAG = "is_pending_widget_drag"
const val EXTRA_OPEN_WIDGET_PICKER_ON_START = "open_widget_picker_on_start"
+
+ private const val REQUEST_CODE_WIDGET_PICKER = 200
}
/**
@@ -110,7 +112,7 @@
object : ActivityLifecycleCallbacks {
override fun onActivityCreated(
activity: Activity,
- savedInstanceState: Bundle?
+ savedInstanceState: Bundle?,
) {
waitingForResult =
savedInstanceState?.getBoolean(STATE_EXTRA_IS_WAITING_FOR_RESULT)
@@ -172,41 +174,6 @@
if (communalEditWidgetsActivityFinishFix()) ActivityControllerImpl(this)
else NopActivityController()
- private val addWidgetActivityLauncher: ActivityResultLauncher<Intent> =
- registerForActivityResult(StartActivityForResult()) { result ->
- when (result.resultCode) {
- RESULT_OK -> {
- uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
-
- result.data?.let { intent ->
- val isPendingWidgetDrag =
- intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
- // Nothing to do when a widget is being dragged & dropped. The drop
- // target in the communal grid will receive the widget to be added (if
- // the user drops it over).
- if (!isPendingWidgetDrag) {
- val (componentName, user) = getWidgetExtraFromIntent(intent)
- if (componentName != null && user != null) {
- // Add widget at the end.
- communalViewModel.onAddWidget(
- componentName,
- user,
- configurator = widgetConfigurator,
- )
- } else {
- run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
- }
- }
- } ?: run { Log.w(TAG, "No data in result.") }
- }
- else ->
- Log.w(
- TAG,
- "Failed to receive result from widget picker, code=${result.resultCode}"
- )
- }
- }
-
override fun onCreate(savedInstanceState: Bundle?) {
super.onCreate(savedInstanceState)
@@ -226,8 +193,7 @@
PlatformTheme {
Box(
modifier =
- Modifier.fillMaxSize()
- .background(MaterialTheme.colorScheme.surfaceDim),
+ Modifier.fillMaxSize().background(MaterialTheme.colorScheme.surfaceDim)
) {
CommunalHub(
viewModel = communalViewModel,
@@ -274,7 +240,13 @@
private fun onOpenWidgetPicker() {
lifecycleScope.launch {
- communalViewModel.onOpenWidgetPicker(resources, addWidgetActivityLauncher)
+ communalViewModel.onOpenWidgetPicker(resources) { intent: Intent ->
+ startActivityForResultAsUser(
+ intent,
+ REQUEST_CODE_WIDGET_PICKER,
+ userTracker.userHandle,
+ )
+ }
}
}
@@ -285,7 +257,7 @@
communalViewModel.changeScene(
scene = CommunalScenes.Communal,
loggingReason = "edit mode closing",
- transitionKey = CommunalTransitionKeys.FromEditMode
+ transitionKey = CommunalTransitionKeys.FromEditMode,
)
// Wait for the current scene to be idle on communal.
@@ -309,7 +281,7 @@
flagsMask: Int,
flagsValues: Int,
extraFlags: Int,
- options: Bundle?
+ options: Bundle?,
) {
activityController.onWaitingForResult(true)
super.startIntentSenderForResult(
@@ -319,15 +291,46 @@
flagsMask,
flagsValues,
extraFlags,
- options
+ options,
)
}
override fun onActivityResult(requestCode: Int, resultCode: Int, data: Intent?) {
activityController.onWaitingForResult(false)
super.onActivityResult(requestCode, resultCode, data)
- if (requestCode == WidgetConfigurationController.REQUEST_CODE) {
- widgetConfigurator.setConfigurationResult(resultCode)
+
+ when (requestCode) {
+ WidgetConfigurationController.REQUEST_CODE ->
+ widgetConfigurator.setConfigurationResult(resultCode)
+ REQUEST_CODE_WIDGET_PICKER -> {
+ if (resultCode != RESULT_OK) {
+ Log.w(TAG, "Failed to receive result from widget picker, code=$resultCode")
+ return
+ }
+
+ uiEventLogger.log(CommunalUiEvent.COMMUNAL_HUB_WIDGET_PICKER_SHOWN)
+
+ data?.let { intent ->
+ val isPendingWidgetDrag =
+ intent.getBooleanExtra(EXTRA_IS_PENDING_WIDGET_DRAG, false)
+ // Nothing to do when a widget is being dragged & dropped. The drop
+ // target in the communal grid will receive the widget to be added (if
+ // the user drops it over).
+ if (!isPendingWidgetDrag) {
+ val (componentName, user) = getWidgetExtraFromIntent(intent)
+ if (componentName != null && user != null) {
+ // Add widget at the end.
+ communalViewModel.onAddWidget(
+ componentName,
+ user,
+ configurator = widgetConfigurator,
+ )
+ } else {
+ run { Log.w(TAG, "No AppWidgetProviderInfo found in result.") }
+ }
+ }
+ } ?: run { Log.w(TAG, "No data in result.") }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
index b56ed8c..589dbf9 100644
--- a/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/display/DisplayModule.kt
@@ -24,6 +24,8 @@
import com.android.systemui.display.data.repository.DisplayRepositoryImpl
import com.android.systemui.display.data.repository.DisplayScopeRepository
import com.android.systemui.display.data.repository.DisplayScopeRepositoryImpl
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepositoryImpl
import com.android.systemui.display.data.repository.FocusedDisplayRepository
import com.android.systemui.display.data.repository.FocusedDisplayRepositoryImpl
import com.android.systemui.display.domain.interactor.ConnectedDisplayInteractor
@@ -58,6 +60,11 @@
@Binds fun displayScopeRepository(impl: DisplayScopeRepositoryImpl): DisplayScopeRepository
+ @Binds
+ fun displayWindowPropertiesRepository(
+ impl: DisplayWindowPropertiesRepositoryImpl
+ ): DisplayWindowPropertiesRepository
+
companion object {
@Provides
@SysUISingleton
@@ -72,5 +79,19 @@
CoreStartable.NOP
}
}
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(DisplayWindowPropertiesRepository::class)
+ fun displayWindowPropertiesRepoAsCoreStartable(
+ repoLazy: Lazy<DisplayWindowPropertiesRepositoryImpl>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ return repoLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
new file mode 100644
index 0000000..88d3a28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepository.kt
@@ -0,0 +1,115 @@
+/*
+ * 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.display.data.repository
+
+import android.annotation.SuppressLint
+import android.content.Context
+import android.view.Display
+import android.view.WindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.google.common.collect.HashBasedTable
+import com.google.common.collect.Table
+import java.io.PrintWriter
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Provides per display instances of [DisplayWindowProperties]. */
+interface DisplayWindowPropertiesRepository {
+
+ /**
+ * Returns a [DisplayWindowProperties] instance for a given display id and window type.
+ *
+ * @throws IllegalArgumentException if no display with the given display id exists.
+ */
+ fun get(
+ displayId: Int,
+ @WindowManager.LayoutParams.WindowType windowType: Int,
+ ): DisplayWindowProperties
+}
+
+@SysUISingleton
+class DisplayWindowPropertiesRepositoryImpl
+@Inject
+constructor(
+ @Background private val backgroundApplicationScope: CoroutineScope,
+ private val globalContext: Context,
+ private val globalWindowManager: WindowManager,
+ private val displayRepository: DisplayRepository,
+) : DisplayWindowPropertiesRepository, CoreStartable {
+
+ init {
+ StatusBarConnectedDisplays.assertInNewMode()
+ }
+
+ private val properties: Table<Int, Int, DisplayWindowProperties> = HashBasedTable.create()
+
+ override fun get(
+ displayId: Int,
+ @WindowManager.LayoutParams.WindowType windowType: Int,
+ ): DisplayWindowProperties {
+ val display =
+ displayRepository.getDisplay(displayId)
+ ?: throw IllegalArgumentException("Display with id $displayId doesn't exist")
+ return properties.get(displayId, windowType)
+ ?: create(display, windowType).also { properties.put(displayId, windowType, it) }
+ }
+
+ override fun start() {
+ backgroundApplicationScope.launch(
+ CoroutineName("DisplayWindowPropertiesRepositoryImpl#start")
+ ) {
+ displayRepository.displayRemovalEvent.collect { removedDisplayId ->
+ properties.row(removedDisplayId).clear()
+ }
+ }
+ }
+
+ private fun create(display: Display, windowType: Int): DisplayWindowProperties {
+ val displayId = display.displayId
+ return if (displayId == Display.DEFAULT_DISPLAY) {
+ // For the default display, we can just reuse the global/application properties.
+ // Creating a window context is expensive, therefore we avoid it.
+ DisplayWindowProperties(
+ displayId = displayId,
+ windowType = windowType,
+ context = globalContext,
+ windowManager = globalWindowManager,
+ )
+ } else {
+ val context = createWindowContext(display, windowType)
+ @SuppressLint("NonInjectedService") // Need to manually get the service
+ val windowManager = context.getSystemService(WindowManager::class.java) as WindowManager
+ DisplayWindowProperties(displayId, windowType, context, windowManager)
+ }
+ }
+
+ private fun createWindowContext(display: Display, windowType: Int): Context =
+ globalContext.createWindowContext(display, windowType, /* options= */ null).also {
+ it.setTheme(R.style.Theme_SystemUI)
+ }
+
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ pw.write("perDisplayContexts: $properties")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt
new file mode 100644
index 0000000..6acc296
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/display/shared/model/DisplayWindowProperties.kt
@@ -0,0 +1,43 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.shared.model
+
+import android.content.Context
+import android.view.WindowManager
+
+/** Represents a display specific group of window related properties. */
+data class DisplayWindowProperties(
+ /** The id of the display associated with this instance. */
+ val displayId: Int,
+ /**
+ * The window type that was used to create the [Context] in this instance, using
+ * [Context.createWindowContext]. This is the window type that can be used when adding views to
+ * the [WindowManager] associated with this instance.
+ */
+ @WindowManager.LayoutParams.WindowType val windowType: Int,
+ /**
+ * The display specific [Context] created using [Context.createWindowContext] with window type
+ * associated with this instance.
+ */
+ val context: Context,
+
+ /**
+ * The display specific [WindowManager] instance to be used when adding windows of the type
+ * associated with this instance.
+ */
+ val windowManager: WindowManager,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
index 65c29b8..9c5231d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/QSFragmentCompose.kt
@@ -92,6 +92,7 @@
import com.android.systemui.qs.composefragment.SceneKeys.QuickQuickSettings
import com.android.systemui.qs.composefragment.SceneKeys.QuickSettings
import com.android.systemui.qs.composefragment.SceneKeys.toIdleSceneKey
+import com.android.systemui.qs.composefragment.ui.NotificationScrimClipParams
import com.android.systemui.qs.composefragment.ui.notificationScrimClip
import com.android.systemui.qs.composefragment.ui.quickQuickSettingsToQuickSettings
import com.android.systemui.qs.composefragment.viewmodel.QSFragmentComposeViewModel
@@ -149,20 +150,12 @@
private val notificationScrimClippingParams =
object {
var isEnabled by mutableStateOf(false)
- var leftInset by mutableStateOf(0)
- var rightInset by mutableStateOf(0)
- var top by mutableStateOf(0)
- var bottom by mutableStateOf(0)
- var radius by mutableStateOf(0)
+ var params by mutableStateOf(NotificationScrimClipParams())
fun dump(pw: IndentingPrintWriter) {
pw.printSection("NotificationScrimClippingParams") {
pw.println("isEnabled", isEnabled)
- pw.println("leftInset", "${leftInset}px")
- pw.println("rightInset", "${rightInset}px")
- pw.println("top", "${top}px")
- pw.println("bottom", "${bottom}px")
- pw.println("radius", "${radius}px")
+ pw.println("params", params)
}
}
}
@@ -216,7 +209,7 @@
FrameLayoutTouchPassthrough(
context,
{ notificationScrimClippingParams.isEnabled },
- { notificationScrimClippingParams.top },
+ { notificationScrimClippingParams.params.top },
)
frame.addView(
composeView,
@@ -237,13 +230,7 @@
Modifier.windowInsetsPadding(WindowInsets.navigationBars).thenIf(
notificationScrimClippingParams.isEnabled
) {
- Modifier.notificationScrimClip(
- notificationScrimClippingParams.leftInset,
- notificationScrimClippingParams.top,
- notificationScrimClippingParams.rightInset,
- notificationScrimClippingParams.bottom,
- notificationScrimClippingParams.radius,
- )
+ Modifier.notificationScrimClip { notificationScrimClippingParams.params }
},
) {
val isEditing by
@@ -445,13 +432,14 @@
fullWidth: Boolean,
) {
notificationScrimClippingParams.isEnabled = visible
- notificationScrimClippingParams.top = top
- notificationScrimClippingParams.bottom = bottom
- // Full width means that QS will show in the entire width allocated to it (for example
- // phone) vs. showing in a narrower column (for example, tablet portrait).
- notificationScrimClippingParams.leftInset = if (fullWidth) 0 else leftInset
- notificationScrimClippingParams.rightInset = if (fullWidth) 0 else rightInset
- notificationScrimClippingParams.radius = cornerRadius
+ notificationScrimClippingParams.params =
+ NotificationScrimClipParams(
+ top,
+ bottom,
+ if (fullWidth) 0 else leftInset,
+ if (fullWidth) 0 else rightInset,
+ cornerRadius,
+ )
}
override fun isFullyCollapsed(): Boolean {
diff --git a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
index 93c6445..c912bd5 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/composefragment/ui/NotificationScrimClip.kt
@@ -31,87 +31,73 @@
* ([ClipOp.Difference]) a `RoundRect(-leftInset, top, width + rightInset, bottom, radius, radius)`
* from the QS container.
*/
-fun Modifier.notificationScrimClip(
- leftInset: Int,
- top: Int,
- rightInset: Int,
- bottom: Int,
- radius: Int
-): Modifier {
- return this then NotificationScrimClipElement(leftInset, top, rightInset, bottom, radius)
+fun Modifier.notificationScrimClip(clipParams: () -> NotificationScrimClipParams): Modifier {
+ return this then NotificationScrimClipElement(clipParams)
}
-private class NotificationScrimClipNode(
- var leftInset: Float,
- var top: Float,
- var rightInset: Float,
- var bottom: Float,
- var radius: Float,
-) : DrawModifierNode, Modifier.Node() {
+private class NotificationScrimClipNode(var clipParams: () -> NotificationScrimClipParams) :
+ DrawModifierNode, Modifier.Node() {
private val path = Path()
- var invalidated = true
+ private var lastClipParams = NotificationScrimClipParams()
override fun ContentDrawScope.draw() {
- if (invalidated) {
+ val newClipParams = clipParams()
+ if (newClipParams != lastClipParams) {
+ lastClipParams = newClipParams
+ applyClipParams(path, lastClipParams)
+ }
+ clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
+ }
+
+ private fun ContentDrawScope.applyClipParams(
+ path: Path,
+ clipParams: NotificationScrimClipParams,
+ ) {
+ with(clipParams) {
path.rewind()
path
.asAndroidPath()
.addRoundRect(
- -leftInset,
- top,
+ -leftInset.toFloat(),
+ top.toFloat(),
size.width + rightInset,
- bottom,
- radius,
- radius,
- android.graphics.Path.Direction.CW
+ bottom.toFloat(),
+ radius.toFloat(),
+ radius.toFloat(),
+ android.graphics.Path.Direction.CW,
)
- invalidated = false
}
- clipPath(path, ClipOp.Difference) { this@draw.drawContent() }
}
}
-private data class NotificationScrimClipElement(
- val leftInset: Int,
- val top: Int,
- val rightInset: Int,
- val bottom: Int,
- val radius: Int,
-) : ModifierNodeElement<NotificationScrimClipNode>() {
+private data class NotificationScrimClipElement(val clipParams: () -> NotificationScrimClipParams) :
+ ModifierNodeElement<NotificationScrimClipNode>() {
override fun create(): NotificationScrimClipNode {
- return NotificationScrimClipNode(
- leftInset.toFloat(),
- top.toFloat(),
- rightInset.toFloat(),
- bottom.toFloat(),
- radius.toFloat(),
- )
+ return NotificationScrimClipNode(clipParams)
}
override fun update(node: NotificationScrimClipNode) {
- val changed =
- node.leftInset != leftInset.toFloat() ||
- node.top != top.toFloat() ||
- node.rightInset != rightInset.toFloat() ||
- node.bottom != bottom.toFloat() ||
- node.radius != radius.toFloat()
- if (changed) {
- node.leftInset = leftInset.toFloat()
- node.top = top.toFloat()
- node.rightInset = rightInset.toFloat()
- node.bottom = bottom.toFloat()
- node.radius = radius.toFloat()
- node.invalidated = true
- }
+ node.clipParams = clipParams
}
override fun InspectorInfo.inspectableProperties() {
name = "notificationScrimClip"
- properties["leftInset"] = leftInset
- properties["top"] = top
- properties["rightInset"] = rightInset
- properties["bottom"] = bottom
- properties["radius"] = radius
+ with(clipParams()) {
+ properties["leftInset"] = leftInset
+ properties["top"] = top
+ properties["rightInset"] = rightInset
+ properties["bottom"] = bottom
+ properties["radius"] = radius
+ }
}
}
+
+/** Params for [notificationScrimClip]. */
+data class NotificationScrimClipParams(
+ val top: Int = 0,
+ val bottom: Int = 0,
+ val leftInset: Int = 0,
+ val rightInset: Int = 0,
+ val radius: Int = 0,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
index 73ad0e5..da04f6e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/StatusBarStateControllerImpl.java
@@ -46,6 +46,7 @@
import com.android.internal.logging.UiEventLogger;
import com.android.keyguard.KeyguardClockSwitch;
import com.android.systemui.DejankUtils;
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor;
import com.android.systemui.dagger.SysUISingleton;
import com.android.systemui.deviceentry.domain.interactor.DeviceUnlockedInteractor;
import com.android.systemui.deviceentry.shared.model.DeviceUnlockStatus;
@@ -123,6 +124,7 @@
private final Lazy<SceneContainerOcclusionInteractor> mSceneContainerOcclusionInteractorLazy;
private final Lazy<KeyguardClockInteractor> mKeyguardClockInteractorLazy;
private final Lazy<SceneBackInteractor> mSceneBackInteractorLazy;
+ private final Lazy<AlternateBouncerInteractor> mAlternateBouncerInteractorLazy;
private int mState;
private int mLastState;
private int mUpcomingState;
@@ -193,7 +195,8 @@
Lazy<SceneInteractor> sceneInteractorLazy,
Lazy<SceneContainerOcclusionInteractor> sceneContainerOcclusionInteractor,
Lazy<KeyguardClockInteractor> keyguardClockInteractorLazy,
- Lazy<SceneBackInteractor> sceneBackInteractorLazy) {
+ Lazy<SceneBackInteractor> sceneBackInteractorLazy,
+ Lazy<AlternateBouncerInteractor> alternateBouncerInteractorLazy) {
mUiEventLogger = uiEventLogger;
mInteractionJankMonitorLazy = interactionJankMonitorLazy;
mJavaAdapter = javaAdapter;
@@ -205,6 +208,7 @@
mSceneContainerOcclusionInteractorLazy = sceneContainerOcclusionInteractor;
mKeyguardClockInteractorLazy = keyguardClockInteractorLazy;
mSceneBackInteractorLazy = sceneBackInteractorLazy;
+ mAlternateBouncerInteractorLazy = alternateBouncerInteractorLazy;
for (int i = 0; i < HISTORY_SIZE; i++) {
mHistoricalRecords[i] = new HistoricalState();
}
@@ -233,6 +237,7 @@
mSceneInteractorLazy.get().getCurrentOverlays(),
mSceneBackInteractorLazy.get().getBackStack(),
mSceneContainerOcclusionInteractorLazy.get().getInvisibleDueToOcclusion(),
+ mAlternateBouncerInteractorLazy.get().isVisible(),
this::calculateStateFromSceneFramework),
this::onStatusBarStateChanged);
@@ -693,7 +698,8 @@
SceneKey currentScene,
Set<OverlayKey> currentOverlays,
SceneStack backStack,
- boolean isOccluded) {
+ boolean isOccluded,
+ boolean alternateBouncerIsVisible) {
SceneContainerFlag.isUnexpectedlyInLegacyMode();
final boolean onBouncer = currentScene.equals(Scenes.Bouncer);
@@ -714,7 +720,8 @@
final String inputLogString = "currentScene=" + currentScene.getTestTag()
+ " currentOverlays=" + currentOverlays + " backStack=" + backStack
- + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded;
+ + " isUnlocked=" + isUnlocked + " isOccluded=" + isOccluded
+ + " alternateBouncerIsVisible=" + alternateBouncerIsVisible;
int newState;
@@ -722,6 +729,7 @@
// 1. deviceUnlockStatus.isUnlocked changes from false to true.
// 2. Lockscreen changes to Gone, either in currentScene or in backStack.
// 3. Bouncer is removed from currentScene or backStack, if it was present.
+ // 4. the alternate bouncer is hidden, if it was visible.
//
// From this function's perspective, though, deviceUnlockStatus, currentScene, and backStack
// each update separately, and the relative order of those updates is not well-defined. This
@@ -733,6 +741,7 @@
// 1. deviceUnlockStatus.isUnlocked is false.
// 2. currentScene is a keyguardish scene (Lockscreen, Bouncer, or Communal).
// 3. backStack contains a keyguardish scene (Lockscreen or Communal).
+ // 4. the alternate bouncer is visible.
final boolean onKeyguardish = onLockscreen || onBouncer || onCommunal;
final boolean overKeyguardish = overLockscreen || overCommunal;
@@ -741,7 +750,7 @@
// Occlusion is special; even though the device is still technically on the lockscreen,
// the UI behaves as if it is unlocked.
newState = StatusBarState.SHADE;
- } else if (onKeyguardish || overKeyguardish) {
+ } else if (onKeyguardish || overKeyguardish || alternateBouncerIsVisible) {
// We get here if we are on or over a keyguardish scene, even if isUnlocked is true; we
// want to return SHADE_LOCKED or KEYGUARD until we are also neither on nor over a
// keyguardish scene.
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
index dac0102..10090283 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/ConnectivityModule.kt
@@ -17,6 +17,7 @@
package com.android.systemui.statusbar.connectivity
import android.os.UserManager
+import com.android.systemui.bluetooth.qsdialog.dagger.AudioSharingModule
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.flags.Flags.SIGNAL_CALLBACK_DEPRECATION
import com.android.systemui.qs.QsEventLogger
@@ -56,7 +57,7 @@
import dagger.multibindings.IntoMap
import dagger.multibindings.StringKey
-@Module
+@Module(includes = [AudioSharingModule::class])
interface ConnectivityModule {
/** Inject BluetoothTile into tileMap in QSModule */
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
index cf238d5..cd1642e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/dagger/StatusBarModule.kt
@@ -22,15 +22,20 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.LogBufferFactory
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
import com.android.systemui.statusbar.data.StatusBarDataLayerModule
import com.android.systemui.statusbar.phone.LightBarController
import com.android.systemui.statusbar.phone.StatusBarSignalPolicy
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallController
import com.android.systemui.statusbar.phone.ongoingcall.OngoingCallLog
import com.android.systemui.statusbar.ui.SystemBarUtilsProxyImpl
+import com.android.systemui.statusbar.window.MultiDisplayStatusBarWindowControllerStore
+import com.android.systemui.statusbar.window.SingleDisplayStatusBarWindowControllerStore
import com.android.systemui.statusbar.window.StatusBarWindowController
import com.android.systemui.statusbar.window.StatusBarWindowControllerImpl
+import com.android.systemui.statusbar.window.StatusBarWindowControllerStore
import dagger.Binds
+import dagger.Lazy
import dagger.Module
import dagger.Provides
import dagger.multibindings.ClassKey
@@ -62,13 +67,19 @@
@ClassKey(StatusBarSignalPolicy::class)
abstract fun bindStatusBarSignalPolicy(impl: StatusBarSignalPolicy): CoreStartable
+ @Binds
+ @SysUISingleton
+ abstract fun statusBarWindowControllerFactory(
+ implFactory: StatusBarWindowControllerImpl.Factory
+ ): StatusBarWindowController.Factory
+
companion object {
@Provides
@SysUISingleton
- fun statusBarWindowController(
- context: Context?,
- viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager?,
+ fun defaultStatusBarWindowController(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
factory: StatusBarWindowControllerImpl.Factory,
): StatusBarWindowController {
return factory.create(context, viewCaptureAwareWindowManager)
@@ -76,6 +87,33 @@
@Provides
@SysUISingleton
+ fun windowControllerStore(
+ multiDisplayImplLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>,
+ singleDisplayImplLazy: Lazy<SingleDisplayStatusBarWindowControllerStore>,
+ ): StatusBarWindowControllerStore {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ multiDisplayImplLazy.get()
+ } else {
+ singleDisplayImplLazy.get()
+ }
+ }
+
+ @Provides
+ @SysUISingleton
+ @IntoMap
+ @ClassKey(MultiDisplayStatusBarWindowControllerStore::class)
+ fun multiDisplayControllerStoreAsCoreStartable(
+ storeLazy: Lazy<MultiDisplayStatusBarWindowControllerStore>
+ ): CoreStartable {
+ return if (StatusBarConnectedDisplays.isEnabled) {
+ storeLazy.get()
+ } else {
+ CoreStartable.NOP
+ }
+ }
+
+ @Provides
+ @SysUISingleton
@OngoingCallLog
fun provideOngoingCallLogBuffer(factory: LogBufferFactory): LogBuffer {
return factory.create("OngoingCall", 75)
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
index d67947d..4e26ae8 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/row/SingleLineViewInflater.kt
@@ -90,7 +90,7 @@
notification = notification,
isGroupConversation = isGroupConversation,
builder = builder,
- systemUiContext = systemUiContext
+ systemUiContext = systemUiContext,
)
val conversationData =
@@ -98,7 +98,7 @@
// We don't show the sender's name for one-to-one conversation
conversationSenderName =
if (isGroupConversation) conversationTextData?.senderName else null,
- avatar = conversationAvatar
+ avatar = conversationAvatar,
)
return SingleLineViewModel(
@@ -111,7 +111,7 @@
@JvmStatic
fun inflateRedactedSingleLineViewModel(
context: Context,
- isConversation: Boolean = false
+ isConversation: Boolean = false,
): SingleLineViewModel {
val conversationData =
if (isConversation) {
@@ -122,7 +122,7 @@
com.android.systemui.res.R.drawable
.ic_redacted_notification_single_line_icon
)
- )
+ ),
)
} else {
null
@@ -134,7 +134,7 @@
context.getString(
com.android.systemui.res.R.string.redacted_notification_single_line_text
),
- conversationData
+ conversationData,
)
}
@@ -159,11 +159,13 @@
}
// load the sender's name to display
- val name = lastMessage.senderPerson?.name
+ // null senderPerson means the current user.
+ val name = lastMessage.senderPerson?.name ?: user.name
+
val senderName =
systemUiContext.resources.getString(
R.string.conversation_single_line_name_display,
- if (Flags.cleanUpSpansAndNewLines()) name?.toString() else name
+ if (Flags.cleanUpSpansAndNewLines()) name?.toString() else name,
)
// We need to find back-up values for those texts if they are needed and empty
@@ -333,7 +335,7 @@
sender.icon
?: builder.getDefaultAvatar(
name = sender.name,
- uniqueNames = uniqueNames
+ uniqueNames = uniqueNames,
)
lastKey = senderKey
} else {
@@ -341,7 +343,7 @@
sender.icon
?: builder.getDefaultAvatar(
name = sender.name,
- uniqueNames = uniqueNames
+ uniqueNames = uniqueNames,
)
break
}
@@ -424,7 +426,7 @@
private fun Notification.Builder.getDefaultAvatar(
name: CharSequence?,
- uniqueNames: PeopleHelper.NameToPrefixMap? = null
+ uniqueNames: PeopleHelper.NameToPrefixMap? = null,
): Icon {
val layoutColor = getSmallIconColor(/* isHeader= */ false)
if (!name.isNullOrEmpty()) {
@@ -432,7 +434,7 @@
return peopleHelper.createAvatarSymbol(
/* name = */ name,
/* symbol = */ symbol,
- /* layoutColor = */ layoutColor
+ /* layoutColor = */ layoutColor,
)
}
// If name is null, create default avatar with background color
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
index f5cfc8c..e0bf00f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepository.kt
@@ -26,6 +26,7 @@
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_WIFI
import android.net.vcn.VcnTransportInfo
+import android.net.vcn.VcnUtils
import android.net.wifi.WifiInfo
import android.telephony.SubscriptionManager.INVALID_SUBSCRIPTION_ID
import androidx.annotation.ArrayRes
@@ -161,7 +162,9 @@
defaultNetworkCapabilities
.map { networkCapabilities ->
networkCapabilities?.run {
- val subId = (transportInfo as? VcnTransportInfo)?.subId
+ val subId =
+ VcnUtils.getSubIdFromVcnCaps(connectivityManager, networkCapabilities)
+
// Never return an INVALID_SUBSCRIPTION_ID (-1)
if (subId != INVALID_SUBSCRIPTION_ID) {
subId
@@ -245,9 +248,9 @@
* info.
*/
fun NetworkCapabilities.getMainOrUnderlyingWifiInfo(
- connectivityManager: ConnectivityManager,
+ connectivityManager: ConnectivityManager
): WifiInfo? {
- val mainWifiInfo = this.getMainWifiInfo()
+ val mainWifiInfo = this.getMainWifiInfo(connectivityManager)
if (mainWifiInfo != null) {
return mainWifiInfo
}
@@ -264,7 +267,9 @@
// eventually traced to a wifi or carrier merged connection. So, check those underlying
// networks for possible wifi information as well. See b/225902574.
return this.underlyingNetworks?.firstNotNullOfOrNull { underlyingNetwork ->
- connectivityManager.getNetworkCapabilities(underlyingNetwork)?.getMainWifiInfo()
+ connectivityManager
+ .getNetworkCapabilities(underlyingNetwork)
+ ?.getMainWifiInfo(connectivityManager)
}
}
@@ -272,7 +277,9 @@
* Checks the network capabilities for wifi info, but does *not* check the underlying
* networks. See [getMainOrUnderlyingWifiInfo].
*/
- private fun NetworkCapabilities.getMainWifiInfo(): WifiInfo? {
+ private fun NetworkCapabilities.getMainWifiInfo(
+ connectivityManager: ConnectivityManager
+ ): WifiInfo? {
// Wifi info can either come from a WIFI Transport, or from a CELLULAR transport for
// virtual networks like VCN.
val canHaveWifiInfo =
@@ -286,7 +293,7 @@
// [com.android.settingslib.Utils.tryGetWifiInfoForVcn]. It's copied instead of
// re-used because it makes the logic here clearer, and because the method will be
// removed once this pipeline is fully launched.
- is VcnTransportInfo -> currentTransportInfo.wifiInfo
+ is VcnTransportInfo -> VcnUtils.getWifiInfoFromVcnCaps(connectivityManager, this)
is WifiInfo -> currentTransportInfo
else -> null
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
index 421e5c4..e8dc934 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowController.kt
@@ -16,8 +16,10 @@
package com.android.systemui.statusbar.window
+import android.content.Context
import android.view.View
import android.view.ViewGroup
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
import com.android.systemui.animation.ActivityTransitionAnimator
import com.android.systemui.fragments.FragmentHostManager
import java.util.Optional
@@ -73,4 +75,11 @@
* this#setForceStatusBarVisible} together and use some sort of ranking system instead.
*/
fun setOngoingProcessRequiresStatusBarVisible(visible: Boolean)
+
+ interface Factory {
+ fun create(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+ ): StatusBarWindowController
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
index 1ee7cf3..d709e5a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerImpl.java
@@ -354,11 +354,13 @@
}
@AssistedFactory
- public interface Factory {
+ public interface Factory extends StatusBarWindowController.Factory {
/** Creates a new instance. */
+ @NonNull
+ @Override
StatusBarWindowControllerImpl create(
- Context context,
- ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
+ @NonNull Context context,
+ @NonNull ViewCaptureAwareWindowManager viewCaptureAwareWindowManager);
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
new file mode 100644
index 0000000..5f30b37
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/window/StatusBarWindowControllerStore.kt
@@ -0,0 +1,117 @@
+/*
+ * 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.window
+
+import android.view.Display
+import android.view.WindowManager
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Background
+import com.android.systemui.display.data.repository.DisplayRepository
+import com.android.systemui.display.data.repository.DisplayWindowPropertiesRepository
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import java.util.concurrent.ConcurrentHashMap
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineName
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/** Store that allows to retrieve per display instances of [StatusBarWindowController]. */
+interface StatusBarWindowControllerStore {
+ /**
+ * The instance for the default/main display of the device. For example, on a phone or a tablet,
+ * the default display is the internal/built-in display of the device.
+ *
+ * Note that the id of the default display is [Display.DEFAULT_DISPLAY].
+ */
+ val defaultDisplay: StatusBarWindowController
+
+ /**
+ * Returns an instance for a specific display id.
+ *
+ * @throws IllegalArgumentException if [displayId] doesn't match the id of any existing
+ * displays.
+ */
+ fun forDisplay(displayId: Int): StatusBarWindowController
+}
+
+@SysUISingleton
+class MultiDisplayStatusBarWindowControllerStore
+@Inject
+constructor(
+ @Background private val backgroundApplicationScope: CoroutineScope,
+ private val controllerFactory: StatusBarWindowController.Factory,
+ private val displayWindowPropertiesRepository: DisplayWindowPropertiesRepository,
+ private val viewCaptureAwareWindowManagerFactory: ViewCaptureAwareWindowManager.Factory,
+ private val displayRepository: DisplayRepository,
+) : StatusBarWindowControllerStore, CoreStartable {
+
+ init {
+ StatusBarConnectedDisplays.assertInNewMode()
+ }
+
+ private val perDisplayControllers = ConcurrentHashMap<Int, StatusBarWindowController>()
+
+ override fun start() {
+ backgroundApplicationScope.launch(CoroutineName("StatusBarWindowController#start")) {
+ displayRepository.displayRemovalEvent.collect { displayId ->
+ perDisplayControllers.remove(displayId)
+ }
+ }
+ }
+
+ override val defaultDisplay: StatusBarWindowController
+ get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+ override fun forDisplay(displayId: Int): StatusBarWindowController {
+ if (displayRepository.getDisplay(displayId) == null) {
+ throw IllegalArgumentException("Display with id $displayId doesn't exist.")
+ }
+ return perDisplayControllers.computeIfAbsent(displayId) {
+ createControllerForDisplay(displayId)
+ }
+ }
+
+ private fun createControllerForDisplay(displayId: Int): StatusBarWindowController {
+ val statusBarDisplayContext =
+ displayWindowPropertiesRepository.get(
+ displayId = displayId,
+ windowType = WindowManager.LayoutParams.TYPE_STATUS_BAR,
+ )
+ val viewCaptureAwareWindowManager =
+ viewCaptureAwareWindowManagerFactory.create(statusBarDisplayContext.windowManager)
+ return controllerFactory.create(
+ statusBarDisplayContext.context,
+ viewCaptureAwareWindowManager,
+ )
+ }
+}
+
+@SysUISingleton
+class SingleDisplayStatusBarWindowControllerStore
+@Inject
+constructor(private val controller: StatusBarWindowController) : StatusBarWindowControllerStore {
+
+ init {
+ StatusBarConnectedDisplays.assertInLegacyMode()
+ }
+
+ override val defaultDisplay = controller
+
+ override fun forDisplay(displayId: Int) = controller
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
index bfc5429..6879a34 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/BackGestureTutorialScreen.kt
@@ -27,10 +27,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.BackGestureMonitor
@Composable
-fun BackGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun BackGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -39,18 +36,20 @@
titleResId = R.string.touchpad_back_gesture_action_title,
bodyResId = R.string.touchpad_back_gesture_guidance,
titleSuccessResId = R.string.touchpad_back_gesture_success_title,
- bodySuccessResId = R.string.touchpad_back_gesture_success_body
+ bodySuccessResId = R.string.touchpad_back_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_back_edu,
- successResId = R.raw.trackpad_back_success
- )
+ successResId = R.raw.trackpad_back_success,
+ ),
)
val gestureMonitorProvider =
DistanceBasedGestureMonitorProvider(
monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- BackGestureMonitor(distanceThresholdPx, gestureStateCallback)
+ BackGestureMonitor(distanceThresholdPx).also {
+ it.addGestureStateCallback(gestureStateCallback)
+ }
}
)
GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -67,7 +66,7 @@
rememberColorFilterProperty(".tertiaryFixedDim", tertiaryFixedDim),
rememberColorFilterProperty(".onTertiaryFixed", onTertiaryFixed),
rememberColorFilterProperty(".onTertiary", onTertiary),
- rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant)
+ rememberColorFilterProperty(".onTertiaryFixedVariant", onTertiaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
index f2fec5f..a55fa44 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/HomeGestureTutorialScreen.kt
@@ -26,10 +26,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.HomeGestureMonitor
@Composable
-fun HomeGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun HomeGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -38,18 +35,20 @@
titleResId = R.string.touchpad_home_gesture_action_title,
bodyResId = R.string.touchpad_home_gesture_guidance,
titleSuccessResId = R.string.touchpad_home_gesture_success_title,
- bodySuccessResId = R.string.touchpad_home_gesture_success_body
+ bodySuccessResId = R.string.touchpad_home_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_home_edu,
- successResId = R.raw.trackpad_home_success
- )
+ successResId = R.raw.trackpad_home_success,
+ ),
)
val gestureMonitorProvider =
DistanceBasedGestureMonitorProvider(
monitorFactory = { distanceThresholdPx, gestureStateCallback ->
- HomeGestureMonitor(distanceThresholdPx, gestureStateCallback)
+ HomeGestureMonitor(distanceThresholdPx).also {
+ it.addGestureStateCallback(gestureStateCallback)
+ }
}
)
GestureTutorialScreen(screenConfig, gestureMonitorProvider, onDoneButtonClicked, onBack)
@@ -64,7 +63,7 @@
rememberLottieDynamicProperties(
rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
rememberColorFilterProperty(".onPrimaryFixed", onPrimaryFixed),
- rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant)
+ rememberColorFilterProperty(".onPrimaryFixedVariant", onPrimaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
index b2fb6cd..6ee15aa 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/RecentAppsGestureTutorialScreen.kt
@@ -29,10 +29,7 @@
import com.android.systemui.touchpad.tutorial.ui.gesture.TouchpadGestureMonitor
@Composable
-fun RecentAppsGestureTutorialScreen(
- onDoneButtonClicked: () -> Unit,
- onBack: () -> Unit,
-) {
+fun RecentAppsGestureTutorialScreen(onDoneButtonClicked: () -> Unit, onBack: () -> Unit) {
val screenConfig =
TutorialScreenConfig(
colors = rememberScreenColors(),
@@ -41,20 +38,20 @@
titleResId = R.string.touchpad_recent_apps_gesture_action_title,
bodyResId = R.string.touchpad_recent_apps_gesture_guidance,
titleSuccessResId = R.string.touchpad_recent_apps_gesture_success_title,
- bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body
+ bodySuccessResId = R.string.touchpad_recent_apps_gesture_success_body,
),
animations =
TutorialScreenConfig.Animations(
educationResId = R.raw.trackpad_recent_apps_edu,
- successResId = R.raw.trackpad_recent_apps_success
- )
+ successResId = R.raw.trackpad_recent_apps_success,
+ ),
)
val gestureMonitorProvider =
object : GestureMonitorProvider {
@Composable
override fun rememberGestureMonitor(
resources: Resources,
- gestureStateChangedCallback: (GestureState) -> Unit
+ gestureStateChangedCallback: (GestureState) -> Unit,
): TouchpadGestureMonitor {
val distanceThresholdPx =
resources.getDimensionPixelSize(
@@ -63,11 +60,9 @@
val velocityThresholdPxPerMs =
resources.getDimension(R.dimen.touchpad_recent_apps_gesture_velocity_threshold)
return remember(distanceThresholdPx, velocityThresholdPxPerMs) {
- RecentAppsGestureMonitor(
- distanceThresholdPx,
- gestureStateChangedCallback,
- velocityThresholdPxPerMs
- )
+ RecentAppsGestureMonitor(distanceThresholdPx, velocityThresholdPxPerMs).also {
+ it.addGestureStateCallback(gestureStateChangedCallback)
+ }
}
}
}
@@ -83,7 +78,7 @@
rememberLottieDynamicProperties(
rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
- rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+ rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant),
)
val screenColors =
remember(dynamicProperties) {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
index ecb5574..490f04d 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/BackGestureMonitor.kt
@@ -20,18 +20,21 @@
import kotlin.math.abs
/** Monitors for touchpad back gesture, that is three fingers swiping left or right */
-class BackGestureMonitor(
- private val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
- private val distanceTracker = DistanceTracker()
+class BackGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
- override fun processTouchpadEvent(event: MotionEvent) {
+ private val distanceTracker = DistanceTracker()
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
+ }
+
+ override fun accept(event: MotionEvent) {
if (!isThreeFingerTouchpadSwipe(event)) return
- val distanceState = distanceTracker.processEvent(event)
- updateGestureStateBasedOnDistance(
+ val gestureState = distanceTracker.processEvent(event)
+ updateGestureState(
gestureStateChangedCallback,
- distanceState,
+ gestureState,
isFinished = { abs(it.deltaX) >= gestureDistanceThresholdPx },
progress = { 0f },
)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
index 70d9366..d482358 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/DistanceTracker.kt
@@ -38,10 +38,10 @@
}
}
-sealed interface DistanceGestureState
+sealed class DistanceGestureState(val deltaX: Float, val deltaY: Float)
-class Started(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Started(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
-class Moving(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Moving(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
-class Finished(val deltaX: Float, val deltaY: Float) : DistanceGestureState
+class Finished(deltaX: Float, deltaY: Float) : DistanceGestureState(deltaX, deltaY)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
index c1caeb3..f194677 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/GestureStateUpdates.kt
@@ -16,11 +16,8 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
-/**
- * Helper function for gesture recognizers to have common state triggering logic based on distance
- * only.
- */
-inline fun updateGestureStateBasedOnDistance(
+/** Helper function for gesture recognizers to have common state triggering logic */
+inline fun updateGestureState(
gestureStateChangedCallback: (GestureState) -> Unit,
gestureState: DistanceGestureState?,
isFinished: (Finished) -> Boolean,
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
index fdcf9de..83d4f56 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/HomeGestureMonitor.kt
@@ -19,18 +19,21 @@
import android.view.MotionEvent
/** Monitors for touchpad home gesture, that is three fingers swiping up */
-class HomeGestureMonitor(
- private val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit,
-) : TouchpadGestureMonitor {
- private val distanceTracker = DistanceTracker()
+class HomeGestureMonitor(private val gestureDistanceThresholdPx: Int) : TouchpadGestureMonitor {
- override fun processTouchpadEvent(event: MotionEvent) {
+ private val distanceTracker = DistanceTracker()
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
+
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
+ }
+
+ override fun accept(event: MotionEvent) {
if (!isThreeFingerTouchpadSwipe(event)) return
- val distanceState = distanceTracker.processEvent(event)
- updateGestureStateBasedOnDistance(
+ val gestureState = distanceTracker.processEvent(event)
+ updateGestureState(
gestureStateChangedCallback,
- distanceState,
+ gestureState,
isFinished = { -it.deltaY >= gestureDistanceThresholdPx },
progress = { 0f },
)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
index dd31ce3..1731bb8 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/RecentAppsGestureMonitor.kt
@@ -17,7 +17,6 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
-import androidx.compose.ui.input.pointer.util.VelocityTracker1D
import kotlin.math.abs
/**
@@ -27,45 +26,30 @@
*/
class RecentAppsGestureMonitor(
private val gestureDistanceThresholdPx: Int,
- override val gestureStateChangedCallback: (GestureState) -> Unit,
private val velocityThresholdPxPerMs: Float,
- private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false),
+ private val distanceTracker: DistanceTracker = DistanceTracker(),
+ private val velocityTracker: VerticalVelocityTracker = VerticalVelocityTracker(),
) : TouchpadGestureMonitor {
- private var xStart = 0f
- private var yStart = 0f
+ private var gestureStateChangedCallback: (GestureState) -> Unit = {}
- override fun processTouchpadEvent(event: MotionEvent) {
- val action = event.actionMasked
- velocityTracker.addDataPoint(event.eventTime, event.y)
- when (action) {
- MotionEvent.ACTION_DOWN -> {
- if (isThreeFingerTouchpadSwipe(event)) {
- xStart = event.x
- yStart = event.y
- gestureStateChangedCallback(GestureState.InProgress())
- }
- }
- MotionEvent.ACTION_UP -> {
- if (isThreeFingerTouchpadSwipe(event) && isRecentAppsGesture(event)) {
- gestureStateChangedCallback(GestureState.Finished)
- } else {
- gestureStateChangedCallback(GestureState.NotStarted)
- }
- velocityTracker.resetTracking()
- }
- MotionEvent.ACTION_CANCEL -> {
- velocityTracker.resetTracking()
- }
- }
+ override fun addGestureStateCallback(callback: (GestureState) -> Unit) {
+ gestureStateChangedCallback = callback
}
- private fun isRecentAppsGesture(event: MotionEvent): Boolean {
- // below is trying to mirror behavior of TriggerSwipeUpTouchTracker#onGestureEnd.
- // We're diving velocity by 1000, to have the same unit of measure: pixels/ms.
- val swipeDistance = yStart - event.y
- val velocity = velocityTracker.calculateVelocity() / 1000
- return swipeDistance >= gestureDistanceThresholdPx &&
- abs(velocity) <= velocityThresholdPxPerMs
+ override fun accept(event: MotionEvent) {
+ if (!isThreeFingerTouchpadSwipe(event)) return
+ val gestureState = distanceTracker.processEvent(event)
+ velocityTracker.accept(event)
+
+ updateGestureState(
+ gestureStateChangedCallback,
+ gestureState,
+ isFinished = { state ->
+ -state.deltaY >= gestureDistanceThresholdPx &&
+ abs(velocityTracker.calculateVelocity().value) <= velocityThresholdPxPerMs
+ },
+ progress = { 0f },
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
index 88671d4..4b82ba1 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureHandler.kt
@@ -18,13 +18,14 @@
import android.view.InputDevice
import android.view.MotionEvent
+import java.util.function.Consumer
/**
* Allows listening to touchpadGesture and calling onDone when gesture was triggered. Can have all
* motion events passed to [onMotionEvent] and will filter touchpad events accordingly
*/
class TouchpadGestureHandler(
- private val gestureMonitor: TouchpadGestureMonitor,
+ private val gestureMonitor: Consumer<MotionEvent>,
private val easterEggGestureMonitor: EasterEggGestureMonitor,
) {
@@ -40,7 +41,7 @@
if (isTwoFingerSwipe(event)) {
easterEggGestureMonitor.processTouchpadEvent(event)
} else {
- gestureMonitor.processTouchpadEvent(event)
+ gestureMonitor.accept(event)
}
true
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
index 4655c98..9216821 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/TouchpadGestureMonitor.kt
@@ -17,15 +17,11 @@
package com.android.systemui.touchpad.tutorial.ui.gesture
import android.view.MotionEvent
+import java.util.function.Consumer
-/**
- * Monitor for touchpad gestures that calls [gestureStateChangedCallback] when [GestureState]
- * changes. All tracked motion events should be passed to [processTouchpadEvent]
- */
-interface TouchpadGestureMonitor {
- val gestureStateChangedCallback: (GestureState) -> Unit
-
- fun processTouchpadEvent(event: MotionEvent)
+/** Monitor for touchpad gestures that can notify callback when [GestureState] changes. */
+interface TouchpadGestureMonitor : Consumer<MotionEvent> {
+ fun addGestureStateCallback(callback: (GestureState) -> Unit)
}
fun isThreeFingerTouchpadSwipe(event: MotionEvent) = isNFingerTouchpadSwipe(event, fingerCount = 3)
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
new file mode 100644
index 0000000..9b38eca
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/gesture/VelocityTracker.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.touchpad.tutorial.ui.gesture
+
+import android.view.MotionEvent
+import androidx.compose.ui.input.pointer.util.VelocityTracker1D
+import java.util.function.Consumer
+
+/** Velocity in pixels/ms. */
+@JvmInline value class Velocity(val value: Float)
+
+/**
+ * Tracks velocity for processed MotionEvents. Useful for recognizing gestures based on velocity.
+ */
+interface VelocityTracker : Consumer<MotionEvent> {
+
+ fun calculateVelocity(): Velocity
+}
+
+class VerticalVelocityTracker(
+ private val velocityTracker: VelocityTracker1D = VelocityTracker1D(isDataDifferential = false)
+) : VelocityTracker {
+
+ override fun accept(event: MotionEvent) {
+ val action = event.actionMasked
+ if (action == MotionEvent.ACTION_DOWN) {
+ velocityTracker.resetTracking()
+ }
+ velocityTracker.addDataPoint(event.eventTime, event.y)
+ }
+
+ /**
+ * Calculates velocity on demand - this calculation can be expensive so shouldn't be called
+ * after every event.
+ */
+ override fun calculateVelocity() = Velocity(velocityTracker.calculateVelocity() / 1000)
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
index 1f92bc1..bbd8f3dc 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java
@@ -59,7 +59,6 @@
import androidx.annotation.NonNull;
import androidx.annotation.Nullable;
-import androidx.annotation.VisibleForTesting;
import androidx.lifecycle.Observer;
import com.android.internal.annotations.GuardedBy;
@@ -110,8 +109,8 @@
// It is safe to use 99 as the broadcast stream now. There are only 10+ default audio
// streams defined in AudioSystem for now and audio team is in the middle of restructure,
// no new default stream is preferred.
- @VisibleForTesting static final int DYNAMIC_STREAM_BROADCAST = 99;
- private static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
+ public static final int DYNAMIC_STREAM_BROADCAST = 99;
+ public static final int DYNAMIC_STREAM_REMOTE_START_INDEX = 100;
private static final AudioAttributes SONIFICIATION_VIBRATION_ATTRIBUTES =
new AudioAttributes.Builder()
.setContentType(AudioAttributes.CONTENT_TYPE_SONIFICATION)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
index 7166428..7c5116d 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogImpl.java
@@ -537,7 +537,7 @@
mWindow.setAttributes(lp);
mWindow.setLayout(WRAP_CONTENT, WRAP_CONTENT);
- mDialog.setContentView(R.layout.volume_dialog);
+ mDialog.setContentView(R.layout.volume_dialog_legacy);
mDialogView = mDialog.findViewById(R.id.volume_dialog);
mDialogView.setAlpha(0);
mDialogTimeoutMillis = mSecureSettings.get().getInt(
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
index f1443e3..500cc0b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStateModel.kt
@@ -23,7 +23,7 @@
/** Models a state of the Volume Dialog. */
data class VolumeDialogStateModel(
- val states: Map<Int, VolumeDialogStreamStateModel>,
+ val states: Map<Int, VolumeDialogStreamModel>,
val ringerModeInternal: Int = 0,
val ringerModeExternal: Int = 0,
val zenMode: Int = 0,
@@ -39,7 +39,7 @@
constructor(
legacyState: VolumeDialogController.State
) : this(
- states = legacyState.states.mapToMap { VolumeDialogStreamStateModel(it) },
+ states = legacyState.states.mapToMap { VolumeDialogStreamModel(it) },
ringerModeInternal = legacyState.ringerModeInternal,
ringerModeExternal = legacyState.ringerModeExternal,
zenMode = legacyState.zenMode,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
similarity index 92%
rename from packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
rename to packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
index a9d367d..26c96ea 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamStateModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/domain/model/VolumeDialogStreamModel.kt
@@ -16,18 +16,18 @@
package com.android.systemui.volume.dialog.domain.model
-import android.annotation.IntegerRes
+import androidx.annotation.StringRes
import com.android.systemui.plugins.VolumeDialogController
/** Models a state of an audio stream of the Volume Dialog. */
-data class VolumeDialogStreamStateModel(
+data class VolumeDialogStreamModel(
val isDynamic: Boolean = false,
val level: Int = 0,
val levelMin: Int = 0,
val levelMax: Int = 0,
val muted: Boolean = false,
val muteSupported: Boolean = false,
- @IntegerRes val name: Int = 0,
+ @StringRes val name: Int = 0,
val remoteLabel: String? = null,
val routedToBluetooth: Boolean = false,
) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
index ba08876..b2f6cb3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/settings/ui/binder/VolumeDialogSettingsButtonViewBinder.kt
@@ -34,7 +34,7 @@
fun bind(view: View) {
with(view) {
- val button = requireViewById<View>(R.id.settings)
+ val button = requireViewById<View>(R.id.volume_dialog_settings)
repeatWhenAttached {
viewModel(
traceName = "VolumeDialogViewBinder",
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
new file mode 100644
index 0000000..81507ba
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSliderInteractor.kt
@@ -0,0 +1,54 @@
+/*
+ * 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.dialog.sliders.domain.interactor
+
+import com.android.systemui.plugins.VolumeDialogController
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.mapNotNull
+
+/** Operates a state of particular slider of the Volume Dialog. */
+class VolumeDialogSliderInteractor
+@AssistedInject
+constructor(
+ @Assisted private val sliderType: VolumeDialogSliderType,
+ volumeDialogStateInteractor: VolumeDialogStateInteractor,
+ private val volumeDialogController: VolumeDialogController,
+) {
+
+ val slider: Flow<VolumeDialogStreamModel> =
+ volumeDialogStateInteractor.volumeDialogState.mapNotNull {
+ it.states[sliderType.audioStream]
+ }
+
+ fun setStreamVolume(userLevel: Int) {
+ volumeDialogController.setStreamVolume(sliderType.audioStream, userLevel)
+ }
+
+ @VolumeDialogScope
+ @AssistedFactory
+ interface Factory {
+
+ fun create(sliderType: VolumeDialogSliderType): VolumeDialogSliderInteractor
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
new file mode 100644
index 0000000..325e4c95
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/interactor/VolumeDialogSlidersInteractor.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.dialog.sliders.domain.interactor
+
+import com.android.systemui.volume.VolumeDialogControllerImpl
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.domain.interactor.VolumeDialogStateInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSlidersModel
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.map
+
+/** Provides a state for the Sliders section of the Volume Dialog. */
+@VolumeDialogScope
+class VolumeDialogSlidersInteractor
+@Inject
+constructor(volumeDialogStateInteractor: VolumeDialogStateInteractor) {
+
+ val sliders: Flow<VolumeDialogSlidersModel> =
+ volumeDialogStateInteractor.volumeDialogState.map {
+ val sliderTypes: List<VolumeDialogSliderType> =
+ it.states.keys.sortedWith(StreamsSorter).map { audioStream ->
+ when {
+ audioStream == VolumeDialogControllerImpl.DYNAMIC_STREAM_BROADCAST ->
+ VolumeDialogSliderType.AudioSharingStream(audioStream)
+ audioStream >=
+ VolumeDialogControllerImpl.DYNAMIC_STREAM_REMOTE_START_INDEX ->
+ VolumeDialogSliderType.RemoteMediaStream(audioStream)
+ else -> VolumeDialogSliderType.Stream(audioStream)
+ }
+ }
+ VolumeDialogSlidersModel(
+ slider = sliderTypes.first(),
+ floatingSliders = sliderTypes.drop(1),
+ )
+ }
+
+ private object StreamsSorter : Comparator<Int> {
+
+ // TODO(b/369992924) order the streams
+ override fun compare(lhs: Int, rhs: Int): Int {
+ return lhs - rhs
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.kt
new file mode 100644
index 0000000..18a2689
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSliderType.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.dialog.sliders.domain.model
+
+/** Models different possible audio sliders shown in the Volume Dialog. */
+sealed interface VolumeDialogSliderType {
+
+ // VolumeDialogController uses the same model for every slider type. We need to follow the same
+ // logic until we refactor and decouple data and domain layers from the VolumeDialogController
+ // into separated interactors.
+ val audioStream: Int
+
+ class Stream(override val audioStream: Int) : VolumeDialogSliderType
+
+ class RemoteMediaStream(override val audioStream: Int) : VolumeDialogSliderType
+
+ class AudioSharingStream(override val audioStream: Int) : VolumeDialogSliderType
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt
new file mode 100644
index 0000000..91a3328
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/domain/model/VolumeDialogSlidersModel.kt
@@ -0,0 +1,23 @@
+/*
+ * 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.dialog.sliders.domain.model
+
+/** Models a state of the sliders section of the Volume Dialog. */
+data class VolumeDialogSlidersModel(
+ val slider: VolumeDialogSliderType,
+ val floatingSliders: List<VolumeDialogSliderType>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
new file mode 100644
index 0000000..25a5f28
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSliderViewBinder.kt
@@ -0,0 +1,60 @@
+/*
+ * 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.dialog.sliders.ui
+
+import android.view.View
+import androidx.lifecycle.viewmodel.compose.viewModel
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSliderViewModel
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.awaitCancellation
+
+class VolumeDialogSliderViewBinder
+@AssistedInject
+constructor(@Assisted private val viewModelProvider: () -> VolumeDialogSliderViewModel) {
+
+ fun bind(view: View) {
+ with(view) {
+ repeatWhenAttached {
+ viewModel(
+ traceName = "VolumeDialogSliderViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { viewModelProvider() },
+ ) { viewModel ->
+ setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+ }
+
+ @AssistedFactory
+ @VolumeDialogScope
+ interface Factory {
+
+ fun create(
+ viewModelProvider: () -> VolumeDialogSliderViewModel
+ ): VolumeDialogSliderViewBinder
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
new file mode 100644
index 0000000..0a00f70
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/VolumeDialogSlidersViewBinder.kt
@@ -0,0 +1,49 @@
+/*
+ * 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.dialog.sliders.ui
+
+import android.view.View
+import com.android.systemui.lifecycle.WindowLifecycleState
+import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.setSnapshotBinding
+import com.android.systemui.lifecycle.viewModel
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialogScope
+import com.android.systemui.volume.dialog.sliders.ui.viewmodel.VolumeDialogSlidersViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.awaitCancellation
+
+@VolumeDialogScope
+class VolumeDialogSlidersViewBinder
+@Inject
+constructor(private val viewModelFactory: VolumeDialogSlidersViewModel.Factory) {
+
+ fun bind(view: View) {
+ with(view) {
+ repeatWhenAttached {
+ viewModel(
+ traceName = "VolumeDialogSlidersViewBinder",
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { viewModelFactory.create() },
+ ) { viewModel ->
+ setSnapshotBinding {}
+
+ awaitCancellation()
+ }
+ }
+ }
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
new file mode 100644
index 0000000..27b8f2f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSliderViewModel.kt
@@ -0,0 +1,41 @@
+/*
+ * 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.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.domain.model.VolumeDialogStreamModel
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import dagger.assisted.Assisted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.Flow
+
+class VolumeDialogSliderViewModel
+@AssistedInject
+constructor(@Assisted private val interactor: VolumeDialogSliderInteractor) {
+
+ val model: Flow<VolumeDialogStreamModel> = interactor.slider
+
+ fun setStreamVolume(volume: Int) {
+ interactor.setStreamVolume(volume)
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(interactor: VolumeDialogSliderInteractor): VolumeDialogSliderViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
new file mode 100644
index 0000000..b5b292f
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/sliders/ui/viewmodel/VolumeDialogSlidersViewModel.kt
@@ -0,0 +1,73 @@
+/*
+ * 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.dialog.sliders.ui.viewmodel
+
+import com.android.systemui.volume.dialog.dagger.scope.VolumeDialog
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSliderInteractor
+import com.android.systemui.volume.dialog.sliders.domain.interactor.VolumeDialogSlidersInteractor
+import com.android.systemui.volume.dialog.sliders.domain.model.VolumeDialogSliderType
+import com.android.systemui.volume.dialog.sliders.ui.VolumeDialogSliderViewBinder
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.SharingStarted
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.stateIn
+
+class VolumeDialogSlidersViewModel
+@AssistedInject
+constructor(
+ @VolumeDialog coroutineScope: CoroutineScope,
+ private val slidersInteractor: VolumeDialogSlidersInteractor,
+ private val sliderInteractorFactory: VolumeDialogSliderInteractor.Factory,
+ private val sliderViewModelFactory: VolumeDialogSliderViewModel.Factory,
+ private val sliderViewBinderFactory: VolumeDialogSliderViewBinder.Factory,
+) {
+
+ val sliders: Flow<VolumeDialogSliderUiModel> =
+ slidersInteractor.sliders
+ .distinctUntilChanged()
+ .map { slidersModel ->
+ VolumeDialogSliderUiModel(
+ sliderViewBinder = createSliderViewBinder(slidersModel.slider),
+ floatingSliderViewBinders =
+ slidersModel.floatingSliders.map(::createSliderViewBinder),
+ )
+ }
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ .filterNotNull()
+
+ private fun createSliderViewBinder(type: VolumeDialogSliderType): VolumeDialogSliderViewBinder =
+ sliderViewBinderFactory.create {
+ sliderViewModelFactory.create(sliderInteractorFactory.create(type))
+ }
+
+ @AssistedFactory
+ interface Factory {
+
+ fun create(): VolumeDialogSlidersViewModel
+ }
+}
+
+/** Models slider ui */
+data class VolumeDialogSliderUiModel(
+ val sliderViewBinder: VolumeDialogSliderViewBinder,
+ val floatingSliderViewBinders: List<VolumeDialogSliderViewBinder>,
+)
diff --git a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
index 9452d8c..77733fe 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/dialog/ui/binder/VolumeDialogBinder.kt
@@ -50,7 +50,7 @@
dialog.setContentView(R.layout.volume_dialog)
dialog.setCanceledOnTouchOutside(true)
- settingsButtonViewBinder.bind(dialog.requireViewById(R.id.settings_container))
+ settingsButtonViewBinder.bind(dialog.requireViewById(R.id.volume_dialog_settings))
volumeDialogViewBinder.bind(
dialog,
dialog.requireViewById(R.id.volume_dialog_container),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/biometrics/UdfpsViewTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
new file mode 100644
index 0000000..655b2cc
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelTest.kt
@@ -0,0 +1,185 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.android.systemui.util.mockito.whenever
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingButtonViewModelTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val bluetoothState = MutableStateFlow(false)
+ private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
+ @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
+ @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
+ @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
+ @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
+ @Mock private lateinit var deviceItem: DeviceItem
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var audioSharingButtonViewModel: AudioSharingButtonViewModel
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+ whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
+ whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
+ audioSharingButtonViewModel =
+ AudioSharingButtonViewModel(
+ localBluetoothManager,
+ kosmos.audioSharingInteractor,
+ bluetoothStateInteractor,
+ deviceItemInteractor,
+ )
+ audioSharingButtonViewModel.activateIn(testScope)
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ fun testButtonStateUpdate_bluetoothOff_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_noDevice_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothState.value = true
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf())
+ runCurrent()
+ kosmos.bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ runCurrent()
+
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
+ isActive = true,
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_hasSource_returnGone() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ )
+ .thenReturn(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ }
+ }
+
+ @Test
+ fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
+ testScope.runTest {
+ val actual by
+ collectLastValue(audioSharingButtonViewModel.audioSharingButtonStateUpdate)
+ kosmos.bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ cachedBluetoothDevice,
+ localBluetoothManager,
+ )
+ )
+ .thenReturn(false)
+ whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
+ bluetoothState.value = true
+ runCurrent()
+ deviceItemUpdate.emit(listOf(deviceItem))
+ runCurrent()
+
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingButtonState.Visible(
+ R.string.quick_settings_bluetooth_audio_sharing_button,
+ isActive = false,
+ )
+ )
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
new file mode 100644
index 0000000..ce37eee
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorTest.kt
@@ -0,0 +1,193 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+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.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
+import com.android.dx.mockito.inline.extended.StaticMockitoSession
+import com.android.settingslib.bluetooth.BluetoothUtils
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.flags.Flags
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.plugins.activityStarter
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runTest
+import org.junit.After
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.Mockito
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.eq
+import org.mockito.kotlin.never
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDeviceItemActionInteractorTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ private lateinit var actionInteractorImpl: DeviceItemActionInteractor
+ private lateinit var mockitoSession: StaticMockitoSession
+ private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
+ private lateinit var connectedMediaDeviceItem: DeviceItem
+ @Mock private lateinit var dialog: SystemUIDialog
+ @Mock private lateinit var leAudioProfile: LeAudioProfile
+ @Mock private lateinit var bluetoothDevice: BluetoothDevice
+
+ @Before
+ fun setUp() {
+ mockitoSession =
+ mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
+ connectedMediaDeviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = null,
+ background = null,
+ )
+ connectedAudioSharingMediaDeviceItem =
+ DeviceItem(
+ type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
+ deviceName = DEVICE_NAME,
+ connectionSummary = DEVICE_CONNECTION_SUMMARY,
+ iconWithDescription = null,
+ background = null,
+ )
+ actionInteractorImpl = kosmos.audioSharingDeviceItemActionInteractorImpl
+ }
+
+ @After
+ fun tearDown() {
+ mockitoSession.finishMocking()
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+ fun testOnClick_connectedAudioSharingMediaDevice_flagOn_createDialog() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+ verify(dialogTransitionAnimator)
+ .showFromDialog(any(), any(), eq(null), anyBoolean())
+ }
+ }
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_AUDIO_SHARING_QS_DIALOG_IMPROVEMENT)
+ fun testOnClick_connectedAudioSharingMediaDevice_flagOff_shouldLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+ actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
+ verify(activityStarter)
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ verify(dialogTransitionAnimator, never())
+ .showFromDialog(any(), any(), eq(null), anyBoolean())
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+ .thenReturn(listOf(leAudioProfile))
+ whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ )
+ )
+ .thenReturn(true)
+
+ actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+ verify(activityStarter, Mockito.never())
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ }
+ }
+ }
+
+ @Test
+ fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
+ whenever(cachedBluetoothDevice.uiAccessibleProfiles)
+ .thenReturn(listOf(leAudioProfile))
+
+ whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
+ whenever(
+ BluetoothUtils.hasConnectedBroadcastSource(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.any(),
+ )
+ )
+ .thenReturn(false)
+
+ actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
+ verify(activityStarter)
+ .postStartActivityDismissingKeyguard(
+ ArgumentMatchers.any(),
+ ArgumentMatchers.anyInt(),
+ ArgumentMatchers.any(),
+ )
+ }
+ }
+ }
+
+ private companion object {
+ const val DEVICE_NAME = "device"
+ const val DEVICE_CONNECTION_SUMMARY = "active"
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
new file mode 100644
index 0000000..25b85b5
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogDelegateTest.kt
@@ -0,0 +1,118 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import android.widget.Button
+import android.widget.TextView
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.spy
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogDelegateTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
+ private val updateFlow = MutableSharedFlow<Unit>()
+ private lateinit var underTest: AudioSharingDialogDelegate
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ // TODO(b/364515243): use real object instead of mock
+ whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+ whenever(deviceItemInteractor.deviceItemUpdate)
+ .thenReturn(MutableStateFlow(emptyList()))
+ underTest = audioSharingDialogDelegate
+ }
+ }
+
+ @Test
+ fun testCreateDialog() =
+ kosmos.testScope.runTest {
+ val dialog = underTest.createDialog()
+ assertThat(dialog).isInstanceOf(SystemUIDialog::class.java)
+ }
+
+ @Test
+ fun testCreateDialog_showState() =
+ with(kosmos) {
+ testScope.runTest {
+ val availableDeviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(availableDeviceName)
+ val dialog = spy(underTest.createDialog())
+ dialog.show()
+ runCurrent()
+ val subtitleTextView = dialog.findViewById<TextView>(R.id.subtitle)
+ val switchActiveButton = dialog.findViewById<Button>(R.id.switch_active_button)
+ val shareAudioButton = dialog.findViewById<Button>(R.id.share_audio_button)
+ val subtitle =
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ availableDeviceName,
+ ""
+ )
+ val switchButtonText =
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ availableDeviceName
+ )
+ assertThat(subtitleTextView.text).isEqualTo(subtitle)
+ assertThat(switchActiveButton.text).isEqualTo(switchButtonText)
+ assertThat(switchActiveButton.hasOnClickListeners()).isTrue()
+ assertThat(shareAudioButton.hasOnClickListeners()).isTrue()
+
+ switchActiveButton.performClick()
+ verify(dialog).dismiss()
+ }
+ }
+
+ @Test
+ fun testCreateDialog_hideState() =
+ with(kosmos) {
+ testScope.runTest {
+ val dialog = spy(underTest.createDialog())
+ dialog.show()
+ runCurrent()
+ updateFlow.emit(Unit)
+ runCurrent()
+ verify(dialog).dismiss()
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
new file mode 100644
index 0000000..beb816c
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelTest.kt
@@ -0,0 +1,142 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LeAudioProfile
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.bluetooth.cachedBluetoothDeviceManager
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.res.R
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.whenever
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
+class AudioSharingDialogViewModelTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
+ @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+ @Mock private lateinit var leAudioProfile: LeAudioProfile
+ private val updateFlow = MutableSharedFlow<Unit>()
+ private lateinit var underTest: AudioSharingDialogViewModel
+
+ @Before
+ fun setUp() {
+ with(kosmos) {
+ // TODO(b/364515243): use real object instead of mock
+ whenever(deviceItemInteractor.deviceItemUpdateRequest).thenReturn(updateFlow)
+ whenever(deviceItemInteractor.deviceItemUpdate)
+ .thenReturn(MutableStateFlow(emptyList()))
+ underTest = audioSharingDialogViewModel
+ }
+ }
+
+ @Test
+ fun testDialogState_show() =
+ with(kosmos) {
+ testScope.runTest {
+ val deviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ deviceName,
+ ""
+ ),
+ context.getString(
+ R.string
+ .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ deviceName
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDialogState_showWithActiveDeviceName() =
+ with(kosmos) {
+ testScope.runTest {
+ val deviceName = "name"
+ whenever(cachedBluetoothDevice.name).thenReturn(deviceName)
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(localBluetoothManager.cachedDeviceManager)
+ .thenReturn(cachedBluetoothDeviceManager)
+ whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
+ whenever(leAudioProfile.activeDevices).thenReturn(listOf(mock<BluetoothDevice>()))
+ whenever(cachedBluetoothDeviceManager.findDevice(any()))
+ .thenReturn(cachedBluetoothDevice)
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ assertThat(actual)
+ .isEqualTo(
+ AudioSharingDialogState.Show(
+ context.getString(
+ R.string.quick_settings_bluetooth_audio_sharing_dialog_subtitle,
+ deviceName,
+ deviceName
+ ),
+ context.getString(
+ R.string
+ .quick_settings_bluetooth_audio_sharing_dialog_switch_to_button,
+ deviceName
+ )
+ )
+ )
+ }
+ }
+
+ @Test
+ fun testDialogState_hide() =
+ with(kosmos) {
+ testScope.runTest {
+ val actual by collectLastValue(underTest.dialogState)
+ runCurrent()
+ updateFlow.emit(Unit)
+ assertThat(actual).isEqualTo(AudioSharingDialogState.Hide)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
index 2c53fd6..25f9565 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorTest.kt
@@ -16,158 +16,197 @@
package com.android.systemui.bluetooth.qsdialog
+import android.bluetooth.BluetoothLeBroadcast
import android.testing.TestableLooper
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LocalBluetoothManager
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
import com.android.systemui.SysuiTestCase
import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.res.R
-import com.android.systemui.util.mockito.whenever
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableSharedFlow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
+import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.ArgumentCaptor
+import org.mockito.Captor
import org.mockito.Mock
+import org.mockito.Mockito.verify
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
-@ExperimentalCoroutinesApi
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
class AudioSharingInteractorTest : SysuiTestCase() {
- private val testDispatcher = UnconfinedTestDispatcher()
- private val testScope = TestScope(testDispatcher)
- private val bluetoothState = MutableStateFlow(false)
- private val deviceItemUpdate: MutableSharedFlow<List<DeviceItem>> = MutableSharedFlow()
- @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
- @Mock private lateinit var localBluetoothManager: LocalBluetoothManager
- @Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
- @Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
- @Mock private lateinit var deviceItem: DeviceItem
- private lateinit var mockitoSession: StaticMockitoSession
- private lateinit var audioSharingInteractor: AudioSharingInteractor
+ @get:Rule val mockito: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
+ @Mock private lateinit var localBluetoothLeBroadcast: LocalBluetoothLeBroadcast
+ @Captor private lateinit var callbackCaptor: ArgumentCaptor<BluetoothLeBroadcast.Callback>
+ private lateinit var underTest: AudioSharingInteractor
@Before
fun setUp() {
- mockitoSession =
- mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
- whenever(bluetoothStateInteractor.bluetoothStateUpdate).thenReturn(bluetoothState)
- whenever(deviceItemInteractor.deviceItemUpdate).thenReturn(deviceItemUpdate)
- audioSharingInteractor =
- AudioSharingInteractor(
- localBluetoothManager,
- bluetoothStateInteractor,
- deviceItemInteractor,
- testScope.backgroundScope,
- testDispatcher,
- )
- }
-
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
+ with(kosmos) { underTest = audioSharingInteractor }
}
@Test
- fun testButtonStateUpdate_bluetoothOff_returnGone() {
- testScope.runTest {
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
+ fun testIsAudioSharingOn_flagOff_false() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ assertThat(value).isFalse()
+ }
}
- }
@Test
- fun testButtonStateUpdate_noDevice_returnGone() {
- testScope.runTest {
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- runCurrent()
+ fun testIsAudioSharingOn_flagOn_notInAudioSharing_false() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ assertThat(value).isFalse()
+ }
}
- }
@Test
- fun testButtonStateUpdate_isBroadcasting_returnSharingAudio() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(true)
+ fun testIsAudioSharingOn_flagOn_inAudioSharing_true() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ val value by collectLastValue(underTest.isAudioSharingOn)
+ runCurrent()
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf())
- runCurrent()
+ assertThat(value).isTrue()
+ }
+ }
- assertThat(actual)
- .isEqualTo(
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button_sharing,
- isActive = true
- )
+ @Test
+ fun testAudioSourceStateUpdate_notInAudioSharing_returnEmpty() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ val value by collectLastValue(underTest.audioSourceStateUpdate)
+ runCurrent()
+
+ assertThat(value).isNull()
+ }
+ }
+
+ @Test
+ fun testAudioSourceStateUpdate_inAudioSharing_returnUnit() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ val value by collectLastValue(underTest.audioSourceStateUpdate)
+ runCurrent()
+ bluetoothTileDialogAudioSharingRepository.emitAudioSourceStateUpdate()
+ runCurrent()
+
+ assertThat(value).isNull()
+ }
+ }
+
+ @Test
+ fun testHandleAudioSourceWhenReady_flagOff_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(false)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
+ }
+
+ @Test
+ fun testHandleAudioSourceWhenReady_noProfile_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(null)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
+ }
+
+ @Test
+ fun testHandleAudioSourceWhenReady_hasProfileButAudioSharingOff_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(false)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
}
- }
@Test
- fun testButtonStateUpdate_hasSource_returnGone() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
- whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- cachedBluetoothDevice,
- localBluetoothManager
- )
+ fun testHandleAudioSourceWhenReady_audioSharingOnButNoPlayback_sourceNotAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
)
- .thenReturn(true)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+ verify(localBluetoothLeBroadcast)
+ .registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf(deviceItem))
- runCurrent()
-
- assertThat(actual).isEqualTo(AudioSharingButtonState.Gone)
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isFalse()
+ job.cancel()
+ }
}
- }
@Test
- fun testButtonStateUpdate_hasActiveDevice_returnAudioSharing() {
- testScope.runTest {
- whenever(BluetoothUtils.isBroadcasting(localBluetoothManager)).thenReturn(false)
- whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- cachedBluetoothDevice,
- localBluetoothManager
- )
+ fun testHandleAudioSourceWhenReady_audioSharingOnAndPlaybackStarts_sourceAdded() =
+ with(kosmos) {
+ testScope.runTest {
+ bluetoothTileDialogAudioSharingRepository.setInAudioSharing(true)
+ bluetoothTileDialogAudioSharingRepository.setAudioSharingAvailable(true)
+ bluetoothTileDialogAudioSharingRepository.setLeAudioBroadcastProfile(
+ localBluetoothLeBroadcast
)
- .thenReturn(false)
- whenever(BluetoothUtils.isActiveLeAudioDevice(cachedBluetoothDevice)).thenReturn(true)
+ val job = launch { underTest.handleAudioSourceWhenReady() }
+ runCurrent()
+ verify(localBluetoothLeBroadcast)
+ .registerServiceCallBack(any(), callbackCaptor.capture())
+ runCurrent()
+ callbackCaptor.value.onPlaybackStarted(0, 0)
+ runCurrent()
- val actual by collectLastValue(audioSharingInteractor.audioSharingButtonStateUpdate)
- bluetoothState.value = true
- deviceItemUpdate.emit(listOf(deviceItem))
- runCurrent()
-
- assertThat(actual)
- .isEqualTo(
- AudioSharingButtonState.Visible(
- R.string.quick_settings_bluetooth_audio_sharing_button,
- isActive = false
- )
- )
+ assertThat(bluetoothTileDialogAudioSharingRepository.sourceAdded).isTrue()
+ job.cancel()
+ }
}
- }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
new file mode 100644
index 0000000..c9e8813
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryTest.kt
@@ -0,0 +1,183 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.bluetooth.BluetoothDevice
+import android.bluetooth.BluetoothLeBroadcastMetadata
+import android.testing.AndroidTestingRunner
+import android.testing.TestableLooper
+import androidx.test.filters.SmallTest
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+import com.android.systemui.volume.data.repository.audioSharingRepository
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Rule
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.ArgumentMatchers.anyBoolean
+import org.mockito.Mock
+import org.mockito.junit.MockitoJUnit
+import org.mockito.junit.MockitoRule
+import org.mockito.kotlin.any
+import org.mockito.kotlin.never
+import org.mockito.kotlin.verify
+import org.mockito.kotlin.whenever
+
+@ExperimentalCoroutinesApi
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+class AudioSharingRepositoryTest : SysuiTestCase() {
+ @get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ @Mock private lateinit var profileManager: LocalBluetoothProfileManager
+ @Mock private lateinit var leAudioBroadcastProfile: LocalBluetoothLeBroadcast
+ @Mock private lateinit var leAudioBroadcastAssistant: LocalBluetoothLeBroadcastAssistant
+ @Mock private lateinit var metadata: BluetoothLeBroadcastMetadata
+ @Mock private lateinit var bluetoothDevice: BluetoothDevice
+ private val kosmos = testKosmos()
+ private lateinit var underTest: AudioSharingRepository
+
+ @Before
+ fun setUp() {
+ underTest =
+ AudioSharingRepositoryImpl(
+ kosmos.localBluetoothManager,
+ kosmos.audioSharingRepository,
+ kosmos.testDispatcher,
+ )
+ }
+
+ @Test
+ fun testSwitchActive() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(true)
+ underTest.setActive(cachedBluetoothDevice)
+ verify(cachedBluetoothDevice).setActive()
+ }
+ }
+
+ @Test
+ fun testSwitchActive_flagOff_doNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+ underTest.setActive(cachedBluetoothDevice)
+ verify(cachedBluetoothDevice, never()).setActive()
+ }
+ }
+
+ @Test
+ fun testStartAudioSharing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ underTest.startAudioSharing()
+ verify(leAudioBroadcastProfile).startPrivateBroadcast()
+ }
+ }
+
+ @Test
+ fun testStartAudioSharing_flagOff_doNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+ underTest.startAudioSharing()
+ verify(leAudioBroadcastProfile, never()).startPrivateBroadcast()
+ }
+ }
+
+ @Test
+ fun testAddSource_flagOff_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ audioSharingRepository.setAudioSharingAvailable(false)
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+ }
+ }
+
+ @Test
+ fun testAddSource_noMetadata_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(null)
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).allConnectedDevices
+ }
+ }
+
+ @Test
+ fun testAddSource_noConnectedDevice_doesNothing() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ whenever(profileManager.leAudioBroadcastAssistantProfile)
+ .thenReturn(leAudioBroadcastAssistant)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(metadata)
+ whenever(leAudioBroadcastAssistant.allConnectedDevices).thenReturn(emptyList())
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant, never()).addSource(any(), any(), anyBoolean())
+ }
+ }
+
+ @Test
+ fun testAddSource_hasConnectedDeviceAndMetadata_addSource() =
+ with(kosmos) {
+ testScope.runTest {
+ whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
+ whenever(profileManager.leAudioBroadcastProfile).thenReturn(leAudioBroadcastProfile)
+ whenever(profileManager.leAudioBroadcastAssistantProfile)
+ .thenReturn(leAudioBroadcastAssistant)
+ audioSharingRepository.setAudioSharingAvailable(true)
+ whenever(leAudioBroadcastProfile.latestBluetoothLeBroadcastMetadata)
+ .thenReturn(metadata)
+ whenever(leAudioBroadcastAssistant.allConnectedDevices)
+ .thenReturn(listOf(bluetoothDevice))
+
+ underTest.addSource()
+ runCurrent()
+
+ verify(leAudioBroadcastAssistant).addSource(bluetoothDevice, metadata, false)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
index d7bea66..a56c2cb 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/BluetoothTileDialogViewModelTest.kt
@@ -31,8 +31,11 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.animation.Expandable
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.android.systemui.testKosmos
import com.android.systemui.util.FakeSharedPreferences
import com.android.systemui.util.concurrency.FakeExecutor
import com.android.systemui.util.kotlin.getMutableStateFlow
@@ -42,12 +45,12 @@
import com.android.systemui.util.time.FakeSystemClock
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableSharedFlow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.test.TestCoroutineScheduler
import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.UnconfinedTestDispatcher
+import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Rule
@@ -64,10 +67,12 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@OptIn(ExperimentalCoroutinesApi::class)
@EnableFlags(Flags.FLAG_BLUETOOTH_QS_TILE_DIALOG_AUTO_ON_TOGGLE)
class BluetoothTileDialogViewModelTest : SysuiTestCase() {
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
+ private val kosmos = testKosmos()
private val fakeSystemClock = FakeSystemClock()
private val backgroundExecutor = FakeExecutor(fakeSystemClock)
@@ -75,8 +80,6 @@
@Mock private lateinit var bluetoothStateInteractor: BluetoothStateInteractor
- @Mock private lateinit var audioSharingInteractor: AudioSharingInteractor
-
@Mock private lateinit var bluetoothDeviceMetadataInteractor: BluetoothDeviceMetadataInteractor
@Mock private lateinit var deviceItemInteractor: DeviceItemInteractor
@@ -111,15 +114,15 @@
private val sharedPreferences = FakeSharedPreferences()
- private lateinit var scheduler: TestCoroutineScheduler
private lateinit var dispatcher: CoroutineDispatcher
private lateinit var testScope: TestScope
@Before
fun setUp() {
- scheduler = TestCoroutineScheduler()
- dispatcher = UnconfinedTestDispatcher(scheduler)
- testScope = TestScope(dispatcher)
+ dispatcher = kosmos.testDispatcher
+ testScope = kosmos.testScope
+ // TODO(b/364515243): use real object instead of mock
+ whenever(kosmos.deviceItemInteractor.deviceItemUpdate).thenReturn(MutableSharedFlow())
bluetoothTileDialogViewModel =
BluetoothTileDialogViewModel(
deviceItemInteractor,
@@ -139,11 +142,13 @@
dispatcher
)
),
- audioSharingInteractor,
+ kosmos.audioSharingInteractor,
+ kosmos.audioSharingButtonViewModelFactory,
bluetoothDeviceMetadataInteractor,
mDialogTransitionAnimator,
activityStarter,
uiEventLogger,
+ bluetoothTileDialogLogger,
testScope.backgroundScope,
dispatcher,
dispatcher,
@@ -161,13 +166,10 @@
whenever(sysuiDialog.context).thenReturn(mContext)
whenever(bluetoothTileDialogDelegate.bluetoothStateToggle)
.thenReturn(getMutableStateFlow(false))
- whenever(bluetoothTileDialogDelegate.deviceItemClick)
- .thenReturn(getMutableStateFlow(deviceItem))
+ whenever(bluetoothTileDialogDelegate.deviceItemClick).thenReturn(MutableSharedFlow())
whenever(bluetoothTileDialogDelegate.contentHeight).thenReturn(getMutableStateFlow(0))
whenever(bluetoothTileDialogDelegate.bluetoothAutoOnToggle)
.thenReturn(getMutableStateFlow(false))
- whenever(audioSharingInteractor.audioSharingButtonStateUpdate)
- .thenReturn(getMutableStateFlow(AudioSharingButtonState.Gone))
whenever(expandable.dialogTransitionController(any())).thenReturn(controller)
}
@@ -175,6 +177,7 @@
fun testShowDialog_noAnimation() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
verify(mDialogTransitionAnimator, never()).show(any(), any(), any())
}
@@ -184,6 +187,7 @@
fun testShowDialog_animated() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(expandable)
+ runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@@ -194,6 +198,7 @@
testScope.runTest {
backgroundExecutor.execute {
bluetoothTileDialogViewModel.showDialog(expandable)
+ runCurrent()
verify(mDialogTransitionAnimator).show(any(), any(), anyBoolean())
}
@@ -204,6 +209,7 @@
fun testShowDialog_fetchDeviceItem() {
testScope.runTest {
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
verify(deviceItemInteractor).deviceItemUpdate
}
@@ -214,6 +220,7 @@
testScope.runTest {
whenever(deviceItem.cachedBluetoothDevice).thenReturn(cachedBluetoothDevice)
bluetoothTileDialogViewModel.showDialog(null)
+ runCurrent()
val clickedView = View(context)
bluetoothTileDialogViewModel.onPairNewDeviceClicked(clickedView)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
index 681ea75..9c427c6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorTest.kt
@@ -15,34 +15,22 @@
*/
package com.android.systemui.bluetooth.qsdialog
-import android.bluetooth.BluetoothDevice
import android.testing.AndroidTestingRunner
import android.testing.TestableLooper
import androidx.test.filters.SmallTest
-import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
-import com.android.dx.mockito.inline.extended.StaticMockitoSession
-import com.android.settingslib.bluetooth.BluetoothUtils
-import com.android.settingslib.bluetooth.CachedBluetoothDevice
-import com.android.settingslib.bluetooth.LeAudioProfile
-import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcastAssistant
-import com.android.settingslib.bluetooth.LocalBluetoothProfileManager
import com.android.systemui.SysuiTestCase
import com.android.systemui.kosmos.testDispatcher
import com.android.systemui.kosmos.testScope
-import com.android.systemui.plugins.activityStarter
import com.android.systemui.statusbar.phone.SystemUIDialog
import com.android.systemui.testKosmos
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import org.junit.After
import org.junit.Before
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
-import org.mockito.ArgumentMatchers
import org.mockito.Mock
-import org.mockito.Mockito
import org.mockito.Mockito.verify
import org.mockito.junit.MockitoJUnit
import org.mockito.junit.MockitoRule
@@ -56,28 +44,18 @@
@get:Rule val mockitoRule: MockitoRule = MockitoJUnit.rule()
private val kosmos = testKosmos().apply { testDispatcher = UnconfinedTestDispatcher() }
private lateinit var actionInteractorImpl: DeviceItemActionInteractor
- private lateinit var mockitoSession: StaticMockitoSession
private lateinit var activeMediaDeviceItem: DeviceItem
private lateinit var notConnectedDeviceItem: DeviceItem
- private lateinit var connectedAudioSharingMediaDeviceItem: DeviceItem
private lateinit var connectedMediaDeviceItem: DeviceItem
private lateinit var connectedOtherDeviceItem: DeviceItem
@Mock private lateinit var dialog: SystemUIDialog
- @Mock private lateinit var profileManager: LocalBluetoothProfileManager
- @Mock private lateinit var leAudioProfile: LeAudioProfile
- @Mock private lateinit var assistantProfile: LocalBluetoothLeBroadcastAssistant
- @Mock private lateinit var bluetoothDevice: BluetoothDevice
- @Mock private lateinit var bluetoothDeviceGroupId2: BluetoothDevice
- @Mock private lateinit var cachedBluetoothDevice: CachedBluetoothDevice
@Before
fun setUp() {
- mockitoSession =
- mockitoSession().initMocks(this).mockStatic(BluetoothUtils::class.java).startMocking()
activeMediaDeviceItem =
DeviceItem(
type = DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -86,7 +64,7 @@
notConnectedDeviceItem =
DeviceItem(
type = DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -95,16 +73,7 @@
connectedMediaDeviceItem =
DeviceItem(
type = DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
- deviceName = DEVICE_NAME,
- connectionSummary = DEVICE_CONNECTION_SUMMARY,
- iconWithDescription = null,
- background = null
- )
- connectedAudioSharingMediaDeviceItem =
- DeviceItem(
- type = DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
@@ -113,18 +82,13 @@
connectedOtherDeviceItem =
DeviceItem(
type = DeviceItemType.CONNECTED_BLUETOOTH_DEVICE,
- cachedBluetoothDevice = cachedBluetoothDevice,
+ cachedBluetoothDevice = kosmos.cachedBluetoothDevice,
deviceName = DEVICE_NAME,
connectionSummary = DEVICE_CONNECTION_SUMMARY,
iconWithDescription = null,
background = null
)
- actionInteractorImpl = kosmos.deviceItemActionInteractor
- }
-
- @After
- fun tearDown() {
- mockitoSession.finishMocking()
+ actionInteractorImpl = kosmos.deviceItemActionInteractorImpl
}
@Test
@@ -132,14 +96,8 @@
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
verify(cachedBluetoothDevice).setActive()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.AVAILABLE_MEDIA_BLUETOOTH_DEVICE
- )
}
}
}
@@ -149,14 +107,8 @@
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
verify(cachedBluetoothDevice).disconnect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.ACTIVE_MEDIA_BLUETOOTH_DEVICE
- )
}
}
}
@@ -166,14 +118,8 @@
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(connectedOtherDeviceItem, dialog)
verify(cachedBluetoothDevice).disconnect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
- )
}
}
}
@@ -183,293 +129,8 @@
with(kosmos) {
testScope.runTest {
whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
verify(cachedBluetoothDevice).connect()
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.SAVED_BLUETOOTH_DEVICE
- )
- }
- }
- }
-
- @Test
- fun testOnClick_connectedAudioSharingMediaDevice_logClick() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- actionInteractorImpl.onClick(connectedAudioSharingMediaDeviceItem, dialog)
- verify(bluetoothTileDialogLogger)
- .logDeviceClick(
- cachedBluetoothDevice.address,
- DeviceItemType.AVAILABLE_AUDIO_SHARING_MEDIA_BLUETOOTH_DEVICE
- )
- }
- }
- }
-
- @Test
- fun testOnClick_audioSharingDisabled_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_inAudioSharing_clickedDeviceHasSource_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.uiAccessibleProfiles)
- .thenReturn(listOf(leAudioProfile))
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(true)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_inAudioSharing_clickedDeviceNoSource_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.uiAccessibleProfiles)
- .thenReturn(listOf(leAudioProfile))
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(BluetoothUtils.isBroadcasting(ArgumentMatchers.any())).thenReturn(true)
- whenever(
- BluetoothUtils.hasConnectedBroadcastSource(
- ArgumentMatchers.any(),
- ArgumentMatchers.any()
- )
- )
- .thenReturn(false)
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_noConnectedLeDevice_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedNonLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedLe_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
- whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasOneConnectedLeDevice_clickedConnectedLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice))
-
- actionInteractorImpl.onClick(connectedMediaDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasTwoConnectedLeDevice_clickedNotConnectedLe_shouldNotLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
- whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
- val device = it.arguments.first() as BluetoothDevice
- if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
- }
-
- actionInteractorImpl.onClick(notConnectedDeviceItem, dialog)
- verify(activityStarter, Mockito.never())
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
- }
- }
- }
-
- @Test
- fun testOnClick_hasTwoConnectedLeDevice_clickedActiveLe_shouldLaunchSettings() {
- with(kosmos) {
- testScope.runTest {
- whenever(cachedBluetoothDevice.device).thenReturn(bluetoothDevice)
- whenever(cachedBluetoothDevice.address).thenReturn(DEVICE_ADDRESS)
- whenever(cachedBluetoothDevice.profiles).thenReturn(listOf(leAudioProfile))
- whenever(leAudioProfile.isEnabled(ArgumentMatchers.any())).thenReturn(true)
-
- whenever(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
- whenever(localBluetoothManager.profileManager).thenReturn(profileManager)
- whenever(profileManager.leAudioProfile).thenReturn(leAudioProfile)
- whenever(profileManager.leAudioBroadcastAssistantProfile)
- .thenReturn(assistantProfile)
-
- whenever(
- assistantProfile.getDevicesMatchingConnectionStates(ArgumentMatchers.any())
- )
- .thenReturn(listOf(bluetoothDevice, bluetoothDeviceGroupId2))
- whenever(leAudioProfile.getGroupId(ArgumentMatchers.any())).thenAnswer {
- val device = it.arguments.first() as BluetoothDevice
- if (device == bluetoothDevice) GROUP_ID_1 else GROUP_ID_2
- }
-
- actionInteractorImpl.onClick(activeMediaDeviceItem, dialog)
- verify(activityStarter)
- .postStartActivityDismissingKeyguard(
- ArgumentMatchers.any(),
- ArgumentMatchers.anyInt(),
- ArgumentMatchers.any()
- )
}
}
}
@@ -478,7 +139,5 @@
const val DEVICE_NAME = "device"
const val DEVICE_CONNECTION_SUMMARY = "active"
const val DEVICE_ADDRESS = "address"
- const val GROUP_ID_1 = 1
- const val GROUP_ID_2 = 2
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
index ef441c1..10c3457 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemFactoryTest.kt
@@ -133,8 +133,8 @@
@Test
fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_flagOff_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
- // source or assistant.
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING off or the device doesn't support broadcast
+ // source or assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(false)
assertThat(
@@ -145,9 +145,9 @@
}
@Test
- fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isActiveDevice_false() {
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(true)
@@ -159,9 +159,9 @@
}
@Test
- fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_returnsFalse() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_isNotAvailable_false() {
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
`when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
@@ -177,8 +177,8 @@
@Test
fun testAvailableAudioSharingMediaDeviceItemFactory_isFilterMatched_returnsTrue() {
- // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
- // assistant.
+ // Flags.FLAG_ENABLE_LE_AUDIO_SHARING on and the device support broadcast source and
+ // assistant.
`when`(BluetoothUtils.isAudioSharingEnabled()).thenReturn(true)
`when`(BluetoothUtils.isActiveMediaDevice(any())).thenReturn(false)
`when`(BluetoothUtils.isAvailableMediaBluetoothDevice(any(), any())).thenReturn(true)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
index 194590c..c39b9a6 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemInteractorTest.kt
@@ -83,18 +83,6 @@
fun setUp() {
dispatcher = UnconfinedTestDispatcher()
testScope = TestScope(dispatcher)
- interactor =
- DeviceItemInteractor(
- bluetoothTileDialogRepository,
- audioManager,
- adapter,
- localBluetoothManager,
- fakeSystemClock,
- logger,
- testScope.backgroundScope,
- dispatcher
- )
-
`when`(deviceItem1.cachedBluetoothDevice).thenReturn(cachedDevice1)
`when`(deviceItem2.cachedBluetoothDevice).thenReturn(cachedDevice2)
`when`(cachedDevice1.address).thenReturn("ADDRESS")
@@ -108,9 +96,19 @@
fun testUpdateDeviceItems_noCachedDevice_returnEmpty() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(emptyList())
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -125,9 +123,19 @@
fun testUpdateDeviceItems_hasCachedDevice_filterNotMatch_returnEmpty() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ false }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ false }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -142,9 +150,19 @@
fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnDeviceItem() {
testScope.runTest {
`when`(bluetoothTileDialogRepository.cachedDevices).thenReturn(listOf(cachedDevice1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem1))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem1)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -159,9 +177,22 @@
fun testUpdateDeviceItems_hasCachedDevice_filterMatch_returnMultipleDeviceItem() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ false }, deviceItem1), createFactory({ true }, deviceItem2))
- )
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory({ false }, deviceItem1),
+ createFactory({ true }, deviceItem2)
+ ),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
@@ -176,18 +207,31 @@
fun testUpdateDeviceItems_sortByDisplayPriority() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(
- createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
- createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device1 },
+ deviceItem1
+ ),
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device2 },
+ deviceItem2
+ )
+ ),
+ listOf(
+ DeviceItemType.SAVED_BLUETOOTH_DEVICE,
+ DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
+ ),
+ testScope.backgroundScope,
+ dispatcher
)
- )
- interactor.setDisplayPriorityForTesting(
- listOf(
- DeviceItemType.SAVED_BLUETOOTH_DEVICE,
- DeviceItemType.CONNECTED_BLUETOOTH_DEVICE
- )
- )
`when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
`when`(deviceItem2.type).thenReturn(DeviceItemType.SAVED_BLUETOOTH_DEVICE)
@@ -204,15 +248,28 @@
fun testUpdateDeviceItems_sameType_sortByRecentlyConnected() {
testScope.runTest {
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(listOf(device2, device1))
- interactor.setDeviceItemFactoryListForTesting(
- listOf(
- createFactory({ cachedDevice -> cachedDevice.device == device1 }, deviceItem1),
- createFactory({ cachedDevice -> cachedDevice.device == device2 }, deviceItem2)
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device1 },
+ deviceItem1
+ ),
+ createFactory(
+ { cachedDevice -> cachedDevice.device == device2 },
+ deviceItem2
+ )
+ ),
+ listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE),
+ testScope.backgroundScope,
+ dispatcher
)
- )
- interactor.setDisplayPriorityForTesting(
- listOf(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
- )
`when`(deviceItem1.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
`when`(deviceItem2.type).thenReturn(DeviceItemType.CONNECTED_BLUETOOTH_DEVICE)
@@ -231,10 +288,19 @@
`when`(bluetoothTileDialogRepository.cachedDevices)
.thenReturn(listOf(cachedDevice2, cachedDevice2, cachedDevice2, cachedDevice2))
`when`(adapter.mostRecentlyConnectedDevices).thenReturn(null)
- interactor.setDeviceItemFactoryListForTesting(
- listOf(createFactory({ true }, deviceItem2))
- )
-
+ interactor =
+ DeviceItemInteractor(
+ bluetoothTileDialogRepository,
+ audioManager,
+ adapter,
+ localBluetoothManager,
+ fakeSystemClock,
+ logger,
+ listOf(createFactory({ true }, deviceItem2)),
+ emptyList(),
+ testScope.backgroundScope,
+ dispatcher
+ )
val latest by collectLastValue(interactor.deviceItemUpdate)
val latestShowSeeAll by collectLastValue(interactor.showSeeAllUpdate)
interactor.updateDeviceItems(mContext, DeviceFetchTrigger.FIRST_LOAD)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
index 85e8ab4..5741d64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/clipboardoverlay/ClipboardModelTest.kt
@@ -122,6 +122,7 @@
@Test
@Throws(IOException::class)
+ @DisableFlags(FLAG_CLIPBOARD_USE_DESCRIPTION_MIMETYPE)
fun test_imageClipData_loadFailure() {
whenever(mMockContext.contentResolver).thenReturn(mMockContentResolver)
whenever(mMockContext.resources).thenReturn(mContext.resources)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/communal/data/backup/CommunalBackupUtilsTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt b/packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/communal/data/db/CommunalWidgetDaoTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
new file mode 100644
index 0000000..ff3186a
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryImplTest.kt
@@ -0,0 +1,158 @@
+/*
+ * 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.display.data.repository
+
+import android.content.testableContext
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.mockWindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+class DisplayWindowPropertiesRepositoryImplTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val fakeDisplayRepository = kosmos.displayRepository
+ private val testScope = kosmos.testScope
+
+ private val applicationContext = kosmos.testableContext
+ private val applicationWindowManager = kosmos.mockWindowManager
+
+ private val repo =
+ DisplayWindowPropertiesRepositoryImpl(
+ kosmos.applicationCoroutineScope,
+ applicationContext,
+ applicationWindowManager,
+ fakeDisplayRepository,
+ )
+
+ @Before
+ fun start() {
+ repo.start()
+ }
+
+ @Before
+ fun addDisplays() = runBlocking {
+ fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+ }
+
+ @Test
+ fun get_defaultDisplayId_returnsDefaultProperties() =
+ testScope.runTest {
+ val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext)
+ .isEqualTo(
+ DisplayWindowProperties(
+ displayId = DEFAULT_DISPLAY_ID,
+ windowType = WINDOW_TYPE_FOO,
+ context = applicationContext,
+ windowManager = applicationWindowManager,
+ )
+ )
+ }
+
+ @Test
+ fun get_nonDefaultDisplayId_returnsNewStatusBarContext() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext.context).isNotSameInstanceAs(applicationContext)
+ }
+
+ @Test
+ fun get_nonDefaultDisplayId_returnsNewWindowManager() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(displayContext.windowManager).isNotSameInstanceAs(applicationWindowManager)
+ }
+
+ @Test
+ fun get_multipleCallsForDefaultDisplay_returnsSameInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_multipleCallsForNonDefaultDisplay_returnsSameInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_multipleCalls_differentType_returnsNewInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_BAR))
+ .isNotSameInstanceAs(displayContext)
+ }
+
+ @Test
+ fun get_afterDisplayRemoved_returnsNewInstance() =
+ testScope.runTest {
+ val displayContext = repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO)
+
+ fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+ assertThat(repo.get(NON_DEFAULT_DISPLAY_ID, WINDOW_TYPE_FOO))
+ .isNotSameInstanceAs(displayContext)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun get_nonExistingDisplayId_throws() =
+ testScope.runTest { repo.get(NON_EXISTING_DISPLAY_ID, WINDOW_TYPE_FOO) }
+
+ private fun createDisplay(displayId: Int) =
+ mock<Display> { on { getDisplayId() } doReturn displayId }
+
+ companion object {
+ private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+ private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+ private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+ private const val WINDOW_TYPE_FOO = 123
+ private const val WINDOW_TYPE_BAR = 321
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyboard/data/repository/KeyboardRepositoryTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardBottomAreaViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/ActivatableTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/ActivatableTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/QSImplTest.java
rename to packages/SystemUI/tests/src/com/android/systemui/qs/QSImplTest.java
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/DragAndDropTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/qs/panels/ui/compose/ResizingTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/recordissue/RecordIssueDialogDelegateTest.kt
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
rename to packages/SystemUI/tests/src/com/android/systemui/screenshot/ScreenshotDetectionControllerTest.kt
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
index 6febb91..7a579ba 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/connectivity/NetworkControllerWifiTest.java
@@ -58,7 +58,7 @@
private static final int MIN_RSSI = -100;
private static final int MAX_RSSI = -55;
private WifiInfo mWifiInfo = mock(WifiInfo.class);
- private VcnTransportInfo mVcnTransportInfo = mock(VcnTransportInfo.class);
+ private VcnTransportInfo mVcnTransportInfo = new VcnTransportInfo.Builder().build();
@Before
public void setUp() throws Exception {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
index 59fc0d1..87cda64 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/stack/NotificationStackScrollLayoutTest.java
@@ -591,8 +591,8 @@
ArgumentCaptor<FooterView> captor = ArgumentCaptor.forClass(FooterView.class);
verify(mStackScroller).setFooterView(captor.capture());
- assertNotNull(captor.getValue().findViewById(R.id.manage_text).hasOnClickListeners());
- assertNotNull(captor.getValue().findViewById(R.id.dismiss_text).hasOnClickListeners());
+ assertNotNull(captor.getValue().findViewById(R.id.manage_text));
+ assertNotNull(captor.getValue().findViewById(R.id.dismiss_text));
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
index 328d310..c48898a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/mobile/data/repository/prod/MobileConnectionsRepositoryTest.kt
@@ -136,6 +136,7 @@
private val wifiLogBuffer = LogBuffer("wifi", maxSize = 100, logcatEchoTracker = mock())
private val wifiPickerTrackerCallback =
argumentCaptor<WifiPickerTracker.WifiPickerTrackerCallback>()
+ private val vcnTransportInfo = VcnTransportInfo.Builder().build()
private val testDispatcher = StandardTestDispatcher()
private val testScope = TestScope(testDispatcher)
@@ -1003,6 +1004,18 @@
assertThat(latest).isTrue()
}
+ private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(it.transportInfo).thenReturn(wifiInfo)
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
/** Regression test for b/272586234. */
@Test
fun hasCarrierMergedConnection_carrierMergedViaWifiWithVcnTransport_isTrue() =
@@ -1012,10 +1025,12 @@
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val caps =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1034,10 +1049,12 @@
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val caps =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val latest by collectLastValue(underTest.hasCarrierMergedConnection)
@@ -1094,10 +1111,15 @@
whenever(this.isCarrierMerged).thenReturn(true)
whenever(this.isPrimary).thenReturn(true)
}
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
index 0945742..88f262b 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/pipeline/shared/data/repository/ConnectivityRepositoryImplTest.kt
@@ -23,6 +23,7 @@
import android.net.NetworkCapabilities.TRANSPORT_ETHERNET
import android.net.NetworkCapabilities.TRANSPORT_VPN
import android.net.NetworkCapabilities.TRANSPORT_WIFI
+import android.net.TelephonyNetworkSpecifier
import android.net.VpnTransportInfo
import android.net.vcn.VcnTransportInfo
import android.net.wifi.WifiInfo
@@ -74,6 +75,8 @@
private val testScope = kosmos.testScope
private val tunerService = mock<TunerService>()
+ private val vcnTransportInfo = VcnTransportInfo.Builder().build()
+
@Before
fun setUp() {
createAndSetRepo()
@@ -343,6 +346,30 @@
assertThat(latest!!.wifi.isDefault).isTrue()
}
+ private fun newWifiNetwork(wifiInfo: WifiInfo): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
+ whenever(it.transportInfo).thenReturn(wifiInfo)
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
+ private fun newCellNetwork(subId: Int): Network {
+ val network = mock<Network>()
+ val capabilities =
+ mock<NetworkCapabilities>().also {
+ whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
+ whenever(it.networkSpecifier).thenReturn(TelephonyNetworkSpecifier(subId))
+ }
+ whenever(connectivityManager.getNetworkCapabilities(network)).thenReturn(capabilities)
+
+ return network
+ }
+
@Test
fun defaultConnections_carrierMergedViaWifiWithVcnTransport_wifiAndCarrierMergedDefault() =
testScope.runTest {
@@ -350,10 +377,12 @@
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(false)
whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
}
@@ -373,10 +402,12 @@
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
whenever(it.hasTransport(TRANSPORT_ETHERNET)).thenReturn(false)
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(false)
}
@@ -561,10 +592,12 @@
val underlyingCarrierMergedNetwork = mock<Network>()
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+ val underlyingWifi = newWifiNetwork(carrierMergedInfo)
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
@@ -645,14 +678,15 @@
@Test
fun vcnSubId_tracksVcnTransportInfo() =
testScope.runTest {
- val vcnInfo = VcnTransportInfo(SUB_1_ID)
+ val underlyingCell = newCellNetwork(SUB_1_ID)
val latest by collectLastValue(underTest.vcnSubId)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -663,14 +697,15 @@
@Test
fun vcnSubId_filersOutInvalid() =
testScope.runTest {
- val vcnInfo = VcnTransportInfo(INVALID_SUBSCRIPTION_ID)
+ val underlyingCell = newCellNetwork(INVALID_SUBSCRIPTION_ID)
val latest by collectLastValue(underTest.vcnSubId)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingCell))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -703,11 +738,12 @@
val latest by collectLastValue(underTest.vcnSubId)
val wifiInfo = mock<WifiInfo>()
- val vcnInfo = VcnTransportInfo(wifiInfo)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -721,14 +757,15 @@
val latest by collectLastValue(underTest.vcnSubId)
val wifiInfo = mock<WifiInfo>()
- val wifiVcnInfo = VcnTransportInfo(wifiInfo)
- val sub1VcnInfo = VcnTransportInfo(SUB_1_ID)
- val sub2VcnInfo = VcnTransportInfo(SUB_2_ID)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
+ val underlyingCell1 = newCellNetwork(SUB_1_ID)
+ val underlyingCell2 = newCellNetwork(SUB_2_ID)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(wifiVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
// WIFI VCN info
@@ -738,14 +775,16 @@
// Cellular VCN info with subId 1
whenever(capabilities.hasTransport(eq(TRANSPORT_CELLULAR))).thenReturn(true)
- whenever(capabilities.transportInfo).thenReturn(sub1VcnInfo)
+ whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell1))
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
assertThat(latest).isEqualTo(SUB_1_ID)
// Cellular VCN info with subId 2
- whenever(capabilities.transportInfo).thenReturn(sub2VcnInfo)
+ whenever(capabilities.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(capabilities.underlyingNetworks).thenReturn(listOf(underlyingCell2))
getDefaultNetworkCallback().onCapabilitiesChanged(NETWORK, capabilities)
@@ -776,11 +815,12 @@
@Test
fun getMainOrUnderlyingWifiInfo_vcnWithWifi_hasInfo() {
val wifiInfo = mock<WifiInfo>()
- val vcnInfo = VcnTransportInfo(wifiInfo)
+ val underlyingWifi = newWifiNetwork(wifiInfo)
val capabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(vcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(underlyingWifi))
}
val result = capabilities.getMainOrUnderlyingWifiInfo(connectivityManager)
@@ -860,11 +900,15 @@
fun getMainOrUnderlyingWifiInfo_cellular_underlyingVcnWithWifi_hasInfo() {
val wifiInfo = mock<WifiInfo>()
val underlyingNetwork = mock<Network>()
- val underlyingVcnInfo = VcnTransportInfo(wifiInfo)
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(wifiInfo)
+
val underlyingWifiCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
.thenReturn(underlyingWifiCapabilities)
@@ -887,11 +931,15 @@
@DisableFlags(FLAG_STATUS_BAR_ALWAYS_CHECK_UNDERLYING_NETWORKS)
fun getMainOrUnderlyingWifiInfo_notCellular_underlyingVcnWithWifi_noInfo() {
val underlyingNetwork = mock<Network>()
- val underlyingVcnInfo = VcnTransportInfo(mock<WifiInfo>())
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(mock<WifiInfo>())
+
val underlyingWifiCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_WIFI)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(underlyingVcnInfo)
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingNetwork))
.thenReturn(underlyingWifiCapabilities)
@@ -917,10 +965,15 @@
val underlyingCarrierMergedNetwork = mock<Network>()
val carrierMergedInfo =
mock<WifiInfo>().apply { whenever(this.isCarrierMerged).thenReturn(true) }
+
+ // The Wifi network that is under the VCN network
+ val physicalWifiNetwork = newWifiNetwork(carrierMergedInfo)
+
val underlyingCapabilities =
mock<NetworkCapabilities>().also {
whenever(it.hasTransport(TRANSPORT_CELLULAR)).thenReturn(true)
- whenever(it.transportInfo).thenReturn(VcnTransportInfo(carrierMergedInfo))
+ whenever(it.transportInfo).thenReturn(vcnTransportInfo)
+ whenever(it.underlyingNetworks).thenReturn(listOf(physicalWifiNetwork))
}
whenever(connectivityManager.getNetworkCapabilities(underlyingCarrierMergedNetwork))
.thenReturn(underlyingCapabilities)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
new file mode 100644
index 0000000..faaa4c4
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/window/MultiDisplayStatusBarWindowControllerStoreTest.kt
@@ -0,0 +1,120 @@
+/*
+ * 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.window
+
+import android.platform.test.annotations.EnableFlags
+import android.view.Display
+import android.view.WindowManager
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+import com.android.app.viewcapture.mockViewCaptureAwareWindowManager
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.display.data.repository.displayRepository
+import com.android.systemui.display.data.repository.fakeDisplayWindowPropertiesRepository
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.unconfinedTestDispatcher
+import com.android.systemui.statusbar.core.StatusBarConnectedDisplays
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+
+@RunWith(AndroidJUnit4::class)
+@SmallTest
+@EnableFlags(StatusBarConnectedDisplays.FLAG_NAME)
+class MultiDisplayStatusBarWindowControllerStoreTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos().also { it.testDispatcher = it.unconfinedTestDispatcher }
+ private val testScope = kosmos.testScope
+ private val fakeDisplayRepository = kosmos.displayRepository
+
+ private val store =
+ MultiDisplayStatusBarWindowControllerStore(
+ backgroundApplicationScope = kosmos.applicationCoroutineScope,
+ controllerFactory = kosmos.fakeStatusBarWindowControllerFactory,
+ displayWindowPropertiesRepository = kosmos.fakeDisplayWindowPropertiesRepository,
+ viewCaptureAwareWindowManagerFactory =
+ object : ViewCaptureAwareWindowManager.Factory {
+ override fun create(
+ windowManager: WindowManager
+ ): ViewCaptureAwareWindowManager {
+ return kosmos.mockViewCaptureAwareWindowManager
+ }
+ },
+ displayRepository = fakeDisplayRepository,
+ )
+
+ @Before
+ fun start() {
+ store.start()
+ }
+
+ @Before
+ fun addDisplays() = runBlocking {
+ fakeDisplayRepository.addDisplay(createDisplay(DEFAULT_DISPLAY_ID))
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+ }
+
+ @Test
+ fun forDisplay_defaultDisplay_multipleCalls_returnsSameInstance() =
+ testScope.runTest {
+ val controller = store.defaultDisplay
+
+ assertThat(store.defaultDisplay).isSameInstanceAs(controller)
+ }
+
+ @Test
+ fun forDisplay_nonDefaultDisplay_multipleCalls_returnsSameInstance() =
+ testScope.runTest {
+ val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isSameInstanceAs(controller)
+ }
+
+ @Test
+ fun forDisplay_nonDefaultDisplay_afterDisplayRemoved_returnsNewInstance() =
+ testScope.runTest {
+ val controller = store.forDisplay(NON_DEFAULT_DISPLAY_ID)
+
+ fakeDisplayRepository.removeDisplay(NON_DEFAULT_DISPLAY_ID)
+ fakeDisplayRepository.addDisplay(createDisplay(NON_DEFAULT_DISPLAY_ID))
+
+ assertThat(store.forDisplay(NON_DEFAULT_DISPLAY_ID)).isNotSameInstanceAs(controller)
+ }
+
+ @Test(expected = IllegalArgumentException::class)
+ fun forDisplay_nonExistingDisplayId_throws() =
+ testScope.runTest { store.forDisplay(NON_EXISTING_DISPLAY_ID) }
+
+ private fun createDisplay(displayId: Int): Display = mock {
+ on { getDisplayId() } doReturn displayId
+ }
+
+ companion object {
+ private const val DEFAULT_DISPLAY_ID = Display.DEFAULT_DISPLAY
+ private const val NON_DEFAULT_DISPLAY_ID = DEFAULT_DISPLAY_ID + 1
+ private const val NON_EXISTING_DISPLAY_ID = DEFAULT_DISPLAY_ID + 2
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt
new file mode 100644
index 0000000..e1c6699
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/app/viewcapture/ViewCaptureAwareWindowManagerKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.app.viewcapture
+
+import com.android.systemui.kosmos.Kosmos
+import org.mockito.kotlin.mock
+
+val Kosmos.mockViewCaptureAwareWindowManager by
+ Kosmos.Fixture { mock<ViewCaptureAwareWindowManager>() }
+
+var Kosmos.viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager by
+ Kosmos.Fixture { mockViewCaptureAwareWindowManager }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
new file mode 100644
index 0000000..cac4ff3
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingButtonViewModelKosmos.kt
@@ -0,0 +1,38 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.audioSharingButtonViewModel: AudioSharingButtonViewModel by
+ Kosmos.Fixture {
+ AudioSharingButtonViewModel(
+ localBluetoothManager,
+ audioSharingInteractor,
+ bluetoothStateInteractor,
+ deviceItemInteractor,
+ )
+ }
+
+val Kosmos.audioSharingButtonViewModelFactory: AudioSharingButtonViewModel.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingButtonViewModel.Factory {
+ override fun create(): AudioSharingButtonViewModel {
+ return audioSharingButtonViewModel
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
new file mode 100644
index 0000000..8019efc
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDeviceItemActionInteractorKosmos.kt
@@ -0,0 +1,50 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.plugins.activityStarter
+
+val Kosmos.audioSharingDeviceItemActionInteractorImpl: AudioSharingDeviceItemActionInteractorImpl by
+ Kosmos.Fixture {
+ AudioSharingDeviceItemActionInteractorImpl(
+ activityStarter,
+ audioSharingInteractor,
+ dialogTransitionAnimator,
+ localBluetoothManager,
+ testDispatcher,
+ testDispatcher,
+ bluetoothTileDialogLogger,
+ uiEventLogger,
+ audioSharingDialogDelegateFactory,
+ deviceItemActionInteractorImpl,
+ )
+ }
+
+val Kosmos.audioSharingDialogDelegateFactory: AudioSharingDialogDelegate.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingDialogDelegate.Factory {
+ override fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice
+ ): AudioSharingDialogDelegate {
+ return audioSharingDialogDelegate
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
new file mode 100644
index 0000000..b8899de8
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingDialogViewModelKosmos.kt
@@ -0,0 +1,65 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import android.content.applicationContext
+import com.android.internal.logging.uiEventLogger
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.phone.systemUIDialogDotFactory
+import kotlinx.coroutines.CoroutineScope
+import org.mockito.kotlin.mock
+
+val Kosmos.cachedBluetoothDevice: CachedBluetoothDevice by Kosmos.Fixture { mock {} }
+
+val Kosmos.audioSharingDialogViewModel: AudioSharingDialogViewModel by
+ Kosmos.Fixture {
+ AudioSharingDialogViewModel(
+ deviceItemInteractor,
+ audioSharingInteractor,
+ applicationContext,
+ localBluetoothManager,
+ cachedBluetoothDevice,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+ }
+
+val Kosmos.audioSharingDialogViewModelFactory: AudioSharingDialogViewModel.Factory by
+ Kosmos.Fixture {
+ object : AudioSharingDialogViewModel.Factory {
+ override fun create(
+ cachedBluetoothDevice: CachedBluetoothDevice,
+ coroutineScope: CoroutineScope
+ ): AudioSharingDialogViewModel {
+ return audioSharingDialogViewModel
+ }
+ }
+ }
+
+val Kosmos.audioSharingDialogDelegate: AudioSharingDialogDelegate by
+ Kosmos.Fixture {
+ AudioSharingDialogDelegate(
+ cachedBluetoothDevice,
+ testScope.backgroundScope,
+ audioSharingDialogViewModelFactory,
+ systemUIDialogDotFactory,
+ uiEventLogger
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
new file mode 100644
index 0000000..4f4d1da
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingInteractorKosmos.kt
@@ -0,0 +1,29 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.audioSharingInteractor: AudioSharingInteractor by
+ Kosmos.Fixture {
+ AudioSharingInteractorImpl(
+ localBluetoothManager,
+ bluetoothTileDialogAudioSharingRepository,
+ testDispatcher,
+ )
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt
new file mode 100644
index 0000000..d15d0e5
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/AudioSharingRepositoryKosmos.kt
@@ -0,0 +1,22 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.bluetoothTileDialogAudioSharingRepository by
+ Kosmos.Fixture { FakeAudioSharingRepository() }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
similarity index 100%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothDeviceMetadataInteractorKosmos.kt
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt
new file mode 100644
index 0000000..aaa918c
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/BluetoothStateInteractorKosmos.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.bluetooth.qsdialog
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+import com.android.systemui.kosmos.testScope
+
+val Kosmos.bluetoothStateInteractor: BluetoothStateInteractor by
+ Kosmos.Fixture {
+ BluetoothStateInteractor(
+ localBluetoothManager,
+ bluetoothTileDialogLogger,
+ testScope.backgroundScope,
+ testDispatcher
+ )
+ }
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
similarity index 79%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
index 5ff4634..b5b2f5e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/DeviceItemActionInteractorKosmos.kt
@@ -20,8 +20,7 @@
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testDispatcher
-import com.android.systemui.plugins.activityStarter
-import com.android.systemui.util.mockito.mock
+import org.mockito.kotlin.mock
val Kosmos.bluetoothTileDialogLogger: BluetoothTileDialogLogger by Kosmos.Fixture { mock {} }
@@ -29,14 +28,10 @@
val Kosmos.dialogTransitionAnimator: DialogTransitionAnimator by Kosmos.Fixture { mock {} }
-val Kosmos.deviceItemActionInteractor: DeviceItemActionInteractor by
+val Kosmos.deviceItemActionInteractorImpl: DeviceItemActionInteractorImpl by
Kosmos.Fixture {
- DeviceItemActionInteractor(
- activityStarter,
- dialogTransitionAnimator,
- localBluetoothManager,
+ DeviceItemActionInteractorImpl(
testDispatcher,
- bluetoothTileDialogLogger,
uiEventLogger,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
new file mode 100644
index 0000000..a839f17
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/bluetooth/qsdialog/FakeAudioSharingRepository.kt
@@ -0,0 +1,70 @@
+/*
+ * 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.bluetooth.qsdialog
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice
+import com.android.settingslib.bluetooth.LocalBluetoothLeBroadcast
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.MutableSharedFlow
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+
+class FakeAudioSharingRepository : AudioSharingRepository {
+ private var mutableAvailable: Boolean = false
+
+ private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
+
+ private val mutableAudioSourceStateUpdate = MutableSharedFlow<Unit>()
+
+ var sourceAdded: Boolean = false
+ private set
+
+ private var profile: LocalBluetoothLeBroadcast? = null
+
+ override val leAudioBroadcastProfile: LocalBluetoothLeBroadcast?
+ get() = profile
+
+ override val audioSourceStateUpdate: Flow<Unit> = mutableAudioSourceStateUpdate
+
+ override val inAudioSharing: StateFlow<Boolean> = mutableInAudioSharing
+
+ override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
+ override suspend fun addSource() {
+ sourceAdded = true
+ }
+
+ override suspend fun setActive(cachedBluetoothDevice: CachedBluetoothDevice) {}
+
+ override suspend fun startAudioSharing() {}
+
+ fun setAudioSharingAvailable(available: Boolean) {
+ mutableAvailable = available
+ }
+
+ fun setInAudioSharing(state: Boolean) {
+ mutableInAudioSharing.value = state
+ }
+
+ fun setLeAudioBroadcastProfile(leAudioBroadcastProfile: LocalBluetoothLeBroadcast?) {
+ profile = leAudioBroadcastProfile
+ }
+
+ fun emitAudioSourceStateUpdate() {
+ mutableAudioSourceStateUpdate.tryEmit(Unit)
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt
new file mode 100644
index 0000000..ff4ba61
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayScopeRepositoryKosmos.kt
@@ -0,0 +1,26 @@
+/*
+ * 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.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testDispatcher
+
+val Kosmos.fakeDisplayScopeRepository by
+ Kosmos.Fixture { FakeDisplayScopeRepository(testDispatcher) }
+
+var Kosmos.displayScopeRepository: DisplayScopeRepository by
+ Kosmos.Fixture { fakeDisplayScopeRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
new file mode 100644
index 0000000..65b18c1
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/DisplayWindowPropertiesRepositoryKosmos.kt
@@ -0,0 +1,25 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.display.data.repository
+
+import com.android.systemui.kosmos.Kosmos
+
+val Kosmos.fakeDisplayWindowPropertiesRepository by
+ Kosmos.Fixture { FakeDisplayWindowPropertiesRepository() }
+
+var Kosmos.displayWindowPropertiesRepository: DisplayWindowPropertiesRepository by
+ Kosmos.Fixture { fakeDisplayWindowPropertiesRepository }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.kt
new file mode 100644
index 0000000..3c25924
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayScopeRepository.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.display.data.repository
+
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.CoroutineScope
+
+class FakeDisplayScopeRepository(private val dispatcher: CoroutineDispatcher) :
+ DisplayScopeRepository {
+
+ private val perDisplayScopes = mutableMapOf<Int, CoroutineScope>()
+
+ override fun scopeForDisplay(displayId: Int): CoroutineScope {
+ return perDisplayScopes.computeIfAbsent(displayId) { CoroutineScope(dispatcher) }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt
new file mode 100644
index 0000000..9282f27
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/display/data/repository/FakeDisplayWindowPropertiesRepository.kt
@@ -0,0 +1,37 @@
+/*
+ * 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.display.data.repository
+
+import com.android.systemui.display.shared.model.DisplayWindowProperties
+import com.google.common.collect.HashBasedTable
+import org.mockito.kotlin.mock
+
+class FakeDisplayWindowPropertiesRepository : DisplayWindowPropertiesRepository {
+
+ private val properties = HashBasedTable.create<Int, Int, DisplayWindowProperties>()
+
+ override fun get(displayId: Int, windowType: Int): DisplayWindowProperties {
+ return properties.get(displayId, windowType)
+ ?: DisplayWindowProperties(
+ displayId = displayId,
+ windowType = windowType,
+ context = mock(),
+ windowManager = mock(),
+ )
+ .also { properties.put(displayId, windowType, it) }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
index f97f303..522c387 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/kosmos/KosmosJavaAdapter.kt
@@ -23,6 +23,7 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.bouncer.data.repository.bouncerRepository
import com.android.systemui.bouncer.data.repository.fakeKeyguardBouncerRepository
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.classifier.falsingCollector
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
@@ -152,6 +153,7 @@
val wifiInteractor by lazy { kosmos.wifiInteractor }
val fakeWifiRepository by lazy { kosmos.fakeWifiRepository }
val volumeDialogInteractor by lazy { kosmos.volumeDialogInteractor }
+ val alternateBouncerInteractor by lazy { kosmos.alternateBouncerInteractor }
val ongoingActivityChipsViewModel by lazy { kosmos.ongoingActivityChipsViewModel }
val scrimController by lazy { kosmos.scrimController }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
index cfc31c7..10b073e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/plugins/statusbar/StatusBarStateControllerKosmos.kt
@@ -17,6 +17,7 @@
package com.android.systemui.plugins.statusbar
import com.android.internal.logging.uiEventLogger
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceUnlockedInteractor
import com.android.systemui.jank.interactionJankMonitor
import com.android.systemui.keyguard.domain.interactor.keyguardClockInteractor
@@ -45,5 +46,6 @@
{ sceneContainerOcclusionInteractor },
{ keyguardClockInteractor },
{ sceneBackInteractor },
+ { alternateBouncerInteractor },
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
new file mode 100644
index 0000000..10f328b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerFactory.kt
@@ -0,0 +1,27 @@
+/*
+ * 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.window
+
+import android.content.Context
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager
+
+class FakeStatusBarWindowControllerFactory : StatusBarWindowController.Factory {
+ override fun create(
+ context: Context,
+ viewCaptureAwareWindowManager: ViewCaptureAwareWindowManager,
+ ) = FakeStatusBarWindowController()
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt
new file mode 100644
index 0000000..d19e322
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/FakeStatusBarWindowControllerStore.kt
@@ -0,0 +1,31 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.statusbar.window
+
+import android.view.Display
+
+class FakeStatusBarWindowControllerStore : StatusBarWindowControllerStore {
+
+ private val perDisplayControllers = mutableMapOf<Int, FakeStatusBarWindowController>()
+
+ override val defaultDisplay
+ get() = forDisplay(Display.DEFAULT_DISPLAY)
+
+ override fun forDisplay(displayId: Int): StatusBarWindowController {
+ return perDisplayControllers.computeIfAbsent(displayId) { FakeStatusBarWindowController() }
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
index c198b35..6c6f243 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/window/StatusBarWindowControllerKosmos.kt
@@ -21,3 +21,15 @@
val Kosmos.fakeStatusBarWindowController by Kosmos.Fixture { FakeStatusBarWindowController() }
var Kosmos.statusBarWindowController by Kosmos.Fixture { fakeStatusBarWindowController }
+
+val Kosmos.fakeStatusBarWindowControllerStore by
+ Kosmos.Fixture { FakeStatusBarWindowControllerStore() }
+
+var Kosmos.statusBarWindowControllerStore: StatusBarWindowControllerStore by
+ Kosmos.Fixture { fakeStatusBarWindowControllerStore }
+
+val Kosmos.fakeStatusBarWindowControllerFactory by
+ Kosmos.Fixture { FakeStatusBarWindowControllerFactory() }
+
+var Kosmos.statusBarWindowControllerFactory: StatusBarWindowController.Factory by
+ Kosmos.Fixture { fakeStatusBarWindowControllerFactory }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
index a4719e5..5da6ee9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/data/repository/FakeAudioSharingRepository.kt
@@ -22,6 +22,7 @@
import kotlinx.coroutines.flow.StateFlow
class FakeAudioSharingRepository : AudioSharingRepository {
+ private var mutableAvailable: Boolean = false
private val mutableInAudioSharing: MutableStateFlow<Boolean> = MutableStateFlow(false)
private val mutablePrimaryGroupId: MutableStateFlow<Int> =
MutableStateFlow(TEST_GROUP_ID_INVALID)
@@ -34,8 +35,14 @@
override val secondaryGroupId: StateFlow<Int> = mutableSecondaryGroupId
override val volumeMap: StateFlow<GroupIdToVolumes> = mutableVolumeMap
+ override suspend fun audioSharingAvailable(): Boolean = mutableAvailable
+
override suspend fun setSecondaryVolume(volume: Int) {}
+ fun setAudioSharingAvailable(available: Boolean) {
+ mutableAvailable = available
+ }
+
fun setInAudioSharing(state: Boolean) {
mutableInAudioSharing.value = state
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 9629a87..8896d77 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -12,11 +12,19 @@
}
filegroup {
+ name: "ravenwood-common-policies",
+ srcs: [
+ "texts/ravenwood-common-policies.txt",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+filegroup {
name: "ravenwood-services-policies",
srcs: [
"texts/ravenwood-services-policies.txt",
],
- visibility: ["//visibility:public"],
+ visibility: ["//visibility:private"],
}
filegroup {
@@ -24,7 +32,7 @@
srcs: [
"texts/ravenwood-framework-policies.txt",
],
- visibility: ["//visibility:public"],
+ visibility: ["//visibility:private"],
}
filegroup {
@@ -32,7 +40,7 @@
srcs: [
"texts/ravenwood-standard-options.txt",
],
- visibility: ["//visibility:public"],
+ visibility: ["//visibility:private"],
}
filegroup {
@@ -40,7 +48,7 @@
srcs: [
"texts/ravenwood-annotation-allowed-classes.txt",
],
- visibility: ["//visibility:public"],
+ visibility: ["//visibility:private"],
}
// This and the next module contain the same classes with different implementations.
@@ -337,6 +345,30 @@
],
}
+// JARs in "ravenwood-runtime" are set to the classpath, sorted alphabetically.
+// Rename some of the dependencies to make sure they're included in the intended order.
+
+java_library {
+ name: "100-framework-minus-apex.ravenwood",
+ installable: false,
+ static_libs: ["framework-minus-apex.ravenwood"],
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "200-kxml2-android",
+ installable: false,
+ static_libs: ["kxml2-android"],
+ visibility: ["//visibility:private"],
+}
+
+java_library {
+ name: "z00-all-updatable-modules-system-stubs",
+ installable: false,
+ static_libs: ["all-updatable-modules-system-stubs-for-host"],
+ visibility: ["//visibility:private"],
+}
+
android_ravenwood_libgroup {
name: "ravenwood-runtime",
data: [
@@ -395,3 +427,7 @@
"inline-mockito-ravenwood-prebuilt",
],
}
+
+build = [
+ "Framework.bp",
+]
diff --git a/ravenwood/Framework.bp b/ravenwood/Framework.bp
new file mode 100644
index 0000000..5cb1479
--- /dev/null
+++ b/ravenwood/Framework.bp
@@ -0,0 +1,292 @@
+// 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.
+
+// This file hosts all the genrule and module definitions for all Android specific
+// code that needs further post-processing by hoststubgen to support Ravenwood.
+
+/////////////////////////
+// framework-minus-apex
+/////////////////////////
+
+// Process framework-minus-apex with hoststubgen for Ravenwood.
+// This step takes several tens of seconds, so we manually shard it to multiple modules.
+// All the copies have to be kept in sync.
+// TODO: Do the sharding better, either by making hostsubgen support sharding natively, or
+// making a better build rule.
+
+genrule_defaults {
+ name: "framework-minus-apex.ravenwood-base_defaults",
+ tools: ["hoststubgen"],
+ srcs: [
+ ":framework-minus-apex-for-host",
+ ":ravenwood-common-policies",
+ ":ravenwood-framework-policies",
+ ":ravenwood-standard-options",
+ ":ravenwood-annotation-allowed-classes",
+ ],
+ out: [
+ "ravenwood.jar",
+ "hoststubgen_framework-minus-apex.log",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+framework_minus_apex_cmd = "$(location hoststubgen) " +
+ "@$(location :ravenwood-standard-options) " +
+ "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+ "--out-jar $(location ravenwood.jar) " +
+ "--in-jar $(location :framework-minus-apex-for-host) " +
+ "--policy-override-file $(location :ravenwood-common-policies) " +
+ "--policy-override-file $(location :ravenwood-framework-policies) " +
+ "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) "
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X0",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 0",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X1",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 1",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X2",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 2",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X3",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 3",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X4",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 4",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X5",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X6",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 6",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X7",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 7",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X8",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 8",
+}
+
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_X9",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 9",
+}
+
+// Build framework-minus-apex.ravenwood-base without sharding.
+// We extract the various dump files from this one, rather than the sharded ones, because
+// some dumps use the output from other classes (e.g. base classes) which may not be in the
+// same shard. Also some of the dump files ("apis") may be slow even when sharded, because
+// the output contains the information from all the input classes, rather than the output classes.
+// Not using sharding is fine for this module because it's only used for collecting the
+// dump / stats files, which don't have to happen regularly.
+java_genrule {
+ name: "framework-minus-apex.ravenwood-base_all",
+ defaults: ["framework-minus-apex.ravenwood-base_defaults"],
+ cmd: framework_minus_apex_cmd +
+ "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
+ "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
+
+ "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
+ "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
+
+ out: [
+ "hoststubgen_framework-minus-apex_keep_all.txt",
+ "hoststubgen_framework-minus-apex_dump.txt",
+ "hoststubgen_framework-minus-apex_stats.csv",
+ "hoststubgen_framework-minus-apex_apis.csv",
+ ],
+}
+
+// Marge all the sharded jars
+java_genrule {
+ name: "framework-minus-apex.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-java"],
+ cmd: "$(location merge_zips) $(out) $(in)",
+ tools: ["merge_zips"],
+ srcs: [
+ ":framework-minus-apex.ravenwood-base_X0{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X1{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X2{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X3{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X4{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X5{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X6{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X7{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X8{ravenwood.jar}",
+ ":framework-minus-apex.ravenwood-base_X9{ravenwood.jar}",
+ ],
+ out: [
+ "framework-minus-apex.ravenwood.jar",
+ ],
+}
+
+//////////////////
+// services.core
+//////////////////
+
+java_library {
+ name: "services.core-for-host",
+ installable: false, // host only jar.
+ static_libs: [
+ "services.core",
+ ],
+ sdk_version: "core_platform",
+ visibility: ["//visibility:private"],
+}
+
+java_genrule {
+ name: "services.core.ravenwood-base",
+ tools: ["hoststubgen"],
+ cmd: "$(location hoststubgen) " +
+ "@$(location :ravenwood-standard-options) " +
+
+ "--debug-log $(location hoststubgen_services.core.log) " +
+ "--stats-file $(location hoststubgen_services.core_stats.csv) " +
+ "--supported-api-list-file $(location hoststubgen_services.core_apis.csv) " +
+ "--gen-keep-all-file $(location hoststubgen_services.core_keep_all.txt) " +
+ "--gen-input-dump-file $(location hoststubgen_services.core_dump.txt) " +
+
+ "--out-jar $(location ravenwood.jar) " +
+ "--in-jar $(location :services.core-for-host) " +
+
+ "--policy-override-file $(location :ravenwood-common-policies) " +
+ "--policy-override-file $(location :ravenwood-services-policies) " +
+ "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ srcs: [
+ ":services.core-for-host",
+ ":ravenwood-common-policies",
+ ":ravenwood-services-policies",
+ ":ravenwood-standard-options",
+ ":ravenwood-annotation-allowed-classes",
+ ],
+ out: [
+ "ravenwood.jar",
+
+ // Following files are created just as FYI.
+ "hoststubgen_services.core_keep_all.txt",
+ "hoststubgen_services.core_dump.txt",
+
+ "hoststubgen_services.core.log",
+ "hoststubgen_services.core_stats.csv",
+ "hoststubgen_services.core_apis.csv",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+java_genrule {
+ name: "services.core.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
+ cmd: "cp $(in) $(out)",
+ srcs: [
+ ":services.core.ravenwood-base{ravenwood.jar}",
+ ],
+ out: [
+ "services.core.ravenwood.jar",
+ ],
+}
+
+// TODO(b/313930116) This jarjar is a bit slow. We should use hoststubgen for renaming,
+// but services.core.ravenwood has complex dependencies, so it'll take more than
+// just using hoststubgen "rename"s.
+java_library {
+ name: "services.core.ravenwood-jarjar",
+ defaults: ["ravenwood-internal-only-visibility-java"],
+ installable: false,
+ static_libs: [
+ "services.core.ravenwood",
+ ],
+ jarjar_rules: ":ravenwood-services-jarjar-rules",
+}
+
+///////////////
+// core-icu4j
+///////////////
+
+java_genrule {
+ name: "core-icu4j-for-host.ravenwood-base",
+ tools: ["hoststubgen"],
+ cmd: "$(location hoststubgen) " +
+ "@$(location :ravenwood-standard-options) " +
+
+ "--debug-log $(location hoststubgen_core-icu4j-for-host.log) " +
+ "--stats-file $(location hoststubgen_core-icu4j-for-host_stats.csv) " +
+ "--supported-api-list-file $(location hoststubgen_core-icu4j-for-host_apis.csv) " +
+ "--gen-keep-all-file $(location hoststubgen_core-icu4j-for-host_keep_all.txt) " +
+ "--gen-input-dump-file $(location hoststubgen_core-icu4j-for-host_dump.txt) " +
+
+ "--out-jar $(location ravenwood.jar) " +
+ "--in-jar $(location :core-icu4j-for-host) " +
+
+ "--policy-override-file $(location :ravenwood-common-policies) " +
+ "--policy-override-file $(location :icu-ravenwood-policies) ",
+ srcs: [
+ ":core-icu4j-for-host",
+
+ ":ravenwood-common-policies",
+ ":icu-ravenwood-policies",
+ ":ravenwood-standard-options",
+ ],
+ out: [
+ "ravenwood.jar",
+
+ // Following files are created just as FYI.
+ "hoststubgen_core-icu4j-for-host_keep_all.txt",
+ "hoststubgen_core-icu4j-for-host_dump.txt",
+
+ "hoststubgen_core-icu4j-for-host.log",
+ "hoststubgen_core-icu4j-for-host_stats.csv",
+ "hoststubgen_core-icu4j-for-host_apis.csv",
+ ],
+ visibility: ["//visibility:private"],
+}
+
+java_genrule {
+ name: "core-icu4j-for-host.ravenwood",
+ defaults: ["ravenwood-internal-only-visibility-genrule"],
+ cmd: "cp $(in) $(out)",
+ srcs: [
+ ":core-icu4j-for-host.ravenwood-base{ravenwood.jar}",
+ ],
+ out: [
+ "core-icu4j-for-host.ravenwood.jar",
+ ],
+}
diff --git a/ravenwood/texts/ravenwood-common-policies.txt b/ravenwood/texts/ravenwood-common-policies.txt
new file mode 100644
index 0000000..08f53977
--- /dev/null
+++ b/ravenwood/texts/ravenwood-common-policies.txt
@@ -0,0 +1,20 @@
+# Ravenwood "policy" that should apply to all code.
+
+# Keep all AIDL interfaces
+class :aidl keepclass
+
+# Keep all feature flag implementations
+class :feature_flags keepclass
+
+# Keep all sysprops generated code implementations
+class :sysprops keepclass
+
+# Keep all resource R classes
+class :r keepclass
+
+# Support APIs not available in standard JRE
+class java.io.FileDescriptor keep
+ method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
+ method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
+class java.util.LinkedHashMap keep
+ method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
diff --git a/ravenwood/texts/ravenwood-framework-policies.txt b/ravenwood/texts/ravenwood-framework-policies.txt
index d962c82..3649f0e 100644
--- a/ravenwood/texts/ravenwood-framework-policies.txt
+++ b/ravenwood/texts/ravenwood-framework-policies.txt
@@ -1,29 +1,10 @@
# Ravenwood "policy" file for framework-minus-apex.
-# Keep all AIDL interfaces
-class :aidl keepclass
-
-# Keep all feature flag implementations
-class :feature_flags keepclass
-
-# Keep all sysprops generated code implementations
-class :sysprops keepclass
-
-# Keep all resource R classes
-class :r keepclass
-
# To avoid VerifyError on nano proto files (b/324063814), we rename nano proto classes.
# Note: The "rename" directive must use slashes (/) as a package name separator.
rename com/.*/nano/ devicenano/
rename android/.*/nano/ devicenano/
-# Support APIs not available in standard JRE
-class java.io.FileDescriptor keep
- method getInt$ ()I @com.android.ravenwood.RavenwoodJdkPatch.getInt$
- method setInt$ (I)V @com.android.ravenwood.RavenwoodJdkPatch.setInt$
-class java.util.LinkedHashMap keep
- method eldest ()Ljava/util/Map$Entry; @com.android.ravenwood.RavenwoodJdkPatch.eldest
-
# Exported to Mainline modules; cannot use annotations
class com.android.internal.util.FastXmlSerializer keepclass
class com.android.internal.util.FileRotator keepclass
diff --git a/ravenwood/texts/ravenwood-services-policies.txt b/ravenwood/texts/ravenwood-services-policies.txt
index 5cdb4f7..cc2fa60 100644
--- a/ravenwood/texts/ravenwood-services-policies.txt
+++ b/ravenwood/texts/ravenwood-services-policies.txt
@@ -1,7 +1 @@
# Ravenwood "policy" file for services.core.
-
-# Keep all AIDL interfaces
-class :aidl keepclass
-
-# Keep all feature flag implementations
-class :feature_flags keepclass
diff --git a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
index c5fef19..5d57408 100644
--- a/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
+++ b/services/appfunctions/java/com/android/server/appfunctions/AppFunctionManagerServiceImpl.java
@@ -65,8 +65,6 @@
import com.android.internal.infra.AndroidFuture;
import com.android.internal.util.DumpUtils;
import com.android.server.SystemService.TargetUser;
-import com.android.server.appfunctions.RemoteServiceCaller.RunServiceCallCallback;
-import com.android.server.appfunctions.RemoteServiceCaller.ServiceUsageCompleteListener;
import java.io.FileDescriptor;
import java.io.PrintWriter;
@@ -233,6 +231,9 @@
"Caller does not have permission to execute the"
+ " appfunction",
/* extras= */ null));
+ throw new SecurityException(
+ "Caller does not have permission to execute the"
+ + " appfunction");
}
})
.thenCompose(
@@ -380,7 +381,8 @@
runtimeMetadataSearchSession));
AppFunctionRuntimeMetadata newMetadata =
new AppFunctionRuntimeMetadata.Builder(existingMetadata)
- .setEnabled(enabledState).build();
+ .setEnabled(enabledState)
+ .build();
AppSearchBatchResult<String, Void> putDocumentBatchResult =
runtimeMetadataSearchSession
.put(
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
index 3bcca1c..3d67ed4 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceImpl.java
@@ -16,10 +16,13 @@
package com.android.server.companion.virtual;
+import static android.Manifest.permission.ADD_ALWAYS_UNLOCKED_DISPLAY;
+import static android.Manifest.permission.ADD_TRUSTED_DISPLAY;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_ENABLED;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_NOT_CONTROLLED_BY_POLICY;
import static android.app.admin.DevicePolicyManager.NEARBY_STREAMING_SAME_MANAGED_ACCOUNT_ONLY;
import static android.companion.virtual.VirtualDeviceParams.ACTIVITY_POLICY_DEFAULT_ALLOWED;
+import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_CUSTOM;
import static android.companion.virtual.VirtualDeviceParams.DEVICE_POLICY_DEFAULT;
import static android.companion.virtual.VirtualDeviceParams.NAVIGATION_POLICY_DEFAULT_ALLOWED;
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_ACTIVITY;
@@ -30,7 +33,6 @@
import static android.companion.virtual.VirtualDeviceParams.POLICY_TYPE_RECENTS;
import static android.companion.virtualdevice.flags.Flags.virtualCameraServiceDiscovery;
-import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -95,7 +97,6 @@
import android.os.IBinder;
import android.os.LocaleList;
import android.os.Looper;
-import android.os.PermissionEnforcer;
import android.os.PowerManager;
import android.os.RemoteException;
import android.os.ResultReceiver;
@@ -402,7 +403,6 @@
VirtualDeviceParams params,
DisplayManagerGlobal displayManager,
VirtualCameraController virtualCameraController) {
- super(PermissionEnforcer.fromContext(context));
mVirtualDeviceLog = virtualDeviceLog;
mOwnerPackageName = attributionSource.getPackageName();
mAttributionSource = attributionSource;
@@ -425,6 +425,27 @@
mDisplayManager = displayManager;
mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
mPowerManager = context.getSystemService(PowerManager.class);
+
+ if (mDevicePolicies.get(POLICY_TYPE_CLIPBOARD, DEVICE_POLICY_DEFAULT)
+ != DEVICE_POLICY_DEFAULT) {
+ if (mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ + "set a custom clipboard policy.");
+ }
+ }
+
+ int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
+ if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
+ if (mContext.checkCallingOrSelfPermission(ADD_ALWAYS_UNLOCKED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_ALWAYS_UNLOCKED_DISPLAY permission to "
+ + "create an always unlocked virtual device.");
+ }
+ flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
+ }
+ mBaseVirtualDisplayFlags = flags;
+
if (inputController == null) {
mInputController = new InputController(
context.getMainThreadHandler(),
@@ -467,12 +488,6 @@
: mParams.getAllowedActivities();
}
- int flags = DEFAULT_VIRTUAL_DISPLAY_FLAGS;
- if (mParams.getLockState() == VirtualDeviceParams.LOCK_STATE_ALWAYS_UNLOCKED) {
- flags |= DisplayManager.VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED;
- }
- mBaseVirtualDisplayFlags = flags;
-
if (Flags.vdmCustomIme() && mParams.getInputMethodComponent() != null) {
final String imeId = mParams.getInputMethodComponent().flattenToShortString();
Slog.d(TAG, "Setting custom input method " + imeId + " as default for virtual device "
@@ -547,10 +562,8 @@
* object is created before the returned VirtualDeviceInternal one.
*/
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setListeners(@NonNull IVirtualDeviceActivityListener activityListener,
@NonNull IVirtualDeviceSoundEffectListener soundEffectListener) {
- super.setListeners_enforcePermission();
mActivityListener = Objects.requireNonNull(activityListener);
mSoundEffectListener = Objects.requireNonNull(soundEffectListener);
}
@@ -597,9 +610,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void goToSleep() {
- super.goToSleep_enforcePermission();
+ checkCallerIsDeviceOwner();
synchronized (mVirtualDeviceLock) {
mRequestedToBeAwake = false;
}
@@ -612,9 +624,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void wakeUp() {
- super.wakeUp_enforcePermission();
+ checkCallerIsDeviceOwner();
synchronized (mVirtualDeviceLock) {
mRequestedToBeAwake = true;
if (mLockdownActive) {
@@ -632,16 +643,12 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void launchPendingIntent(int displayId, PendingIntent pendingIntent,
ResultReceiver resultReceiver) {
- super.launchPendingIntent_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(pendingIntent);
synchronized (mVirtualDeviceLock) {
- if (!mVirtualDisplays.contains(displayId)) {
- throw new SecurityException("Display ID " + displayId
- + " not found for this virtual device");
- }
+ checkDisplayOwnedByVirtualDeviceLocked(displayId);
}
if (pendingIntent.isActivity()) {
try {
@@ -673,9 +680,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void addActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
- super.addActivityPolicyExemption_enforcePermission();
+ checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
@@ -711,9 +717,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void removeActivityPolicyExemption(@NonNull ActivityPolicyExemption exemption) {
- super.removeActivityPolicyExemption_enforcePermission();
+ checkCallerIsDeviceOwner();
final int displayId = exemption.getDisplayId();
if (exemption.getComponentName() == null || displayId != Display.INVALID_DISPLAY) {
if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
@@ -764,9 +769,7 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void close() {
- super.close_enforcePermission();
// Remove about-to-be-closed virtual device from the service before butchering it.
if (!mService.removeVirtualDevice(mDeviceId)) {
// Device is already closed.
@@ -841,11 +844,10 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionStarting(int displayId,
@NonNull IAudioRoutingCallback routingCallback,
@Nullable IAudioConfigChangedCallback configChangedCallback) {
- super.onAudioSessionStarting_enforcePermission();
+ checkCallerIsDeviceOwner();
synchronized (mVirtualDeviceLock) {
checkDisplayOwnedByVirtualDeviceLocked(displayId);
if (mVirtualAudioController == null) {
@@ -859,9 +861,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAudioSessionEnded() {
- super.onAudioSessionEnded_enforcePermission();
+ checkCallerIsDeviceOwner();
synchronized (mVirtualDeviceLock) {
if (mVirtualAudioController != null) {
mVirtualAudioController.stopListening();
@@ -871,10 +872,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setDevicePolicy(@VirtualDeviceParams.DynamicPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
- super.setDevicePolicy_enforcePermission();
+ checkCallerIsDeviceOwner();
if (!Flags.dynamicPolicy()) {
return;
}
@@ -884,8 +884,12 @@
synchronized (mVirtualDeviceLock) {
mDevicePolicies.put(policyType, devicePolicy);
for (int i = 0; i < mVirtualDisplays.size(); i++) {
- mVirtualDisplays.valueAt(i).getWindowPolicyController()
- .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (wrapper.isTrusted()) {
+ wrapper.getWindowPolicyController()
+ .setShowInHostDeviceRecents(
+ devicePolicy == DEVICE_POLICY_DEFAULT);
+ }
}
}
break;
@@ -905,7 +909,20 @@
break;
case POLICY_TYPE_CLIPBOARD:
if (Flags.crossDeviceClipboard()) {
+ if (policyType == DEVICE_POLICY_CUSTOM
+ && mContext.checkCallingOrSelfPermission(ADD_TRUSTED_DISPLAY)
+ != PackageManager.PERMISSION_GRANTED) {
+ throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ + "set a custom clipboard policy.");
+ }
synchronized (mVirtualDeviceLock) {
+ for (int i = 0; i < mVirtualDisplays.size(); i++) {
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+ throw new SecurityException("All displays must be trusted for "
+ + "devices with custom clipboard policy.");
+ }
+ }
mDevicePolicies.put(policyType, devicePolicy);
}
}
@@ -924,11 +941,10 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setDevicePolicyForDisplay(int displayId,
@VirtualDeviceParams.DynamicDisplayPolicyType int policyType,
@VirtualDeviceParams.DevicePolicy int devicePolicy) {
- super.setDevicePolicyForDisplay_enforcePermission();
+ checkCallerIsDeviceOwner();
if (!android.companion.virtualdevice.flags.Flags.activityControlApi()) {
return;
}
@@ -936,8 +952,11 @@
checkDisplayOwnedByVirtualDeviceLocked(displayId);
switch (policyType) {
case POLICY_TYPE_RECENTS:
- mVirtualDisplays.get(displayId).getWindowPolicyController()
- .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+ if (wrapper.isTrusted()) {
+ wrapper.getWindowPolicyController()
+ .setShowInHostDeviceRecents(devicePolicy == DEVICE_POLICY_DEFAULT);
+ }
break;
case POLICY_TYPE_ACTIVITY:
mVirtualDisplays.get(displayId).getWindowPolicyController()
@@ -951,9 +970,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualDpad(VirtualDpadConfig config, @NonNull IBinder deviceToken) {
- super.createVirtualDpad_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
final long ident = Binder.clearCallingIdentity();
@@ -969,9 +987,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualKeyboard(VirtualKeyboardConfig config, @NonNull IBinder deviceToken) {
- super.createVirtualKeyboard_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
final long ident = Binder.clearCallingIdentity();
@@ -991,9 +1008,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualMouse(VirtualMouseConfig config, @NonNull IBinder deviceToken) {
- super.createVirtualMouse_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
final long ident = Binder.clearCallingIdentity();
@@ -1008,10 +1024,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualTouchscreen(VirtualTouchscreenConfig config,
@NonNull IBinder deviceToken) {
- super.createVirtualTouchscreen_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
final long ident = Binder.clearCallingIdentity();
@@ -1027,10 +1042,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualNavigationTouchpad(VirtualNavigationTouchpadConfig config,
@NonNull IBinder deviceToken) {
- super.createVirtualNavigationTouchpad_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
final long ident = Binder.clearCallingIdentity();
@@ -1048,10 +1062,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualStylus(@NonNull VirtualStylusConfig config,
@NonNull IBinder deviceToken) {
- super.createVirtualStylus_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
Objects.requireNonNull(deviceToken);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
@@ -1068,10 +1081,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void createVirtualRotaryEncoder(@NonNull VirtualRotaryEncoderConfig config,
@NonNull IBinder deviceToken) {
- super.createVirtualRotaryEncoder_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(config);
Objects.requireNonNull(deviceToken);
checkVirtualInputDeviceDisplayIdAssociation(config.getAssociatedDisplayId());
@@ -1088,9 +1100,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterInputDevice(IBinder token) {
- super.unregisterInputDevice_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
mInputController.unregisterInputDevice(token);
@@ -1100,9 +1111,7 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public int getInputDeviceId(IBinder token) {
- super.getInputDeviceId_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.getInputDeviceId(token);
@@ -1113,9 +1122,8 @@
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendDpadKeyEvent(IBinder token, VirtualKeyEvent event) {
- super.sendDpadKeyEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendDpadKeyEvent(token, event);
@@ -1125,9 +1133,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendKeyEvent(IBinder token, VirtualKeyEvent event) {
- super.sendKeyEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendKeyEvent(token, event);
@@ -1137,9 +1144,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendButtonEvent(IBinder token, VirtualMouseButtonEvent event) {
- super.sendButtonEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendButtonEvent(token, event);
@@ -1149,9 +1155,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendTouchEvent(IBinder token, VirtualTouchEvent event) {
- super.sendTouchEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendTouchEvent(token, event);
@@ -1161,9 +1166,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendRelativeEvent(IBinder token, VirtualMouseRelativeEvent event) {
- super.sendRelativeEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendRelativeEvent(token, event);
@@ -1173,9 +1177,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendScrollEvent(IBinder token, VirtualMouseScrollEvent event) {
- super.sendScrollEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendScrollEvent(token, event);
@@ -1185,9 +1188,7 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public PointF getCursorPosition(IBinder token) {
- super.getCursorPosition_enforcePermission();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.getCursorPosition(token);
@@ -1197,10 +1198,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendStylusMotionEvent(@NonNull IBinder token,
@NonNull VirtualStylusMotionEvent event) {
- super.sendStylusMotionEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(token);
Objects.requireNonNull(event);
final long ident = Binder.clearCallingIdentity();
@@ -1212,10 +1212,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendStylusButtonEvent(@NonNull IBinder token,
@NonNull VirtualStylusButtonEvent event) {
- super.sendStylusButtonEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(token);
Objects.requireNonNull(event);
final long ident = Binder.clearCallingIdentity();
@@ -1227,10 +1226,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendRotaryEncoderScrollEvent(@NonNull IBinder token,
@NonNull VirtualRotaryEncoderScrollEvent event) {
- super.sendRotaryEncoderScrollEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mInputController.sendRotaryEncoderScrollEvent(token, event);
@@ -1240,17 +1238,19 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setShowPointerIcon(boolean showPointerIcon) {
- super.setShowPointerIcon_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
synchronized (mVirtualDeviceLock) {
mDefaultShowPointerIcon = showPointerIcon;
- }
- final int[] displayIds = getDisplayIds();
- for (int i = 0; i < displayIds.length; ++i) {
- mInputController.setShowPointerIcon(showPointerIcon, displayIds[i]);
+ for (int i = 0; i < mVirtualDisplays.size(); i++) {
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.valueAt(i);
+ if (wrapper.isTrusted() || wrapper.isMirror()) {
+ mInputController.setShowPointerIcon(
+ mDefaultShowPointerIcon, mVirtualDisplays.keyAt(i));
+ }
+ }
}
} finally {
Binder.restoreCallingIdentity(ident);
@@ -1258,14 +1258,10 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void setDisplayImePolicy(int displayId, @WindowManager.DisplayImePolicy int policy) {
- super.setDisplayImePolicy_enforcePermission();
+ checkCallerIsDeviceOwner();
synchronized (mVirtualDeviceLock) {
- if (!mVirtualDisplays.contains(displayId)) {
- throw new SecurityException("Display ID " + displayId
- + " not found for this virtual device");
- }
+ checkDisplayOwnedByVirtualDeviceLocked(displayId);
}
final long ident = Binder.clearCallingIdentity();
try {
@@ -1276,10 +1272,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Nullable
public List<VirtualSensor> getVirtualSensorList() {
- super.getVirtualSensorList_enforcePermission();
+ checkCallerIsDeviceOwner();
return mSensorController.getSensorList();
}
@@ -1289,9 +1284,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public boolean sendSensorEvent(@NonNull IBinder token, @NonNull VirtualSensorEvent event) {
- super.sendSensorEvent_enforcePermission();
+ checkCallerIsDeviceOwner();
final long ident = Binder.clearCallingIdentity();
try {
return mSensorController.sendSensorEvent(token, event);
@@ -1301,10 +1295,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerIntentInterceptor(IVirtualDeviceIntentInterceptor intentInterceptor,
IntentFilter filter) {
- super.registerIntentInterceptor_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(intentInterceptor);
Objects.requireNonNull(filter);
synchronized (mVirtualDeviceLock) {
@@ -1313,10 +1306,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterIntentInterceptor(
@NonNull IVirtualDeviceIntentInterceptor intentInterceptor) {
- super.unregisterIntentInterceptor_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(intentInterceptor);
synchronized (mVirtualDeviceLock) {
mIntentInterceptors.remove(intentInterceptor.asBinder());
@@ -1324,10 +1316,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void registerVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
throws RemoteException {
- super.registerVirtualCamera_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(cameraConfig);
if (mVirtualCameraController == null) {
throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1336,10 +1327,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void unregisterVirtualCamera(@NonNull VirtualCameraConfig cameraConfig)
throws RemoteException {
- super.unregisterVirtualCamera_enforcePermission();
+ checkCallerIsDeviceOwner();
Objects.requireNonNull(cameraConfig);
if (mVirtualCameraController == null) {
throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1348,10 +1338,8 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public String getVirtualCameraId(@NonNull VirtualCameraConfig cameraConfig)
throws RemoteException {
- super.getVirtualCameraId_enforcePermission();
Objects.requireNonNull(cameraConfig);
if (mVirtualCameraController == null) {
throw new UnsupportedOperationException("Virtual camera controller is not available");
@@ -1474,10 +1462,9 @@
}
@Override // Binder call
- @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public int createVirtualDisplay(@NonNull VirtualDisplayConfig virtualDisplayConfig,
@NonNull IVirtualDisplayCallback callback) {
- super.createVirtualDisplay_enforcePermission();
+ checkCallerIsDeviceOwner();
GenericWindowPolicyController gwpc;
synchronized (mVirtualDeviceLock) {
gwpc = createWindowPolicyControllerLocked(virtualDisplayConfig.getDisplayCategories());
@@ -1491,6 +1478,12 @@
boolean isTrustedDisplay =
(mDisplayManagerInternal.getDisplayInfo(displayId).flags & Display.FLAG_TRUSTED)
== Display.FLAG_TRUSTED;
+ if (!isTrustedDisplay) {
+ if (getDevicePolicy(POLICY_TYPE_CLIPBOARD) != DEVICE_POLICY_DEFAULT) {
+ throw new SecurityException("All displays must be trusted for devices with custom"
+ + "clipboard policy.");
+ }
+ }
boolean showPointer;
synchronized (mVirtualDeviceLock) {
@@ -1500,7 +1493,8 @@
"Virtual device already has a virtual display with ID " + displayId);
}
- PowerManager.WakeLock wakeLock = createAndAcquireWakeLockForDisplay(displayId);
+ PowerManager.WakeLock wakeLock =
+ isTrustedDisplay ? createAndAcquireWakeLockForDisplay(displayId) : null;
mVirtualDisplays.put(displayId, new VirtualDisplayWrapper(callback, gwpc, wakeLock,
isTrustedDisplay, isMirrorDisplay));
showPointer = mDefaultShowPointerIcon;
@@ -1508,14 +1502,15 @@
final long token = Binder.clearCallingIdentity();
try {
- mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setMousePointerAccelerationEnabled(false, displayId);
mInputController.setDisplayEligibilityForPointerCapture(/* isEligible= */ false,
displayId);
- // WM throws a SecurityException if the display is untrusted.
if (isTrustedDisplay) {
+ mInputController.setShowPointerIcon(showPointer, displayId);
mInputController.setDisplayImePolicy(displayId,
WindowManager.DISPLAY_IME_POLICY_LOCAL);
+ } else {
+ gwpc.setShowInHostDeviceRecents(true);
}
} finally {
Binder.restoreCallingIdentity(token);
@@ -1616,6 +1611,11 @@
!= PackageManager.PERMISSION_GRANTED) {
synchronized (mVirtualDeviceLock) {
checkDisplayOwnedByVirtualDeviceLocked(displayId);
+ VirtualDisplayWrapper wrapper = mVirtualDisplays.get(displayId);
+ if (!wrapper.isTrusted() && !wrapper.isMirror()) {
+ throw new SecurityException(
+ "Cannot create input device associated with an untrusted display");
+ }
}
}
}
@@ -1629,6 +1629,13 @@
}
}
+ private void checkCallerIsDeviceOwner() {
+ if (Binder.getCallingUid() != mOwnerUid) {
+ throw new SecurityException(
+ "Caller is not the owner of this virtual device");
+ }
+ }
+
void goToSleepInternal(@PowerManager.GoToSleepReason int reason) {
final long now = SystemClock.uptimeMillis();
synchronized (mVirtualDeviceLock) {
@@ -1665,7 +1672,7 @@
* @param virtualDisplayWrapper - VirtualDisplayWrapper to release resources for.
*/
private void releaseOwnedVirtualDisplayResources(VirtualDisplayWrapper virtualDisplayWrapper) {
- virtualDisplayWrapper.getWakeLock().release();
+ virtualDisplayWrapper.releaseWakeLock();
virtualDisplayWrapper.getWindowPolicyController().unregisterRunningAppsChangedListener(
this);
}
@@ -1833,10 +1840,10 @@
VirtualDisplayWrapper(@NonNull IVirtualDisplayCallback token,
@NonNull GenericWindowPolicyController windowPolicyController,
- @NonNull PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
+ @Nullable PowerManager.WakeLock wakeLock, boolean isTrusted, boolean isMirror) {
mToken = Objects.requireNonNull(token);
mWindowPolicyController = Objects.requireNonNull(windowPolicyController);
- mWakeLock = Objects.requireNonNull(wakeLock);
+ mWakeLock = wakeLock;
mIsTrusted = isTrusted;
mIsMirror = isMirror;
}
@@ -1845,8 +1852,10 @@
return mWindowPolicyController;
}
- PowerManager.WakeLock getWakeLock() {
- return mWakeLock;
+ void releaseWakeLock() {
+ if (mWakeLock != null && mWakeLock.isHeld()) {
+ mWakeLock.release();
+ }
}
boolean isTrusted() {
diff --git a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
index 41b6a85..f87e3c3 100644
--- a/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
+++ b/services/companion/java/com/android/server/companion/virtual/VirtualDeviceManagerService.java
@@ -21,6 +21,7 @@
import static com.android.server.wm.ActivityInterceptorCallback.VIRTUAL_DEVICE_SERVICE_ORDERED_ID;
+import android.annotation.EnforcePermission;
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.annotation.RequiresPermission;
@@ -122,7 +123,6 @@
private final CompanionDeviceManager.OnAssociationsChangedListener mCdmAssociationListener =
new CompanionDeviceManager.OnAssociationsChangedListener() {
@Override
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
public void onAssociationsChanged(@NonNull List<AssociationInfo> associations) {
syncVirtualDevicesToCdmAssociations(associations);
}
@@ -339,7 +339,6 @@
return true;
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void syncVirtualDevicesToCdmAssociations(List<AssociationInfo> associations) {
Set<VirtualDeviceImpl> virtualDevicesToRemove = new HashSet<>();
synchronized (mVirtualDeviceManagerLock) {
@@ -382,7 +381,6 @@
cdm.removeOnAssociationsChangedListener(mCdmAssociationListener);
}
- @RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
void onCdmAssociationsChanged(List<AssociationInfo> associations) {
ArrayMap<String, AssociationInfo> vdmAssociations = new ArrayMap<>();
for (int i = 0; i < associations.size(); ++i) {
@@ -452,7 +450,7 @@
}
};
- @android.annotation.EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
+ @EnforcePermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
@Override // Binder call
public IVirtualDevice createVirtualDevice(
IBinder token,
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index bcca20b..f42f91e 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -12824,6 +12824,8 @@
final long lostRAM = memInfo.getTotalSizeKb()
- (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
- memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
+ // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
+ + memInfo.getShmemSizeKb()
- kernelUsed - memInfo.getZramTotalSizeKb();
if (!opts.isCompact) {
pw.print(" Used RAM: "); pw.print(stringifyKBSize(ss[INDEX_TOTAL_PSS] - cachedPss
@@ -13337,6 +13339,8 @@
long lostRAM = memInfo.getTotalSizeKb()
- (ss[INDEX_TOTAL_PSS] - ss[INDEX_TOTAL_SWAP_PSS])
- memInfo.getFreeSizeKb() - memInfo.getCachedSizeKb()
+ // NR_SHMEM is subtracted twice (getCachedSizeKb() and getKernelUsedSizeKb())
+ + memInfo.getShmemSizeKb()
- memInfo.getKernelUsedSizeKb() - memInfo.getZramTotalSizeKb();
proto.write(MemInfoDumpProto.USED_PSS_KB, ss[INDEX_TOTAL_PSS] - cachedPss);
proto.write(MemInfoDumpProto.USED_KERNEL_KB, memInfo.getKernelUsedSizeKb());
diff --git a/services/core/java/com/android/server/am/ProcessList.java b/services/core/java/com/android/server/am/ProcessList.java
index 2485626..5236b03 100644
--- a/services/core/java/com/android/server/am/ProcessList.java
+++ b/services/core/java/com/android/server/am/ProcessList.java
@@ -3629,42 +3629,68 @@
}
@GuardedBy({"mService", "mProcLock"})
- private int updateLruProcessInternalLSP(ProcessRecord app, long now, int index,
- int lruSeq, String what, Object obj, ProcessRecord srcApp) {
+ private int offerLruProcessInternalLSP(ProcessRecord app, long now, String what, Object obj,
+ ProcessRecord srcApp) {
app.setLastActivityTime(now);
if (app.hasActivitiesOrRecentTasks()) {
// Don't want to touch dependent processes that are hosting activities.
- return index;
+ return -1;
}
- int lrui = mLruProcesses.lastIndexOf(app);
+ final int lrui = mLruProcesses.lastIndexOf(app);
if (lrui < 0) {
Slog.wtf(TAG, "Adding dependent process " + app + " not on LRU list: "
+ what + " " + obj + " from " + srcApp);
- return index;
}
+ return lrui;
+ }
- if (lrui >= index) {
- // Don't want to cause this to move dependent processes *back* in the
- // list as if they were less frequently used.
- return index;
- }
+ /**
+ * This method is called after the indices array is populated by the indices offered by
+ * {@link #offerLruProcessInternalLSP} to actually move the processes to the desired locations
+ * in the LRU list. Since the indices array is a SparseBooleanArray, the indices are sorted
+ * and this allows us to preserve the previous order of the processes relative to each other.
+ * Key of the indices array holds the current index of the process in the LRU list and the value
+ * is a boolean indicating whether the process is an activity process or not. Activity processes
+ * are moved to the nextActivityIndex and non-activity processes are moved to the nextIndex
+ * positions, which are provided by the caller.
+ *
+ * @param indices The indices of the processes to move.
+ * @param nextActivityIndex The next index to insert an activity process.
+ * @param nextIndex The next index to insert a non-activity process.
+ */
+ @GuardedBy({"mService", "mProcLock"})
+ private void completeLruProcessInternalLSP(SparseBooleanArray indices, int nextActivityIndex,
+ int nextIndex) {
+ for (int i = indices.size() - 1; i >= 0; i--) {
+ final int lrui = indices.keyAt(i);
+ if (lrui < 0) {
+ // Rest of the indices are invalid, we can return early.
+ return;
+ }
+ final boolean isActivity = indices.valueAt(i);
+ int index = isActivity ? nextActivityIndex : nextIndex;
- if (lrui >= mLruProcessActivityStart && index < mLruProcessActivityStart) {
- // Don't want to touch dependent processes that are hosting activities.
- return index;
- }
+ if (lrui >= index) {
+ // Don't want to cause this to move dependent processes *back* in the
+ // list as if they were less frequently used.
+ continue;
+ }
- mLruProcesses.remove(lrui);
- if (index > 0) {
+ final ProcessRecord app = mLruProcesses.remove(lrui);
index--;
+ if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
+ + " in LRU list: " + app);
+ mLruProcesses.add(index, app);
+ app.setLruSeq(mLruSeq);
+
+ if (isActivity) {
+ nextActivityIndex = index;
+ } else {
+ nextIndex = index;
+ }
}
- if (DEBUG_LRU) Slog.d(TAG_LRU, "Moving dep from " + lrui + " to " + index
- + " in LRU list: " + app);
- mLruProcesses.add(index, app);
- app.setLruSeq(lruSeq);
- return index;
}
/**
@@ -4058,6 +4084,15 @@
app.setLruSeq(mLruSeq);
+ // Key of the indices array holds the current index of the process in the LRU list and the
+ // value is a boolean indicating whether the process is an activity process or not.
+ // Activity processes will be moved to the nextActivityIndex and non-activity processes will
+ // be moved to the nextIndex positions when completeLruProcessInternalLSP is called.
+ // Since SparseBooleanArray's keys are sorted, we'll be able to keep the existing order of
+ // the processes relative to each other after the move.
+ final SparseBooleanArray indices = new SparseBooleanArray(psr.numberOfConnections()
+ + app.mProviders.numberOfProviderConnections());
+
// If the app is currently using a content provider or service,
// bump those processes as well.
for (int j = psr.numberOfConnections() - 1; j >= 0; j--) {
@@ -4069,16 +4104,12 @@
&& !cr.binding.service.app.isPersistent()) {
if (cr.binding.service.app.mServices.hasClientActivities()) {
if (nextActivityIndex >= 0) {
- nextActivityIndex = updateLruProcessInternalLSP(cr.binding.service.app,
- now,
- nextActivityIndex, mLruSeq,
- "service connection", cr, app);
+ indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+ "service connection", cr, app), true);
}
} else {
- nextIndex = updateLruProcessInternalLSP(cr.binding.service.app,
- now,
- nextIndex, mLruSeq,
- "service connection", cr, app);
+ indices.append(offerLruProcessInternalLSP(cr.binding.service.app, now,
+ "service connection", cr, app), false);
}
}
}
@@ -4086,10 +4117,11 @@
for (int j = ppr.numberOfProviderConnections() - 1; j >= 0; j--) {
ContentProviderRecord cpr = ppr.getProviderConnectionAt(j).provider;
if (cpr.proc != null && cpr.proc.getLruSeq() != mLruSeq && !cpr.proc.isPersistent()) {
- nextIndex = updateLruProcessInternalLSP(cpr.proc, now, nextIndex, mLruSeq,
- "provider reference", cpr, app);
+ indices.append(offerLruProcessInternalLSP(cpr.proc, now,
+ "provider reference", cpr, app), false);
}
}
+ completeLruProcessInternalLSP(indices, nextActivityIndex, nextIndex);
}
@GuardedBy(anyOf = {"mService", "mProcLock"})
diff --git a/services/core/java/com/android/server/audio/AudioService.java b/services/core/java/com/android/server/audio/AudioService.java
index d206b20..fdf7dec 100644
--- a/services/core/java/com/android/server/audio/AudioService.java
+++ b/services/core/java/com/android/server/audio/AudioService.java
@@ -286,7 +286,6 @@
import java.util.Set;
import java.util.TreeSet;
import java.util.concurrent.CancellationException;
-import java.util.concurrent.ConcurrentLinkedQueue;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.Executor;
import java.util.concurrent.Executors;
@@ -8049,7 +8048,14 @@
}
synchronized (mAbsoluteVolumeDeviceInfoMapLock) {
if (mAbsoluteVolumeDeviceInfoMap.containsKey(audioSystemDeviceOut)) {
- return mAbsoluteVolumeDeviceInfoMap.get(audioSystemDeviceOut).mDeviceVolumeBehavior;
+ final AbsoluteVolumeDeviceInfo deviceInfo = mAbsoluteVolumeDeviceInfoMap.get(
+ audioSystemDeviceOut);
+ if (deviceInfo != null) {
+ return deviceInfo.mDeviceVolumeBehavior;
+ }
+
+ Log.e(TAG,
+ "Null absolute volume device info stored for key " + audioSystemDeviceOut);
}
}
@@ -15043,6 +15049,11 @@
private void addAudioSystemDeviceOutToAbsVolumeDevices(int audioSystemDeviceOut,
AbsoluteVolumeDeviceInfo info) {
+ if (info == null) {
+ Log.e(TAG, "Cannot add null absolute volume info for audioSystemDeviceOut "
+ + audioSystemDeviceOut);
+ return;
+ }
if (DEBUG_VOL) {
Log.d(TAG, "Adding DeviceType: 0x" + Integer.toHexString(audioSystemDeviceOut)
+ " to mAbsoluteVolumeDeviceInfoMap with behavior "
diff --git a/services/core/java/com/android/server/compat/PlatformCompat.java b/services/core/java/com/android/server/compat/PlatformCompat.java
index a9fe8cb..8d64383 100644
--- a/services/core/java/com/android/server/compat/PlatformCompat.java
+++ b/services/core/java/com/android/server/compat/PlatformCompat.java
@@ -242,7 +242,8 @@
boolean enabled = true;
final int userId = UserHandle.getUserId(uid);
for (String packageName : packages) {
- final var appInfo = getApplicationInfo(packageName, userId);
+ final var appInfo =
+ fixTargetSdk(getApplicationInfo(packageName, userId), uid);
enabled &= isChangeEnabledInternal(changeId, appInfo);
}
return enabled;
@@ -261,7 +262,8 @@
boolean enabled = true;
final int userId = UserHandle.getUserId(uid);
for (String packageName : packages) {
- final var appInfo = getApplicationInfo(packageName, userId);
+ final var appInfo =
+ fixTargetSdk(getApplicationInfo(packageName, userId), uid);
enabled &= isChangeEnabledInternalNoLogging(changeId, appInfo);
}
return enabled;
@@ -504,6 +506,15 @@
packageName, 0, Process.myUid(), userId);
}
+ private ApplicationInfo fixTargetSdk(ApplicationInfo appInfo, int uid) {
+ // b/282922910 - we don't want apps sharing system uid and targeting
+ // older target sdk to impact all system uid apps
+ if (Flags.systemUidTargetSystemSdk() && uid == Process.SYSTEM_UID) {
+ appInfo.targetSdkVersion = Build.VERSION.SDK_INT;
+ }
+ return appInfo;
+ }
+
private void killPackage(String packageName) {
int uid = LocalServices.getService(PackageManagerInternal.class).getPackageUid(packageName,
0, UserHandle.myUserId());
diff --git a/services/core/java/com/android/server/compat/platform_compat_flags.aconfig b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
new file mode 100644
index 0000000..fb32323
--- /dev/null
+++ b/services/core/java/com/android/server/compat/platform_compat_flags.aconfig
@@ -0,0 +1,10 @@
+package: "com.android.server.compat"
+container: "system"
+
+flag {
+ name: "system_uid_target_system_sdk"
+ namespace: "app_compat"
+ description: "Compat framework feature flag for forcing all system uid apps to target system sdk"
+ bug: "29702703"
+ is_fixed_read_only: true
+}
diff --git a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
index 6216a58..8b9c664 100644
--- a/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
+++ b/services/core/java/com/android/server/devicestate/DeviceStateManagerService.java
@@ -778,7 +778,8 @@
processRecord.notifyRequestActiveAsync(request.getToken());
}
- private void registerProcess(int pid, IDeviceStateManagerCallback callback) {
+ @Nullable
+ private DeviceStateInfo registerProcess(int pid, IDeviceStateManagerCallback callback) {
synchronized (mLock) {
if (mProcessRecords.contains(pid)) {
throw new SecurityException("The calling process has already registered an"
@@ -794,15 +795,20 @@
}
mProcessRecords.put(pid, record);
- // Callback clients should not be notified of invalid device states, so calls to
- // #getDeviceStateInfoLocked should be gated on checks if a committed state is present
- // before getting the device state info.
final DeviceStateInfo currentInfo =
mCommittedState.isPresent() ? getDeviceStateInfoLocked() : null;
- if (currentInfo != null) {
- // If there is not a committed state we'll wait to notify the process of the initial
- // value.
- record.notifyDeviceStateInfoAsync(currentInfo);
+ if (com.android.window.flags.Flags.wlinfoOncreate()) {
+ return currentInfo;
+ } else {
+ // Callback clients should not be notified of invalid device states, so calls to
+ // #getDeviceStateInfoLocked should be gated on checks if a committed state is
+ // present before getting the device state info.
+ if (currentInfo != null) {
+ // If there is not a committed state we'll wait to notify the process of the
+ // initial value.
+ record.notifyDeviceStateInfoAsync(currentInfo);
+ }
+ return null;
}
}
}
@@ -1286,8 +1292,9 @@
}
}
+ @Nullable
@Override // Binder call
- public void registerCallback(IDeviceStateManagerCallback callback) {
+ public DeviceStateInfo registerCallback(IDeviceStateManagerCallback callback) {
if (callback == null) {
throw new IllegalArgumentException("Device state callback must not be null.");
}
@@ -1295,7 +1302,7 @@
final int callingPid = Binder.getCallingPid();
final long token = Binder.clearCallingIdentity();
try {
- registerProcess(callingPid, callback);
+ return registerProcess(callingPid, callback);
} finally {
Binder.restoreCallingIdentity(token);
}
diff --git a/services/core/java/com/android/server/display/DisplayManagerService.java b/services/core/java/com/android/server/display/DisplayManagerService.java
index 179ec63..c7a70fa 100644
--- a/services/core/java/com/android/server/display/DisplayManagerService.java
+++ b/services/core/java/com/android/server/display/DisplayManagerService.java
@@ -1769,10 +1769,11 @@
flags &= ~VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
}
// Put the display in the virtual device's display group only if it's not a mirror display,
- // and if it doesn't need its own display group. So effectively, mirror displays go into the
- // default display group.
+ // it is a trusted display, and it doesn't need its own display group. So effectively,
+ // mirror and untrusted displays go into the default display group.
if ((flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) == 0
&& (flags & VIRTUAL_DISPLAY_FLAG_AUTO_MIRROR) == 0
+ && (flags & VIRTUAL_DISPLAY_FLAG_TRUSTED) == VIRTUAL_DISPLAY_FLAG_TRUSTED
&& virtualDevice != null) {
flags |= VIRTUAL_DISPLAY_FLAG_DEVICE_DISPLAY_GROUP;
}
@@ -1848,9 +1849,7 @@
if (callingUid != Process.SYSTEM_UID
&& (flags & VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP) != 0) {
- // The virtualDevice instance has been validated above using isValidVirtualDevice
- if (virtualDevice == null
- && !checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
+ if (!checkCallingPermission(ADD_TRUSTED_DISPLAY, "createVirtualDisplay()")) {
throw new SecurityException("Requires ADD_TRUSTED_DISPLAY permission to "
+ "create a virtual display which is not in the default DisplayGroup.");
}
@@ -5667,6 +5666,11 @@
displayPowerController.stylusGestureStarted(eventTime);
}
}
+
+ @Override
+ public boolean isDisplayReadyForMirroring(int displayId) {
+ return mExternalDisplayPolicy.isDisplayReadyForMirroring(displayId);
+ }
}
class DesiredDisplayModeSpecsObserver
diff --git a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
index 28a0b28..f34d2cc 100644
--- a/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
+++ b/services/core/java/com/android/server/display/ExternalDisplayPolicy.java
@@ -375,6 +375,54 @@
}
}
+ boolean isDisplayReadyForMirroring(int displayId) {
+ if (!mFlags.isWaitingConfirmationBeforeMirroringEnabled()) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring CONFIRMED - "
+ + " flag 'waiting for confirmation before mirroring' is disabled");
+ }
+ return true;
+ }
+
+ synchronized (mSyncRoot) {
+ if (!mIsBootCompleted) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "boot is in progress");
+ }
+ return false;
+ }
+
+ var logicalDisplay = mLogicalDisplayMapper.getDisplayLocked(displayId);
+ if (logicalDisplay == null) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay is null");
+ }
+ return false;
+ }
+
+ if (!isExternalDisplayLocked(logicalDisplay)) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay" + logicalDisplay.getDisplayIdLocked()
+ + " type is " + logicalDisplay.getDisplayInfoLocked().type);
+ }
+ return false;
+ }
+
+ if (!logicalDisplay.isEnabledLocked()) {
+ if (DEBUG) {
+ Slog.d(TAG, "isDisplayReadyForMirroring: mirroring is not confirmed - "
+ + "logicalDisplay is disabled");
+ }
+ return false;
+ }
+ }
+
+ return true;
+ }
+
private final class SkinThermalStatusObserver extends IThermalEventListener.Stub {
@Override
public void notifyThrottling(@NonNull final Temperature temp) {
diff --git a/services/core/java/com/android/server/display/LogicalDisplayMapper.java b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
index 06a9103..09fa4e6 100644
--- a/services/core/java/com/android/server/display/LogicalDisplayMapper.java
+++ b/services/core/java/com/android/server/display/LogicalDisplayMapper.java
@@ -16,6 +16,7 @@
package com.android.server.display;
+import static android.hardware.devicestate.DeviceState.PROPERTY_EMULATED_ONLY;
import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_SLEEP;
import static android.hardware.devicestate.DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE;
import static android.hardware.devicestate.DeviceStateManager.INVALID_DEVICE_STATE;
@@ -594,6 +595,13 @@
boolean shouldDeviceBeWoken(DeviceState pendingState, DeviceState currentState,
boolean isInteractive, boolean isBootCompleted) {
if (mDeviceStateManagerFlags.deviceStatePropertyMigration()) {
+ if (currentState.hasProperties(PROPERTY_EMULATED_ONLY)
+ && !pendingState.hasProperties(PROPERTY_EMULATED_ONLY)) {
+ // Do not wake the device, since this transition may occur due to the user pressing
+ // the power button to exit an emulated state.
+ return false;
+ }
+
return pendingState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
&& !currentState.equals(INVALID_DEVICE_STATE)
&& !currentState.hasProperty(PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE)
diff --git a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
index 99ced7f..b2e98bc 100644
--- a/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
+++ b/services/core/java/com/android/server/display/feature/DisplayManagerFlags.java
@@ -217,6 +217,11 @@
Flags::enableUserRefreshRateForExternalDisplay
);
+ private final FlagState mEnableWaitingConfirmationBeforeMirroring = new FlagState(
+ Flags.FLAG_ENABLE_WAITING_CONFIRMATION_BEFORE_MIRRORING,
+ Flags::enableWaitingConfirmationBeforeMirroring
+ );
+
private final FlagState mEnableBatteryStatsForAllDisplays = new FlagState(
Flags.FLAG_ENABLE_BATTERY_STATS_FOR_ALL_DISPLAYS,
Flags::enableBatteryStatsForAllDisplays
@@ -445,6 +450,14 @@
}
/**
+ * @return {@code true} if mirroring won't be enabled until boot completes and the user enables
+ * the display.
+ */
+ public boolean isWaitingConfirmationBeforeMirroringEnabled() {
+ return mEnableWaitingConfirmationBeforeMirroring.isEnabled();
+ }
+
+ /**
* @return {@code true} if battery stats is enabled for all displays, not just the primary
* display.
*/
@@ -511,6 +524,7 @@
pw.println(" " + mVirtualDisplayLimit);
pw.println(" " + mNormalBrightnessForDozeParameter);
pw.println(" " + mIdleScreenConfigInSubscribingLightSensor);
+ pw.println(" " + mEnableWaitingConfirmationBeforeMirroring);
pw.println(" " + mEnableBatteryStatsForAllDisplays);
pw.println(" " + mBlockAutobrightnessChangesOnStylusUsage);
pw.println(" " + mIsUserRefreshRateForExternalDisplayEnabled);
diff --git a/services/core/java/com/android/server/display/feature/display_flags.aconfig b/services/core/java/com/android/server/display/feature/display_flags.aconfig
index 2f04d9e..df62638 100644
--- a/services/core/java/com/android/server/display/feature/display_flags.aconfig
+++ b/services/core/java/com/android/server/display/feature/display_flags.aconfig
@@ -367,6 +367,17 @@
}
flag {
+ name: "enable_waiting_confirmation_before_mirroring"
+ namespace: "display_manager"
+ description: "Allow ContentRecorder checking whether user confirmed mirroring after boot"
+ bug: "361698995"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "enable_battery_stats_for_all_displays"
namespace: "display_manager"
description: "Flag to enable battery stats for all displays."
diff --git a/services/core/java/com/android/server/input/InputSettingsObserver.java b/services/core/java/com/android/server/input/InputSettingsObserver.java
index d70bd8b..d1a6d3b 100644
--- a/services/core/java/com/android/server/input/InputSettingsObserver.java
+++ b/services/core/java/com/android/server/input/InputSettingsObserver.java
@@ -63,6 +63,12 @@
mObservers = Map.ofEntries(
Map.entry(Settings.System.getUriFor(Settings.System.POINTER_SPEED),
(reason) -> updateMousePointerSpeed()),
+ Map.entry(Settings.System.getUriFor(
+ Settings.System.MOUSE_REVERSE_VERTICAL_SCROLLING),
+ (reason) -> updateMouseReverseVerticalScrolling()),
+ Map.entry(Settings.System.getUriFor(
+ Settings.System.MOUSE_SWAP_PRIMARY_BUTTON),
+ (reason) -> updateMouseSwapPrimaryButton()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_POINTER_SPEED),
(reason) -> updateTouchpadPointerSpeed()),
Map.entry(Settings.System.getUriFor(Settings.System.TOUCHPAD_NATURAL_SCROLLING),
@@ -163,6 +169,16 @@
mNative.setPointerSpeed(constrainPointerSpeedValue(speed));
}
+ private void updateMouseReverseVerticalScrolling() {
+ mNative.setMouseReverseVerticalScrollingEnabled(
+ InputSettings.isMouseReverseVerticalScrollingEnabled(mContext));
+ }
+
+ private void updateMouseSwapPrimaryButton() {
+ mNative.setMouseSwapPrimaryButtonEnabled(
+ InputSettings.isMouseSwapPrimaryButtonEnabled(mContext));
+ }
+
private void updateTouchpadPointerSpeed() {
mNative.setTouchpadPointerSpeed(
constrainPointerSpeedValue(InputSettings.getTouchpadPointerSpeed(mContext)));
diff --git a/services/core/java/com/android/server/input/NativeInputManagerService.java b/services/core/java/com/android/server/input/NativeInputManagerService.java
index 4404d63..21e8bcc 100644
--- a/services/core/java/com/android/server/input/NativeInputManagerService.java
+++ b/services/core/java/com/android/server/input/NativeInputManagerService.java
@@ -127,6 +127,10 @@
void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
+ void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+ void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
void setTouchpadPointerSpeed(int speed);
void setTouchpadNaturalScrollingEnabled(boolean enabled);
@@ -388,6 +392,12 @@
public native void setMousePointerAccelerationEnabled(int displayId, boolean enabled);
@Override
+ public native void setMouseReverseVerticalScrollingEnabled(boolean enabled);
+
+ @Override
+ public native void setMouseSwapPrimaryButtonEnabled(boolean enabled);
+
+ @Override
public native void setTouchpadPointerSpeed(int speed);
@Override
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index f0fb33e..35b5171 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -84,6 +84,7 @@
import android.content.pm.ServiceInfo;
import android.content.pm.UserInfo;
import android.content.res.Resources;
+import android.hardware.display.DisplayManagerInternal;
import android.hardware.input.InputManager;
import android.inputmethodservice.InputMethodService;
import android.inputmethodservice.InputMethodService.BackDispositionMode;
@@ -119,6 +120,7 @@
import android.util.Slog;
import android.util.SparseArray;
import android.util.proto.ProtoOutputStream;
+import android.view.Display;
import android.view.InputChannel;
import android.view.InputDevice;
import android.view.MotionEvent;
@@ -448,6 +450,8 @@
private AudioManagerInternal mAudioManagerInternal = null;
@Nullable
private VirtualDeviceManagerInternal mVdmInternal = null;
+ @Nullable
+ private DisplayManagerInternal mDisplayManagerInternal = null;
// Mapping from deviceId to the device-specific imeId for that device.
@GuardedBy("ImfLock.class")
@@ -2165,7 +2169,18 @@
final var bindingController = getInputMethodBindingController(userId);
final int oldDeviceId = bindingController.getDeviceIdToShowIme();
final int displayIdToShowIme = bindingController.getDisplayIdToShowIme();
- final int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+ int newDeviceId = mVdmInternal.getDeviceIdForDisplayId(displayIdToShowIme);
+ if (newDeviceId != DEVICE_ID_DEFAULT) {
+ // Only show custom IME on trusted displays.
+ if (mDisplayManagerInternal == null) {
+ mDisplayManagerInternal = LocalServices.getService(DisplayManagerInternal.class);
+ }
+ int displayFlags = mDisplayManagerInternal.getDisplayInfo(displayIdToShowIme).flags;
+ if ((displayFlags & Display.FLAG_TRUSTED) != Display.FLAG_TRUSTED) {
+ // If the display is not trusted, fallback to the default device IME.
+ newDeviceId = DEVICE_ID_DEFAULT;
+ }
+ }
bindingController.setDeviceIdToShowIme(newDeviceId);
if (newDeviceId == DEVICE_ID_DEFAULT) {
if (oldDeviceId == DEVICE_ID_DEFAULT) {
diff --git a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
index c02b103..404c841 100644
--- a/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
+++ b/services/core/java/com/android/server/locksettings/recoverablekeystore/storage/RecoverySnapshotStorage.java
@@ -19,7 +19,6 @@
import android.annotation.Nullable;
import android.os.Environment;
import android.security.keystore.recovery.KeyChainSnapshot;
-import android.util.Log;
import android.util.SparseArray;
import com.android.internal.annotations.GuardedBy;
@@ -29,9 +28,11 @@
import com.android.server.locksettings.recoverablekeystore.serialization
.KeyChainSnapshotParserException;
import com.android.server.locksettings.recoverablekeystore.serialization.KeyChainSnapshotSerializer;
+import com.android.server.utils.Slogf;
import java.io.File;
import java.io.FileInputStream;
+import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.security.cert.CertificateEncodingException;
@@ -81,12 +82,14 @@
public synchronized void put(int uid, KeyChainSnapshot snapshot) {
mSnapshotByUid.put(uid, snapshot);
- try {
- writeToDisk(uid, snapshot);
+ File snapshotFile = getSnapshotFile(uid);
+ try (FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)) {
+ KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
} catch (IOException | CertificateEncodingException e) {
- Log.e(TAG,
- String.format(Locale.US, "Error persisting snapshot for %d to disk", uid),
- e);
+ // If we fail to write the latest snapshot, we should delete any older snapshot that
+ // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
+ snapshotFile.delete();
+ Slogf.e(TAG, e, "Error persisting snapshot for %d to disk", uid);
}
}
@@ -100,10 +103,17 @@
return snapshot;
}
- try {
- return readFromDisk(uid);
+ File snapshotFile = getSnapshotFile(uid);
+ try (FileInputStream fileInputStream = new FileInputStream(snapshotFile)) {
+ return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
+ } catch (FileNotFoundException e) {
+ Slogf.i(TAG, "Snapshot for uid %d not found", uid);
+ return null;
} catch (IOException | KeyChainSnapshotParserException e) {
- Log.e(TAG, String.format(Locale.US, "Error reading snapshot for %d from disk", uid), e);
+ // If we fail to read the latest snapshot, we should delete it in case it is in some way
+ // corrupted. We can regenerate snapshots anyway.
+ snapshotFile.delete();
+ Slogf.e(TAG, e, "Error reading snapshot for %d from disk", uid);
return null;
}
}
@@ -116,50 +126,6 @@
getSnapshotFile(uid).delete();
}
- /**
- * Writes the snapshot for recovery agent {@code uid} to disk.
- *
- * @throws IOException if an IO error occurs writing to disk.
- */
- private void writeToDisk(int uid, KeyChainSnapshot snapshot)
- throws IOException, CertificateEncodingException {
- File snapshotFile = getSnapshotFile(uid);
-
- try (
- FileOutputStream fileOutputStream = new FileOutputStream(snapshotFile)
- ) {
- KeyChainSnapshotSerializer.serialize(snapshot, fileOutputStream);
- } catch (IOException | CertificateEncodingException e) {
- // If we fail to write the latest snapshot, we should delete any older snapshot that
- // happens to be around. Otherwise snapshot syncs might end up going 'back in time'.
- snapshotFile.delete();
- throw e;
- }
- }
-
- /**
- * Reads the last snapshot for recovery agent {@code uid} from disk.
- *
- * @return The snapshot, or null if none existed.
- * @throws IOException if an IO error occurs reading from disk.
- */
- @Nullable
- private KeyChainSnapshot readFromDisk(int uid)
- throws IOException, KeyChainSnapshotParserException {
- File snapshotFile = getSnapshotFile(uid);
-
- try (
- FileInputStream fileInputStream = new FileInputStream(snapshotFile)
- ) {
- return KeyChainSnapshotDeserializer.deserialize(fileInputStream);
- } catch (IOException | KeyChainSnapshotParserException e) {
- // If we fail to read the latest snapshot, we should delete it in case it is in some way
- // corrupted. We can regenerate snapshots anyway.
- snapshotFile.delete();
- throw e;
- }
- }
-
private File getSnapshotFile(int uid) {
File folder = getStorageFolder();
String fileName = getSnapshotFileName(uid);
diff --git a/services/core/java/com/android/server/pm/InstallRequest.java b/services/core/java/com/android/server/pm/InstallRequest.java
index 1f79ac0..089bbb7 100644
--- a/services/core/java/com/android/server/pm/InstallRequest.java
+++ b/services/core/java/com/android/server/pm/InstallRequest.java
@@ -16,7 +16,6 @@
package com.android.server.pm;
-import static android.content.pm.Flags.improveInstallFreeze;
import static android.content.pm.PackageInstaller.SessionParams.USER_ACTION_UNSPECIFIED;
import static android.content.pm.PackageManager.INSTALL_REASON_UNKNOWN;
import static android.content.pm.PackageManager.INSTALL_SCENARIO_DEFAULT;
@@ -1050,13 +1049,13 @@
}
public void onFreezeStarted() {
- if (mPackageMetrics != null && improveInstallFreeze()) {
+ if (mPackageMetrics != null) {
mPackageMetrics.onStepStarted(PackageMetrics.STEP_FREEZE_INSTALL);
}
}
public void onFreezeCompleted() {
- if (mPackageMetrics != null && improveInstallFreeze()) {
+ if (mPackageMetrics != null) {
mPackageMetrics.onStepFinished(PackageMetrics.STEP_FREEZE_INSTALL);
}
}
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 8bab9de..708e067 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -1101,7 +1101,7 @@
if (android.multiuser.Flags.cachesNotInvalidatedAtStartReadOnly()) {
UserManager.invalidateIsUserUnlockedCache();
UserManager.invalidateQuietModeEnabledCache();
- UserManager.invalidateUserSerialNumberCache();
+ UserManager.invalidateCacheOnUserListChange();
}
}
@@ -4448,7 +4448,7 @@
if (userData != null) {
synchronized (mUsersLock) {
- mUsers.put(userData.info.id, userData);
+ addUserDataLU(userData);
if (mNextSerialNumber < 0
|| mNextSerialNumber <= userData.info.id) {
mNextSerialNumber = userData.info.id + 1;
@@ -5724,7 +5724,7 @@
userData.info = userInfo;
userData.userProperties = new UserProperties(
userTypeDetails.getDefaultUserPropertiesReference());
- mUsers.put(userId, userData);
+ addUserDataLU(userData);
}
writeUserLP(userData);
writeUserListLP();
@@ -6138,7 +6138,7 @@
final UserData userData = new UserData();
userData.info = userInfo;
synchronized (mUsersLock) {
- mUsers.put(userInfo.id, userData);
+ addUserDataLU(userData);
}
updateUserIds();
return userData;
@@ -6148,8 +6148,7 @@
@VisibleForTesting
void removeUserInfo(@UserIdInt int userId) {
synchronized (mUsersLock) {
- UserManager.invalidateUserSerialNumberCache();
- mUsers.remove(userId);
+ removeUserDataLU(userId);
}
}
@@ -6579,8 +6578,7 @@
// Remove this user from the list
synchronized (mUsersLock) {
- UserManager.invalidateUserSerialNumberCache();
- mUsers.remove(userId);
+ removeUserDataLU(userId);
mIsUserManaged.delete(userId);
}
synchronized (mUserStates) {
@@ -6969,6 +6967,26 @@
}
/**
+ * Adding user data to mUsers list in one place to invalidate related caches.
+ */
+ @GuardedBy("mUsersLock")
+ private void addUserDataLU(UserData userData) {
+ if (android.multiuser.Flags.invalidateCacheOnUsersChangedReadOnly()) {
+ UserManager.invalidateCacheOnUserListChange();
+ }
+ mUsers.put(userData.info.id, userData);
+ }
+
+ /**
+ * Removing user data to mUsers list in one place to invalidate related caches.
+ */
+ @GuardedBy("mUsersLock")
+ private void removeUserDataLU(@UserIdInt int userId) {
+ UserManager.invalidateCacheOnUserListChange();
+ mUsers.remove(userId);
+ }
+
+ /**
* Caches the list of user ids in an array, adjusting the array size when necessary.
*/
private void updateUserIds() {
diff --git a/services/core/java/com/android/server/wm/ConfigurationContainer.java b/services/core/java/com/android/server/wm/ConfigurationContainer.java
index 670a61d..05dcbb7 100644
--- a/services/core/java/com/android/server/wm/ConfigurationContainer.java
+++ b/services/core/java/com/android/server/wm/ConfigurationContainer.java
@@ -25,6 +25,7 @@
import static android.app.WindowConfiguration.ROTATION_UNDEFINED;
import static android.app.WindowConfiguration.WINDOWING_MODE_FREEFORM;
import static android.app.WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+import static android.app.WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW;
import static android.app.WindowConfiguration.WINDOWING_MODE_PINNED;
import static android.app.WindowConfiguration.WINDOWING_MODE_UNDEFINED;
import static android.app.WindowConfiguration.activityTypeToString;
@@ -268,7 +269,16 @@
}
final DisplayPolicy.DecorInsets.Info decor =
displayContent.getDisplayPolicy().getDecorInsetsInfo(rotation, dw, dh);
- outAppBounds.intersectUnchecked(decor.mOverrideNonDecorFrame);
+ if (!outAppBounds.intersect(decor.mOverrideNonDecorFrame)) {
+ // TODO (b/364883053): When a split screen is requested from an app intent for a new
+ // task, the bounds is not the final bounds, and this is also not a bounds change
+ // event handled correctly with the offset. Revert back to legacy method for this
+ // case.
+ if (inOutConfig.windowConfiguration.getWindowingMode()
+ == WINDOWING_MODE_MULTI_WINDOW) {
+ outAppBounds.inset(decor.mOverrideNonDecorInsets);
+ }
+ }
if (task != null && (task.mOffsetYForInsets != 0 || task.mOffsetXForInsets != 0)) {
outAppBounds.offset(-task.mOffsetXForInsets, -task.mOffsetYForInsets);
}
diff --git a/services/core/java/com/android/server/wm/ContentRecorder.java b/services/core/java/com/android/server/wm/ContentRecorder.java
index bc33946..0b5872b 100644
--- a/services/core/java/com/android/server/wm/ContentRecorder.java
+++ b/services/core/java/com/android/server/wm/ContentRecorder.java
@@ -285,6 +285,11 @@
}
}
+ private boolean isDisplayReadyForMirroring() {
+ return mDisplayContent.getDisplayInfo().type != Display.TYPE_EXTERNAL
+ || mDisplayContent.mWmService.mDisplayManagerInternal.isDisplayReadyForMirroring(
+ mDisplayContent.getDisplayId());
+ }
/**
* Ensure recording does not fall back to the display stack; ensure the recording is stopped
@@ -335,7 +340,7 @@
return;
}
- if (mContentRecordingSession.isWaitingForConsent()) {
+ if (mContentRecordingSession.isWaitingForConsent() || !isDisplayReadyForMirroring()) {
ProtoLog.v(WM_DEBUG_CONTENT_RECORDING, "Content Recording: waiting to record, so do "
+ "nothing");
return;
diff --git a/services/core/jni/com_android_server_input_InputManagerService.cpp b/services/core/jni/com_android_server_input_InputManagerService.cpp
index efca902..248ed1a 100644
--- a/services/core/jni/com_android_server_input_InputManagerService.cpp
+++ b/services/core/jni/com_android_server_input_InputManagerService.cpp
@@ -337,6 +337,8 @@
int32_t getMousePointerSpeed();
void setPointerSpeed(int32_t speed);
void setMousePointerAccelerationEnabled(ui::LogicalDisplayId displayId, bool enabled);
+ void setMouseReverseVerticalScrollingEnabled(bool enabled);
+ void setMouseSwapPrimaryButtonEnabled(bool enabled);
void setTouchpadPointerSpeed(int32_t speed);
void setTouchpadNaturalScrollingEnabled(bool enabled);
void setTouchpadTapToClickEnabled(bool enabled);
@@ -482,6 +484,12 @@
// True if stylus button reporting through motion events is enabled.
bool stylusButtonMotionEventsEnabled{true};
+ // True if mouse vertical scrolling is reversed.
+ bool mouseReverseVerticalScrollingEnabled{false};
+
+ // True if the mouse primary button is swapped (left/right buttons).
+ bool mouseSwapPrimaryButtonEnabled{false};
+
// The touchpad pointer speed, as a number from -7 (slowest) to 7 (fastest).
int32_t touchpadPointerSpeed{0};
@@ -762,6 +770,10 @@
outConfig->defaultPointerDisplayId = mLocked.pointerDisplayId;
+ outConfig->mouseReverseVerticalScrollingEnabled =
+ mLocked.mouseReverseVerticalScrollingEnabled;
+ outConfig->mouseSwapPrimaryButtonEnabled = mLocked.mouseSwapPrimaryButtonEnabled;
+
outConfig->touchpadPointerSpeed = mLocked.touchpadPointerSpeed;
outConfig->touchpadNaturalScrollingEnabled = mLocked.touchpadNaturalScrollingEnabled;
outConfig->touchpadTapToClickEnabled = mLocked.touchpadTapToClickEnabled;
@@ -1317,6 +1329,36 @@
return mLocked.pointerSpeed;
}
+void NativeInputManager::setMouseReverseVerticalScrollingEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.mouseReverseVerticalScrollingEnabled == enabled) {
+ return;
+ }
+
+ mLocked.mouseReverseVerticalScrollingEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
+void NativeInputManager::setMouseSwapPrimaryButtonEnabled(bool enabled) {
+ { // acquire lock
+ std::scoped_lock _l(mLock);
+
+ if (mLocked.mouseSwapPrimaryButtonEnabled == enabled) {
+ return;
+ }
+
+ mLocked.mouseSwapPrimaryButtonEnabled = enabled;
+ } // release lock
+
+ mInputManager->getReader().requestRefreshConfiguration(
+ InputReaderConfiguration::Change::MOUSE_SETTINGS);
+}
+
void NativeInputManager::setPointerSpeed(int32_t speed) {
{ // acquire lock
std::scoped_lock _l(mLock);
@@ -3002,6 +3044,18 @@
return static_cast<jint>(im->getInputManager()->getReader().getLastUsedInputDeviceId());
}
+static void nativeSetMouseReverseVerticalScrollingEnabled(JNIEnv* env, jobject nativeImplObj,
+ bool enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->setMouseReverseVerticalScrollingEnabled(enabled);
+}
+
+static void nativeSetMouseSwapPrimaryButtonEnabled(JNIEnv* env, jobject nativeImplObj,
+ bool enabled) {
+ NativeInputManager* im = getNativeInputManager(env, nativeImplObj);
+ im->setMouseSwapPrimaryButtonEnabled(enabled);
+}
+
// ----------------------------------------------------------------------------
static const JNINativeMethod gInputManagerMethods[] = {
@@ -3048,6 +3102,9 @@
{"setPointerSpeed", "(I)V", (void*)nativeSetPointerSpeed},
{"setMousePointerAccelerationEnabled", "(IZ)V",
(void*)nativeSetMousePointerAccelerationEnabled},
+ {"setMouseReverseVerticalScrollingEnabled", "(Z)V",
+ (void*)nativeSetMouseReverseVerticalScrollingEnabled},
+ {"setMouseSwapPrimaryButtonEnabled", "(Z)V", (void*)nativeSetMouseSwapPrimaryButtonEnabled},
{"setTouchpadPointerSpeed", "(I)V", (void*)nativeSetTouchpadPointerSpeed},
{"setTouchpadNaturalScrollingEnabled", "(Z)V",
(void*)nativeSetTouchpadNaturalScrollingEnabled},
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 2836d46..2add5b0 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -349,7 +349,7 @@
return nullptr;
}
- // Return the currently watched pids. The lock must be held.
+ // Return the currently watched pids as a comma-separated list. The lock must be held.
std::string watchedPidsLocked() const {
if (watched_.size() == 0) return "none";
bool first = true;
@@ -357,6 +357,7 @@
for (auto i = watched_.cbegin(); i != watched_.cend(); i++) {
if (first) {
result += StringPrintf("%d", *i);
+ first = false;
} else {
result += StringPrintf(",%d", *i);
}
diff --git a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
index 1a1c8e5..94eab9c 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/DisplayManagerServiceTest.java
@@ -25,6 +25,7 @@
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_CONTENT_ONLY;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP;
import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_PRESENTATION;
+import static android.hardware.display.DisplayManager.VIRTUAL_DISPLAY_FLAG_TRUSTED;
import static android.view.ContentRecordingSession.RECORD_CONTENT_DISPLAY;
import static android.view.ContentRecordingSession.RECORD_CONTENT_TASK;
import static android.view.Display.HdrCapabilities.HDR_TYPE_INVALID;
@@ -1068,9 +1069,9 @@
firstDisplayId);
}
- /** Tests that the virtual device is created in a device display group. */
+ /** Tests that a trusted virtual display is created in a device display group. */
@Test
- public void createVirtualDisplay_addsDisplaysToDeviceDisplayGroups() throws Exception {
+ public void createVirtualDisplay_addsTrustedDisplaysToDeviceDisplayGroups() throws Exception {
DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
DisplayManagerInternal localService = displayManager.new LocalService();
@@ -1081,12 +1082,16 @@
IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+
+ when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
// Create a first virtual display. A display group should be created for this display on the
// virtual device.
final VirtualDisplayConfig.Builder builder1 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group 1");
-
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId1 =
localService.createVirtualDisplay(
builder1.build(),
@@ -1097,12 +1102,14 @@
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+ assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
// Create a second virtual display. This should be added to the previously created display
// group.
final VirtualDisplayConfig.Builder builder2 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group 1");
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId2 =
localService.createVirtualDisplay(
@@ -1121,6 +1128,36 @@
displayGroupId2);
}
+ /** Tests that an untrusted virtual display is created in the default display group. */
+ @Test
+ public void createVirtualDisplay_addsUntrustedDisplayToDefaultDisplayGroups() throws Exception {
+ DisplayManagerService displayManager = new DisplayManagerService(mContext, mBasicInjector);
+ DisplayManagerInternal localService = displayManager.new LocalService();
+
+ registerDefaultDisplays(displayManager);
+ when(mMockAppToken.asBinder()).thenReturn(mMockAppToken);
+
+ IVirtualDevice virtualDevice = mock(IVirtualDevice.class);
+ when(virtualDevice.getDeviceId()).thenReturn(1);
+ when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ // Create the virtual display. It is untrusted, so it should go into the default group.
+ final VirtualDisplayConfig.Builder builder =
+ new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
+ .setUniqueId("uniqueId --- device display group");
+
+ int displayId =
+ localService.createVirtualDisplay(
+ builder.build(),
+ mMockAppToken /* callback */,
+ virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class),
+ PACKAGE_NAME);
+ verify(mMockProjectionService, never()).setContentRecordingSession(any(),
+ nullable(IMediaProjection.class));
+ int displayGroupId = localService.getDisplayInfo(displayId).displayGroupId;
+ assertEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
+ }
+
/**
* Tests that the virtual display is not added to the device display group when
* VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is set.
@@ -1138,11 +1175,15 @@
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
+ when(mContext.checkCallingPermission(ADD_TRUSTED_DISPLAY))
+ .thenReturn(PackageManager.PERMISSION_GRANTED);
+
// Create a first virtual display. A display group should be created for this display on the
// virtual device.
final VirtualDisplayConfig.Builder builder1 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setUniqueId("uniqueId --- device display group");
+ .setUniqueId("uniqueId --- device display group")
+ .setFlags(VIRTUAL_DISPLAY_FLAG_TRUSTED);
int displayId1 =
localService.createVirtualDisplay(
@@ -1154,12 +1195,14 @@
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId1 = localService.getDisplayInfo(displayId1).displayGroupId;
+ assertNotEquals(displayGroupId1, Display.DEFAULT_DISPLAY_GROUP);
// Create a second virtual display. With the flag VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP,
// the display should not be added to the previously created display group.
final VirtualDisplayConfig.Builder builder2 =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
- .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED)
.setUniqueId("uniqueId --- own display group");
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
@@ -1174,6 +1217,7 @@
verify(mMockProjectionService, never()).setContentRecordingSession(any(),
nullable(IMediaProjection.class));
int displayGroupId2 = localService.getDisplayInfo(displayId2).displayGroupId;
+ assertNotEquals(displayGroupId2, Display.DEFAULT_DISPLAY_GROUP);
assertNotEquals(
"Display 1 should be in the device display group and display 2 in its own display"
@@ -1208,7 +1252,8 @@
final VirtualDisplayConfig deviceDisplayGroupDisplayConfig =
new VirtualDisplayConfig.Builder(VIRTUAL_DISPLAY_NAME, 600, 800, 320)
.setUniqueId("uniqueId --- device display group 1")
- .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED)
+ .setFlags(VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED)
.build();
int deviceDisplayGroupDisplayId =
@@ -1235,6 +1280,7 @@
.setUniqueId("uniqueId --- own display group 1")
.setFlags(
VIRTUAL_DISPLAY_FLAG_ALWAYS_UNLOCKED
+ | VIRTUAL_DISPLAY_FLAG_TRUSTED
| VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP)
.build();
@@ -1852,7 +1898,7 @@
/**
* Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when the permission
- * ADD_TRUSTED_DISPLAY is granted.
+ * ADD_TRUSTED_DISPLAY is granted and that display is not in the default display group.
*/
@Test
public void testOwnDisplayGroup_allowCreationWithAddTrustedDisplayPermission()
@@ -1881,6 +1927,9 @@
DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
assertNotNull(ddi);
assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+
+ int displayGroupId = bs.getDisplayInfo(displayId).displayGroupId;
+ assertNotEquals(displayGroupId, Display.DEFAULT_DISPLAY_GROUP);
}
/**
@@ -1915,11 +1964,11 @@
}
/**
- * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is allowed when called with
- * a virtual device, even if ADD_TRUSTED_DISPLAY is not granted.
+ * Tests that specifying VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP is not allowed when called with
+ * a virtual device, if ADD_TRUSTED_DISPLAY is not granted.
*/
@Test
- public void testOwnDisplayGroup_allowCreationWithVirtualDevice() throws Exception {
+ public void testOwnDisplayGroup_disallowCreationWithVirtualDevice() throws Exception {
DisplayManagerService displayManager =
new DisplayManagerService(mContext, mBasicInjector);
DisplayManagerInternal localService = displayManager.new LocalService();
@@ -1940,16 +1989,16 @@
when(virtualDevice.getDeviceId()).thenReturn(1);
when(mIVirtualDeviceManager.isValidVirtualDeviceId(1)).thenReturn(true);
- int displayId = localService.createVirtualDisplay(builder.build(),
- mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
- mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
- verify(mMockProjectionService, never()).setContentRecordingSession(any(),
- nullable(IMediaProjection.class));
- performTraversalInternal(displayManager);
- displayManager.getDisplayHandler().runWithScissors(() -> {}, 0 /* now */);
- DisplayDeviceInfo ddi = displayManager.getDisplayDeviceInfoInternal(displayId);
- assertNotNull(ddi);
- assertNotEquals(0, ddi.flags & DisplayDeviceInfo.FLAG_OWN_DISPLAY_GROUP);
+ try {
+ localService.createVirtualDisplay(builder.build(),
+ mMockAppToken /* callback */, virtualDevice /* virtualDeviceToken */,
+ mock(DisplayWindowPolicyController.class), PACKAGE_NAME);
+ fail("Creating virtual display with VIRTUAL_DISPLAY_FLAG_OWN_DISPLAY_GROUP without "
+ + "ADD_TRUSTED_DISPLAY permission should throw SecurityException even if "
+ + "called with a virtual device.");
+ } catch (SecurityException e) {
+ // SecurityException is expected
+ }
}
/**
diff --git a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
index f728168..782262d 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/ExternalDisplayPolicyTest.java
@@ -18,6 +18,7 @@
import static android.hardware.display.DisplayManagerGlobal.EVENT_DISPLAY_CONNECTED;
import static android.view.Display.TYPE_EXTERNAL;
+import static android.view.Display.TYPE_INTERNAL;
import static com.google.common.truth.Truth.assertThat;
@@ -36,6 +37,7 @@
import android.os.IThermalService;
import android.os.RemoteException;
import android.os.Temperature;
+import android.view.Display;
import android.view.DisplayInfo;
import androidx.test.filters.SmallTest;
@@ -97,6 +99,8 @@
@Mock
private LogicalDisplay mMockedLogicalDisplay;
@Mock
+ private LogicalDisplay mMockedDefaultDisplay;
+ @Mock
private DisplayNotificationManager mMockedDisplayNotificationManager;
@Mock
private ExternalDisplayStatsService mMockedExternalDisplayStatsService;
@@ -141,6 +145,15 @@
when(mMockedLogicalDisplay.getDisplayInfoLocked()).thenReturn(mockedLogicalDisplayInfo);
when(mMockedLogicalDisplayMapper.getDisplayLocked(EXTERNAL_DISPLAY_ID)).thenReturn(
mMockedLogicalDisplay);
+
+ // Initialize default logical display
+ when(mMockedDefaultDisplay.getDisplayIdLocked()).thenReturn(Display.DEFAULT_DISPLAY);
+ when(mMockedDefaultDisplay.isEnabledLocked()).thenReturn(true);
+ final var mockedDefaultDisplayInfo = new DisplayInfo();
+ mockedDefaultDisplayInfo.type = TYPE_INTERNAL;
+ when(mMockedDefaultDisplay.getDisplayInfoLocked()).thenReturn(mockedDefaultDisplayInfo);
+ when(mMockedLogicalDisplayMapper.getDisplayLocked(Display.DEFAULT_DISPLAY)).thenReturn(
+ mMockedDefaultDisplay);
}
@Test
@@ -293,6 +306,52 @@
verify(mMockedLogicalDisplayMapper, never()).forEachLocked(any());
}
+ @Test
+ public void testMirroringAlwaysConfirmedByUser_flagDisabled() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(false);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID)).isTrue();
+ }
+
+ @Test
+ public void testMirroringConfirmed_afterBootForEnabledDisplay() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isTrue();
+ }
+
+ @Test
+ public void testMirroringNotConfirmed_afterBootForDisabledDisplay() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ when(mMockedLogicalDisplay.isEnabledLocked()).thenReturn(false);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_forNonExternalDisplays() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.DEFAULT_DISPLAY))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_forNonExistingDisplays() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ mExternalDisplayPolicy.onBootCompleted();
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(Display.INVALID_DISPLAY))
+ .isFalse();
+ }
+
+ @Test
+ public void testMirroringNeverConfirmed_duringBoot() {
+ when(mMockedFlags.isWaitingConfirmationBeforeMirroringEnabled()).thenReturn(true);
+ assertThat(mExternalDisplayPolicy.isDisplayReadyForMirroring(EXTERNAL_DISPLAY_ID))
+ .isFalse();
+ }
+
private void setTemperature(final IThermalEventListener thermalEventListener,
final List<Temperature> temperature) throws RemoteException {
for (var t : temperature) {
diff --git a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
index 1729ad5..d831cf8 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/LogicalDisplayMapperTest.java
@@ -121,6 +121,8 @@
Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
private static final DeviceState DEVICE_STATE_OPEN = createDeviceState(2, "Two",
Set.of(DeviceState.PROPERTY_POWER_CONFIGURATION_TRIGGER_WAKE), Collections.emptySet());
+ private static final DeviceState DEVICE_STATE_EMULATED = createDeviceState(3, "Three",
+ Set.of(DeviceState.PROPERTY_EMULATED_ONLY), Collections.emptySet());
private static final int FLAG_GO_TO_SLEEP_ON_FOLD = 0;
private static final int FLAG_GO_TO_SLEEP_FLAG_SOFT_SLEEP = 2;
private static int sNextNonDefaultDisplayId = DEFAULT_DISPLAY + 1;
@@ -686,6 +688,14 @@
}
@Test
+ public void testDeviceShouldNotBeWokenWhenExitingEmulatedState() {
+ assertFalse(mLogicalDisplayMapper.shouldDeviceBeWoken(DEVICE_STATE_OPEN,
+ DEVICE_STATE_EMULATED,
+ /* isInteractive= */false,
+ /* isBootCompleted= */true));
+ }
+
+ @Test
public void testDeviceShouldBePutToSleep() {
assertTrue(mLogicalDisplayMapper.shouldDeviceBePutToSleep(DEVICE_STATE_CLOSED,
DEVICE_STATE_OPEN,
diff --git a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
index 5676a38..6d14065 100644
--- a/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
+++ b/services/tests/displayservicetests/src/com/android/server/display/PersistentDataStoreTest.java
@@ -459,7 +459,6 @@
ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());
newInjector.setReadStream(bais);
newDataStore.loadIfNeeded();
- assertNotNull(newDataStore.getUserPreferredRefreshRate(testDisplayDevice));
assertEquals(85.3f, mDataStore.getUserPreferredRefreshRate(testDisplayDevice), 01.f);
assertEquals(85.3f, newDataStore.getUserPreferredRefreshRate(testDisplayDevice), 0.1f);
}
diff --git a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
index 584fd62..40b9c61 100644
--- a/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/job/controllers/QuotaControllerTest.java
@@ -25,6 +25,7 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.spyOn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static com.android.server.job.Flags.FLAG_COUNT_QUOTA_FIX;
+import static com.android.server.job.Flags.FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS;
import static com.android.server.job.JobSchedulerService.ACTIVE_INDEX;
import static com.android.server.job.JobSchedulerService.EXEMPTED_INDEX;
import static com.android.server.job.JobSchedulerService.FREQUENT_INDEX;
@@ -303,6 +304,12 @@
}
}
+ private int getProcessStateQuotaFreeThreshold() {
+ synchronized (mQuotaController.mLock) {
+ return mQuotaController.getProcessStateQuotaFreeThreshold();
+ }
+ }
+
private void setProcessState(int procState) {
setProcessState(procState, mSourceUid);
}
@@ -315,7 +322,7 @@
final boolean contained = foregroundUids.get(uid);
mUidObserver.onUidStateChanged(uid, procState, 0,
ActivityManager.PROCESS_CAPABILITY_NONE);
- if (procState <= ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE) {
+ if (procState <= getProcessStateQuotaFreeThreshold()) {
if (!contained) {
verify(foregroundUids, timeout(2 * SECOND_IN_MILLIS).times(1))
.put(eq(uid), eq(true));
@@ -1371,7 +1378,7 @@
}
setDischarging();
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(timeUntilQuotaConsumedMs,
mQuotaController.getMaxJobExecutionTimeMsLocked((job)));
@@ -1473,7 +1480,7 @@
}
setDischarging();
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -1505,7 +1512,7 @@
createTimingSession(sElapsedRealtimeClock.millis() - mQcConstants.EJ_WINDOW_SIZE_MS,
timeUsedMs, 5), true);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
assertEquals(mQcConstants.EJ_LIMIT_WORKING_MS / 2,
mQuotaController.getMaxJobExecutionTimeMsLocked(job));
@@ -4126,7 +4133,7 @@
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
// Change to a state that should still be considered foreground.
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
advanceElapsedClock(5 * SECOND_IN_MILLIS);
synchronized (mQuotaController.mLock) {
mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
@@ -4134,6 +4141,36 @@
assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
}
+ /** Tests that Timers count FOREGROUND_SERVICE jobs. */
+ @Test
+ @RequiresFlagsEnabled(FLAG_ENFORCE_QUOTA_POLICY_TO_FGS_JOBS)
+ public void testTimerTracking_Fgs() {
+ setDischarging();
+
+ JobStatus jobStatus = createJobStatus("testTimerTracking_Fgs", 1);
+ setProcessState(ActivityManager.PROCESS_STATE_BOUND_TOP);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStartTrackingJobLocked(jobStatus, null);
+ }
+
+ assertNull(mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.prepareForExecutionLocked(jobStatus);
+ }
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ // Change to FOREGROUND_SERVICE state that should count.
+ setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ long start = JobSchedulerService.sElapsedRealtimeClock.millis();
+ advanceElapsedClock(5 * SECOND_IN_MILLIS);
+ synchronized (mQuotaController.mLock) {
+ mQuotaController.maybeStopTrackingJobLocked(jobStatus, null);
+ }
+ List<TimingSession> expected = new ArrayList<>();
+ expected.add(createTimingSession(start, 5 * SECOND_IN_MILLIS, 1));
+ assertEquals(expected, mQuotaController.getTimingSessions(SOURCE_USER_ID, SOURCE_PACKAGE));
+ }
+
/**
* Tests that Timers properly track sessions when switching between foreground and background
* states.
@@ -4180,7 +4217,7 @@
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -4213,7 +4250,7 @@
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -4262,7 +4299,7 @@
}
assertEquals(0, stats.jobCountInRateLimitingWindow);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -4412,7 +4449,7 @@
mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -4625,7 +4662,7 @@
// App still in foreground so everything should be in quota.
advanceElapsedClock(20 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
assertTrue(jobTop.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
assertTrue(jobFg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
assertTrue(jobBg.isConstraintSatisfied(JobStatus.CONSTRAINT_WITHIN_QUOTA));
@@ -5901,7 +5938,7 @@
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -5935,7 +5972,7 @@
}
advanceElapsedClock(10 * SECOND_IN_MILLIS);
expected.add(createTimingSession(start, 10 * SECOND_IN_MILLIS, 1));
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg3);
}
@@ -6056,7 +6093,7 @@
mQuotaController.maybeStopTrackingJobLocked(jobBg1, jobBg1);
}
advanceElapsedClock(5 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
synchronized (mQuotaController.mLock) {
mQuotaController.prepareForExecutionLocked(jobFg1);
}
@@ -6534,7 +6571,7 @@
// App still in foreground so everything should be in quota.
advanceElapsedClock(20 * SECOND_IN_MILLIS);
- setProcessState(ActivityManager.PROCESS_STATE_FOREGROUND_SERVICE);
+ setProcessState(getProcessStateQuotaFreeThreshold());
assertTrue(jobTop2.isExpeditedQuotaApproved());
assertTrue(jobFg.isExpeditedQuotaApproved());
assertTrue(jobBg.isExpeditedQuotaApproved());
diff --git a/services/tests/servicestests/Android.bp b/services/tests/servicestests/Android.bp
index 6ede334..359755a 100644
--- a/services/tests/servicestests/Android.bp
+++ b/services/tests/servicestests/Android.bp
@@ -67,6 +67,7 @@
"androidx.test.ext.junit",
"cts-wm-util",
"platform-compat-test-rules",
+ "platform-parametric-runner-lib",
"mockito-target-minus-junit4",
"mockito-kotlin2",
"platform-test-annotations",
diff --git a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
index dad45b3..8a1bb00 100644
--- a/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/companion/virtual/VirtualDeviceManagerServiceTest.java
@@ -161,8 +161,8 @@
private static final int DISPLAY_ID_1 = 2;
private static final int DISPLAY_ID_2 = 3;
private static final int NON_EXISTENT_DISPLAY_ID = 42;
- private static final int DEVICE_OWNER_UID_1 = 50;
- private static final int DEVICE_OWNER_UID_2 = 51;
+ private static final int DEVICE_OWNER_UID_1 = Process.myUid();
+ private static final int DEVICE_OWNER_UID_2 = DEVICE_OWNER_UID_1 + 1;
private static final int UID_1 = 0;
private static final int UID_2 = 10;
private static final int UID_3 = 10000;
@@ -245,6 +245,8 @@
@Mock
private IDisplayManager mIDisplayManager;
@Mock
+ private WindowManager mWindowManager;
+ @Mock
private VirtualDeviceImpl.PendingTrampolineCallback mPendingTrampolineCallback;
@Mock
private DevicePolicyManager mDevicePolicyManagerMock;
@@ -383,8 +385,7 @@
// Allow virtual devices to be created on the looper thread for testing.
final InputController.DeviceCreationThreadVerifier threadVerifier = () -> true;
mInputController = new InputController(mNativeWrapperMock,
- new Handler(TestableLooper.get(this).getLooper()),
- mContext.getSystemService(WindowManager.class),
+ new Handler(TestableLooper.get(this).getLooper()), mWindowManager,
AttributionSource.myAttributionSource(), threadVerifier);
mCameraAccessController =
new CameraAccessController(mContext, mLocalService, mCameraAccessBlockedCallback);
@@ -535,7 +536,7 @@
.build();
mDeviceImpl.close();
mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
GenericWindowPolicyController gwpc =
mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
@@ -543,6 +544,21 @@
}
@Test
+ public void getDevicePolicy_customRecentsPolicy_untrustedDisplaygwpcShowsRecentsOnHostDevice() {
+ VirtualDeviceParams params = new VirtualDeviceParams
+ .Builder()
+ .setDevicePolicy(POLICY_TYPE_RECENTS, DEVICE_POLICY_CUSTOM)
+ .build();
+ mDeviceImpl.close();
+ mDeviceImpl = createVirtualDevice(VIRTUAL_DEVICE_ID_1, DEVICE_OWNER_UID_1, params);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+
+ GenericWindowPolicyController gwpc =
+ mDeviceImpl.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_1);
+ assertThat(gwpc.canShowTasksInHostDeviceRecents()).isTrue();
+ }
+
+ @Test
public void getDeviceOwnerUid_oneDevice_returnsCorrectId() {
int ownerUid = mLocalService.getDeviceOwnerUid(mDeviceImpl.getDeviceId());
assertThat(ownerUid).isEqualTo(mDeviceImpl.getOwnerUid());
@@ -660,7 +676,7 @@
@Test
public void getDeviceIdsForUid_twoDevicesUidOnOne_returnsCorrectId() {
VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
- DEVICE_OWNER_UID_2);
+ DEVICE_OWNER_UID_1);
addVirtualDisplay(secondDevice, DISPLAY_ID_2);
secondDevice.getDisplayWindowPolicyControllerForTest(DISPLAY_ID_2).onRunningAppsChanged(
@@ -675,7 +691,7 @@
public void getDeviceIdsForUid_twoDevicesUidOnBoth_returnsCorrectId() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
- DEVICE_OWNER_UID_2);
+ DEVICE_OWNER_UID_1);
addVirtualDisplay(secondDevice, DISPLAY_ID_2);
@@ -692,7 +708,7 @@
@Test
public void getPreferredLocaleListForApp_keyboardAttached_returnLocaleHints() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
mVdms.notifyRunningAppsChanged(mDeviceImpl.getDeviceId(), Sets.newArraySet(UID_1));
@@ -713,7 +729,7 @@
@Test
public void getPreferredLocaleListForApp_appOnMultipleVD_localeOnFirstVDReturned() {
VirtualDeviceImpl secondDevice = createVirtualDevice(VIRTUAL_DEVICE_ID_2,
- DEVICE_OWNER_UID_2);
+ DEVICE_OWNER_UID_1);
Binder secondBinder = new Binder("secondBinder");
VirtualKeyboardConfig firstKeyboardConfig =
new VirtualKeyboardConfig.Builder()
@@ -732,8 +748,8 @@
.setLanguageTag("fr-FR")
.build();
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- addVirtualDisplay(secondDevice, DISPLAY_ID_2);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
+ addVirtualDisplay(secondDevice, DISPLAY_ID_2, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(firstKeyboardConfig, BINDER);
secondDevice.createVirtualKeyboard(secondKeyboardConfig, secondBinder);
@@ -751,7 +767,7 @@
assertThat(mCameraAccessController.getObserverCount()).isEqualTo(1);
VirtualDeviceImpl secondDevice =
- createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_2);
+ createVirtualDevice(VIRTUAL_DEVICE_ID_2, DEVICE_OWNER_UID_1);
assertThat(mCameraAccessController.getObserverCount()).isEqualTo(2);
mDeviceImpl.close();
@@ -910,11 +926,24 @@
}
@Test
- public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+ public void onVirtualDisplayCreatedLocked_notTrustedDisplay_noWakeLockIsAcquired()
+ throws RemoteException {
verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
nullable(String.class), anyInt(), eq(null));
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ TestableLooper.get(this).processAllMessages();
+ verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+ nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+ nullable(String.class), anyInt(), eq(null));
+ }
+
+ @Test
+ public void onVirtualDisplayCreatedLocked_wakeLockIsAcquired() throws RemoteException {
+ verify(mIPowerManagerMock, never()).acquireWakeLock(any(Binder.class), anyInt(),
+ nullable(String.class), nullable(String.class), nullable(WorkSource.class),
+ nullable(String.class), anyInt(), eq(null));
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
verify(mIPowerManagerMock).acquireWakeLock(any(Binder.class), anyInt(),
nullable(String.class), nullable(String.class), nullable(WorkSource.class),
nullable(String.class), eq(DISPLAY_ID_1), eq(null));
@@ -923,7 +952,7 @@
@Test
public void onVirtualDisplayCreatedLocked_duplicateCalls_onlyOneWakeLockIsAcquired()
throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
assertThrows(IllegalStateException.class,
() -> addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1));
TestableLooper.get(this).processAllMessages();
@@ -934,7 +963,7 @@
@Test
public void onVirtualDisplayRemovedLocked_wakeLockIsReleased() throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -949,7 +978,7 @@
@Test
public void addVirtualDisplay_displayNotReleased_wakeLockIsReleased() throws RemoteException {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
ArgumentCaptor<IBinder> wakeLockCaptor = ArgumentCaptor.forClass(IBinder.class);
TestableLooper.get(this).processAllMessages();
verify(mIPowerManagerMock).acquireWakeLock(wakeLockCaptor.capture(),
@@ -970,24 +999,52 @@
}
@Test
+ public void createVirtualDpad_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualKeyboard_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
}
@Test
+ public void createVirtualKeyboard_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualMouse_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
}
@Test
+ public void createVirtualMouse_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualTouchscreen_noDisplay_failsSecurityException() {
assertThrows(SecurityException.class,
() -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
}
@Test
+ public void createVirtualTouchscreen_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER));
+ }
+
+ @Test
public void createVirtualTouchscreen_zeroDisplayDimension_failsIllegalArgumentException() {
assertThrows(IllegalArgumentException.class,
() -> new VirtualTouchscreenConfig.Builder(
@@ -1003,7 +1060,7 @@
@Test
public void createVirtualTouchscreen_positiveDisplayDimension_successful() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
VirtualTouchscreenConfig positiveConfig =
new VirtualTouchscreenConfig.Builder(
/* touchscrenWidth= */ 600, /* touchscreenHeight= */ 800)
@@ -1026,6 +1083,14 @@
}
@Test
+ public void createVirtualNavigationTouchpad_untrustedDisplay_failsSecurityException() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ assertThrows(SecurityException.class,
+ () -> mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG,
+ BINDER));
+ }
+
+ @Test
public void createVirtualNavigationTouchpad_zeroDisplayDimension_failsWithException() {
assertThrows(IllegalArgumentException.class,
() -> new VirtualNavigationTouchpadConfig.Builder(
@@ -1041,7 +1106,7 @@
@Test
public void createVirtualNavigationTouchpad_positiveDisplayDimension_successful() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
VirtualNavigationTouchpadConfig positiveConfig =
new VirtualNavigationTouchpadConfig.Builder(
/* touchpadHeight= */ 50, /* touchpadWidth= */ 50)
@@ -1065,72 +1130,8 @@
}
@Test
- public void createVirtualDpad_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER)));
- }
-
- @Test
- public void createVirtualKeyboard_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER)));
- }
-
- @Test
- public void createVirtualMouse_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER)));
- }
-
- @Test
- public void createVirtualTouchscreen_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER)));
- }
-
- @Test
- public void createVirtualNavigationTouchpad_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.createVirtualNavigationTouchpad(
- NAVIGATION_TOUCHPAD_CONFIG,
- BINDER)));
- }
-
- @Test
- public void onAudioSessionStarting_noPermission_failsSecurityException() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class,
- () -> mDeviceImpl.onAudioSessionStarting(
- DISPLAY_ID_1, mRoutingCallback, mConfigChangedCallback)));
- }
-
- @Test
- public void onAudioSessionEnded_noPermission_failsSecurityException() {
- // Shell doesn't have CREATE_VIRTUAL_DEVICE permission.
- SystemUtil.runWithShellPermissionIdentity(() ->
- assertThrows(SecurityException.class, () -> mDeviceImpl.onAudioSessionEnded()));
- }
-
- @Test
public void createVirtualDpad_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualDpad(DPAD_CONFIG, BINDER);
assertWithMessage("Virtual dpad should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1140,7 +1141,7 @@
@Test
public void createVirtualKeyboard_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1150,7 +1151,7 @@
@Test
public void createVirtualKeyboard_keyboardCreated_localeUpdated() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(KEYBOARD_CONFIG, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches")
.that(mInputController.getInputDeviceDescriptors())
@@ -1171,7 +1172,7 @@
.setAssociatedDisplayId(DISPLAY_ID_1)
.build();
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualKeyboard(configWithoutExplicitLayoutInfo, BINDER);
assertWithMessage("Virtual keyboard should register fd when the display matches")
.that(mInputController.getInputDeviceDescriptors())
@@ -1192,7 +1193,7 @@
@Test
public void createVirtualMouse_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualMouse(MOUSE_CONFIG, BINDER);
assertWithMessage("Virtual mouse should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1202,7 +1203,7 @@
@Test
public void createVirtualTouchscreen_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualTouchscreen(TOUCHSCREEN_CONFIG, BINDER);
assertWithMessage("Virtual touchscreen should register fd when the display matches").that(
mInputController.getInputDeviceDescriptors()).isNotEmpty();
@@ -1212,7 +1213,7 @@
@Test
public void createVirtualNavigationTouchpad_hasDisplay_obtainFileDescriptor() {
- addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1, Display.FLAG_TRUSTED);
mDeviceImpl.createVirtualNavigationTouchpad(NAVIGATION_TOUCHPAD_CONFIG, BINDER);
assertWithMessage("Virtual navigation touchpad should register fd when the display matches")
.that(
@@ -1472,9 +1473,9 @@
@Test
public void setShowPointerIcon_setsValueForAllDisplays() {
- addVirtualDisplay(mDeviceImpl, 1);
- addVirtualDisplay(mDeviceImpl, 2);
- addVirtualDisplay(mDeviceImpl, 3);
+ addVirtualDisplay(mDeviceImpl, 1, Display.FLAG_TRUSTED);
+ addVirtualDisplay(mDeviceImpl, 2, Display.FLAG_TRUSTED);
+ addVirtualDisplay(mDeviceImpl, 3, Display.FLAG_TRUSTED);
VirtualMouseConfig config1 = new VirtualMouseConfig.Builder()
.setAssociatedDisplayId(1)
.setInputDeviceName(DEVICE_NAME_1)
@@ -1507,6 +1508,14 @@
}
@Test
+ public void setShowPointerIcon_untrustedDisplay_pointerIconIsAlwaysShown() {
+ addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
+ clearInvocations(mInputManagerInternalMock);
+ mDeviceImpl.setShowPointerIcon(false);
+ verify(mInputManagerInternalMock, times(0)).setPointerIconVisible(eq(false), anyInt());
+ }
+
+ @Test
public void openNonBlockedAppOnVirtualDisplay_doesNotStartBlockedAlertActivity() {
addVirtualDisplay(mDeviceImpl, DISPLAY_ID_1);
GenericWindowPolicyController gwpc = mDeviceImpl.getDisplayWindowPolicyControllerForTest(
@@ -1968,15 +1977,20 @@
}
private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId) {
+ addVirtualDisplay(virtualDevice, displayId, /* flags= */ 0);
+ }
+
+ private void addVirtualDisplay(VirtualDeviceImpl virtualDevice, int displayId, int flags) {
when(mDisplayManagerInternalMock.createVirtualDisplay(any(), eq(mVirtualDisplayCallback),
eq(virtualDevice), any(), any())).thenReturn(displayId);
- virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
final String uniqueId = UNIQUE_ID + displayId;
doAnswer(inv -> {
final DisplayInfo displayInfo = new DisplayInfo();
displayInfo.uniqueId = uniqueId;
+ displayInfo.flags = flags;
return displayInfo;
}).when(mDisplayManagerInternalMock).getDisplayInfo(eq(displayId));
+ virtualDevice.createVirtualDisplay(VIRTUAL_DISPLAY_CONFIG, mVirtualDisplayCallback);
mInputManagerMockHelper.addDisplayIdMapping(uniqueId, displayId);
}
diff --git a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
index 9df7a36..1d07540 100644
--- a/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
+++ b/services/tests/servicestests/src/com/android/server/compat/PlatformCompatTest.java
@@ -20,6 +20,7 @@
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyLong;
import static org.mockito.Mockito.never;
import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.verify;
@@ -33,6 +34,11 @@
import android.content.pm.PackageManager;
import android.content.pm.PackageManagerInternal;
import android.os.Build;
+import android.os.Process;
+
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.test.runner.AndroidJUnit4;
@@ -43,6 +49,7 @@
import com.android.server.LocalServices;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mock;
@@ -55,6 +62,8 @@
public class PlatformCompatTest {
private static final String PACKAGE_NAME = "my.package";
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Mock
private Context mContext;
@Mock
@@ -441,4 +450,79 @@
assertThat(mPlatformCompat.isChangeEnabled(3L, systemAppInfo)).isTrue();
verify(mChangeReporter).reportChange(123, 3L, ChangeReporter.STATE_ENABLED, true, false);
}
+
+ @DisableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+ @Test
+ public void testSharedSystemUidFlagOff() throws Exception {
+ testSharedSystemUid(false);
+ }
+
+ @EnableFlags(Flags.FLAG_SYSTEM_UID_TARGET_SYSTEM_SDK)
+ @Test
+ public void testSharedSystemUidFlagOn() throws Exception {
+ testSharedSystemUid(true);
+ }
+
+ private void testSharedSystemUid(Boolean expectSystemUidTargetSystemSdk) throws Exception {
+ final String systemUidPackageNameTargetsR = "systemuid.package1";
+ final String systemUidPackageNameTargetsQ = "systemuid.package2";
+ final String nonSystemUidPackageNameTargetsR = "nonsystemuid.package1";
+ final String nonSystemUidPackageNameTargetsQ = "nonsystemuid.package2";
+ final int nonSystemUid = 123;
+
+ mCompatConfig =
+ CompatConfigBuilder.create(mBuildClassifier, mContext)
+ .addEnableSinceSdkChangeWithId(Build.VERSION_CODES.R, 1L)
+ .build();
+ mCompatConfig.forceNonDebuggableFinalForTest(true);
+ mPlatformCompat =
+ new PlatformCompat(mContext, mCompatConfig, mBuildClassifier, mChangeReporter);
+
+ ApplicationInfo systemUidAppInfo1 = ApplicationInfoBuilder.create()
+ .withPackageName(systemUidPackageNameTargetsR)
+ .withUid(Process.SYSTEM_UID)
+ .withTargetSdk(Build.VERSION_CODES.R)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(systemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+ .thenReturn(systemUidAppInfo1);
+
+ ApplicationInfo systemUidAppInfo2 = ApplicationInfoBuilder.create()
+ .withPackageName(systemUidPackageNameTargetsQ)
+ .withUid(Process.SYSTEM_UID)
+ .withTargetSdk(Build.VERSION_CODES.Q)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(systemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+ .thenReturn(systemUidAppInfo2);
+
+ ApplicationInfo nonSystemUidAppInfo1 = ApplicationInfoBuilder.create()
+ .withPackageName(nonSystemUidPackageNameTargetsR)
+ .withUid(nonSystemUid)
+ .withTargetSdk(Build.VERSION_CODES.R)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(nonSystemUidPackageNameTargetsR), anyLong(), anyInt(), anyInt()))
+ .thenReturn(nonSystemUidAppInfo1);
+
+ ApplicationInfo nonSystemUidAppInfo2 = ApplicationInfoBuilder.create()
+ .withPackageName(nonSystemUidPackageNameTargetsQ)
+ .withUid(nonSystemUid)
+ .withTargetSdk(Build.VERSION_CODES.Q)
+ .build();
+ when(mPackageManagerInternal.getApplicationInfo(
+ eq(nonSystemUidPackageNameTargetsQ), anyLong(), anyInt(), anyInt()))
+ .thenReturn(nonSystemUidAppInfo2);
+
+ when(mPackageManager.getPackagesForUid(eq(Process.SYSTEM_UID)))
+ .thenReturn(new String[] {systemUidPackageNameTargetsR, systemUidPackageNameTargetsQ});
+ when(mPackageManager.getPackagesForUid(eq(nonSystemUid)))
+ .thenReturn(new String[] {
+ nonSystemUidPackageNameTargetsR, nonSystemUidPackageNameTargetsQ
+ });
+
+ assertThat(mPlatformCompat.isChangeEnabledByUid(1L, Process.SYSTEM_UID))
+ .isEqualTo(expectSystemUidTargetSystemSdk);
+ assertThat(mPlatformCompat.isChangeEnabledByUid(1L, nonSystemUid)).isFalse();
+ }
}
diff --git a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
index b565f4b..ab5a5a9 100644
--- a/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/devicestate/DeviceStateManagerServiceTest.java
@@ -35,17 +35,20 @@
import android.os.IBinder;
import android.os.RemoteException;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.flag.junit.FlagsParameterization;
+import android.platform.test.flag.junit.SetFlagsRule;
import androidx.annotation.NonNull;
-import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.FlakyTest;
import androidx.test.platform.app.InstrumentationRegistry;
import com.android.compatibility.common.util.PollingCheck;
import com.android.server.wm.ActivityTaskManagerInternal;
import com.android.server.wm.WindowProcessController;
+import com.android.window.flags.Flags;
import org.junit.Before;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
@@ -58,6 +61,9 @@
import javax.annotation.Nullable;
+import platform.test.runner.parameterized.ParameterizedAndroidJunit4;
+import platform.test.runner.parameterized.Parameters;
+
/**
* Unit tests for {@link DeviceStateManagerService}.
*
@@ -65,7 +71,7 @@
* atest FrameworksServicesTests:DeviceStateManagerServiceTest
*/
@Presubmit
-@RunWith(AndroidJUnit4.class)
+@RunWith(ParameterizedAndroidJunit4.class)
public final class DeviceStateManagerServiceTest {
private static final DeviceState DEFAULT_DEVICE_STATE = new DeviceState(
new DeviceState.Configuration.Builder(0, "DEFAULT").build());
@@ -99,6 +105,14 @@
private static final int TIMEOUT = 2000;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule;
+
+ @Parameters(name = "{0}")
+ public static List<FlagsParameterization> getParams() {
+ return FlagsParameterization.allCombinationsOf(Flags.FLAG_WLINFO_ONCREATE);
+ }
+
private final Context mContext = InstrumentationRegistry.getInstrumentation().getContext();
@NonNull
private TestDeviceStatePolicy mPolicy;
@@ -111,13 +125,19 @@
@NonNull
private WindowProcessController mWindowProcessController;
+ public DeviceStateManagerServiceTest(FlagsParameterization flags) {
+ mSetFlagsRule = new SetFlagsRule(flags);
+ }
+
@Before
public void setup() {
mProvider = new TestDeviceStateProvider();
mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
mSysPropSetter = new TestSystemPropertySetter();
setupDeviceStateManagerService();
- flushHandler(); // Flush the handler to ensure the initial values are committed.
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler(); // Flush the handler to ensure the initial values are committed.
+ }
}
private void setupDeviceStateManagerService() {
@@ -255,9 +275,11 @@
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- // An initial callback will be triggered on registration, so we clear it here.
- flushHandler();
- callback.clearLastNotifiedInfo();
+ if (!Flags.wlinfoOncreate()) {
+ // An initial callback will be triggered on registration, so we clear it here.
+ flushHandler();
+ callback.clearLastNotifiedInfo();
+ }
assertThat(mService.getCommittedState()).hasValue(DEFAULT_DEVICE_STATE);
assertThat(mService.getPendingState()).isEmpty();
@@ -301,7 +323,9 @@
mProvider = new TestDeviceStateProvider(null /* initialState */);
mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
setupDeviceStateManagerService();
- flushHandler(); // Flush the handler to ensure the initial values are committed.
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler(); // Flush the handler to ensure the initial values are committed.
+ }
final DeviceStateInfo info = mService.getBinderService().getDeviceStateInfo();
@@ -317,6 +341,9 @@
mService.getBinderService().registerCallback(callback);
mProvider.setState(OTHER_DEVICE_STATE_IDENTIFIER);
+ if (Flags.wlinfoOncreate()) {
+ waitAndAssert(() -> callback.getLastNotifiedInfo() != null);
+ }
waitAndAssert(() -> callback.getLastNotifiedInfo().baseState.getIdentifier()
== OTHER_DEVICE_STATE_IDENTIFIER);
waitAndAssert(() -> callback.getLastNotifiedInfo().currentState.getIdentifier()
@@ -350,9 +377,14 @@
public void registerCallback_initialValueAvailable_emitsDeviceState() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
- mService.getBinderService().registerCallback(callback);
- flushHandler();
- final DeviceStateInfo stateInfo = callback.getLastNotifiedInfo();
+ final DeviceStateInfo stateInfo;
+ if (Flags.wlinfoOncreate()) {
+ stateInfo = mService.getBinderService().registerCallback(callback);
+ } else {
+ mService.getBinderService().registerCallback(callback);
+ flushHandler();
+ stateInfo = callback.getLastNotifiedInfo();
+ }
assertThat(stateInfo).isNotNull();
assertThat(stateInfo.baseState).isEqualTo(DEFAULT_DEVICE_STATE);
@@ -365,14 +397,22 @@
mProvider = new TestDeviceStateProvider(null /* initialState */);
mPolicy = new TestDeviceStatePolicy(mContext, mProvider);
setupDeviceStateManagerService();
- flushHandler(); // Flush the handler to ensure the initial values are committed.
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler(); // Flush the handler to ensure the initial values are committed.
+ }
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
- mService.getBinderService().registerCallback(callback);
- flushHandler();
- final DeviceStateInfo stateInfo = callback.getLastNotifiedInfo();
+ final DeviceStateInfo stateInfo;
+ if (Flags.wlinfoOncreate()) {
+ // Return null when the base state is not set yet.
+ stateInfo = mService.getBinderService().registerCallback(callback);
+ } else {
+ mService.getBinderService().registerCallback(callback);
+ flushHandler();
+ // The callback should never be called when the base state is not set yet.
+ stateInfo = callback.getLastNotifiedInfo();
+ }
- // The callback should never be called when the base state is not set yet.
assertThat(stateInfo).isNull();
}
@@ -380,7 +420,9 @@
public void requestState() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
@@ -424,7 +466,9 @@
public void requestState_pendingStateAtRequest() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
mPolicy.blockConfigure();
@@ -498,7 +542,9 @@
public void requestState_sameAsBaseState() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
@@ -516,7 +562,9 @@
public void requestState_flagCancelWhenBaseChanges() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
@@ -613,7 +661,9 @@
public void requestState_becomesUnsupported() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
@@ -687,7 +737,9 @@
public void requestBaseStateOverride() throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
@@ -833,7 +885,9 @@
) throws RemoteException {
final TestDeviceStateManagerCallback callback = new TestDeviceStateManagerCallback();
mService.getBinderService().registerCallback(callback);
- flushHandler();
+ if (!Flags.wlinfoOncreate()) {
+ flushHandler();
+ }
final IBinder token = new Binder();
assertThat(callback.getLastNotifiedStatus(token))
diff --git a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
index e443696..c51261f 100644
--- a/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/ContentRecorderTests.java
@@ -52,6 +52,7 @@
import android.os.IBinder;
import android.platform.test.annotations.Presubmit;
import android.view.ContentRecordingSession;
+import android.view.Display;
import android.view.DisplayInfo;
import android.view.Gravity;
import android.view.SurfaceControl;
@@ -93,9 +94,11 @@
private boolean mHandleAnisotropicDisplayMirroring = false;
@Before public void setUp() {
+ mDisplayInfo.type = Display.TYPE_VIRTUAL;
MockitoAnnotations.initMocks(this);
doReturn(INVALID_DISPLAY).when(mWm.mDisplayManagerInternal).getDisplayIdToMirror(anyInt());
+ doReturn(false).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
// Skip unnecessary operations of relayout.
spyOn(mWm.mWindowPlacerLocked);
@@ -163,6 +166,25 @@
}
@Test
+ public void testUpdateRecording_externalDisplayWithoutUserConfirmation() {
+ mDisplayInfo.type = Display.TYPE_EXTERNAL;
+ defaultInit();
+ mContentRecorder.setContentRecordingSession(mDisplaySession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isFalse();
+ }
+
+ @Test
+ public void testUpdateRecording_externalDisplayWithUserConfirmation() {
+ doReturn(true).when(mWm.mDisplayManagerInternal).isDisplayReadyForMirroring(anyInt());
+ mDisplayInfo.type = Display.TYPE_EXTERNAL;
+ defaultInit();
+ mContentRecorder.setContentRecordingSession(mDisplaySession);
+ mContentRecorder.updateRecording();
+ assertThat(mContentRecorder.isCurrentlyRecording()).isTrue();
+ }
+
+ @Test
public void testUpdateRecording_display_invalidDisplayIdToMirror() {
defaultInit();
ContentRecordingSession session = ContentRecordingSession.createDisplaySession(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
index 5e8f347..c8fc482 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayRotationImmersiveAppCompatPolicyTests.java
@@ -26,7 +26,6 @@
import static com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.mock;
-import static com.android.dx.mockito.inline.extended.ExtendedMockito.spy;
import static com.android.dx.mockito.inline.extended.ExtendedMockito.when;
import static org.junit.Assert.assertFalse;
@@ -73,7 +72,6 @@
when(mMockWindowState.getRequestedVisibleTypes()).thenReturn(0);
when(mMockActivityRecord.findMainWindow()).thenReturn(mMockWindowState);
- spy(mDisplayContent);
doReturn(mMockActivityRecord).when(mDisplayContent).topRunningActivity();
when(mDisplayContent.getIgnoreOrientationRequest()).thenReturn(true);
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index 49ca6f3..44de65a 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -1965,13 +1965,14 @@
}
/**
- * Inform whether the device is aligned with the satellite for demo mode.
+ * Inform whether the device is aligned with the satellite in both real and demo mode.
*
- * Framework can send datagram to modem only when device is aligned with the satellite.
- * This method helps framework to simulate the experience of sending datagram over satellite.
+ * In demo mode, framework will send datagram to modem only when device is aligned with
+ * the satellite. This method helps framework to simulate the experience of sending datagram
+ * over satellite.
*
- * @param isAligned {@true} Device is aligned with the satellite for demo mode
- * {@false} Device is not aligned with the satellite for demo mode
+ * @param isAligned {code @true} Device is aligned with the satellite
+ * {code @false} Device is not aligned with the satellite
*
* @throws SecurityException if the caller doesn't have required permission.
* @throws IllegalStateException if the Telephony process is not currently available.
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 61f0146..231c8f5 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -2977,10 +2977,10 @@
void requestTimeForNextSatelliteVisibility(in ResultReceiver receiver);
/**
- * Inform whether the device is aligned with the satellite within in margin for demo mode.
+ * Inform whether the device is aligned with the satellite in both real and demo mode.
*
- * @param isAligned {@true} Device is aligned with the satellite for demo mode
- * {@false} Device is not aligned with the satellite for demo mode
+ * @param isAligned {@true} Device is aligned with the satellite.
+ * {@false} Device is not aligned with the satellite.
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 165bb57..6d8d7b7 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -27,7 +27,7 @@
import com.android.hoststubgen.filters.KeepNativeFilter
import com.android.hoststubgen.filters.OutputFilter
import com.android.hoststubgen.filters.SanitizationFilter
-import com.android.hoststubgen.filters.createFilterFromTextPolicyFile
+import com.android.hoststubgen.filters.TextFileFilterPolicyParser
import com.android.hoststubgen.filters.printAsTextPolicy
import com.android.hoststubgen.utils.ClassFilter
import com.android.hoststubgen.visitors.BaseAdapter
@@ -178,8 +178,10 @@
// Next, "text based" filter, which allows to override polices without touching
// the target code.
- options.policyOverrideFile.ifSet {
- filter = createFilterFromTextPolicyFile(it, allClasses, filter)
+ if (options.policyOverrideFiles.isNotEmpty()) {
+ val parser = TextFileFilterPolicyParser(allClasses, filter)
+ options.policyOverrideFiles.forEach(parser::parse)
+ filter = parser.createOutputFilter()
}
// Apply the implicit filter.
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
index b083d89..55e853e 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenOptions.kt
@@ -100,7 +100,7 @@
var defaultClassLoadHook: SetOnce<String?> = SetOnce(null),
var defaultMethodCallHook: SetOnce<String?> = SetOnce(null),
- var policyOverrideFile: SetOnce<String?> = SetOnce(null),
+ var policyOverrideFiles: MutableList<String> = mutableListOf(),
var defaultPolicy: SetOnce<FilterPolicy> = SetOnce(FilterPolicy.Remove),
@@ -164,7 +164,7 @@
"--out-jar", "--out-impl-jar" -> ret.outJar.set(nextArg())
"--policy-override-file" ->
- ret.policyOverrideFile.set(nextArg())!!.ensureFileExists()
+ ret.policyOverrideFiles.add(nextArg().ensureFileExists())
"--clean-up-on-error" -> ret.cleanUpOnError.set(true)
"--no-clean-up-on-error" -> ret.cleanUpOnError.set(false)
@@ -291,7 +291,7 @@
annotationAllowedClassesFile=$annotationAllowedClassesFile,
defaultClassLoadHook=$defaultClassLoadHook,
defaultMethodCallHook=$defaultMethodCallHook,
- policyOverrideFile=$policyOverrideFile,
+ policyOverrideFiles=${policyOverrideFiles.toTypedArray().contentToString()},
defaultPolicy=$defaultPolicy,
cleanUpOnError=$cleanUpOnError,
enableClassChecker=$enableClassChecker,
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
index 073b503..caf80eb 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/filters/TextFileFilterPolicyParser.kt
@@ -23,13 +23,10 @@
import com.android.hoststubgen.log
import com.android.hoststubgen.normalizeTextLine
import com.android.hoststubgen.whitespaceRegex
-import org.objectweb.asm.Opcodes
-import org.objectweb.asm.tree.ClassNode
-import java.io.BufferedReader
-import java.io.FileReader
+import java.io.File
import java.io.PrintWriter
-import java.util.Objects
import java.util.regex.Pattern
+import org.objectweb.asm.tree.ClassNode
/**
* Print a class node as a "keep" policy.
@@ -49,256 +46,56 @@
}
}
-/** Return true if [access] is either public or protected. */
-private fun isVisible(access: Int): Boolean {
- return (access and (Opcodes.ACC_PUBLIC or Opcodes.ACC_PROTECTED)) != 0
-}
-
private const val FILTER_REASON = "file-override"
-/**
- * Read a given "policy" file and return as an [OutputFilter]
- */
-fun createFilterFromTextPolicyFile(
- filename: String,
- classes: ClassNodes,
- fallback: OutputFilter,
- ): OutputFilter {
- log.i("Loading offloaded annotations from $filename ...")
- log.withIndent {
- val subclassFilter = SubclassFilter(classes, fallback)
- val packageFilter = PackageFilter(subclassFilter)
- val imf = InMemoryOutputFilter(classes, packageFilter)
+private enum class SpecialClass {
+ NotSpecial,
+ Aidl,
+ FeatureFlags,
+ Sysprops,
+ RFile,
+}
- var lineNo = 0
+class TextFileFilterPolicyParser(
+ private val classes: ClassNodes,
+ fallback: OutputFilter
+) {
+ private val subclassFilter = SubclassFilter(classes, fallback)
+ private val packageFilter = PackageFilter(subclassFilter)
+ private val imf = InMemoryOutputFilter(classes, packageFilter)
+ private var aidlPolicy: FilterPolicyWithReason? = null
+ private var featureFlagsPolicy: FilterPolicyWithReason? = null
+ private var syspropsPolicy: FilterPolicyWithReason? = null
+ private var rFilePolicy: FilterPolicyWithReason? = null
+ private val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
+ private val methodReplaceSpec =
+ mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
- var aidlPolicy: FilterPolicyWithReason? = null
- var featureFlagsPolicy: FilterPolicyWithReason? = null
- var syspropsPolicy: FilterPolicyWithReason? = null
- var rFilePolicy: FilterPolicyWithReason? = null
- val typeRenameSpec = mutableListOf<TextFilePolicyRemapperFilter.TypeRenameSpec>()
- val methodReplaceSpec =
- mutableListOf<TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec>()
+ private lateinit var currentClassName: String
- try {
- BufferedReader(FileReader(filename)).use { reader ->
- var className = ""
-
- while (true) {
- var line = reader.readLine() ?: break
+ /**
+ * Read a given "policy" file and return as an [OutputFilter]
+ */
+ fun parse(file: String) {
+ log.i("Loading offloaded annotations from $file ...")
+ log.withIndent {
+ var lineNo = 0
+ try {
+ File(file).forEachLine {
lineNo++
-
- line = normalizeTextLine(line)
-
+ val line = normalizeTextLine(it)
if (line.isEmpty()) {
- continue // skip empty lines.
+ return@forEachLine // skip empty lines.
}
-
-
- // TODO: Method too long, break it up.
-
- val fields = line.split(whitespaceRegex).toTypedArray()
- when (fields[0].lowercase()) {
- "p", "package" -> {
- if (fields.size < 3) {
- throw ParseException("Package ('p') expects 2 fields.")
- }
- val name = fields[1]
- val rawPolicy = fields[2]
- if (resolveExtendingClass(name) != null) {
- throw ParseException("Package can't be a super class type")
- }
- if (resolveSpecialClass(name) != SpecialClass.NotSpecial) {
- throw ParseException("Package can't be a special class type")
- }
- if (rawPolicy.startsWith("!")) {
- throw ParseException("Package can't have a substitution")
- }
- if (rawPolicy.startsWith("~")) {
- throw ParseException("Package can't have a class load hook")
- }
- val policy = parsePolicy(rawPolicy)
- if (!policy.isUsableWithClasses) {
- throw ParseException("Package can't have policy '$policy'")
- }
- packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
- }
-
- "c", "class" -> {
- if (fields.size < 3) {
- throw ParseException("Class ('c') expects 2 fields.")
- }
- className = fields[1]
-
- // superClass is set when the class name starts with a "*".
- val superClass = resolveExtendingClass(className)
-
- // :aidl, etc?
- val classType = resolveSpecialClass(className)
-
- if (fields[2].startsWith("!")) {
- if (classType != SpecialClass.NotSpecial) {
- // We could support it, but not needed at least for now.
- throw ParseException(
- "Special class can't have a substitution")
- }
- // It's a redirection class.
- val toClass = fields[2].substring(1)
- imf.setRedirectionClass(className, toClass)
- } else if (fields[2].startsWith("~")) {
- if (classType != SpecialClass.NotSpecial) {
- // We could support it, but not needed at least for now.
- throw ParseException(
- "Special class can't have a class load hook")
- }
- // It's a class-load hook
- val callback = fields[2].substring(1)
- imf.setClassLoadHook(className, callback)
- } else {
- val policy = parsePolicy(fields[2])
- if (!policy.isUsableWithClasses) {
- throw ParseException("Class can't have policy '$policy'")
- }
- Objects.requireNonNull(className)
-
- when (classType) {
- SpecialClass.NotSpecial -> {
- // TODO: Duplicate check, etc
- if (superClass == null) {
- imf.setPolicyForClass(
- className, policy.withReason(FILTER_REASON)
- )
- } else {
- subclassFilter.addPolicy(superClass,
- policy.withReason("extends $superClass"))
- }
- }
- SpecialClass.Aidl -> {
- if (aidlPolicy != null) {
- throw ParseException(
- "Policy for AIDL classes already defined")
- }
- aidlPolicy = policy.withReason(
- "$FILTER_REASON (special-class AIDL)")
- }
- SpecialClass.FeatureFlags -> {
- if (featureFlagsPolicy != null) {
- throw ParseException(
- "Policy for feature flags already defined")
- }
- featureFlagsPolicy = policy.withReason(
- "$FILTER_REASON (special-class feature flags)")
- }
- SpecialClass.Sysprops -> {
- if (syspropsPolicy != null) {
- throw ParseException(
- "Policy for sysprops already defined")
- }
- syspropsPolicy = policy.withReason(
- "$FILTER_REASON (special-class sysprops)")
- }
- SpecialClass.RFile -> {
- if (rFilePolicy != null) {
- throw ParseException(
- "Policy for R file already defined")
- }
- rFilePolicy = policy.withReason(
- "$FILTER_REASON (special-class R file)")
- }
- }
- }
- }
-
- "f", "field" -> {
- if (fields.size < 3) {
- throw ParseException("Field ('f') expects 2 fields.")
- }
- val name = fields[1]
- val policy = parsePolicy(fields[2])
- if (!policy.isUsableWithFields) {
- throw ParseException("Field can't have policy '$policy'")
- }
- Objects.requireNonNull(className)
-
- // TODO: Duplicate check, etc
- imf.setPolicyForField(className, name, policy.withReason(FILTER_REASON))
- }
-
- "m", "method" -> {
- if (fields.size < 4) {
- throw ParseException("Method ('m') expects 3 fields.")
- }
- val name = fields[1]
- val signature = fields[2]
- val policy = parsePolicy(fields[3])
-
- if (!policy.isUsableWithMethods) {
- throw ParseException("Method can't have policy '$policy'")
- }
-
- Objects.requireNonNull(className)
-
- imf.setPolicyForMethod(className, name, signature,
- policy.withReason(FILTER_REASON))
- if (policy == FilterPolicy.Substitute) {
- val fromName = fields[3].substring(1)
-
- if (fromName == name) {
- throw ParseException(
- "Substitution must have a different name")
- }
-
- // Set the policy for the "from" method.
- imf.setPolicyForMethod(className, fromName, signature,
- FilterPolicy.Keep.withReason(FILTER_REASON))
-
- val classAndMethod = splitWithLastPeriod(fromName)
- if (classAndMethod != null) {
- // If the substitution target contains a ".", then
- // it's a method call redirect.
- methodReplaceSpec.add(
- TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
- className.toJvmClassName(),
- name,
- signature,
- classAndMethod.first.toJvmClassName(),
- classAndMethod.second,
- )
- )
- } else {
- // It's an in-class replace.
- // ("@RavenwoodReplace" equivalent)
- imf.setRenameTo(className, fromName, signature, name)
- }
- }
- }
- "r", "rename" -> {
- if (fields.size < 3) {
- throw ParseException("Rename ('r') expects 2 fields.")
- }
- // Add ".*" to make it a prefix match.
- val pattern = Pattern.compile(fields[1] + ".*")
-
- // Removing the leading /'s from the prefix. This allows
- // using a single '/' as an empty suffix, which is useful to have a
- // "negative" rename rule to avoid subsequent raname's from getting
- // applied. (Which is needed for services.jar)
- val prefix = fields[2].trimStart('/')
-
- typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
- pattern, prefix)
- }
-
- else -> {
- throw ParseException("Unknown directive \"${fields[0]}\"")
- }
- }
+ parseLine(line)
}
+ } catch (e: ParseException) {
+ throw e.withSourceInfo(file, lineNo)
}
- } catch (e: ParseException) {
- throw e.withSourceInfo(filename, lineNo)
}
+ }
+ fun createOutputFilter(): OutputFilter {
var ret: OutputFilter = imf
if (typeRenameSpec.isNotEmpty()) {
ret = TextFilePolicyRemapperFilter(typeRenameSpec, ret)
@@ -309,54 +106,271 @@
// Wrap the in-memory-filter with AHF.
ret = AndroidHeuristicsFilter(
- classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret)
+ classes, aidlPolicy, featureFlagsPolicy, syspropsPolicy, rFilePolicy, ret
+ )
return ret
}
-}
-private enum class SpecialClass {
- NotSpecial,
- Aidl,
- FeatureFlags,
- Sysprops,
- RFile,
-}
-
-private fun resolveSpecialClass(className: String): SpecialClass {
- if (!className.startsWith(":")) {
- return SpecialClass.NotSpecial
+ private fun parseLine(line: String) {
+ val fields = line.split(whitespaceRegex).toTypedArray()
+ when (fields[0].lowercase()) {
+ "p", "package" -> parsePackage(fields)
+ "c", "class" -> parseClass(fields)
+ "f", "field" -> parseField(fields)
+ "m", "method" -> parseMethod(fields)
+ "r", "rename" -> parseRename(fields)
+ else -> throw ParseException("Unknown directive \"${fields[0]}\"")
+ }
}
- when (className.lowercase()) {
- ":aidl" -> return SpecialClass.Aidl
- ":feature_flags" -> return SpecialClass.FeatureFlags
- ":sysprops" -> return SpecialClass.Sysprops
- ":r" -> return SpecialClass.RFile
- }
- throw ParseException("Invalid special class name \"$className\"")
-}
-private fun resolveExtendingClass(className: String): String? {
- if (!className.startsWith("*")) {
- return null
+ private fun resolveSpecialClass(className: String): SpecialClass {
+ if (!className.startsWith(":")) {
+ return SpecialClass.NotSpecial
+ }
+ when (className.lowercase()) {
+ ":aidl" -> return SpecialClass.Aidl
+ ":feature_flags" -> return SpecialClass.FeatureFlags
+ ":sysprops" -> return SpecialClass.Sysprops
+ ":r" -> return SpecialClass.RFile
+ }
+ throw ParseException("Invalid special class name \"$className\"")
}
- return className.substring(1)
-}
-private fun parsePolicy(s: String): FilterPolicy {
- return when (s.lowercase()) {
- "k", "keep" -> FilterPolicy.Keep
- "t", "throw" -> FilterPolicy.Throw
- "r", "remove" -> FilterPolicy.Remove
- "kc", "keepclass" -> FilterPolicy.KeepClass
- "i", "ignore" -> FilterPolicy.Ignore
- "rdr", "redirect" -> FilterPolicy.Redirect
- else -> {
- if (s.startsWith("@")) {
- FilterPolicy.Substitute
- } else {
- throw ParseException("Invalid policy \"$s\"")
+ private fun resolveExtendingClass(className: String): String? {
+ if (!className.startsWith("*")) {
+ return null
+ }
+ return className.substring(1)
+ }
+
+ private fun parsePolicy(s: String): FilterPolicy {
+ return when (s.lowercase()) {
+ "k", "keep" -> FilterPolicy.Keep
+ "t", "throw" -> FilterPolicy.Throw
+ "r", "remove" -> FilterPolicy.Remove
+ "kc", "keepclass" -> FilterPolicy.KeepClass
+ "i", "ignore" -> FilterPolicy.Ignore
+ "rdr", "redirect" -> FilterPolicy.Redirect
+ else -> {
+ if (s.startsWith("@")) {
+ FilterPolicy.Substitute
+ } else {
+ throw ParseException("Invalid policy \"$s\"")
+ }
}
}
}
+
+ private fun parsePackage(fields: Array<String>) {
+ if (fields.size < 3) {
+ throw ParseException("Package ('p') expects 2 fields.")
+ }
+ val name = fields[1]
+ val rawPolicy = fields[2]
+ if (resolveExtendingClass(name) != null) {
+ throw ParseException("Package can't be a super class type")
+ }
+ if (resolveSpecialClass(name) != SpecialClass.NotSpecial) {
+ throw ParseException("Package can't be a special class type")
+ }
+ if (rawPolicy.startsWith("!")) {
+ throw ParseException("Package can't have a substitution")
+ }
+ if (rawPolicy.startsWith("~")) {
+ throw ParseException("Package can't have a class load hook")
+ }
+ val policy = parsePolicy(rawPolicy)
+ if (!policy.isUsableWithClasses) {
+ throw ParseException("Package can't have policy '$policy'")
+ }
+ packageFilter.addPolicy(name, policy.withReason(FILTER_REASON))
+ }
+
+ private fun parseClass(fields: Array<String>) {
+ if (fields.size < 3) {
+ throw ParseException("Class ('c') expects 2 fields.")
+ }
+ currentClassName = fields[1]
+
+ // superClass is set when the class name starts with a "*".
+ val superClass = resolveExtendingClass(currentClassName)
+
+ // :aidl, etc?
+ val classType = resolveSpecialClass(currentClassName)
+
+ if (fields[2].startsWith("!")) {
+ if (classType != SpecialClass.NotSpecial) {
+ // We could support it, but not needed at least for now.
+ throw ParseException(
+ "Special class can't have a substitution"
+ )
+ }
+ // It's a redirection class.
+ val toClass = fields[2].substring(1)
+ imf.setRedirectionClass(currentClassName, toClass)
+ } else if (fields[2].startsWith("~")) {
+ if (classType != SpecialClass.NotSpecial) {
+ // We could support it, but not needed at least for now.
+ throw ParseException(
+ "Special class can't have a class load hook"
+ )
+ }
+ // It's a class-load hook
+ val callback = fields[2].substring(1)
+ imf.setClassLoadHook(currentClassName, callback)
+ } else {
+ val policy = parsePolicy(fields[2])
+ if (!policy.isUsableWithClasses) {
+ throw ParseException("Class can't have policy '$policy'")
+ }
+
+ when (classType) {
+ SpecialClass.NotSpecial -> {
+ // TODO: Duplicate check, etc
+ if (superClass == null) {
+ imf.setPolicyForClass(
+ currentClassName, policy.withReason(FILTER_REASON)
+ )
+ } else {
+ subclassFilter.addPolicy(
+ superClass,
+ policy.withReason("extends $superClass")
+ )
+ }
+ }
+
+ SpecialClass.Aidl -> {
+ if (aidlPolicy != null) {
+ throw ParseException(
+ "Policy for AIDL classes already defined"
+ )
+ }
+ aidlPolicy = policy.withReason(
+ "$FILTER_REASON (special-class AIDL)"
+ )
+ }
+
+ SpecialClass.FeatureFlags -> {
+ if (featureFlagsPolicy != null) {
+ throw ParseException(
+ "Policy for feature flags already defined"
+ )
+ }
+ featureFlagsPolicy = policy.withReason(
+ "$FILTER_REASON (special-class feature flags)"
+ )
+ }
+
+ SpecialClass.Sysprops -> {
+ if (syspropsPolicy != null) {
+ throw ParseException(
+ "Policy for sysprops already defined"
+ )
+ }
+ syspropsPolicy = policy.withReason(
+ "$FILTER_REASON (special-class sysprops)"
+ )
+ }
+
+ SpecialClass.RFile -> {
+ if (rFilePolicy != null) {
+ throw ParseException(
+ "Policy for R file already defined"
+ )
+ }
+ rFilePolicy = policy.withReason(
+ "$FILTER_REASON (special-class R file)"
+ )
+ }
+ }
+ }
+ }
+
+ private fun parseField(fields: Array<String>) {
+ if (fields.size < 3) {
+ throw ParseException("Field ('f') expects 2 fields.")
+ }
+ val name = fields[1]
+ val policy = parsePolicy(fields[2])
+ if (!policy.isUsableWithFields) {
+ throw ParseException("Field can't have policy '$policy'")
+ }
+ require(this::currentClassName.isInitialized)
+
+ // TODO: Duplicate check, etc
+ imf.setPolicyForField(currentClassName, name, policy.withReason(FILTER_REASON))
+ }
+
+ private fun parseMethod(fields: Array<String>) {
+ if (fields.size < 4) {
+ throw ParseException("Method ('m') expects 3 fields.")
+ }
+ val name = fields[1]
+ val signature = fields[2]
+ val policy = parsePolicy(fields[3])
+
+ if (!policy.isUsableWithMethods) {
+ throw ParseException("Method can't have policy '$policy'")
+ }
+
+ require(this::currentClassName.isInitialized)
+
+ imf.setPolicyForMethod(
+ currentClassName, name, signature,
+ policy.withReason(FILTER_REASON)
+ )
+ if (policy == FilterPolicy.Substitute) {
+ val fromName = fields[3].substring(1)
+
+ if (fromName == name) {
+ throw ParseException(
+ "Substitution must have a different name"
+ )
+ }
+
+ // Set the policy for the "from" method.
+ imf.setPolicyForMethod(
+ currentClassName, fromName, signature,
+ FilterPolicy.Keep.withReason(FILTER_REASON)
+ )
+
+ val classAndMethod = splitWithLastPeriod(fromName)
+ if (classAndMethod != null) {
+ // If the substitution target contains a ".", then
+ // it's a method call redirect.
+ methodReplaceSpec.add(
+ TextFilePolicyMethodReplaceFilter.MethodCallReplaceSpec(
+ currentClassName.toJvmClassName(),
+ name,
+ signature,
+ classAndMethod.first.toJvmClassName(),
+ classAndMethod.second,
+ )
+ )
+ } else {
+ // It's an in-class replace.
+ // ("@RavenwoodReplace" equivalent)
+ imf.setRenameTo(currentClassName, fromName, signature, name)
+ }
+ }
+ }
+
+ private fun parseRename(fields: Array<String>) {
+ if (fields.size < 3) {
+ throw ParseException("Rename ('r') expects 2 fields.")
+ }
+ // Add ".*" to make it a prefix match.
+ val pattern = Pattern.compile(fields[1] + ".*")
+
+ // Removing the leading /'s from the prefix. This allows
+ // using a single '/' as an empty suffix, which is useful to have a
+ // "negative" rename rule to avoid subsequent raname's from getting
+ // applied. (Which is needed for services.jar)
+ val prefix = fields[2].trimStart('/')
+
+ typeRenameSpec += TextFilePolicyRemapperFilter.TypeRenameSpec(
+ pattern, prefix
+ )
+ }
}