Merge "Register ProtoLog service" into main
diff --git a/AconfigFlags.bp b/AconfigFlags.bp
index 7d9e95b..4e34b63 100644
--- a/AconfigFlags.bp
+++ b/AconfigFlags.bp
@@ -172,6 +172,7 @@
// Window
aconfig_declarations {
name: "com.android.window.flags.window-aconfig",
+ exportable: true,
package: "com.android.window.flags",
container: "system",
srcs: ["core/java/android/window/flags/*.aconfig"],
diff --git a/Ravenwood.bp b/Ravenwood.bp
index 7faa33f..5f32ba0 100644
--- a/Ravenwood.bp
+++ b/Ravenwood.bp
@@ -25,13 +25,15 @@
visibility: ["//visibility:private"],
}
-// Generate the stub/impl from framework-all, with hidden APIs.
+// 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.
+// 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",
@@ -41,148 +43,101 @@
],
out: [
"ravenwood.jar",
-
- // Following files are created just as FYI.
- "hoststubgen_framework-minus-apex_keep_all.txt",
- "hoststubgen_framework-minus-apex_dump.txt",
-
"hoststubgen_framework-minus-apex.log",
- "hoststubgen_framework-minus-apex_stats.csv",
- "hoststubgen_framework-minus-apex_apis.csv",
],
- visibility: ["//visibility:private"],
}
+framework_minus_apex_cmd = "$(location hoststubgen) " +
+ "@$(location :ravenwood-standard-options) " +
+ "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+ "--out-impl-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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 0 " + // Only this line differs
-
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 1 " + // Only this line differs
-
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 2 " + // Only this line differs
-
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 3 " + // Only this line differs
-
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 4 " + // Only this line differs
-
- "@$(location :ravenwood-standard-options) " +
-
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
- "--stats-file $(location hoststubgen_framework-minus-apex_stats.csv) " +
- "--supported-api-list-file $(location hoststubgen_framework-minus-apex_apis.csv) " +
-
- "--out-impl-jar $(location ravenwood.jar) " +
-
- "--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
-
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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: "$(location hoststubgen) " +
- "--num-shards 6 --shard-index 5 " + // Only this line differs
+ cmd: framework_minus_apex_cmd + " --num-shards 10 --shard-index 5",
+}
- "@$(location :ravenwood-standard-options) " +
+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",
+}
- "--debug-log $(location hoststubgen_framework-minus-apex.log) " +
+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) " +
- "--out-impl-jar $(location ravenwood.jar) " +
-
"--gen-keep-all-file $(location hoststubgen_framework-minus-apex_keep_all.txt) " +
- "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) " +
+ "--gen-input-dump-file $(location hoststubgen_framework-minus-apex_dump.txt) ",
- "--in-jar $(location :framework-minus-apex-for-hoststubgen) " +
- "--policy-override-file $(location :ravenwood-framework-policies) " +
- "--annotation-allowed-classes-file $(location :ravenwood-annotation-allowed-classes) ",
+ 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
@@ -198,73 +153,16 @@
":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",
],
}
-// Merge the sharded text files
-genrule {
- name: "hoststubgen_framework-minus-apex_stats.csv",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cat $(in) > $(out)",
- srcs: [
- ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_stats.csv}",
- ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_stats.csv}",
- ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_stats.csv}",
- ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_stats.csv}",
- ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_stats.csv}",
- ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_stats.csv}",
- ],
- out: ["hoststubgen_framework-minus-apex_stats.csv"],
-}
-
-genrule {
- name: "hoststubgen_framework-minus-apex_apis.csv",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cat $(in) > $(out)",
- srcs: [
- ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_apis.csv}",
- ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_apis.csv}",
- ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_apis.csv}",
- ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_apis.csv}",
- ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_apis.csv}",
- ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_apis.csv}",
- ],
- out: ["hoststubgen_framework-minus-apex_apis.csv"],
-}
-
-genrule {
- name: "hoststubgen_framework-minus-apex_keep_all.txt",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cat $(in) > $(out)",
- srcs: [
- ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_keep_all.txt}",
- ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_keep_all.txt}",
- ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_keep_all.txt}",
- ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_keep_all.txt}",
- ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_keep_all.txt}",
- ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_keep_all.txt}",
- ],
- out: ["hoststubgen_framework-minus-apex_keep_all.txt"],
-}
-
-genrule {
- name: "hoststubgen_framework-minus-apex_dump.txt",
- defaults: ["ravenwood-internal-only-visibility-genrule"],
- cmd: "cat $(in) > $(out)",
- srcs: [
- ":framework-minus-apex.ravenwood-base_X0{hoststubgen_framework-minus-apex_dump.txt}",
- ":framework-minus-apex.ravenwood-base_X1{hoststubgen_framework-minus-apex_dump.txt}",
- ":framework-minus-apex.ravenwood-base_X2{hoststubgen_framework-minus-apex_dump.txt}",
- ":framework-minus-apex.ravenwood-base_X3{hoststubgen_framework-minus-apex_dump.txt}",
- ":framework-minus-apex.ravenwood-base_X4{hoststubgen_framework-minus-apex_dump.txt}",
- ":framework-minus-apex.ravenwood-base_X5{hoststubgen_framework-minus-apex_dump.txt}",
- ],
- out: ["hoststubgen_framework-minus-apex_dump.txt"],
-}
-
java_library {
name: "services.core-for-hoststubgen",
installable: false, // host only jar.
@@ -325,6 +223,9 @@
],
}
+// 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"],
@@ -337,7 +238,6 @@
// 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.
-// Also apply jarjar.
java_library {
name: "100-framework-minus-apex.ravenwood",
defaults: ["ravenwood-internal-only-visibility-java"],
diff --git a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
index c00c8d5..06cd942 100644
--- a/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
+++ b/apct-tests/perftests/settingsprovider/src/android/provider/SettingsProviderPerfTest.java
@@ -36,7 +36,7 @@
@RunWith(AndroidJUnit4.class)
public final class SettingsProviderPerfTest {
- private static final String NAMESPACE = "test@namespace";
+ private static final String NAMESPACE = "testing";
private static final String SETTING_NAME1 = "test:setting1";
private static final String SETTING_NAME2 = "test-setting2";
private static final String UNSET_SETTING = "test_unset_setting";
diff --git a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
index 18ee6f2..ba66ff7 100644
--- a/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
+++ b/apex/jobscheduler/service/java/com/android/server/alarm/AlarmManagerService.java
@@ -4441,6 +4441,11 @@
public void run() {
ArrayList<Alarm> triggerList = new ArrayList<Alarm>();
+ synchronized (mLock) {
+ mLastTimeChangeClockTime = mInjector.getCurrentTimeMillis();
+ mLastTimeChangeRealtime = mInjector.getElapsedRealtimeMillis();
+ }
+
while (true) {
int result = mInjector.waitForAlarm();
final long nowRTC = mInjector.getCurrentTimeMillis();
@@ -4464,10 +4469,9 @@
expectedClockTime = lastTimeChangeClockTime
+ (nowELAPSED - mLastTimeChangeRealtime);
}
- if (lastTimeChangeClockTime == 0 || nowRTC < (expectedClockTime - 1000)
+ if (nowRTC < (expectedClockTime - 1000)
|| nowRTC > (expectedClockTime + 1000)) {
- // The change is by at least +/- 1000 ms (or this is the first change),
- // let's do it!
+ // The change is by at least +/- 1000 ms, let's do it!
if (DEBUG_BATCH) {
Slog.v(TAG, "Time changed notification from kernel; rebatching");
}
diff --git a/core/api/current.txt b/core/api/current.txt
index 354e26b..ea039a7 100644
--- a/core/api/current.txt
+++ b/core/api/current.txt
@@ -59034,7 +59034,7 @@
}
public static interface CompoundButton.OnCheckedChangeListener {
- method public void onCheckedChanged(android.widget.CompoundButton, boolean);
+ method public void onCheckedChanged(@NonNull android.widget.CompoundButton, boolean);
}
public abstract class CursorAdapter extends android.widget.BaseAdapter implements android.widget.Filterable android.widget.ThemedSpinnerAdapter {
@@ -60099,7 +60099,7 @@
}
public static interface RadioGroup.OnCheckedChangeListener {
- method public void onCheckedChanged(android.widget.RadioGroup, @IdRes int);
+ method public void onCheckedChanged(@NonNull android.widget.RadioGroup, @IdRes int);
}
public class RatingBar extends android.widget.AbsSeekBar {
diff --git a/core/api/system-current.txt b/core/api/system-current.txt
index 15e5706..445a572 100644
--- a/core/api/system-current.txt
+++ b/core/api/system-current.txt
@@ -11981,6 +11981,7 @@
field public static final String ACTION_MANAGE_APP_OVERLAY_PERMISSION = "android.settings.MANAGE_APP_OVERLAY_PERMISSION";
field public static final String ACTION_MANAGE_DOMAIN_URLS = "android.settings.MANAGE_DOMAIN_URLS";
field public static final String ACTION_MANAGE_MORE_DEFAULT_APPS_SETTINGS = "android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS";
+ field @FlaggedApi("android.nfc.nfc_action_manage_services_settings") public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS = "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS";
field public static final String ACTION_NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS = "android.settings.NOTIFICATION_POLICY_ACCESS_DETAIL_SETTINGS";
field public static final String ACTION_REQUEST_ENABLE_CONTENT_CAPTURE = "android.settings.REQUEST_ENABLE_CONTENT_CAPTURE";
field public static final String ACTION_SHOW_ADMIN_SUPPORT_DETAILS = "android.settings.SHOW_ADMIN_SUPPORT_DETAILS";
diff --git a/core/java/android/app/Activity.java b/core/java/android/app/Activity.java
index 90de7ab..4fb35c3 100644
--- a/core/java/android/app/Activity.java
+++ b/core/java/android/app/Activity.java
@@ -7312,7 +7312,7 @@
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
private void finish(int finishTask) {
if (DEBUG_FINISH_ACTIVITY) {
- Log.d("Instrumentation", "finishActivity: finishTask=" + finishTask, new Throwable());
+ Log.d(Instrumentation.TAG, "finishActivity: finishTask=" + finishTask, new Throwable());
}
if (mParent == null) {
int resultCode;
diff --git a/core/java/android/app/ActivityManager.java b/core/java/android/app/ActivityManager.java
index 74e9583..be70de2 100644
--- a/core/java/android/app/ActivityManager.java
+++ b/core/java/android/app/ActivityManager.java
@@ -16,6 +16,7 @@
package android.app;
+import static android.app.Instrumentation.DEBUG_FINISH_ACTIVITY;
import static android.app.WindowConfiguration.activityTypeToString;
import static android.app.WindowConfiguration.windowingModeToString;
import static android.content.Intent.FLAG_ACTIVITY_EXCLUDE_FROM_RECENTS;
@@ -80,6 +81,7 @@
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.DisplayMetrics;
+import android.util.Log;
import android.util.Singleton;
import android.util.Size;
import android.view.WindowInsetsController.Appearance;
@@ -6011,6 +6013,10 @@
* Finishes all activities in this task and removes it from the recent tasks list.
*/
public void finishAndRemoveTask() {
+ if (DEBUG_FINISH_ACTIVITY) {
+ Log.d(Instrumentation.TAG, "AppTask#finishAndRemoveTask: task="
+ + getTaskInfo(), new Throwable());
+ }
try {
mAppTaskImpl.finishAndRemoveTask();
} catch (RemoteException e) {
diff --git a/core/java/android/app/ActivityManagerInternal.java b/core/java/android/app/ActivityManagerInternal.java
index 89efa9b..d318812 100644
--- a/core/java/android/app/ActivityManagerInternal.java
+++ b/core/java/android/app/ActivityManagerInternal.java
@@ -961,6 +961,17 @@
@Nullable VoiceInteractionManagerProvider provider);
/**
+ * Get whether or not the previous user's packages will be killed before the user is
+ * stopped during a user switch.
+ *
+ * <p> The primary use case of this method is for {@link com.android.server.SystemService}
+ * classes to call this API in their
+ * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent
+ * restarting any of the previous user's processes that will be killed during the user switch.
+ */
+ public abstract boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId);
+
+ /**
* Sets whether the current foreground user (and its profiles) should be stopped after switched
* out.
*/
diff --git a/core/java/android/app/AppOpsManager.java b/core/java/android/app/AppOpsManager.java
index 9d63be7..21396a1 100644
--- a/core/java/android/app/AppOpsManager.java
+++ b/core/java/android/app/AppOpsManager.java
@@ -10768,8 +10768,13 @@
final long key = makeKey(uidState, flag);
NoteOpEvent event = events.get(key);
- if (lastEvent == null
- || event != null && event.getNoteTime() > lastEvent.getNoteTime()) {
+ if (event == null) {
+ continue;
+ }
+
+ if (lastEvent == null || event.getNoteTime() > lastEvent.getNoteTime()
+ || (event.getNoteTime() == lastEvent.getNoteTime()
+ && event.getDuration() > lastEvent.getDuration())) {
lastEvent = event;
}
}
diff --git a/core/java/android/app/Instrumentation.java b/core/java/android/app/Instrumentation.java
index be27046..45852c7 100644
--- a/core/java/android/app/Instrumentation.java
+++ b/core/java/android/app/Instrumentation.java
@@ -98,7 +98,7 @@
*/
public static final String REPORT_KEY_STREAMRESULT = "stream";
- private static final String TAG = "Instrumentation";
+ static final String TAG = "Instrumentation";
private static final long CONNECT_TIMEOUT_MILLIS = 60_000;
diff --git a/core/java/android/app/ResourcesManager.java b/core/java/android/app/ResourcesManager.java
index fd4d8e9..0cc210b 100644
--- a/core/java/android/app/ResourcesManager.java
+++ b/core/java/android/app/ResourcesManager.java
@@ -1836,9 +1836,10 @@
// have shared library asset paths appended if there are any.
if (r.getImpl() != null) {
final ResourcesImpl oldImpl = r.getImpl();
+ final AssetManager oldAssets = oldImpl.getAssets();
// ResourcesImpl constructor will help to append shared library asset paths.
- if (oldImpl.getAssets().isUpToDate()) {
- final ResourcesImpl newImpl = new ResourcesImpl(oldImpl.getAssets(),
+ if (oldAssets != AssetManager.getSystem() && oldAssets.isUpToDate()) {
+ final ResourcesImpl newImpl = new ResourcesImpl(oldAssets,
oldImpl.getMetrics(), oldImpl.getConfiguration(),
oldImpl.getDisplayAdjustments());
r.setImpl(newImpl);
diff --git a/core/java/android/app/admin/flags/flags.aconfig b/core/java/android/app/admin/flags/flags.aconfig
index c789af3..9148e3c 100644
--- a/core/java/android/app/admin/flags/flags.aconfig
+++ b/core/java/android/app/admin/flags/flags.aconfig
@@ -37,7 +37,6 @@
}
}
-
flag {
name: "onboarding_bugreport_v2_enabled"
is_exported: true
@@ -403,3 +402,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "dont_read_policy_definition"
+ namespace: "enterprise"
+ description: "Rely on <policy-key-entry> to determine policy definition and ignore <policy-definition-entry>"
+ bug: "335663055"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/app/appfunctions/OWNERS b/core/java/android/app/appfunctions/OWNERS
new file mode 100644
index 0000000..c6827cc
--- /dev/null
+++ b/core/java/android/app/appfunctions/OWNERS
@@ -0,0 +1,6 @@
+avayvod@google.com
+oadesina@google.com
+toki@google.com
+tonymak@google.com
+mingweiliao@google.com
+anothermark@google.com
diff --git a/core/java/android/content/Intent.java b/core/java/android/content/Intent.java
index 111e6a8..cb57c7b 100644
--- a/core/java/android/content/Intent.java
+++ b/core/java/android/content/Intent.java
@@ -7485,7 +7485,7 @@
/**
* This flag is only used for split-screen multi-window mode. The new activity will be displayed
- * adjacent to the one launching it. This can only be used in conjunction with
+ * adjacent to the one launching it if possible. This can only be used in conjunction with
* {@link #FLAG_ACTIVITY_NEW_TASK}. Also, setting {@link #FLAG_ACTIVITY_MULTIPLE_TASK} is
* required if you want a new instance of an existing activity to be created.
*/
diff --git a/core/java/android/content/pm/multiuser.aconfig b/core/java/android/content/pm/multiuser.aconfig
index e370e85..273519b 100644
--- a/core/java/android/content/pm/multiuser.aconfig
+++ b/core/java/android/content/pm/multiuser.aconfig
@@ -151,6 +151,16 @@
}
flag {
+ name: "fix_avatar_cross_user_leak"
+ namespace: "multiuser"
+ description: "Fix cross-user picture uri leak for avatar picker apps."
+ bug: "341688848"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "fix_get_user_property_cache"
namespace: "multiuser"
description: "Cache is not optimised for getUserProperty for values below 0, eg. UserHandler.USER_NULL or UserHandle.USER_ALL"
@@ -278,6 +288,13 @@
}
flag {
+ name: "stop_previous_user_apps"
+ namespace: "multiuser"
+ description: "Stop the previous user apps early in a user switch"
+ bug: "323200731"
+}
+
+flag {
name: "disable_private_space_items_on_home"
namespace: "profile_experiences"
description: "Disables adding items belonging to Private Space on Home Screen manually as well as automatically"
@@ -386,4 +403,7 @@
description: "Refactorings related to unicorn mode to work on HSUM mode (Read only flag)"
bug: "339201286"
is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
}
\ No newline at end of file
diff --git a/core/java/android/hardware/biometrics/BiometricManager.java b/core/java/android/hardware/biometrics/BiometricManager.java
index 678bd6b..de1cac4 100644
--- a/core/java/android/hardware/biometrics/BiometricManager.java
+++ b/core/java/android/hardware/biometrics/BiometricManager.java
@@ -415,7 +415,7 @@
@RequiresPermission(TEST_BIOMETRIC)
public BiometricTestSession createTestSession(int sensorId) {
try {
- return new BiometricTestSession(mContext, sensorId,
+ return new BiometricTestSession(mContext, getSensorProperties(), sensorId,
(context, sensorId1, callback) -> mService
.createTestSession(sensorId1, callback, context.getOpPackageName()));
} catch (RemoteException e) {
diff --git a/core/java/android/hardware/biometrics/BiometricPrompt.java b/core/java/android/hardware/biometrics/BiometricPrompt.java
index 9007b62..b11961c 100644
--- a/core/java/android/hardware/biometrics/BiometricPrompt.java
+++ b/core/java/android/hardware/biometrics/BiometricPrompt.java
@@ -638,17 +638,17 @@
* Set caller's component name for getting logo icon/description. This should only be used
* by ConfirmDeviceCredentialActivity, see b/337082634 for more context.
*
- * @param componentNameForConfirmDeviceCredentialActivity set the component name for
- * ConfirmDeviceCredentialActivity.
+ * @param realCaller set the component name of real caller for
+ * ConfirmDeviceCredentialActivity.
* @return This builder.
* @hide
*/
@NonNull
@RequiresPermission(anyOf = {TEST_BIOMETRIC, USE_BIOMETRIC_INTERNAL})
- public Builder setComponentNameForConfirmDeviceCredentialActivity(
- ComponentName componentNameForConfirmDeviceCredentialActivity) {
- mPromptInfo.setComponentNameForConfirmDeviceCredentialActivity(
- componentNameForConfirmDeviceCredentialActivity);
+ public Builder setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) {
+ mPromptInfo.setRealCallerForConfirmDeviceCredentialActivity(realCaller);
+ mPromptInfo.setClassNameIfItIsConfirmDeviceCredentialActivity(
+ mContext.getClass().getName());
return this;
}
diff --git a/core/java/android/hardware/biometrics/BiometricTestSession.java b/core/java/android/hardware/biometrics/BiometricTestSession.java
index 027d101..8bd3528 100644
--- a/core/java/android/hardware/biometrics/BiometricTestSession.java
+++ b/core/java/android/hardware/biometrics/BiometricTestSession.java
@@ -27,12 +27,15 @@
import android.util.ArraySet;
import android.util.Log;
+import java.util.ArrayList;
+import java.util.List;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.TimeUnit;
/**
* Common set of interfaces to test biometric-related APIs, including {@link BiometricPrompt} and
* {@link android.hardware.fingerprint.FingerprintManager}.
+ *
* @hide
*/
@TestApi
@@ -48,21 +51,29 @@
@NonNull ITestSessionCallback callback) throws RemoteException;
}
- private final Context mContext;
private final int mSensorId;
- private final ITestSession mTestSession;
+ private final List<ITestSession> mTestSessionsForAllSensors = new ArrayList<>();
+ private ITestSession mTestSession;
// Keep track of users that were tested, which need to be cleaned up when finishing.
- @NonNull private final ArraySet<Integer> mTestedUsers;
+ @NonNull
+ private final ArraySet<Integer> mTestedUsers;
// Track the users currently cleaning up, and provide a latch that gets notified when all
// users have finished cleaning up. This is an imperfect system, as there can technically be
// multiple cleanups per user. Theoretically we should track the cleanup's BaseClientMonitor's
// unique ID, but it's complicated to plumb it through. This should be fine for now.
- @Nullable private CountDownLatch mCloseLatch;
- @NonNull private final ArraySet<Integer> mUsersCleaningUp;
+ @Nullable
+ private CountDownLatch mCloseLatch;
+ @NonNull
+ private final ArraySet<Integer> mUsersCleaningUp;
- private final ITestSessionCallback mCallback = new ITestSessionCallback.Stub() {
+ private class TestSessionCallbackIml extends ITestSessionCallback.Stub {
+ private final int mSensorId;
+ private TestSessionCallbackIml(int sensorId) {
+ mSensorId = sensorId;
+ }
+
@Override
public void onCleanupStarted(int userId) {
Log.d(getTag(), "onCleanupStarted, sensor: " + mSensorId + ", userId: " + userId);
@@ -76,19 +87,30 @@
mUsersCleaningUp.remove(userId);
if (mUsersCleaningUp.isEmpty() && mCloseLatch != null) {
+ Log.d(getTag(), "counting down");
mCloseLatch.countDown();
}
}
- };
+ }
/**
* @hide
*/
- public BiometricTestSession(@NonNull Context context, int sensorId,
- @NonNull TestSessionProvider testSessionProvider) throws RemoteException {
- mContext = context;
+ public BiometricTestSession(@NonNull Context context, List<SensorProperties> sensors,
+ int sensorId, @NonNull TestSessionProvider testSessionProvider) throws RemoteException {
mSensorId = sensorId;
- mTestSession = testSessionProvider.createTestSession(context, sensorId, mCallback);
+ // When any of the sensors should create the test session, all the other sensors should
+ // set test hal enabled too.
+ for (SensorProperties sensor : sensors) {
+ final int id = sensor.getSensorId();
+ final ITestSession session = testSessionProvider.createTestSession(context, id,
+ new TestSessionCallbackIml(id));
+ mTestSessionsForAllSensors.add(session);
+ if (id == sensorId) {
+ mTestSession = session;
+ }
+ }
+
mTestedUsers = new ArraySet<>();
mUsersCleaningUp = new ArraySet<>();
setTestHalEnabled(true);
@@ -107,8 +129,11 @@
@RequiresPermission(TEST_BIOMETRIC)
private void setTestHalEnabled(boolean enabled) {
try {
- Log.w(getTag(), "setTestHalEnabled, sensor: " + mSensorId + " enabled: " + enabled);
- mTestSession.setTestHalEnabled(enabled);
+ for (ITestSession session : mTestSessionsForAllSensors) {
+ Log.w(getTag(), "setTestHalEnabled, sensor: " + session.getSensorId() + " enabled: "
+ + enabled);
+ session.setTestHalEnabled(enabled);
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -175,10 +200,12 @@
/**
* Simulates an acquired message from the HAL.
*
- * @param userId User that this command applies to.
+ * @param userId User that this command applies to.
* @param acquireInfo See
- * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and
- * {@link FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)}
+ * {@link
+ * BiometricPrompt.AuthenticationCallback#onAuthenticationAcquired(int)} and
+ * {@link
+ * FingerprintManager.AuthenticationCallback#onAuthenticationAcquired(int)}
*/
@RequiresPermission(TEST_BIOMETRIC)
public void notifyAcquired(int userId, int acquireInfo) {
@@ -192,10 +219,12 @@
/**
* Simulates an error message from the HAL.
*
- * @param userId User that this command applies to.
+ * @param userId User that this command applies to.
* @param errorCode See
- * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int, CharSequence)} and
- * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int, CharSequence)}
+ * {@link BiometricPrompt.AuthenticationCallback#onAuthenticationError(int,
+ * CharSequence)} and
+ * {@link FingerprintManager.AuthenticationCallback#onAuthenticationError(int,
+ * CharSequence)}
*/
@RequiresPermission(TEST_BIOMETRIC)
public void notifyError(int userId, int errorCode) {
@@ -220,8 +249,20 @@
Log.w(getTag(), "Cleanup already in progress for user: " + userId);
}
- mUsersCleaningUp.add(userId);
- mTestSession.cleanupInternalState(userId);
+ for (ITestSession session : mTestSessionsForAllSensors) {
+ mUsersCleaningUp.add(userId);
+ Log.d(getTag(), "cleanupInternalState for sensor: " + session.getSensorId());
+ mCloseLatch = new CountDownLatch(1);
+ session.cleanupInternalState(userId);
+
+ try {
+ Log.d(getTag(), "Awaiting latch...");
+ mCloseLatch.await(3, TimeUnit.SECONDS);
+ Log.d(getTag(), "Finished awaiting");
+ } catch (InterruptedException e) {
+ Log.e(getTag(), "Latch interrupted", e);
+ }
+ }
} catch (RemoteException e) {
throw e.rethrowFromSystemServer();
}
@@ -234,18 +275,9 @@
// Cleanup can be performed using the test HAL, since it always responds to enumerate with
// zero enrollments.
if (!mTestedUsers.isEmpty()) {
- mCloseLatch = new CountDownLatch(1);
for (int user : mTestedUsers) {
cleanupInternalState(user);
}
-
- try {
- Log.d(getTag(), "Awaiting latch...");
- mCloseLatch.await(3, TimeUnit.SECONDS);
- Log.d(getTag(), "Finished awaiting");
- } catch (InterruptedException e) {
- Log.e(getTag(), "Latch interrupted", e);
- }
}
if (!mUsersCleaningUp.isEmpty()) {
diff --git a/core/java/android/hardware/biometrics/ITestSession.aidl b/core/java/android/hardware/biometrics/ITestSession.aidl
index df9f504..bd99606 100644
--- a/core/java/android/hardware/biometrics/ITestSession.aidl
+++ b/core/java/android/hardware/biometrics/ITestSession.aidl
@@ -59,4 +59,8 @@
// HAL is disabled (e.g. to clean up after a test).
@EnforcePermission("TEST_BIOMETRIC")
void cleanupInternalState(int userId);
+
+ // Get the sensor id of the current test session.
+ @EnforcePermission("TEST_BIOMETRIC")
+ int getSensorId();
}
diff --git a/core/java/android/hardware/biometrics/PromptInfo.java b/core/java/android/hardware/biometrics/PromptInfo.java
index 901f6b7..df5d864 100644
--- a/core/java/android/hardware/biometrics/PromptInfo.java
+++ b/core/java/android/hardware/biometrics/PromptInfo.java
@@ -57,7 +57,8 @@
private boolean mIsForLegacyFingerprintManager = false;
private boolean mShowEmergencyCallButton = false;
private boolean mUseParentProfileForDeviceCredential = false;
- private ComponentName mComponentNameForConfirmDeviceCredentialActivity = null;
+ private ComponentName mRealCallerForConfirmDeviceCredentialActivity = null;
+ private String mClassNameIfItIsConfirmDeviceCredentialActivity = null;
public PromptInfo() {
@@ -89,8 +90,9 @@
mIsForLegacyFingerprintManager = in.readBoolean();
mShowEmergencyCallButton = in.readBoolean();
mUseParentProfileForDeviceCredential = in.readBoolean();
- mComponentNameForConfirmDeviceCredentialActivity = in.readParcelable(
+ mRealCallerForConfirmDeviceCredentialActivity = in.readParcelable(
ComponentName.class.getClassLoader(), ComponentName.class);
+ mClassNameIfItIsConfirmDeviceCredentialActivity = in.readString();
}
public static final Creator<PromptInfo> CREATOR = new Creator<PromptInfo>() {
@@ -136,7 +138,8 @@
dest.writeBoolean(mIsForLegacyFingerprintManager);
dest.writeBoolean(mShowEmergencyCallButton);
dest.writeBoolean(mUseParentProfileForDeviceCredential);
- dest.writeParcelable(mComponentNameForConfirmDeviceCredentialActivity, 0);
+ dest.writeParcelable(mRealCallerForConfirmDeviceCredentialActivity, 0);
+ dest.writeString(mClassNameIfItIsConfirmDeviceCredentialActivity);
}
// LINT.IfChange
@@ -155,7 +158,7 @@
return true;
} else if (mShowEmergencyCallButton) {
return true;
- } else if (mComponentNameForConfirmDeviceCredentialActivity != null) {
+ } else if (mRealCallerForConfirmDeviceCredentialActivity != null) {
return true;
}
return false;
@@ -321,10 +324,8 @@
mShowEmergencyCallButton = showEmergencyCallButton;
}
- public void setComponentNameForConfirmDeviceCredentialActivity(
- ComponentName componentNameForConfirmDeviceCredentialActivity) {
- mComponentNameForConfirmDeviceCredentialActivity =
- componentNameForConfirmDeviceCredentialActivity;
+ public void setRealCallerForConfirmDeviceCredentialActivity(ComponentName realCaller) {
+ mRealCallerForConfirmDeviceCredentialActivity = realCaller;
}
public void setUseParentProfileForDeviceCredential(
@@ -332,6 +333,14 @@
mUseParentProfileForDeviceCredential = useParentProfileForDeviceCredential;
}
+ /**
+ * Set the class name of ConfirmDeviceCredentialActivity.
+ */
+ void setClassNameIfItIsConfirmDeviceCredentialActivity(String className) {
+ mClassNameIfItIsConfirmDeviceCredentialActivity = className;
+ }
+
+
// Getters
/**
@@ -455,8 +464,15 @@
return mShowEmergencyCallButton;
}
- public ComponentName getComponentNameForConfirmDeviceCredentialActivity() {
- return mComponentNameForConfirmDeviceCredentialActivity;
+ public ComponentName getRealCallerForConfirmDeviceCredentialActivity() {
+ return mRealCallerForConfirmDeviceCredentialActivity;
}
+ /**
+ * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is
+ * not ConfirmDeviceCredentialActivity.
+ */
+ public String getClassNameIfItIsConfirmDeviceCredentialActivity() {
+ return mClassNameIfItIsConfirmDeviceCredentialActivity;
+ }
}
diff --git a/core/java/android/hardware/camera2/params/SessionConfiguration.java b/core/java/android/hardware/camera2/params/SessionConfiguration.java
index 0c55ed5..9bd4860 100644
--- a/core/java/android/hardware/camera2/params/SessionConfiguration.java
+++ b/core/java/android/hardware/camera2/params/SessionConfiguration.java
@@ -17,8 +17,6 @@
package android.hardware.camera2.params;
-import static com.android.internal.util.Preconditions.*;
-
import android.annotation.CallbackExecutor;
import android.annotation.FlaggedApi;
import android.annotation.IntDef;
@@ -32,8 +30,6 @@
import android.hardware.camera2.CameraDevice.CameraDeviceSetup;
import android.hardware.camera2.CaptureRequest;
import android.hardware.camera2.impl.CameraMetadataNative;
-import android.hardware.camera2.params.InputConfiguration;
-import android.hardware.camera2.params.OutputConfiguration;
import android.hardware.camera2.utils.HashCodeHelpers;
import android.media.ImageReader;
import android.os.Parcel;
@@ -46,6 +42,7 @@
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
+import java.util.Objects;
import java.util.concurrent.Executor;
/**
@@ -95,8 +92,8 @@
public @interface SessionMode {};
// Camera capture session related parameters.
- private List<OutputConfiguration> mOutputConfigurations;
- private CameraCaptureSession.StateCallback mStateCallback;
+ private final @NonNull List<OutputConfiguration> mOutputConfigurations;
+ private CameraCaptureSession.StateCallback mStateCallback = null;
private int mSessionType;
private Executor mExecutor = null;
private InputConfiguration mInputConfig = null;
@@ -268,7 +265,8 @@
*/
@Override
public int hashCode() {
- return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(), mInputConfig.hashCode(),
+ return HashCodeHelpers.hashCode(mOutputConfigurations.hashCode(),
+ Objects.hashCode(mInputConfig),
mSessionType);
}
diff --git a/core/java/android/hardware/fingerprint/FingerprintManager.java b/core/java/android/hardware/fingerprint/FingerprintManager.java
index 903e916..7f1cac0 100644
--- a/core/java/android/hardware/fingerprint/FingerprintManager.java
+++ b/core/java/android/hardware/fingerprint/FingerprintManager.java
@@ -172,7 +172,7 @@
@RequiresPermission(TEST_BIOMETRIC)
public BiometricTestSession createTestSession(int sensorId) {
try {
- return new BiometricTestSession(mContext, sensorId,
+ return new BiometricTestSession(mContext, getSensorProperties(), sensorId,
(context, sensorId1, callback) -> mService
.createTestSession(sensorId1, callback, context.getOpPackageName()));
} catch (RemoteException e) {
diff --git a/core/java/android/os/ExternalVibrationScale.aidl b/core/java/android/os/ExternalVibrationScale.aidl
index cf6f8ed..644bece 100644
--- a/core/java/android/os/ExternalVibrationScale.aidl
+++ b/core/java/android/os/ExternalVibrationScale.aidl
@@ -33,12 +33,24 @@
SCALE_VERY_HIGH = 2
}
+ // TODO(b/345186129): remove this once we finish migrating to scale factor.
/**
* The scale level that will be applied to external vibrations.
*/
ScaleLevel scaleLevel = ScaleLevel.SCALE_NONE;
/**
+ * The scale factor that will be applied to external vibrations.
+ *
+ * Values in (0,1) will scale down the vibrations, values > 1 will scale up vibrations within
+ * hardware limits. A zero scale factor indicates the external vibration should be muted.
+ *
+ * TODO(b/345186129): update this once we finish migrating, negative should not be expected.
+ * Negative values should be ignored in favour of the legacy ScaleLevel.
+ */
+ float scaleFactor = -1f; // undefined
+
+ /**
* The adaptive haptics scale that will be applied to external vibrations.
*/
float adaptiveHapticsScale = 1f;
diff --git a/core/java/android/os/Trace.java b/core/java/android/os/Trace.java
index edb3a64..4a37e0a 100644
--- a/core/java/android/os/Trace.java
+++ b/core/java/android/os/Trace.java
@@ -520,8 +520,20 @@
* @param counterValue The counter value.
*/
public static void setCounter(@NonNull String counterName, long counterValue) {
- if (isTagEnabled(TRACE_TAG_APP)) {
- nativeTraceCounter(TRACE_TAG_APP, counterName, counterValue);
+ setCounter(TRACE_TAG_APP, counterName, counterValue);
+ }
+
+ /**
+ * Writes trace message to indicate the value of a given counter under a given trace tag.
+ *
+ * @param traceTag The trace tag.
+ * @param counterName The counter name to appear in the trace.
+ * @param counterValue The counter value.
+ * @hide
+ */
+ public static void setCounter(long traceTag, @NonNull String counterName, long counterValue) {
+ if (isTagEnabled(traceTag)) {
+ nativeTraceCounter(traceTag, counterName, counterValue);
}
}
diff --git a/core/java/android/os/UserManager.java b/core/java/android/os/UserManager.java
index 06c516a..28f2c25 100644
--- a/core/java/android/os/UserManager.java
+++ b/core/java/android/os/UserManager.java
@@ -4824,6 +4824,7 @@
* <p>Note that this does not alter the user's pre-existing user restrictions.
*
* @param userId the id of the user to become admin
+ * @throws SecurityException if changing ADMIN status of the user is not allowed
* @hide
*/
@RequiresPermission(allOf = {
@@ -4844,6 +4845,7 @@
* <p>Note that this does not alter the user's pre-existing user restrictions.
*
* @param userId the id of the user to revoke admin rights from
+ * @throws SecurityException if changing ADMIN status of the user is not allowed
* @hide
*/
@RequiresPermission(allOf = {
diff --git a/core/java/android/os/VibrationEffect.java b/core/java/android/os/VibrationEffect.java
index f3ef9e1..e68b746 100644
--- a/core/java/android/os/VibrationEffect.java
+++ b/core/java/android/os/VibrationEffect.java
@@ -663,6 +663,15 @@
* @hide
*/
public static float scale(float intensity, float scaleFactor) {
+ if (Flags.hapticsScaleV2Enabled()) {
+ if (Float.compare(scaleFactor, 1) <= 0 || Float.compare(intensity, 0) == 0) {
+ // Scaling down or scaling zero intensity is straightforward.
+ return scaleFactor * intensity;
+ }
+ // Using S * x / (1 + (S - 1) * x^2) as the scale up function to converge to 1.0.
+ return (scaleFactor * intensity) / (1 + (scaleFactor - 1) * intensity * intensity);
+ }
+
// Applying gamma correction to the scale factor, which is the same as encoding the input
// value, scaling it, then decoding the scaled value.
float scale = MathUtils.pow(scaleFactor, 1f / SCALE_GAMMA);
diff --git a/core/java/android/os/vibrator/VibrationConfig.java b/core/java/android/os/vibrator/VibrationConfig.java
index a4164e9..e6e5a27 100644
--- a/core/java/android/os/vibrator/VibrationConfig.java
+++ b/core/java/android/os/vibrator/VibrationConfig.java
@@ -49,8 +49,22 @@
*/
public class VibrationConfig {
+ /**
+ * Hardcoded default scale level gain to be applied between each scale level to define their
+ * scale factor value.
+ *
+ * <p>Default gain defined as 3 dBs.
+ */
+ private static final float DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
+
+ /**
+ * Hardcoded default amplitude to be used when device config is invalid, i.e. not in [1,255].
+ */
+ private static final int DEFAULT_AMPLITUDE = 255;
+
// TODO(b/191150049): move these to vibrator static config file
private final float mHapticChannelMaxVibrationAmplitude;
+ private final int mDefaultVibrationAmplitude;
private final int mRampStepDurationMs;
private final int mRampDownDurationMs;
private final int mRequestVibrationParamsTimeoutMs;
@@ -75,8 +89,10 @@
/** @hide */
public VibrationConfig(@Nullable Resources resources) {
+ mDefaultVibrationAmplitude = resources.getInteger(
+ com.android.internal.R.integer.config_defaultVibrationAmplitude);
mHapticChannelMaxVibrationAmplitude = loadFloat(resources,
- com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude, 0);
+ com.android.internal.R.dimen.config_hapticChannelMaxVibrationAmplitude);
mRampDownDurationMs = loadInteger(resources,
com.android.internal.R.integer.config_vibrationWaveformRampDownDuration, 0);
mRampStepDurationMs = loadInteger(resources,
@@ -87,9 +103,9 @@
com.android.internal.R.array.config_requestVibrationParamsForUsages);
mIgnoreVibrationsOnWirelessCharger = loadBoolean(resources,
- com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger, false);
+ com.android.internal.R.bool.config_ignoreVibrationsOnWirelessCharger);
mKeyboardVibrationSettingsSupported = loadBoolean(resources,
- com.android.internal.R.bool.config_keyboardVibrationSettingsSupported, false);
+ com.android.internal.R.bool.config_keyboardVibrationSettingsSupported);
mDefaultAlarmVibrationIntensity = loadDefaultIntensity(resources,
com.android.internal.R.integer.config_defaultAlarmVibrationIntensity);
@@ -115,16 +131,16 @@
return value;
}
- private static float loadFloat(@Nullable Resources res, int resId, float defaultValue) {
- return res != null ? res.getFloat(resId) : defaultValue;
+ private static float loadFloat(@Nullable Resources res, int resId) {
+ return res != null ? res.getFloat(resId) : 0f;
}
private static int loadInteger(@Nullable Resources res, int resId, int defaultValue) {
return res != null ? res.getInteger(resId) : defaultValue;
}
- private static boolean loadBoolean(@Nullable Resources res, int resId, boolean defaultValue) {
- return res != null ? res.getBoolean(resId) : defaultValue;
+ private static boolean loadBoolean(@Nullable Resources res, int resId) {
+ return res != null && res.getBoolean(resId);
}
private static int[] loadIntArray(@Nullable Resources res, int resId) {
@@ -145,6 +161,26 @@
}
/**
+ * Return the device default vibration amplitude value to replace the
+ * {@link android.os.VibrationEffect#DEFAULT_AMPLITUDE} constant.
+ */
+ public int getDefaultVibrationAmplitude() {
+ if (mDefaultVibrationAmplitude < 1 || mDefaultVibrationAmplitude > 255) {
+ return DEFAULT_AMPLITUDE;
+ }
+ return mDefaultVibrationAmplitude;
+ }
+
+ /**
+ * Return the device default gain to be applied between scale levels to define the scale factor
+ * for each level.
+ */
+ public float getDefaultVibrationScaleLevelGain() {
+ // TODO(b/356407380): add device config for this
+ return DEFAULT_SCALE_LEVEL_GAIN;
+ }
+
+ /**
* The duration, in milliseconds, that should be applied to the ramp to turn off the vibrator
* when a vibration is cancelled or finished at non-zero amplitude.
*/
@@ -233,6 +269,7 @@
public String toString() {
return "VibrationConfig{"
+ "mIgnoreVibrationsOnWirelessCharger=" + mIgnoreVibrationsOnWirelessCharger
+ + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude
+ ", mHapticChannelMaxVibrationAmplitude=" + mHapticChannelMaxVibrationAmplitude
+ ", mRampStepDurationMs=" + mRampStepDurationMs
+ ", mRampDownDurationMs=" + mRampDownDurationMs
@@ -258,6 +295,7 @@
pw.println("VibrationConfig:");
pw.increaseIndent();
pw.println("ignoreVibrationsOnWirelessCharger = " + mIgnoreVibrationsOnWirelessCharger);
+ pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude);
pw.println("hapticChannelMaxAmplitude = " + mHapticChannelMaxVibrationAmplitude);
pw.println("rampStepDurationMs = " + mRampStepDurationMs);
pw.println("rampDownDurationMs = " + mRampDownDurationMs);
diff --git a/core/java/android/os/vibrator/flags.aconfig b/core/java/android/os/vibrator/flags.aconfig
index 1a19bb2..53a1a67d 100644
--- a/core/java/android/os/vibrator/flags.aconfig
+++ b/core/java/android/os/vibrator/flags.aconfig
@@ -103,3 +103,13 @@
purpose: PURPOSE_FEATURE
}
}
+
+flag {
+ namespace: "haptics"
+ name: "haptics_scale_v2_enabled"
+ description: "Enables new haptics scaling function across all usages"
+ bug: "345186129"
+ metadata {
+ purpose: PURPOSE_FEATURE
+ }
+}
diff --git a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
index a26c6f4..a95ce79 100644
--- a/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
+++ b/core/java/android/os/vibrator/persistence/VibrationXmlSerializer.java
@@ -104,7 +104,7 @@
public static void serialize(@NonNull VibrationEffect effect, @NonNull Writer writer,
@Flags int flags) throws IOException {
// Serialize effect first to fail early.
- XmlSerializedVibration<VibrationEffect> serializedVibration =
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration =
toSerializedVibration(effect, flags);
TypedXmlSerializer xmlSerializer = Xml.newFastSerializer();
xmlSerializer.setFeature(XML_FEATURE_INDENT_OUTPUT, (flags & FLAG_PRETTY_PRINT) != 0);
@@ -114,9 +114,9 @@
xmlSerializer.endDocument();
}
- private static XmlSerializedVibration<VibrationEffect> toSerializedVibration(
+ private static XmlSerializedVibration<? extends VibrationEffect> toSerializedVibration(
VibrationEffect effect, @Flags int flags) throws SerializationFailedException {
- XmlSerializedVibration<VibrationEffect> serializedVibration;
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration;
int serializerFlags = 0;
if ((flags & FLAG_ALLOW_HIDDEN_APIS) != 0) {
serializerFlags |= XmlConstants.FLAG_ALLOW_HIDDEN_APIS;
diff --git a/core/java/android/provider/Settings.java b/core/java/android/provider/Settings.java
index 791b306..7ca40ea 100644
--- a/core/java/android/provider/Settings.java
+++ b/core/java/android/provider/Settings.java
@@ -2287,6 +2287,26 @@
"android.settings.MANAGE_MORE_DEFAULT_APPS_SETTINGS";
/**
+ * Activity Action: Show Other NFC services settings.
+ * <p>
+ * If a Settings activity handles this intent action, an "Other NFC services" entry will be
+ * shown in the Default payment app settings, and clicking it will launch that activity.
+ * <p>
+ * In some cases, a matching Activity may not exist, so ensure you safeguard against this.
+ * <p>
+ * Input: Nothing.
+ * <p>
+ * Output: Nothing.
+ *
+ * @hide
+ */
+ @FlaggedApi(android.nfc.Flags.FLAG_NFC_ACTION_MANAGE_SERVICES_SETTINGS)
+ @SdkConstant(SdkConstantType.ACTIVITY_INTENT_ACTION)
+ @SystemApi
+ public static final String ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS =
+ "android.settings.MANAGE_OTHER_NFC_SERVICES_SETTINGS";
+
+ /**
* Activity Action: Show app screen size list settings for user to override app aspect
* ratio.
* <p>
@@ -20171,6 +20191,36 @@
*/
public static final int PHONE_SWITCHING_STATUS_IN_PROGRESS_MIGRATION_SUCCESS = 11;
+ /**
+ * Phone switching request source
+ * @hide
+ */
+ public static final String PHONE_SWITCHING_REQUEST_SOURCE =
+ "phone_switching_request_source";
+
+ /**
+ * No phone switching request source
+ * @hide
+ */
+ public static final int PHONE_SWITCHING_REQUEST_SOURCE_NONE = 0;
+
+ /**
+ * Phone switching triggered by watch
+ * @hide
+ */
+ public static final int PHONE_SWITCHING_REQUEST_SOURCE_WATCH = 1;
+
+ /**
+ * Phone switching triggered by companion, user confirmation required
+ * @hide
+ */
+ public static final int PHONE_SWITCHING_REQUEST_SOURCE_COMPANION_USER_CONFIRMATION = 2;
+
+ /**
+ * Phone switching triggered by companion, user confirmation not required
+ * @hide
+ */
+ public static final int PHONE_SWITCHING_REQUEST_SOURCE_COMPANION = 3;
/**
* Whether the device has enabled the feature to reduce motion and animation
@@ -20218,14 +20268,6 @@
public static final int TETHERED_CONFIG_RESTRICTED = 3;
/**
- * Whether phone switching is supported.
- *
- * (0 = false, 1 = true)
- * @hide
- */
- public static final String PHONE_SWITCHING_SUPPORTED = "phone_switching_supported";
-
- /**
* Setting indicating the name of the Wear OS package that hosts the Media Controls UI.
*
* @hide
diff --git a/core/java/android/provider/Telephony.java b/core/java/android/provider/Telephony.java
index d019bad..6eaef78f 100644
--- a/core/java/android/provider/Telephony.java
+++ b/core/java/android/provider/Telephony.java
@@ -4985,6 +4985,16 @@
*/
public static final String COLUMN_SATELLITE_ESOS_SUPPORTED = "satellite_esos_supported";
+ /**
+ * TelephonyProvider column name for satellite provisioned status. The value of this
+ * column is set based on whether carrier roaming or OEM-enabled NB-IOT satellite service is
+ * provisioned or not. By default, it's disabled.
+ *
+ * @hide
+ */
+ public static final String COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM =
+ "is_satellite_provisioned_for_non_ip_datagram";
+
/** All columns in {@link SimInfo} table. */
private static final List<String> ALL_COLUMNS = List.of(
COLUMN_UNIQUE_KEY_SUBSCRIPTION_ID,
@@ -5061,7 +5071,8 @@
COLUMN_TRANSFER_STATUS,
COLUMN_SATELLITE_ENTITLEMENT_STATUS,
COLUMN_SATELLITE_ENTITLEMENT_PLMNS,
- COLUMN_SATELLITE_ESOS_SUPPORTED
+ COLUMN_SATELLITE_ESOS_SUPPORTED,
+ COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM
);
/**
diff --git a/core/java/android/security/net/config/SystemCertificateSource.java b/core/java/android/security/net/config/SystemCertificateSource.java
index 3a254c1..bdda42a 100644
--- a/core/java/android/security/net/config/SystemCertificateSource.java
+++ b/core/java/android/security/net/config/SystemCertificateSource.java
@@ -19,6 +19,8 @@
import android.os.Environment;
import android.os.UserHandle;
+import com.android.internal.util.ArrayUtils;
+
import java.io.File;
/**
@@ -45,7 +47,7 @@
}
File updatable_dir = new File("/apex/com.android.conscrypt/cacerts");
if (updatable_dir.exists()
- && !(updatable_dir.list().length == 0)) {
+ && !(ArrayUtils.isEmpty(updatable_dir.list()))) {
return updatable_dir;
}
return new File(System.getenv("ANDROID_ROOT") + "/etc/security/cacerts");
diff --git a/core/java/android/service/dreams/DreamOverlayService.java b/core/java/android/service/dreams/DreamOverlayService.java
index 17d2790..013ec5f 100644
--- a/core/java/android/service/dreams/DreamOverlayService.java
+++ b/core/java/android/service/dreams/DreamOverlayService.java
@@ -28,7 +28,9 @@
import android.util.Log;
import android.view.WindowManager;
+import java.lang.ref.WeakReference;
import java.util.concurrent.Executor;
+import java.util.function.Consumer;
/**
@@ -52,43 +54,51 @@
// An {@link IDreamOverlayClient} implementation that identifies itself when forwarding
// requests to the {@link DreamOverlayService}
private static class OverlayClient extends IDreamOverlayClient.Stub {
- private final DreamOverlayService mService;
+ private final WeakReference<DreamOverlayService> mService;
private boolean mShowComplications;
private ComponentName mDreamComponent;
IDreamOverlayCallback mDreamOverlayCallback;
- OverlayClient(DreamOverlayService service) {
+ OverlayClient(WeakReference<DreamOverlayService> service) {
mService = service;
}
+ private void applyToDream(Consumer<DreamOverlayService> consumer) {
+ final DreamOverlayService service = mService.get();
+
+ if (service != null) {
+ consumer.accept(service);
+ }
+ }
+
@Override
public void startDream(WindowManager.LayoutParams params, IDreamOverlayCallback callback,
String dreamComponent, boolean shouldShowComplications) throws RemoteException {
mDreamComponent = ComponentName.unflattenFromString(dreamComponent);
mShowComplications = shouldShowComplications;
mDreamOverlayCallback = callback;
- mService.startDream(this, params);
+ applyToDream(dreamOverlayService -> dreamOverlayService.startDream(this, params));
}
@Override
public void wakeUp() {
- mService.wakeUp(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.wakeUp(this));
}
@Override
public void endDream() {
- mService.endDream(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.endDream(this));
}
@Override
public void comeToFront() {
- mService.comeToFront(this);
+ applyToDream(dreamOverlayService -> dreamOverlayService.comeToFront(this));
}
@Override
public void onWakeRequested() {
if (Flags.dreamWakeRedirect()) {
- mService.onWakeRequested();
+ applyToDream(DreamOverlayService::onWakeRequested);
}
}
@@ -161,17 +171,24 @@
});
}
- private IDreamOverlay mDreamOverlay = new IDreamOverlay.Stub() {
+ private static class DreamOverlay extends IDreamOverlay.Stub {
+ private final WeakReference<DreamOverlayService> mService;
+
+ DreamOverlay(DreamOverlayService service) {
+ mService = new WeakReference<>(service);
+ }
+
@Override
public void getClient(IDreamOverlayClientCallback callback) {
try {
- callback.onDreamOverlayClient(
- new OverlayClient(DreamOverlayService.this));
+ callback.onDreamOverlayClient(new OverlayClient(mService));
} catch (RemoteException e) {
Log.e(TAG, "could not send client to callback", e);
}
}
- };
+ }
+
+ private final IDreamOverlay mDreamOverlay = new DreamOverlay(this);
public DreamOverlayService() {
}
@@ -195,6 +212,12 @@
}
}
+ @Override
+ public void onDestroy() {
+ mCurrentClient = null;
+ super.onDestroy();
+ }
+
@Nullable
@Override
public final IBinder onBind(@NonNull Intent intent) {
diff --git a/core/java/android/service/notification/ZenModeConfig.java b/core/java/android/service/notification/ZenModeConfig.java
index 224379b..fc6c2e8 100644
--- a/core/java/android/service/notification/ZenModeConfig.java
+++ b/core/java/android/service/notification/ZenModeConfig.java
@@ -25,7 +25,6 @@
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_PEEK;
import static android.app.NotificationManager.Policy.SUPPRESSED_EFFECT_SCREEN_OFF;
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
-import static android.service.notification.Condition.STATE_TRUE;
import static android.service.notification.SystemZenRules.PACKAGE_ANDROID;
import static android.service.notification.ZenAdapters.peopleTypeToPrioritySenders;
import static android.service.notification.ZenAdapters.prioritySendersToPeopleType;
@@ -76,8 +75,9 @@
import android.util.Slog;
import android.util.proto.ProtoOutputStream;
+import androidx.annotation.VisibleForTesting;
+
import com.android.internal.R;
-import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.util.XmlUtils;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
@@ -1074,7 +1074,7 @@
rt.manualRule.type = AutomaticZenRule.TYPE_OTHER;
rt.manualRule.condition = new Condition(
rt.manualRule.conditionId != null ? rt.manualRule.conditionId
- : Uri.EMPTY, "", STATE_TRUE);
+ : Uri.EMPTY, "", Condition.STATE_TRUE);
}
}
return rt;
@@ -2540,10 +2540,34 @@
}
public static class ZenRule implements Parcelable {
+
+ /** No manual override. Rule owner can decide its state. */
+ public static final int OVERRIDE_NONE = 0;
+ /**
+ * User has manually activated a mode. This will temporarily overrule the rule owner's
+ * decision to deactivate it (see {@link #reconsiderConditionOverride}).
+ */
+ public static final int OVERRIDE_ACTIVATE = 1;
+ /**
+ * User has manually deactivated an active mode, or setting ZEN_MODE_OFF (for the few apps
+ * still allowed to do that) snoozed the mode. This will temporarily overrule the rule
+ * owner's decision to activate it (see {@link #reconsiderConditionOverride}).
+ */
+ public static final int OVERRIDE_DEACTIVATE = 2;
+
+ @IntDef(prefix = { "OVERRIDE" }, value = {
+ OVERRIDE_NONE,
+ OVERRIDE_ACTIVATE,
+ OVERRIDE_DEACTIVATE
+ })
+ @Retention(RetentionPolicy.SOURCE)
+ public @interface ConditionOverride {}
+
@UnsupportedAppUsage
public boolean enabled;
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
- public boolean snoozing; // user manually disabled this instance
+ @Deprecated
+ public boolean snoozing; // user manually disabled this instance. Obsolete with MODES_UI
@UnsupportedAppUsage(maxTargetSdk = Build.VERSION_CODES.R, trackingBug = 170729553)
public String name; // required for automatic
@UnsupportedAppUsage
@@ -2579,6 +2603,15 @@
// ZenPolicy, so we store them here, only for the manual rule.
@FlaggedApi(Flags.FLAG_MODES_UI)
int legacySuppressedEffects;
+ /**
+ * Signals a user's action to (temporarily or permanently) activate or deactivate this
+ * rule, overruling the condition set by the owner. This value is not stored to disk, as
+ * it shouldn't survive reboots or be involved in B&R. It might be reset by certain
+ * owner-provided state transitions as well.
+ */
+ @FlaggedApi(Flags.FLAG_MODES_UI)
+ @ConditionOverride
+ int conditionOverride = OVERRIDE_NONE;
public ZenRule() { }
@@ -2620,6 +2653,7 @@
if (Flags.modesUi()) {
disabledOrigin = source.readInt();
legacySuppressedEffects = source.readInt();
+ conditionOverride = source.readInt();
}
}
}
@@ -2698,6 +2732,7 @@
if (Flags.modesUi()) {
dest.writeInt(disabledOrigin);
dest.writeInt(legacySuppressedEffects);
+ dest.writeInt(conditionOverride);
}
}
}
@@ -2708,9 +2743,16 @@
.append("id=").append(id)
.append(",state=").append(condition == null ? "STATE_FALSE"
: Condition.stateToString(condition.state))
- .append(",enabled=").append(String.valueOf(enabled).toUpperCase())
- .append(",snoozing=").append(snoozing)
- .append(",name=").append(name)
+ .append(",enabled=").append(String.valueOf(enabled).toUpperCase());
+
+ if (Flags.modesUi()) {
+ sb.append(",conditionOverride=")
+ .append(conditionOverrideToString(conditionOverride));
+ } else {
+ sb.append(",snoozing=").append(snoozing);
+ }
+
+ sb.append(",name=").append(name)
.append(",zenMode=").append(Global.zenModeToString(zenMode))
.append(",conditionId=").append(conditionId)
.append(",pkg=").append(pkg)
@@ -2753,6 +2795,15 @@
return sb.append(']').toString();
}
+ private static String conditionOverrideToString(@ConditionOverride int value) {
+ return switch(value) {
+ case OVERRIDE_ACTIVATE -> "OVERRIDE_ACTIVATE";
+ case OVERRIDE_DEACTIVATE -> "OVERRIDE_DEACTIVATE";
+ case OVERRIDE_NONE -> "OVERRIDE_NONE";
+ default -> "UNKNOWN";
+ };
+ }
+
/** @hide */
// TODO: add configuration activity
public void dumpDebug(ProtoOutputStream proto, long fieldId) {
@@ -2763,7 +2814,11 @@
proto.write(ZenRuleProto.CREATION_TIME_MS, creationTime);
proto.write(ZenRuleProto.ENABLED, enabled);
proto.write(ZenRuleProto.ENABLER, enabler);
- proto.write(ZenRuleProto.IS_SNOOZING, snoozing);
+ if (Flags.modesApi() && Flags.modesUi()) {
+ proto.write(ZenRuleProto.IS_SNOOZING, conditionOverride == OVERRIDE_DEACTIVATE);
+ } else {
+ proto.write(ZenRuleProto.IS_SNOOZING, snoozing);
+ }
proto.write(ZenRuleProto.ZEN_MODE, zenMode);
if (conditionId != null) {
proto.write(ZenRuleProto.CONDITION_ID, conditionId.toString());
@@ -2816,7 +2871,8 @@
if (Flags.modesUi()) {
finalEquals = finalEquals
&& other.disabledOrigin == disabledOrigin
- && other.legacySuppressedEffects == legacySuppressedEffects;
+ && other.legacySuppressedEffects == legacySuppressedEffects
+ && other.conditionOverride == conditionOverride;
}
}
@@ -2832,7 +2888,8 @@
zenDeviceEffects, modified, allowManualInvocation, iconResName,
triggerDescription, type, userModifiedFields,
zenPolicyUserModifiedFields, zenDeviceEffectsUserModifiedFields,
- deletionInstant, disabledOrigin, legacySuppressedEffects);
+ deletionInstant, disabledOrigin, legacySuppressedEffects,
+ conditionOverride);
} else {
return Objects.hash(enabled, snoozing, name, zenMode, conditionId, condition,
component, configurationActivity, pkg, id, enabler, zenPolicy,
@@ -2858,8 +2915,74 @@
}
}
+ // TODO: b/333527800 - Rename to isActive()
public boolean isAutomaticActive() {
- return enabled && !snoozing && getPkg() != null && isTrueOrUnknown();
+ if (Flags.modesApi() && Flags.modesUi()) {
+ if (!enabled || getPkg() == null) {
+ return false;
+ } else if (conditionOverride == OVERRIDE_ACTIVATE) {
+ return true;
+ } else if (conditionOverride == OVERRIDE_DEACTIVATE) {
+ return false;
+ } else {
+ return isTrueOrUnknown();
+ }
+ } else {
+ return enabled && !snoozing && getPkg() != null && isTrueOrUnknown();
+ }
+ }
+
+ @VisibleForTesting(otherwise = VisibleForTesting.NONE)
+ @ConditionOverride
+ public int getConditionOverride() {
+ if (Flags.modesApi() && Flags.modesUi()) {
+ return conditionOverride;
+ } else {
+ return snoozing ? OVERRIDE_DEACTIVATE : OVERRIDE_NONE;
+ }
+ }
+
+ public void setConditionOverride(@ConditionOverride int value) {
+ if (Flags.modesApi() && Flags.modesUi()) {
+ conditionOverride = value;
+ } else {
+ if (value == OVERRIDE_ACTIVATE) {
+ Slog.wtf(TAG, "Shouldn't set OVERRIDE_ACTIVATE if MODES_UI is off");
+ } else if (value == OVERRIDE_DEACTIVATE) {
+ snoozing = true;
+ } else if (value == OVERRIDE_NONE) {
+ snoozing = false;
+ }
+ }
+ }
+
+ public void resetConditionOverride() {
+ setConditionOverride(OVERRIDE_NONE);
+ }
+
+ /**
+ * Possibly remove the override, depending on the rule owner's intended state.
+ *
+ * <p>This allows rule owners to "take over" manually-provided state with their smartness,
+ * but only once both agree.
+ *
+ * <p>For example, a manually activated rule wins over rule owner's opinion that it should
+ * be off, until the owner says it should be on, at which point it will turn off (without
+ * manual intervention) when the rule owner says it should be off. And symmetrically for
+ * manual deactivation (which used to be called "snoozing").
+ */
+ public void reconsiderConditionOverride() {
+ if (Flags.modesApi() && Flags.modesUi()) {
+ if (conditionOverride == OVERRIDE_ACTIVATE && isTrueOrUnknown()) {
+ resetConditionOverride();
+ } else if (conditionOverride == OVERRIDE_DEACTIVATE && !isTrueOrUnknown()) {
+ resetConditionOverride();
+ }
+ } else {
+ if (snoozing && !isTrueOrUnknown()) {
+ snoozing = false;
+ }
+ }
}
public String getPkg() {
diff --git a/core/java/android/service/notification/ZenModeDiff.java b/core/java/android/service/notification/ZenModeDiff.java
index a37e227..05c2a9c 100644
--- a/core/java/android/service/notification/ZenModeDiff.java
+++ b/core/java/android/service/notification/ZenModeDiff.java
@@ -454,6 +454,8 @@
*/
public static class RuleDiff extends BaseDiff {
public static final String FIELD_ENABLED = "enabled";
+ public static final String FIELD_CONDITION_OVERRIDE = "conditionOverride";
+ @Deprecated
public static final String FIELD_SNOOZING = "snoozing";
public static final String FIELD_NAME = "name";
public static final String FIELD_ZEN_MODE = "zenMode";
@@ -507,8 +509,15 @@
if (from.enabled != to.enabled) {
addField(FIELD_ENABLED, new FieldDiff<>(from.enabled, to.enabled));
}
- if (from.snoozing != to.snoozing) {
- addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing));
+ if (Flags.modesApi() && Flags.modesUi()) {
+ if (from.conditionOverride != to.conditionOverride) {
+ addField(FIELD_CONDITION_OVERRIDE,
+ new FieldDiff<>(from.conditionOverride, to.conditionOverride));
+ }
+ } else {
+ if (from.snoozing != to.snoozing) {
+ addField(FIELD_SNOOZING, new FieldDiff<>(from.snoozing, to.snoozing));
+ }
}
if (!Objects.equals(from.name, to.name)) {
addField(FIELD_NAME, new FieldDiff<>(from.name, to.name));
diff --git a/core/java/android/text/flags/flags.aconfig b/core/java/android/text/flags/flags.aconfig
index 1b85191..bb3f6c9 100644
--- a/core/java/android/text/flags/flags.aconfig
+++ b/core/java/android/text/flags/flags.aconfig
@@ -266,4 +266,14 @@
metadata {
purpose: PURPOSE_BUGFIX
}
-}
\ No newline at end of file
+}
+
+flag {
+ name: "handwriting_gesture_with_transformation"
+ namespace: "text"
+ description: "Fix handwriting gesture is not working when view has transformation."
+ bug: "342619429"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/core/java/android/view/SurfaceControl.java b/core/java/android/view/SurfaceControl.java
index a7641c0..9e4b27d 100644
--- a/core/java/android/view/SurfaceControl.java
+++ b/core/java/android/view/SurfaceControl.java
@@ -3574,7 +3574,7 @@
checkPreconditions(sc);
if (SurfaceControlRegistry.sCallStackDebuggingEnabled) {
SurfaceControlRegistry.getProcessInstance().checkCallStackDebugging(
- "reparent", this, sc,
+ "setColor", this, sc,
"r=" + color[0] + " g=" + color[1] + " b=" + color[2]);
}
nativeSetColor(mNativeObject, sc.mNativeObject, color);
diff --git a/core/java/android/view/ViewRootImpl.java b/core/java/android/view/ViewRootImpl.java
index b8a885e..1c0700f 100644
--- a/core/java/android/view/ViewRootImpl.java
+++ b/core/java/android/view/ViewRootImpl.java
@@ -2820,6 +2820,12 @@
if (mAttachInfo.mThreadedRenderer != null) {
mAttachInfo.mThreadedRenderer.setSurfaceControl(null, null);
}
+
+ // Also reset the VRR relevant values.
+ mPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT;
+ mLastPreferredFrameRateCategory = FRAME_RATE_CATEGORY_DEFAULT;
+ mPreferredFrameRate = 0;
+ mLastPreferredFrameRate = 0;
}
/**
diff --git a/core/java/android/widget/CompoundButton.java b/core/java/android/widget/CompoundButton.java
index 63f8ee7..ed6ec32 100644
--- a/core/java/android/widget/CompoundButton.java
+++ b/core/java/android/widget/CompoundButton.java
@@ -267,7 +267,7 @@
* @param buttonView The compound button view whose state has changed.
* @param isChecked The new checked state of buttonView.
*/
- void onCheckedChanged(CompoundButton buttonView, boolean isChecked);
+ void onCheckedChanged(@NonNull CompoundButton buttonView, boolean isChecked);
}
/**
diff --git a/core/java/android/widget/RadioGroup.java b/core/java/android/widget/RadioGroup.java
index d445fdc..70fe6d5 100644
--- a/core/java/android/widget/RadioGroup.java
+++ b/core/java/android/widget/RadioGroup.java
@@ -366,7 +366,7 @@
* @param group the group in which the checked radio button has changed
* @param checkedId the unique identifier of the newly checked radio button
*/
- public void onCheckedChanged(RadioGroup group, @IdRes int checkedId);
+ void onCheckedChanged(@NonNull RadioGroup group, @IdRes int checkedId);
}
private class CheckedStateTracker implements CompoundButton.OnCheckedChangeListener {
diff --git a/core/java/android/widget/TextView.java b/core/java/android/widget/TextView.java
index ac899f4..61ecc62 100644
--- a/core/java/android/widget/TextView.java
+++ b/core/java/android/widget/TextView.java
@@ -28,10 +28,10 @@
import static android.view.accessibility.AccessibilityNodeInfo.EXTRA_DATA_TEXT_CHARACTER_LOCATION_KEY;
import static android.view.inputmethod.CursorAnchorInfo.FLAG_HAS_VISIBLE_REGION;
import static android.view.inputmethod.EditorInfo.STYLUS_HANDWRITING_ENABLED_ANDROIDX_EXTRAS_KEY;
+import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
import static com.android.text.flags.Flags.FLAG_FIX_LINE_HEIGHT_FOR_LOCALE;
import static com.android.text.flags.Flags.FLAG_USE_BOUNDS_FOR_WIDTH;
-import static android.view.inputmethod.Flags.initiationWithoutInputConnection;
import android.R;
import android.annotation.CallSuper;
@@ -937,6 +937,7 @@
private TextPaint mTempTextPaint;
private Object mTempCursor;
+ private Matrix mTempMatrix;
@UnsupportedAppUsage
private BoringLayout.Metrics mBoring;
@@ -12106,6 +12107,22 @@
}
private PointF convertFromScreenToContentCoordinates(PointF point) {
+ if (Flags.handwritingGestureWithTransformation()) {
+ if (mTempMatrix == null) {
+ mTempMatrix = new Matrix();
+ }
+ Matrix matrix = mTempMatrix;
+ matrix.reset();
+ transformMatrixToLocal(matrix);
+ matrix.postTranslate(
+ -viewportToContentHorizontalOffset(),
+ -viewportToContentVerticalOffset()
+ );
+
+ float[] copy = new float[] { point.x, point.y };
+ matrix.mapPoints(copy);
+ return new PointF(copy[0], copy[1]);
+ }
int[] screenToViewport = getLocationOnScreen();
PointF copy = new PointF(point);
copy.offset(
@@ -12115,6 +12132,22 @@
}
private RectF convertFromScreenToContentCoordinates(RectF rect) {
+ if (Flags.handwritingGestureWithTransformation()) {
+ if (mTempMatrix == null) {
+ mTempMatrix = new Matrix();
+ }
+ Matrix matrix = mTempMatrix;
+ matrix.reset();
+ transformMatrixToLocal(matrix);
+ matrix.postTranslate(
+ -viewportToContentHorizontalOffset(),
+ -viewportToContentVerticalOffset()
+ );
+
+ RectF copy = new RectF(rect);
+ matrix.mapRect(copy);
+ return copy;
+ }
int[] screenToViewport = getLocationOnScreen();
RectF copy = new RectF(rect);
copy.offset(
@@ -14279,6 +14312,9 @@
}
/**
+ * Don't use, it returns wrong result when the view is scaled. This method can be removed once
+ * Flags.handwritingGestureWithTransformation is enabled.
+ * Assume
* Helper method to set {@code rect} to this TextView's non-clipped area in its own coordinates.
* This method obtains the view's visible rectangle whereas the method
* {@link #getContentVisibleRect} returns the text layout's visible rectangle.
@@ -14299,6 +14335,8 @@
}
/**
+ * Don't use, it returns wrong result when view is scaled. This method can be removed once
+ * Flags.handwritingGestureWithTransformation is enabled.
* Helper method to set {@code rect} to the text content's non-clipped area in the view's
* coordinates.
*
@@ -14314,6 +14352,58 @@
getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom());
}
+ private boolean getEditorAndHandwritingBounds(@NonNull RectF editorBounds,
+ @Nullable RectF handwritingBounds) {
+ if (mTempRect == null) {
+ mTempRect = new Rect();
+ }
+ Rect rect = mTempRect;
+ if (!getGlobalVisibleRect(rect)) {
+ return false;
+ }
+ if (mTempMatrix == null) {
+ mTempMatrix = new Matrix();
+ }
+
+ Matrix matrix = mTempMatrix;
+ matrix.reset();
+ transformMatrixToLocal(matrix);
+ editorBounds.set(rect);
+ // When the view has transformations like scaleX/scaleY computing the global visible
+ // rectangle will already apply the transformations. The getLocalVisibleRect only offsets
+ // the global rectangle to local. And the result is wrong the View is scaled.
+ //
+ // This approach use the local transformation matrix to map the global rectangle to
+ // local instead.
+ //
+ // Note: it doesn't work well with rotation. Because Rect must be
+ // axis-aligned, when a rotated Rect becomes quadrilateral, the quadrilateral's
+ // bounding box is stored at Rect instead. It makes the returned Rect larger than
+ // the correct size.
+ matrix.mapRect(editorBounds);
+
+ if (handwritingBounds != null) {
+ // Similar to editorBounds, handwritingBounds must be computed in global coordinates
+ // and then converted back to local coordinates. Otherwise, if the view is scaled,
+ // the handwritingBoundsOffsets are also scaled, which is not the expected behavior.
+ handwritingBounds.top = rect.top - getHandwritingBoundsOffsetTop();
+ handwritingBounds.left = rect.left - getHandwritingBoundsOffsetLeft();
+ handwritingBounds.bottom = rect.bottom + getHandwritingBoundsOffsetBottom();
+ handwritingBounds.right = rect.right + getHandwritingBoundsOffsetRight();
+ matrix.mapRect(handwritingBounds);
+ }
+ return true;
+ }
+
+ private boolean getContentVisibleRect(RectF rect) {
+ if (!getEditorAndHandwritingBounds(rect, /* handwritingBounds= */null)) {
+ return false;
+ }
+ // Clip the view's visible rect with the text layout's visible rect.
+ return rect.intersect(getCompoundPaddingLeft(), getCompoundPaddingTop(),
+ getWidth() - getCompoundPaddingRight(), getHeight() - getCompoundPaddingBottom());
+ }
+
/**
* Populate requested character bounds in a {@link CursorAnchorInfo.Builder}
*
@@ -14333,9 +14423,15 @@
// character bounds in this case yet.
return;
}
- final Rect rect = new Rect();
- getContentVisibleRect(rect);
- final RectF visibleRect = new RectF(rect);
+ final RectF visibleRect = new RectF();
+
+ if (Flags.handwritingGestureWithTransformation()) {
+ getContentVisibleRect(visibleRect);
+ } else {
+ final Rect rect = new Rect();
+ getContentVisibleRect(rect);
+ visibleRect.set(rect);
+ }
final float[] characterBounds = getCharacterBounds(startIndex, endIndex,
viewportToContentHorizontalOffset, viewportToContentVerticalOffset);
@@ -14438,24 +14534,26 @@
builder.setMatrix(viewToScreenMatrix);
if (includeEditorBounds) {
- if (mTempRect == null) {
- mTempRect = new Rect();
- }
- final Rect bounds = mTempRect;
- final RectF editorBounds;
- final RectF handwritingBounds;
- if (getViewVisibleRect(bounds)) {
- editorBounds = new RectF(bounds);
- handwritingBounds = new RectF(editorBounds);
- handwritingBounds.top -= getHandwritingBoundsOffsetTop();
- handwritingBounds.left -= getHandwritingBoundsOffsetLeft();
- handwritingBounds.bottom += getHandwritingBoundsOffsetBottom();
- handwritingBounds.right += getHandwritingBoundsOffsetRight();
+ final RectF editorBounds = new RectF();
+ final RectF handwritingBounds = new RectF();
+ if (Flags.handwritingGestureWithTransformation()) {
+ getEditorAndHandwritingBounds(editorBounds, handwritingBounds);
} else {
- // The editor is not visible at all, return empty rectangles. We still need to
+ if (mTempRect == null) {
+ mTempRect = new Rect();
+ }
+ final Rect bounds = mTempRect;
+
+ // If the editor is not visible at all, return empty rectangles. We still need to
// return an EditorBoundsInfo because IME has subscribed the EditorBoundsInfo.
- editorBounds = new RectF();
- handwritingBounds = new RectF();
+ if (getViewVisibleRect(bounds)) {
+ editorBounds.set(bounds);
+ handwritingBounds.set(editorBounds);
+ handwritingBounds.top -= getHandwritingBoundsOffsetTop();
+ handwritingBounds.left -= getHandwritingBoundsOffsetLeft();
+ handwritingBounds.bottom += getHandwritingBoundsOffsetBottom();
+ handwritingBounds.right += getHandwritingBoundsOffsetRight();
+ }
}
EditorBoundsInfo.Builder boundsBuilder = new EditorBoundsInfo.Builder();
EditorBoundsInfo editorBoundsInfo = boundsBuilder.setEditorBounds(editorBounds)
@@ -14533,29 +14631,57 @@
}
if (includeVisibleLineBounds) {
- final Rect visibleRect = new Rect();
- if (getContentVisibleRect(visibleRect)) {
- // Subtract the viewportToContentVerticalOffset to convert the view
- // coordinates to layout coordinates.
- final float visibleTop =
- visibleRect.top - viewportToContentVerticalOffset;
- final float visibleBottom =
- visibleRect.bottom - viewportToContentVerticalOffset;
- final int firstLine =
- layout.getLineForVertical((int) Math.floor(visibleTop));
- final int lastLine =
- layout.getLineForVertical((int) Math.ceil(visibleBottom));
+ if (Flags.handwritingGestureWithTransformation()) {
+ RectF visibleRect = new RectF();
+ if (getContentVisibleRect(visibleRect)) {
+ // Subtract the viewportToContentVerticalOffset to convert the view
+ // coordinates to layout coordinates.
+ final float visibleTop =
+ visibleRect.top - viewportToContentVerticalOffset;
+ final float visibleBottom =
+ visibleRect.bottom - viewportToContentVerticalOffset;
+ final int firstLine =
+ layout.getLineForVertical((int) Math.floor(visibleTop));
+ final int lastLine =
+ layout.getLineForVertical((int) Math.ceil(visibleBottom));
- for (int line = firstLine; line <= lastLine; ++line) {
- final float left = layout.getLineLeft(line)
- + viewportToContentHorizontalOffset;
- final float top = layout.getLineTop(line)
- + viewportToContentVerticalOffset;
- final float right = layout.getLineRight(line)
- + viewportToContentHorizontalOffset;
- final float bottom = layout.getLineBottom(line, false)
- + viewportToContentVerticalOffset;
- builder.addVisibleLineBounds(left, top, right, bottom);
+ for (int line = firstLine; line <= lastLine; ++line) {
+ final float left = layout.getLineLeft(line)
+ + viewportToContentHorizontalOffset;
+ final float top = layout.getLineTop(line)
+ + viewportToContentVerticalOffset;
+ final float right = layout.getLineRight(line)
+ + viewportToContentHorizontalOffset;
+ final float bottom = layout.getLineBottom(line, false)
+ + viewportToContentVerticalOffset;
+ builder.addVisibleLineBounds(left, top, right, bottom);
+ }
+ }
+ } else {
+ final Rect visibleRect = new Rect();
+ if (getContentVisibleRect(visibleRect)) {
+ // Subtract the viewportToContentVerticalOffset to convert the view
+ // coordinates to layout coordinates.
+ final float visibleTop =
+ visibleRect.top - viewportToContentVerticalOffset;
+ final float visibleBottom =
+ visibleRect.bottom - viewportToContentVerticalOffset;
+ final int firstLine =
+ layout.getLineForVertical((int) Math.floor(visibleTop));
+ final int lastLine =
+ layout.getLineForVertical((int) Math.ceil(visibleBottom));
+
+ for (int line = firstLine; line <= lastLine; ++line) {
+ final float left = layout.getLineLeft(line)
+ + viewportToContentHorizontalOffset;
+ final float top = layout.getLineTop(line)
+ + viewportToContentVerticalOffset;
+ final float right = layout.getLineRight(line)
+ + viewportToContentHorizontalOffset;
+ final float bottom = layout.getLineBottom(line, false)
+ + viewportToContentVerticalOffset;
+ builder.addVisibleLineBounds(left, top, right, bottom);
+ }
}
}
}
diff --git a/core/java/android/window/flags/windowing_frontend.aconfig b/core/java/android/window/flags/windowing_frontend.aconfig
index d5746e5..9aeccf4 100644
--- a/core/java/android/window/flags/windowing_frontend.aconfig
+++ b/core/java/android/window/flags/windowing_frontend.aconfig
@@ -65,6 +65,14 @@
}
flag {
+ name: "keyguard_going_away_timeout"
+ namespace: "windowing_frontend"
+ description: "Allow a maximum of 10 seconds with keyguardGoingAway=true before force-resetting"
+ bug: "343598832"
+ is_fixed_read_only: true
+}
+
+flag {
name: "close_to_square_config_includes_status_bar"
namespace: "windowing_frontend"
description: "On close to square display, when necessary, configuration includes status bar"
@@ -72,6 +80,17 @@
}
flag {
+ name: "reduce_keyguard_transitions"
+ namespace: "windowing_frontend"
+ description: "Avoid setting keyguard transitions ready unless there are no other changes"
+ bug: "354647472"
+ is_fixed_read_only: true
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+flag {
name: "transit_ready_tracking"
namespace: "windowing_frontend"
description: "Enable accurate transition readiness tracking"
diff --git a/core/java/android/window/flags/windowing_sdk.aconfig b/core/java/android/window/flags/windowing_sdk.aconfig
index b8c2a5f..a6ae948 100644
--- a/core/java/android/window/flags/windowing_sdk.aconfig
+++ b/core/java/android/window/flags/windowing_sdk.aconfig
@@ -19,13 +19,6 @@
flag {
namespace: "windowing_sdk"
- name: "fullscreen_dim_flag"
- description: "Whether to allow showing fullscreen dim on ActivityEmbedding split"
- bug: "293797706"
-}
-
-flag {
- namespace: "windowing_sdk"
name: "activity_embedding_interactive_divider_flag"
description: "Whether the interactive divider feature is enabled"
bug: "293654166"
diff --git a/core/java/com/android/internal/graphics/ColorUtils.java b/core/java/com/android/internal/graphics/ColorUtils.java
index f72a5ca..f210741 100644
--- a/core/java/com/android/internal/graphics/ColorUtils.java
+++ b/core/java/com/android/internal/graphics/ColorUtils.java
@@ -21,7 +21,7 @@
import android.annotation.IntRange;
import android.annotation.NonNull;
import android.graphics.Color;
-
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.cam.Cam;
/**
@@ -29,6 +29,7 @@
*
* A set of color-related utility methods, building upon those available in {@code Color}.
*/
+@RavenwoodKeepWholeClass
public final class ColorUtils {
private static final double XYZ_WHITE_REFERENCE_X = 95.047;
@@ -696,4 +697,4 @@
double calculateContrast(int foreground, int background, int alpha);
}
-}
\ No newline at end of file
+}
diff --git a/core/java/com/android/internal/graphics/cam/Cam.java b/core/java/com/android/internal/graphics/cam/Cam.java
index 1df85c3..49fa37b 100644
--- a/core/java/com/android/internal/graphics/cam/Cam.java
+++ b/core/java/com/android/internal/graphics/cam/Cam.java
@@ -18,6 +18,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.ColorUtils;
@@ -25,6 +26,7 @@
* A color appearance model, based on CAM16, extended to use L* as the lightness dimension, and
* coupled to a gamut mapping algorithm. Creates a color system, enables a digital design system.
*/
+@RavenwoodKeepWholeClass
public class Cam {
// The maximum difference between the requested L* and the L* returned.
private static final float DL_MAX = 0.2f;
diff --git a/core/java/com/android/internal/graphics/cam/CamUtils.java b/core/java/com/android/internal/graphics/cam/CamUtils.java
index f541729..76fabc6 100644
--- a/core/java/com/android/internal/graphics/cam/CamUtils.java
+++ b/core/java/com/android/internal/graphics/cam/CamUtils.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.graphics.Color;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import com.android.internal.graphics.ColorUtils;
@@ -45,6 +46,7 @@
* consistent, and reasonably good. It worked." - Fairchild, Color Models and Systems: Handbook of
* Color Psychology, 2015
*/
+@RavenwoodKeepWholeClass
public final class CamUtils {
private CamUtils() {
}
diff --git a/core/java/com/android/internal/graphics/cam/Frame.java b/core/java/com/android/internal/graphics/cam/Frame.java
index 0ac7cbc..c419fab 100644
--- a/core/java/com/android/internal/graphics/cam/Frame.java
+++ b/core/java/com/android/internal/graphics/cam/Frame.java
@@ -17,6 +17,7 @@
package com.android.internal.graphics.cam;
import android.annotation.NonNull;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
import android.util.MathUtils;
import com.android.internal.annotations.VisibleForTesting;
@@ -33,6 +34,7 @@
* number of calculations during the color => CAM conversion process that depend only on the viewing
* conditions. Caching those calculations in a Frame instance saves a significant amount of time.
*/
+@RavenwoodKeepWholeClass
public final class Frame {
// Standard viewing conditions assumed in RGB specification - Stokes, Anderson, Chandrasekar,
// Motta - A Standard Default Color Space for the Internet: sRGB, 1996.
diff --git a/core/java/com/android/internal/graphics/cam/HctSolver.java b/core/java/com/android/internal/graphics/cam/HctSolver.java
index d7a8691..6e558e7 100644
--- a/core/java/com/android/internal/graphics/cam/HctSolver.java
+++ b/core/java/com/android/internal/graphics/cam/HctSolver.java
@@ -16,6 +16,8 @@
package com.android.internal.graphics.cam;
+import android.ravenwood.annotation.RavenwoodKeepWholeClass;
+
/**
* An efficient algorithm for determining the closest sRGB color to a set of HCT coordinates,
* based on geometrical insights for finding intersections in linear RGB, CAM16, and L*a*b*.
@@ -24,6 +26,7 @@
* Copied from //java/com/google/ux/material/libmonet/hct on May 22 2022.
* ColorUtils/MathUtils functions that were required were added to CamUtils.
*/
+@RavenwoodKeepWholeClass
public class HctSolver {
private HctSolver() {}
diff --git a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
index fbec1f1..e0c90d8 100644
--- a/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
+++ b/core/java/com/android/internal/protolog/PerfettoProtoLogImpl.java
@@ -332,6 +332,8 @@
}
private void onTracingFlush() {
+ Log.d(LOG_TAG, "Executing onTracingFlush");
+
final ExecutorService loggingService;
try {
mBackgroundServiceLock.lock();
@@ -352,15 +354,19 @@
Log.e(LOG_TAG, "Failed to wait for tracing to finish", e);
}
- dumpTransitionTraceConfig();
+ dumpViewerConfig();
+
+ Log.d(LOG_TAG, "Finished onTracingFlush");
}
- private void dumpTransitionTraceConfig() {
+ private void dumpViewerConfig() {
if (mViewerConfigInputStreamProvider == null) {
// No viewer config available
return;
}
+ Log.d(LOG_TAG, "Dumping viewer config to trace");
+
ProtoInputStream pis = mViewerConfigInputStreamProvider.getInputStream();
if (pis == null) {
@@ -390,6 +396,8 @@
Log.e(LOG_TAG, "Failed to read ProtoLog viewer config to dump on tracing end", e);
}
});
+
+ Log.d(LOG_TAG, "Dumped viewer config to trace");
}
private static void writeViewerConfigGroup(
@@ -770,6 +778,8 @@
private synchronized void onTracingInstanceStart(
int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ Log.d(LOG_TAG, "Executing onTracingInstanceStart");
+
final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
for (int i = defaultLogFrom.ordinal(); i < LogLevel.values().length; i++) {
mDefaultLogLevelCounts[i]++;
@@ -800,10 +810,13 @@
mCacheUpdater.run();
this.mTracingInstances.incrementAndGet();
+
+ Log.d(LOG_TAG, "Finished onTracingInstanceStart");
}
private synchronized void onTracingInstanceStop(
int instanceIdx, ProtoLogDataSource.ProtoLogConfig config) {
+ Log.d(LOG_TAG, "Executing onTracingInstanceStop");
this.mTracingInstances.decrementAndGet();
final LogLevel defaultLogFrom = config.getDefaultGroupConfig().logFrom;
@@ -835,6 +848,7 @@
}
mCacheUpdater.run();
+ Log.d(LOG_TAG, "Finished onTracingInstanceStop");
}
private static void logAndPrintln(@Nullable PrintWriter pw, String msg) {
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
index 15ecedd..cd7dcfd 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedAmplitudeStepWaveform.java
@@ -29,7 +29,7 @@
import android.util.IntArray;
import android.util.LongArray;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
similarity index 86%
rename from core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java
rename to core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
index 23df304..6c562c9 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedVibrationEffect.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedComposedEffect.java
@@ -29,24 +29,24 @@
import java.util.Arrays;
/**
- * Serialized representation of a {@link VibrationEffect}.
+ * Serialized representation of a {@link VibrationEffect.Composed}.
*
* <p>The vibration is represented by a list of serialized segments that can be added to a
* {@link VibrationEffect.Composition} during the {@link #deserialize()} procedure.
*
* @hide
*/
-final class SerializedVibrationEffect implements XmlSerializedVibration<VibrationEffect> {
+final class SerializedComposedEffect implements XmlSerializedVibration<VibrationEffect.Composed> {
@NonNull
private final SerializedSegment[] mSegments;
- SerializedVibrationEffect(@NonNull SerializedSegment segment) {
+ SerializedComposedEffect(@NonNull SerializedSegment segment) {
requireNonNull(segment);
mSegments = new SerializedSegment[]{ segment };
}
- SerializedVibrationEffect(@NonNull SerializedSegment[] segments) {
+ SerializedComposedEffect(@NonNull SerializedSegment[] segments) {
requireNonNull(segments);
checkArgument(segments.length > 0, "Unsupported empty vibration");
mSegments = segments;
@@ -54,12 +54,12 @@
@NonNull
@Override
- public VibrationEffect deserialize() {
+ public VibrationEffect.Composed deserialize() {
VibrationEffect.Composition composition = VibrationEffect.startComposition();
for (SerializedSegment segment : mSegments) {
segment.deserializeIntoComposition(composition);
}
- return composition.compose();
+ return (VibrationEffect.Composed) composition.compose();
}
@Override
@@ -79,7 +79,7 @@
@Override
public String toString() {
- return "SerializedVibrationEffect{"
+ return "SerializedComposedEffect{"
+ "segments=" + Arrays.toString(mSegments)
+ '}';
}
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
index db5c7ff..862f7cb 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedCompositionPrimitive.java
@@ -27,7 +27,7 @@
import android.os.VibrationEffect;
import android.os.vibrator.PrimitiveSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
index 8924311..a6f48a4 100644
--- a/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedPredefinedEffect.java
@@ -25,7 +25,7 @@
import android.os.VibrationEffect;
import android.os.vibrator.PrebakedSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName;
import com.android.modules.utils.TypedXmlPullParser;
import com.android.modules.utils.TypedXmlSerializer;
diff --git a/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java
new file mode 100644
index 0000000..aa1b0a23
--- /dev/null
+++ b/core/java/com/android/internal/vibrator/persistence/SerializedVendorEffect.java
@@ -0,0 +1,127 @@
+/*
+ * 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.internal.vibrator.persistence;
+
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
+
+import static java.util.Objects.requireNonNull;
+
+import android.annotation.NonNull;
+import android.annotation.SuppressLint;
+import android.os.PersistableBundle;
+import android.os.VibrationEffect;
+import android.text.TextUtils;
+import android.util.Base64;
+
+import com.android.modules.utils.TypedXmlPullParser;
+import com.android.modules.utils.TypedXmlSerializer;
+
+import java.io.ByteArrayInputStream;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
+
+/**
+ * Serialized representation of a {@link VibrationEffect.VendorEffect}.
+ *
+ * <p>The vibration is represented by an opaque {@link PersistableBundle} that can be used by
+ * {@link VibrationEffect#createVendorEffect(PersistableBundle)} during the {@link #deserialize()}
+ * procedure.
+ *
+ * @hide
+ */
+final class SerializedVendorEffect implements XmlSerializedVibration<VibrationEffect.VendorEffect> {
+
+ @NonNull
+ private final PersistableBundle mVendorData;
+
+ SerializedVendorEffect(@NonNull PersistableBundle vendorData) {
+ requireNonNull(vendorData);
+ mVendorData = vendorData;
+ }
+
+ @SuppressLint("MissingPermission")
+ @NonNull
+ @Override
+ public VibrationEffect.VendorEffect deserialize() {
+ return (VibrationEffect.VendorEffect) VibrationEffect.createVendorEffect(mVendorData);
+ }
+
+ @Override
+ public void write(@NonNull TypedXmlSerializer serializer)
+ throws IOException {
+ serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT);
+ writeContent(serializer);
+ serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VIBRATION_EFFECT);
+ }
+
+ @Override
+ public void writeContent(@NonNull TypedXmlSerializer serializer) throws IOException {
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ mVendorData.writeToStream(outputStream);
+
+ serializer.startTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT);
+ serializer.text(Base64.encodeToString(outputStream.toByteArray(), Base64.NO_WRAP));
+ serializer.endTag(XmlConstants.NAMESPACE, XmlConstants.TAG_VENDOR_EFFECT);
+ }
+
+ @Override
+ public String toString() {
+ return "SerializedVendorEffect{"
+ + "vendorData=" + mVendorData
+ + '}';
+ }
+
+ /** Parser implementation for {@link SerializedVendorEffect}. */
+ static final class Parser {
+
+ @NonNull
+ static SerializedVendorEffect parseNext(@NonNull TypedXmlPullParser parser,
+ @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ XmlValidator.checkStartTag(parser, TAG_VENDOR_EFFECT);
+ XmlValidator.checkTagHasNoUnexpectedAttributes(parser);
+
+ PersistableBundle vendorData;
+ XmlReader.readNextText(parser, TAG_VENDOR_EFFECT);
+
+ try {
+ String text = parser.getText().trim();
+ XmlValidator.checkParserCondition(!text.isEmpty(),
+ "Expected tag %s to have base64 representation of vendor data, got empty",
+ TAG_VENDOR_EFFECT);
+
+ vendorData = PersistableBundle.readFromStream(
+ new ByteArrayInputStream(Base64.decode(text, Base64.DEFAULT)));
+ XmlValidator.checkParserCondition(!vendorData.isEmpty(),
+ "Expected tag %s to have non-empty vendor data, got empty bundle",
+ TAG_VENDOR_EFFECT);
+ } catch (IllegalArgumentException | NullPointerException e) {
+ throw new XmlParserException(
+ TextUtils.formatSimple(
+ "Expected base64 representation of vendor data in tag %s, got %s",
+ TAG_VENDOR_EFFECT, parser.getText()),
+ e);
+ } catch (IOException e) {
+ throw new XmlParserException("Error reading vendor data from decoded bytes", e);
+ }
+
+ // Consume tag
+ XmlReader.readEndTag(parser);
+
+ return new SerializedVendorEffect(vendorData);
+ }
+ }
+}
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
index 2b8b61d..a9fbcaf 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlParser.java
@@ -18,13 +18,15 @@
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PREDEFINED_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_PRIMITIVE_EFFECT;
+import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VENDOR_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_VIBRATION_EFFECT;
import static com.android.internal.vibrator.persistence.XmlConstants.TAG_WAVEFORM_EFFECT;
import android.annotation.NonNull;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.modules.utils.TypedXmlPullParser;
import java.io.IOException;
@@ -80,6 +82,16 @@
* }
* </pre>
*
+ * * Vendor vibration effects
+ *
+ * <pre>
+ * {@code
+ * <vibration-effect>
+ * <vendor-effect>base64-representation-of-persistable-bundle</vendor-effect>
+ * </vibration-effect>
+ * }
+ * </pre>
+ *
* @hide
*/
public class VibrationEffectXmlParser {
@@ -87,11 +99,9 @@
/**
* Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration}
* wrapping a {@link VibrationEffect}.
- *
- * @see XmlParser#parseTag(TypedXmlPullParser)
*/
@NonNull
- public static XmlSerializedVibration<VibrationEffect> parseTag(
+ public static XmlSerializedVibration<? extends VibrationEffect> parseTag(
@NonNull TypedXmlPullParser parser, @XmlConstants.Flags int flags)
throws XmlParserException, IOException {
XmlValidator.checkStartTag(parser, TAG_VIBRATION_EFFECT);
@@ -107,8 +117,9 @@
* <p>This can be reused for reading a vibration from an XML root tag or from within a combined
* vibration, but it should always be called from places that validates the top level tag.
*/
- static SerializedVibrationEffect parseVibrationContent(TypedXmlPullParser parser,
- @XmlConstants.Flags int flags) throws XmlParserException, IOException {
+ private static XmlSerializedVibration<? extends VibrationEffect> parseVibrationContent(
+ TypedXmlPullParser parser, @XmlConstants.Flags int flags)
+ throws XmlParserException, IOException {
String vibrationTagName = parser.getName();
int vibrationTagDepth = parser.getDepth();
@@ -116,11 +127,16 @@
XmlReader.readNextTagWithin(parser, vibrationTagDepth),
"Unsupported empty vibration tag");
- SerializedVibrationEffect serializedVibration;
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration;
switch (parser.getName()) {
+ case TAG_VENDOR_EFFECT:
+ if (Flags.vendorVibrationEffects()) {
+ serializedVibration = SerializedVendorEffect.Parser.parseNext(parser, flags);
+ break;
+ } // else fall through
case TAG_PREDEFINED_EFFECT:
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
SerializedPredefinedEffect.Parser.parseNext(parser, flags));
break;
case TAG_PRIMITIVE_EFFECT:
@@ -128,11 +144,11 @@
do { // First primitive tag already open
primitives.add(SerializedCompositionPrimitive.Parser.parseNext(parser));
} while (XmlReader.readNextTagWithin(parser, vibrationTagDepth));
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
primitives.toArray(new SerializedSegment[primitives.size()]));
break;
case TAG_WAVEFORM_EFFECT:
- serializedVibration = new SerializedVibrationEffect(
+ serializedVibration = new SerializedComposedEffect(
SerializedAmplitudeStepWaveform.Parser.parseNext(parser));
break;
default:
diff --git a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
index f561c14..d74a23d 100644
--- a/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
+++ b/core/java/com/android/internal/vibrator/persistence/VibrationEffectXmlSerializer.java
@@ -17,13 +17,15 @@
package com.android.internal.vibrator.persistence;
import android.annotation.NonNull;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationEffectSegment;
-import com.android.internal.vibrator.persistence.SerializedVibrationEffect.SerializedSegment;
+import com.android.internal.vibrator.persistence.SerializedComposedEffect.SerializedSegment;
import com.android.internal.vibrator.persistence.XmlConstants.PredefinedEffectName;
import com.android.internal.vibrator.persistence.XmlConstants.PrimitiveEffectName;
@@ -41,6 +43,7 @@
* <li>{@link VibrationEffect#createWaveform(long[], int[], int)}
* <li>A composition created exclusively via
* {@link VibrationEffect.Composition#addPrimitive(int, float, int)}
+ * <li>{@link VibrationEffect#createVendorEffect(PersistableBundle)}
* </ul>
*
* @hide
@@ -49,13 +52,16 @@
/**
* Creates a serialized representation of the input {@code vibration}.
- *
- * @see XmlSerializer#serialize
*/
@NonNull
- public static XmlSerializedVibration<VibrationEffect> serialize(
+ public static XmlSerializedVibration<? extends VibrationEffect> serialize(
@NonNull VibrationEffect vibration, @XmlConstants.Flags int flags)
throws XmlSerializerException {
+ if (Flags.vendorVibrationEffects()
+ && (vibration instanceof VibrationEffect.VendorEffect vendorEffect)) {
+ return serializeVendorEffect(vendorEffect);
+ }
+
XmlValidator.checkSerializerCondition(vibration instanceof VibrationEffect.Composed,
"Unsupported VibrationEffect type %s", vibration);
@@ -73,7 +79,7 @@
return serializeWaveformEffect(composed);
}
- private static SerializedVibrationEffect serializePredefinedEffect(
+ private static SerializedComposedEffect serializePredefinedEffect(
VibrationEffect.Composed effect, @XmlConstants.Flags int flags)
throws XmlSerializerException {
List<VibrationEffectSegment> segments = effect.getSegments();
@@ -81,10 +87,15 @@
"Unsupported repeating predefined effect %s", effect);
XmlValidator.checkSerializerCondition(segments.size() == 1,
"Unsupported multiple segments in predefined effect %s", effect);
- return new SerializedVibrationEffect(serializePrebakedSegment(segments.get(0), flags));
+ return new SerializedComposedEffect(serializePrebakedSegment(segments.get(0), flags));
}
- private static SerializedVibrationEffect serializePrimitiveEffect(
+ private static SerializedVendorEffect serializeVendorEffect(
+ VibrationEffect.VendorEffect effect) {
+ return new SerializedVendorEffect(effect.getVendorData());
+ }
+
+ private static SerializedComposedEffect serializePrimitiveEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
List<VibrationEffectSegment> segments = effect.getSegments();
XmlValidator.checkSerializerCondition(effect.getRepeatIndex() == -1,
@@ -95,10 +106,10 @@
primitives[i] = serializePrimitiveSegment(segments.get(i));
}
- return new SerializedVibrationEffect(primitives);
+ return new SerializedComposedEffect(primitives);
}
- private static SerializedVibrationEffect serializeWaveformEffect(
+ private static SerializedComposedEffect serializeWaveformEffect(
VibrationEffect.Composed effect) throws XmlSerializerException {
SerializedAmplitudeStepWaveform.Builder serializedWaveformBuilder =
new SerializedAmplitudeStepWaveform.Builder();
@@ -120,7 +131,7 @@
segment.getDuration(), toAmplitudeInt(segment.getAmplitude()));
}
- return new SerializedVibrationEffect(serializedWaveformBuilder.build());
+ return new SerializedComposedEffect(serializedWaveformBuilder.build());
}
private static SerializedPredefinedEffect serializePrebakedSegment(
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
index 8b92153..2a55d99 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlConstants.java
@@ -40,6 +40,7 @@
public static final String TAG_PREDEFINED_EFFECT = "predefined-effect";
public static final String TAG_PRIMITIVE_EFFECT = "primitive-effect";
+ public static final String TAG_VENDOR_EFFECT = "vendor-effect";
public static final String TAG_WAVEFORM_EFFECT = "waveform-effect";
public static final String TAG_WAVEFORM_ENTRY = "waveform-entry";
public static final String TAG_REPEATING = "repeating";
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParser.java b/core/java/com/android/internal/vibrator/persistence/XmlParser.java
deleted file mode 100644
index 6712f1c..0000000
--- a/core/java/com/android/internal/vibrator/persistence/XmlParser.java
+++ /dev/null
@@ -1,51 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.vibrator.persistence;
-
-import android.annotation.NonNull;
-
-import com.android.modules.utils.TypedXmlPullParser;
-
-import java.io.IOException;
-
-/**
- * Parse XML tags into valid {@link XmlSerializedVibration} instances.
- *
- * @param <T> The vibration type that will be parsed.
- * @see XmlSerializedVibration
- * @hide
- */
-@FunctionalInterface
-public interface XmlParser<T> {
-
- /**
- * Parses the current XML tag with all nested tags into a single {@link XmlSerializedVibration}.
- *
- * <p>This method will consume nested XML tags until it finds the
- * {@link TypedXmlPullParser#END_TAG} for the current tag.
- *
- * <p>The vibration reconstructed by the returned {@link XmlSerializedVibration#deserialize()}
- * is guaranteed to be valid. This method will throw an exception otherwise.
- *
- * @param pullParser The {@link TypedXmlPullParser} with the input XML.
- * @return The parsed vibration wrapped in a {@link XmlSerializedVibration} representation.
- * @throws IOException On any I/O error while reading the input XML
- * @throws XmlParserException If the XML content does not represent a valid vibration.
- */
- XmlSerializedVibration<T> parseTag(@NonNull TypedXmlPullParser pullParser)
- throws XmlParserException, IOException;
-}
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
index 7507864..e2b30e7 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlParserException.java
@@ -23,7 +23,6 @@
/**
* Represents an error while parsing a vibration XML input.
*
- * @see XmlParser
* @hide
*/
public final class XmlParserException extends Exception {
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlReader.java b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
index a5ace84..0ac6fef 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlReader.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlReader.java
@@ -130,6 +130,25 @@
}
/**
+ * Read the next element, ignoring comments and ignorable whitespace, and returns only if it's a
+ * {@link XmlPullParser#TEXT}. Any other tag will fail this check.
+ *
+ * <p>The parser will be pointing to the first next element after skipping comments,
+ * instructions and ignorable whitespace.
+ */
+ public static void readNextText(TypedXmlPullParser parser, String tagName)
+ throws XmlParserException, IOException {
+ try {
+ int type = parser.next(); // skips comments, instruction tokens and ignorable whitespace
+ XmlValidator.checkParserCondition(type == XmlPullParser.TEXT,
+ "Unexpected event %s of type %d, expected text event inside tag %s",
+ parser.getName(), type, tagName);
+ } catch (XmlPullParserException e) {
+ throw XmlParserException.createFromPullParserException("text event", e);
+ }
+ }
+
+ /**
* Check parser has a {@link XmlPullParser#END_TAG} as the next tag, with no nested tags.
*
* <p>The parser will be pointing to the end tag after this method.
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
index 3233fa2..c20b7d2 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializedVibration.java
@@ -26,8 +26,7 @@
* Serialized representation of a generic vibration.
*
* <p>This can be used to represent a {@link android.os.CombinedVibration} or a
- * {@link android.os.VibrationEffect}. Instances can be created from vibration objects via
- * {@link XmlSerializer}, or from XML content via {@link XmlParser}.
+ * {@link android.os.VibrationEffect}.
*
* <p>The separation of serialization and writing procedures enables configurable rules to define
* which vibrations can be successfully serialized before any data is written to the output stream.
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java
deleted file mode 100644
index 102e6c1..0000000
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializer.java
+++ /dev/null
@@ -1,40 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.internal.vibrator.persistence;
-
-import android.annotation.NonNull;
-
-/**
- * Creates a {@link XmlSerializedVibration} instance representing a vibration.
- *
- * @param <T> The vibration type that will be serialized.
- * @see XmlSerializedVibration
- * @hide
- */
-@FunctionalInterface
-public interface XmlSerializer<T> {
-
- /**
- * Creates a serialized representation of the input {@code vibration}.
- *
- * @param vibration The vibration to be serialized
- * @return The serialized representation of the input vibration
- * @throws XmlSerializerException If the input vibration cannot be serialized
- */
- @NonNull
- XmlSerializedVibration<T> serialize(@NonNull T vibration) throws XmlSerializerException;
-}
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
index c57ff5d..2e7ad09 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlSerializerException.java
@@ -19,7 +19,6 @@
/**
* Represents an error while serializing a vibration input.
*
- * @see XmlSerializer
* @hide
*/
public final class XmlSerializerException extends Exception {
diff --git a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
index 84d4f3f..1b5a356 100644
--- a/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
+++ b/core/java/com/android/internal/vibrator/persistence/XmlValidator.java
@@ -18,7 +18,7 @@
import static java.util.Objects.requireNonNull;
-import android.annotation.NonNull;
+import android.os.VibrationEffect;
import android.text.TextUtils;
import com.android.internal.util.ArrayUtils;
@@ -82,11 +82,11 @@
* Check given {@link XmlSerializedVibration} represents the expected {@code vibration} object
* when it's deserialized.
*/
- @NonNull
- public static <T> void checkSerializedVibration(
- XmlSerializedVibration<T> serializedVibration, T expectedVibration)
+ public static void checkSerializedVibration(
+ XmlSerializedVibration<? extends VibrationEffect> serializedVibration,
+ VibrationEffect expectedVibration)
throws XmlSerializerException {
- T deserializedVibration = requireNonNull(serializedVibration.deserialize());
+ VibrationEffect deserializedVibration = requireNonNull(serializedVibration.deserialize());
checkSerializerCondition(Objects.equals(expectedVibration, deserializedVibration),
"Unexpected serialized vibration %s: found deserialization %s, expected %s",
serializedVibration, deserializedVibration, expectedVibration);
diff --git a/core/proto/android/server/vibrator/vibratormanagerservice.proto b/core/proto/android/server/vibrator/vibratormanagerservice.proto
index 12804d4..e7f0560 100644
--- a/core/proto/android/server/vibrator/vibratormanagerservice.proto
+++ b/core/proto/android/server/vibrator/vibratormanagerservice.proto
@@ -163,7 +163,7 @@
optional bool vibrator_under_external_control = 5;
optional bool low_power_mode = 6;
optional bool vibrate_on = 24;
- optional bool keyboard_vibration_on = 25;
+ reserved 25; // prev keyboard_vibration_on
optional int32 default_vibration_amplitude = 26;
optional int32 alarm_intensity = 18;
optional int32 alarm_default_intensity = 19;
diff --git a/core/res/res/layout/list_menu_item_icon.xml b/core/res/res/layout/list_menu_item_icon.xml
index a30be6a..5854e81 100644
--- a/core/res/res/layout/list_menu_item_icon.xml
+++ b/core/res/res/layout/list_menu_item_icon.xml
@@ -18,6 +18,8 @@
android:id="@+id/icon"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
+ android:maxWidth="@dimen/list_menu_item_icon_max_width"
+ android:adjustViewBounds="true"
android:layout_gravity="center_vertical"
android:layout_marginStart="8dip"
android:layout_marginEnd="-8dip"
diff --git a/core/res/res/layout/time_picker_text_input_material.xml b/core/res/res/layout/time_picker_text_input_material.xml
index 4988842..86070b1 100644
--- a/core/res/res/layout/time_picker_text_input_material.xml
+++ b/core/res/res/layout/time_picker_text_input_material.xml
@@ -34,19 +34,29 @@
android:layoutDirection="ltr">
<EditText
android:id="@+id/input_hour"
- android:layout_width="50dp"
+ android:layout_width="50sp"
android:layout_height="wrap_content"
+ android:layout_alignEnd="@id/hour_label_holder"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField"
android:imeOptions="actionNext"/>
- <TextView
- android:id="@+id/label_hour"
+ <!-- Ensure the label_hour takes up at least 50sp of space -->
+ <FrameLayout
+ android:id="@+id/hour_label_holder"
android:layout_width="wrap_content"
android:layout_height="wrap_content"
- android:layout_below="@id/input_hour"
- android:layout_alignStart="@id/input_hour"
- android:labelFor="@+id/input_hour"
- android:text="@string/time_picker_hour_label"/>
+ android:layout_below="@id/input_hour">
+ <TextView
+ android:id="@+id/label_hour"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:labelFor="@+id/input_hour"
+ android:text="@string/time_picker_hour_label"/>
+ <Space
+ android:layout_width="50sp"
+ android:layout_height="0dp"/>
+ </FrameLayout>
<TextView
android:id="@+id/input_separator"
@@ -58,21 +68,30 @@
<EditText
android:id="@+id/input_minute"
- android:layout_width="50dp"
+ android:layout_width="50sp"
android:layout_height="wrap_content"
android:layout_alignBaseline="@id/input_hour"
android:layout_toEndOf="@id/input_separator"
android:inputType="number"
android:textAppearance="@style/TextAppearance.Material.TimePicker.InputField" />
- <TextView
- android:id="@+id/label_minute"
+ <!-- Ensure the label_minute takes up at least 50sp of space -->
+ <FrameLayout
android:layout_width="wrap_content"
android:layout_height="wrap_content"
android:layout_below="@id/input_minute"
android:layout_alignStart="@id/input_minute"
- android:labelFor="@+id/input_minute"
- android:text="@string/time_picker_minute_label"/>
-
+ >
+ <TextView
+ android:id="@+id/label_minute"
+ android:layout_width="wrap_content"
+ android:layout_height="wrap_content"
+ android:layout_gravity="center"
+ android:labelFor="@+id/input_minute"
+ android:text="@string/time_picker_minute_label"/>
+ <Space
+ android:layout_width="50sp"
+ android:layout_height="0dp"/>
+ </FrameLayout>
<TextView
android:visibility="invisible"
android:id="@+id/label_error"
diff --git a/core/res/res/values/dimens.xml b/core/res/res/values/dimens.xml
index 77b5587..f397ef2 100644
--- a/core/res/res/values/dimens.xml
+++ b/core/res/res/values/dimens.xml
@@ -1065,4 +1065,7 @@
<!-- The non-linear progress interval when the screen is wider than the
navigation_edge_action_progress_threshold. -->
<item name="back_progress_non_linear_factor" format="float" type="dimen">0.2</item>
+
+ <!-- The maximum width for a context menu icon -->
+ <dimen name="list_menu_item_icon_max_width">24dp</dimen>
</resources>
diff --git a/core/tests/coretests/Android.bp b/core/tests/coretests/Android.bp
index 5793bbe..2bbaf9c 100644
--- a/core/tests/coretests/Android.bp
+++ b/core/tests/coretests/Android.bp
@@ -249,6 +249,7 @@
],
srcs: [
"src/android/app/ActivityManagerTest.java",
+ "src/android/colormodel/CamTest.java",
"src/android/content/ContextTest.java",
"src/android/content/pm/PackageManagerTest.java",
"src/android/content/pm/UserInfoTest.java",
diff --git a/core/tests/coretests/src/android/colormodel/CamTest.java b/core/tests/coretests/src/android/colormodel/CamTest.java
index 05fc0e0..cf398db 100644
--- a/core/tests/coretests/src/android/colormodel/CamTest.java
+++ b/core/tests/coretests/src/android/colormodel/CamTest.java
@@ -18,9 +18,12 @@
import static org.junit.Assert.assertEquals;
+import android.platform.test.ravenwood.RavenwoodRule;
+
import androidx.test.filters.LargeTest;
import org.junit.Assert;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -35,6 +38,9 @@
static final int GREEN = 0xff00ff00;
static final int BLUE = 0xff0000ff;
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
@Test
public void camFromIntToInt() {
Cam cam = Cam.fromInt(RED);
diff --git a/core/tests/coretests/src/android/view/ViewRootImplTest.java b/core/tests/coretests/src/android/view/ViewRootImplTest.java
index b990f24..e240a08 100644
--- a/core/tests/coretests/src/android/view/ViewRootImplTest.java
+++ b/core/tests/coretests/src/android/view/ViewRootImplTest.java
@@ -1434,8 +1434,47 @@
}
@Test
+ @RequiresFlagsEnabled({FLAG_TOOLKIT_SET_FRAME_RATE_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_FUNCTION_ENABLING_READ_ONLY,
+ FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY})
+ public void votePreferredFrameRate_resetWhenDestroyingSurface()
+ throws Throwable {
+ if (!ViewProperties.vrr_enabled().orElse(true)) {
+ return;
+ }
+ mView = new View(sContext);
+ WindowManager.LayoutParams wmlp = new WindowManager.LayoutParams(TYPE_APPLICATION_OVERLAY);
+ wmlp.token = new Binder(); // Set a fake token to bypass 'is your activity running' check
+
+ sInstrumentation.runOnMainSync(() -> {
+ WindowManager wm = sContext.getSystemService(WindowManager.class);
+ wm.addView(mView, wmlp);
+ });
+ sInstrumentation.waitForIdleSync();
+
+ mViewRootImpl = mView.getViewRootImpl();
+
+ waitForFrameRateCategoryToSettle(mView);
+
+ sInstrumentation.runOnMainSync(() -> {
+ mViewRootImpl.getView().setVisibility(View.INVISIBLE);
+ mViewRootImpl.mSurface.release();
+ mView.invalidate();
+ });
+ sInstrumentation.waitForIdleSync();
+
+ assertEquals(false, mViewRootImpl.mSurface.isValid());
+ assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
+ mViewRootImpl.getLastPreferredFrameRateCategory());
+ assertEquals(FRAME_RATE_CATEGORY_DEFAULT,
+ mViewRootImpl.getPreferredFrameRateCategory());
+ assertEquals(0, mViewRootImpl.getLastPreferredFrameRate(), 0.1);
+ assertEquals(0, mViewRootImpl.getPreferredFrameRate(), 0.1);
+ }
+
+ @Test
@RequiresFlagsEnabled(FLAG_TOOLKIT_FRAME_RATE_VIEW_ENABLING_READ_ONLY)
- public void votePreferredFrameRate_velocityVotedAfterOnDraw() throws Throwable {
+ public void votePreferredFrameRate_reset() throws Throwable {
if (!ViewProperties.vrr_enabled().orElse(true)) {
return;
}
diff --git a/core/tests/coretests/src/com/android/internal/jank/CujTest.java b/core/tests/coretests/src/com/android/internal/jank/CujTest.java
index bf35ed0..2362a4c 100644
--- a/core/tests/coretests/src/com/android/internal/jank/CujTest.java
+++ b/core/tests/coretests/src/com/android/internal/jank/CujTest.java
@@ -35,7 +35,6 @@
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.util.Arrays;
-import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
@@ -47,26 +46,30 @@
public class CujTest {
private static final String ENUM_NAME_PREFIX =
"UIINTERACTION_FRAME_INFO_REPORTED__INTERACTION_TYPE__";
- private static final Set<String> DEPRECATED_VALUES = new HashSet<>() {
- {
- add(ENUM_NAME_PREFIX + "IME_INSETS_ANIMATION");
- }
- };
- private static final Map<Integer, String> ENUM_NAME_EXCEPTION_MAP = new HashMap<>() {
- {
- put(Cuj.CUJ_NOTIFICATION_ADD, getEnumName("SHADE_NOTIFICATION_ADD"));
- put(Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR, getEnumName("SHADE_HEADS_UP_APPEAR"));
- put(Cuj.CUJ_NOTIFICATION_APP_START, getEnumName("SHADE_APP_LAUNCH"));
- put(Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR, getEnumName("SHADE_HEADS_UP_DISAPPEAR"));
- put(Cuj.CUJ_NOTIFICATION_REMOVE, getEnumName("SHADE_NOTIFICATION_REMOVE"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE, getEnumName("NOTIFICATION_SHADE_SWIPE"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE, getEnumName("SHADE_QS_EXPAND_COLLAPSE"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE, getEnumName("SHADE_QS_SCROLL_SWIPE"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_ROW_EXPAND, getEnumName("SHADE_ROW_EXPAND"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE"));
- put(Cuj.CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING"));
- }
- };
+ private static final Set<String> DEPRECATED_VALUES = Set.of(
+ ENUM_NAME_PREFIX + "IME_INSETS_ANIMATION"
+ );
+ private static final Map<Integer, String> ENUM_NAME_EXCEPTION_MAP = Map.ofEntries(
+ Map.entry(Cuj.CUJ_NOTIFICATION_ADD, getEnumName("SHADE_NOTIFICATION_ADD")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_HEADS_UP_APPEAR, getEnumName("SHADE_HEADS_UP_APPEAR")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_APP_START, getEnumName("SHADE_APP_LAUNCH")),
+ Map.entry(
+ Cuj.CUJ_NOTIFICATION_HEADS_UP_DISAPPEAR,
+ getEnumName("SHADE_HEADS_UP_DISAPPEAR")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_REMOVE, getEnumName("SHADE_NOTIFICATION_REMOVE")),
+ Map.entry(
+ Cuj.CUJ_NOTIFICATION_SHADE_EXPAND_COLLAPSE,
+ getEnumName("NOTIFICATION_SHADE_SWIPE")),
+ Map.entry(
+ Cuj.CUJ_NOTIFICATION_SHADE_QS_EXPAND_COLLAPSE,
+ getEnumName("SHADE_QS_EXPAND_COLLAPSE")),
+ Map.entry(
+ Cuj.CUJ_NOTIFICATION_SHADE_QS_SCROLL_SWIPE,
+ getEnumName("SHADE_QS_SCROLL_SWIPE")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_ROW_EXPAND, getEnumName("SHADE_ROW_EXPAND")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_ROW_SWIPE, getEnumName("SHADE_ROW_SWIPE")),
+ Map.entry(Cuj.CUJ_NOTIFICATION_SHADE_SCROLL_FLING, getEnumName("SHADE_SCROLL_FLING"))
+ );
@Rule
public final Expect mExpect = Expect.create();
diff --git a/core/tests/resourceflaggingtests/Android.bp b/core/tests/resourceflaggingtests/Android.bp
index dd86094..efb8437 100644
--- a/core/tests/resourceflaggingtests/Android.bp
+++ b/core/tests/resourceflaggingtests/Android.bp
@@ -22,54 +22,6 @@
default_team: "trendy_team_android_resources",
}
-genrule {
- name: "resource-flagging-test-app-resources-compile",
- tools: ["aapt2"],
- srcs: [
- "flagged_resources_res/values/bools.xml",
- ],
- out: ["values_bools.arsc.flat"],
- cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
- "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
-}
-
-genrule {
- name: "resource-flagging-test-app-resources-compile2",
- tools: ["aapt2"],
- srcs: [
- "flagged_resources_res/values/bools2.xml",
- ],
- out: ["values_bools2.arsc.flat"],
- cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
- "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
-}
-
-genrule {
- name: "resource-flagging-test-app-apk",
- tools: ["aapt2"],
- // The first input file in the list must be the manifest
- srcs: [
- "TestAppAndroidManifest.xml",
- ":resource-flagging-test-app-resources-compile",
- ":resource-flagging-test-app-resources-compile2",
- ],
- out: ["resapp.apk"],
- cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
-}
-
-java_genrule {
- name: "resource-flagging-apk-as-resource",
- srcs: [
- ":resource-flagging-test-app-apk",
- ],
- out: ["apks_as_resources.res.zip"],
- tools: ["soong_zip"],
-
- cmd: "mkdir -p $(genDir)/res/raw && " +
- "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " +
- "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
-}
-
android_test {
name: "ResourceFlaggingTests",
srcs: [
@@ -82,6 +34,6 @@
"testng",
"compatibility-device-util-axt",
],
- resource_zips: [":resource-flagging-apk-as-resource"],
+ resource_zips: [":resource-flagging-test-app-apk-as-resource"],
test_suites: ["device-tests"],
}
diff --git a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
index ad8542e..c1e3578 100644
--- a/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
+++ b/core/tests/resourceflaggingtests/src/com/android/resourceflaggingtests/ResourceFlaggingTest.java
@@ -69,11 +69,23 @@
}
private boolean getBoolean(String name) {
- int resId = mResources.getIdentifier(name, "bool", "com.android.intenal.flaggedresources");
+ int resId = mResources.getIdentifier(
+ name,
+ "bool",
+ "com.android.intenal.flaggedresources");
assertThat(resId).isNotEqualTo(0);
return mResources.getBoolean(resId);
}
+ private String getString(String name) {
+ int resId = mResources.getIdentifier(
+ name,
+ "string",
+ "com.android.intenal.flaggedresources");
+ assertThat(resId).isNotEqualTo(0);
+ return mResources.getString(resId);
+ }
+
private String extractApkAndGetPath(int id) throws Exception {
final Resources resources = mContext.getResources();
try (InputStream is = resources.openRawResource(id)) {
diff --git a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
index e9a08ae..97f1d5e 100644
--- a/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/PrimitiveSegmentTest.java
@@ -27,7 +27,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -36,6 +40,9 @@
public class PrimitiveSegmentTest {
private static final float TOLERANCE = 1e-2f;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
PrimitiveSegment primitive = new PrimitiveSegment(
@@ -87,7 +94,8 @@
}
@Test
- public void testScale_fullPrimitiveScaleValue() {
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_fullPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0);
@@ -102,7 +110,24 @@
}
@Test
- public void testScale_halfPrimitiveScaleValue() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_fullPrimitiveScaleValue() {
+ PrimitiveSegment initial = new PrimitiveSegment(
+ VibrationEffect.Composition.PRIMITIVE_CLICK, 1, 0);
+
+ assertEquals(1f, initial.scale(1).getScale(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getScale(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getScale(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.8f, initial.scale(0.8f).getScale(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0);
@@ -117,6 +142,22 @@
}
@Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfPrimitiveScaleValue() {
+ PrimitiveSegment initial = new PrimitiveSegment(
+ VibrationEffect.Composition.PRIMITIVE_CLICK, 0.5f, 0);
+
+ assertEquals(0.5f, initial.scale(1).getScale(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getScale(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getScale(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getScale(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.4f, initial.scale(0.8f).getScale(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getScale(), TOLERANCE);
+ }
+
+ @Test
public void testScale_zeroPrimitiveScaleValue() {
PrimitiveSegment initial = new PrimitiveSegment(
VibrationEffect.Composition.PRIMITIVE_CLICK, 0, 0);
diff --git a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
index 01013ab..bea8293 100644
--- a/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/RampSegmentTest.java
@@ -29,7 +29,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -38,6 +42,9 @@
public class RampSegmentTest {
private static final float TOLERANCE = 1e-2f;
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
RampSegment ramp = new RampSegment(/* startAmplitude= */ 1, /* endAmplitude= */ 0,
@@ -97,14 +104,18 @@
}
@Test
- public void testScale() {
- RampSegment initial = new RampSegment(0, 1, 0, 0, 0);
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfAndFullAmplitudes() {
+ RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0);
- assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
- assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE);
assertEquals(0.34f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE);
@@ -117,17 +128,38 @@
}
@Test
- public void testScale_halfPrimitiveScaleValue() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfAndFullAmplitudes() {
RampSegment initial = new RampSegment(0.5f, 1, 0, 0, 0);
assertEquals(0.5f, initial.scale(1).getStartAmplitude(), TOLERANCE);
- assertEquals(0.17f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
- assertEquals(0.86f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
- assertEquals(0.47f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.4f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+
+ assertEquals(1f, initial.scale(1).getEndAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getEndAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getEndAmplitude(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getEndAmplitude(), TOLERANCE);
// Does not restore to the exact original value because scale up is a bit offset.
- assertEquals(0.35f, initial.scale(0.8f).getStartAmplitude(), TOLERANCE);
- assertEquals(0.5f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0.81f, initial.scale(0.8f).getEndAmplitude(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getEndAmplitude(), TOLERANCE);
+ }
+
+ @Test
+ public void testScale_zeroAmplitude() {
+ RampSegment initial = new RampSegment(0, 1, 0, 0, 0);
+
+ assertEquals(0f, initial.scale(1).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(0.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(1.5f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(1.5f).scale(2 / 3f).getStartAmplitude(), TOLERANCE);
+ assertEquals(0f, initial.scale(0.8f).scale(1.25f).getStartAmplitude(), TOLERANCE);
}
@Test
diff --git a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
index 40776ab..411074a 100644
--- a/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/StepSegmentTest.java
@@ -27,7 +27,11 @@
import android.os.Parcel;
import android.os.VibrationEffect;
import android.os.VibratorInfo;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -35,6 +39,10 @@
@RunWith(JUnit4.class)
public class StepSegmentTest {
private static final float TOLERANCE = 1e-2f;
+
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void testCreation() {
StepSegment step = new StepSegment(/* amplitude= */ 1f, /* frequencyHz= */ 1f,
@@ -93,7 +101,8 @@
}
@Test
- public void testScale_fullAmplitude() {
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_fullAmplitude() {
StepSegment initial = new StepSegment(1f, 0, 0);
assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE);
@@ -107,7 +116,23 @@
}
@Test
- public void testScale_halfAmplitude() {
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_fullAmplitude() {
+ StepSegment initial = new StepSegment(1f, 0, 0);
+
+ assertEquals(1f, initial.scale(1).getAmplitude(), TOLERANCE);
+ assertEquals(0.5f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(1f, initial.scale(1.5f).getAmplitude(), TOLERANCE);
+ assertEquals(2 / 3f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.8f, initial.scale(0.8f).getAmplitude(), TOLERANCE);
+ assertEquals(0.86f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withLegacyScaling_halfAmplitude() {
StepSegment initial = new StepSegment(0.5f, 0, 0);
assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE);
@@ -121,6 +146,21 @@
}
@Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testScale_withScalingV2_halfAmplitude() {
+ StepSegment initial = new StepSegment(0.5f, 0, 0);
+
+ assertEquals(0.5f, initial.scale(1).getAmplitude(), TOLERANCE);
+ assertEquals(0.25f, initial.scale(0.5f).getAmplitude(), TOLERANCE);
+ // The original value was not scaled up, so this only scales it down.
+ assertEquals(0.66f, initial.scale(1.5f).getAmplitude(), TOLERANCE);
+ assertEquals(0.44f, initial.scale(1.5f).scale(2 / 3f).getAmplitude(), TOLERANCE);
+ // Does not restore to the exact original value because scale up is a bit offset.
+ assertEquals(0.4f, initial.scale(0.8f).getAmplitude(), TOLERANCE);
+ assertEquals(0.48f, initial.scale(0.8f).scale(1.25f).getAmplitude(), TOLERANCE);
+ }
+
+ @Test
public void testScale_zeroAmplitude() {
StepSegment initial = new StepSegment(0, 0, 0);
diff --git a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
index bf9a820..1cc38de 100644
--- a/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
+++ b/core/tests/vibrator/src/android/os/vibrator/persistence/VibrationEffectXmlSerializationTest.java
@@ -24,22 +24,32 @@
import static com.google.common.truth.Truth.assertThat;
+import static org.junit.Assert.assertArrayEquals;
import static org.junit.Assert.assertThrows;
+import android.os.PersistableBundle;
import android.os.VibrationEffect;
+import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.util.Xml;
import com.android.modules.utils.TypedXmlPullParser;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
import org.xmlpull.v1.XmlPullParser;
+import java.io.ByteArrayOutputStream;
+import java.io.IOException;
import java.io.StringReader;
import java.io.StringWriter;
import java.util.Arrays;
+import java.util.Base64;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
@@ -53,6 +63,9 @@
@RunWith(JUnit4.class)
public class VibrationEffectXmlSerializationTest {
+ @Rule
+ public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
+
@Test
public void isSupportedMimeType_onlySupportsVibrationXmlMimeType() {
// Single MIME type supported
@@ -422,6 +435,97 @@
}
}
+ @Test
+ @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testVendorEffect_featureFlagEnabled_allSucceed() throws Exception {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("id", 1);
+ vendorData.putDouble("scale", 0.5);
+ vendorData.putBoolean("loop", false);
+ vendorData.putLongArray("amplitudes", new long[] { 0, 255, 128 });
+ vendorData.putString("label", "vibration");
+
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ vendorData.writeToStream(outputStream);
+ String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray());
+
+ VibrationEffect effect = VibrationEffect.createVendorEffect(vendorData);
+ String xml = "<vibration-effect><vendor-effect> " // test trailing whitespace is ignored
+ + vendorDataStr
+ + " \n </vendor-effect></vibration-effect>";
+
+ assertPublicApisParserSucceeds(xml, effect);
+ assertPublicApisSerializerSucceeds(effect, vendorDataStr);
+ assertPublicApisRoundTrip(effect);
+
+ assertHiddenApisParserSucceeds(xml, effect);
+ assertHiddenApisSerializerSucceeds(effect, vendorDataStr);
+ assertHiddenApisRoundTrip(effect);
+
+ // Check PersistableBundle from round-trip
+ PersistableBundle parsedVendorData =
+ ((VibrationEffect.VendorEffect) parseVibrationEffect(serialize(effect),
+ /* flags= */ 0)).getVendorData();
+ assertThat(parsedVendorData.size()).isEqualTo(vendorData.size());
+ assertThat(parsedVendorData.getInt("id")).isEqualTo(1);
+ assertThat(parsedVendorData.getDouble("scale")).isEqualTo(0.5);
+ assertThat(parsedVendorData.getBoolean("loop")).isFalse();
+ assertArrayEquals(parsedVendorData.getLongArray("amplitudes"), new long[] { 0, 255, 128 });
+ assertThat(parsedVendorData.getString("label")).isEqualTo("vibration");
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testInvalidVendorEffect_featureFlagEnabled_allFail() throws IOException {
+ String emptyTag = "<vibration-effect><vendor-effect/></vibration-effect>";
+ assertPublicApisParserFails(emptyTag);
+ assertHiddenApisParserFails(emptyTag);
+
+ String emptyStringTag =
+ "<vibration-effect><vendor-effect> \n </vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(emptyStringTag);
+ assertHiddenApisParserFails(emptyStringTag);
+
+ String invalidString =
+ "<vibration-effect><vendor-effect>invalid</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(invalidString);
+ assertHiddenApisParserFails(invalidString);
+
+ String validBase64String =
+ "<vibration-effect><vendor-effect>c29tZXNh</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(validBase64String);
+ assertHiddenApisParserFails(validBase64String);
+
+ PersistableBundle emptyData = new PersistableBundle();
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ emptyData.writeToStream(outputStream);
+ String emptyBundleString = "<vibration-effect><vendor-effect>"
+ + Base64.getEncoder().encodeToString(outputStream.toByteArray())
+ + "</vendor-effect></vibration-effect>";
+ assertPublicApisParserFails(emptyBundleString);
+ assertHiddenApisParserFails(emptyBundleString);
+ }
+
+ @Test
+ @DisableFlags(Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ public void testVendorEffect_featureFlagDisabled_allFail() throws Exception {
+ PersistableBundle vendorData = new PersistableBundle();
+ vendorData.putInt("id", 1);
+ ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
+ vendorData.writeToStream(outputStream);
+ String vendorDataStr = Base64.getEncoder().encodeToString(outputStream.toByteArray());
+ String xml = "<vibration-effect><vendor-effect>"
+ + vendorDataStr
+ + "</vendor-effect></vibration-effect>";
+ VibrationEffect vendorEffect = VibrationEffect.createVendorEffect(vendorData);
+
+ assertPublicApisParserFails(xml);
+ assertPublicApisSerializerFails(vendorEffect);
+
+ assertHiddenApisParserFails(xml);
+ assertHiddenApisSerializerFails(vendorEffect);
+ }
+
private void assertPublicApisParserFails(String xml) {
assertThrows("Expected parseVibrationEffect to fail for " + xml,
VibrationXmlParser.ParseFailedException.class,
@@ -493,6 +597,12 @@
() -> serialize(effect));
}
+ private void assertHiddenApisSerializerFails(VibrationEffect effect) {
+ assertThrows("Expected serialization to fail for " + effect,
+ VibrationXmlSerializer.SerializationFailedException.class,
+ () -> serialize(effect, VibrationXmlSerializer.FLAG_ALLOW_HIDDEN_APIS));
+ }
+
private void assertPublicApisSerializerSucceeds(VibrationEffect effect,
String... expectedSegments) throws Exception {
assertSerializationContainsSegments(serialize(effect), expectedSegments);
diff --git a/core/xsd/vibrator/vibration/schema/current.txt b/core/xsd/vibrator/vibration/schema/current.txt
index f0e13c4..280b405 100644
--- a/core/xsd/vibrator/vibration/schema/current.txt
+++ b/core/xsd/vibrator/vibration/schema/current.txt
@@ -41,9 +41,11 @@
ctor public VibrationEffect();
method public com.android.internal.vibrator.persistence.PredefinedEffect getPredefinedEffect_optional();
method public com.android.internal.vibrator.persistence.PrimitiveEffect getPrimitiveEffect_optional();
+ method public byte[] getVendorEffect_optional();
method public com.android.internal.vibrator.persistence.WaveformEffect getWaveformEffect_optional();
method public void setPredefinedEffect_optional(com.android.internal.vibrator.persistence.PredefinedEffect);
method public void setPrimitiveEffect_optional(com.android.internal.vibrator.persistence.PrimitiveEffect);
+ method public void setVendorEffect_optional(byte[]);
method public void setWaveformEffect_optional(com.android.internal.vibrator.persistence.WaveformEffect);
}
diff --git a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
index fcd250b..21a6fac 100644
--- a/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
+++ b/core/xsd/vibrator/vibration/vibration-plus-hidden-apis.xsd
@@ -46,6 +46,9 @@
<!-- Predefined vibration effect -->
<xs:element name="predefined-effect" type="PredefinedEffect"/>
+ <!-- Vendor vibration effect -->
+ <xs:element name="vendor-effect" type="VendorEffect"/>
+
<!-- Primitive composition effect -->
<xs:sequence>
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
@@ -136,6 +139,10 @@
</xs:restriction>
</xs:simpleType>
+ <xs:simpleType name="VendorEffect">
+ <xs:restriction base="xs:base64Binary"/>
+ </xs:simpleType>
+
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
diff --git a/core/xsd/vibrator/vibration/vibration.xsd b/core/xsd/vibrator/vibration/vibration.xsd
index b9de691..d35d777 100644
--- a/core/xsd/vibrator/vibration/vibration.xsd
+++ b/core/xsd/vibrator/vibration/vibration.xsd
@@ -44,6 +44,9 @@
<!-- Predefined vibration effect -->
<xs:element name="predefined-effect" type="PredefinedEffect"/>
+ <!-- Vendor vibration effect -->
+ <xs:element name="vendor-effect" type="VendorEffect"/>
+
<!-- Primitive composition effect -->
<xs:sequence>
<xs:element name="primitive-effect" type="PrimitiveEffect"/>
@@ -113,6 +116,10 @@
</xs:restriction>
</xs:simpleType>
+ <xs:simpleType name="VendorEffect">
+ <xs:restriction base="xs:base64Binary"/>
+ </xs:simpleType>
+
<xs:complexType name="PrimitiveEffect">
<xs:attribute name="name" type="PrimitiveEffectName" use="required"/>
<xs:attribute name="scale" type="PrimitiveScale"/>
diff --git a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
index f1e7ef5..99716e7 100644
--- a/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
+++ b/libs/WindowManager/Jetpack/src/androidx/window/extensions/embedding/SplitPresenter.java
@@ -405,8 +405,7 @@
// Sets the dim area when the two TaskFragments are adjacent.
final boolean dimOnTask = !isStacked
- && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK
- && Flags.fullscreenDimFlag();
+ && splitAttributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK;
setTaskFragmentDimOnTask(wct, primaryContainer.getTaskFragmentToken(), dimOnTask);
setTaskFragmentDimOnTask(wct, secondaryContainer.getTaskFragmentToken(), dimOnTask);
@@ -646,7 +645,6 @@
container);
final boolean isFillParent = relativeBounds.isEmpty();
final boolean dimOnTask = !isFillParent
- && Flags.fullscreenDimFlag()
&& attributes.getWindowAttributes().getDimAreaBehavior() == DIM_AREA_ON_TASK;
final IBinder fragmentToken = container.getTaskFragmentToken();
diff --git a/libs/WindowManager/Shell/Android.bp b/libs/WindowManager/Shell/Android.bp
index 5135e9e..1c3d9c3 100644
--- a/libs/WindowManager/Shell/Android.bp
+++ b/libs/WindowManager/Shell/Android.bp
@@ -166,6 +166,16 @@
},
}
+java_library {
+ name: "WindowManager-Shell-lite-proto",
+
+ srcs: ["src/com/android/wm/shell/desktopmode/education/data/proto/**/*.proto"],
+
+ proto: {
+ type: "lite",
+ },
+}
+
filegroup {
name: "wm_shell-shared-aidls",
@@ -215,6 +225,7 @@
"androidx.core_core-animation",
"androidx.core_core-ktx",
"androidx.arch.core_core-runtime",
+ "androidx.datastore_datastore",
"androidx.compose.material3_material3",
"androidx-constraintlayout_constraintlayout",
"androidx.dynamicanimation_dynamicanimation",
@@ -225,6 +236,7 @@
"//frameworks/libs/systemui:iconloader_base",
"com_android_wm_shell_flags_lib",
"WindowManager-Shell-proto",
+ "WindowManager-Shell-lite-proto",
"WindowManager-Shell-shared",
"perfetto_trace_java_protos",
"dagger2",
diff --git a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
index 5e67333..84f7bb2 100644
--- a/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
+++ b/libs/WindowManager/Shell/multivalentTests/src/com/android/wm/shell/bubbles/BubbleStackViewTest.kt
@@ -53,6 +53,7 @@
import org.mockito.kotlin.mock
import android.platform.test.annotations.DisableFlags
import android.platform.test.annotations.EnableFlags
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
import java.util.concurrent.Semaphore
import java.util.concurrent.TimeUnit
import java.util.function.Consumer
@@ -458,5 +459,7 @@
override fun isShowingAsBubbleBar(): Boolean = false
override fun hideCurrentInputMethod() {}
+
+ override fun updateBubbleBarLocation(location: BubbleBarLocation) {}
}
}
diff --git a/libs/WindowManager/Shell/res/values/ids.xml b/libs/WindowManager/Shell/res/values/ids.xml
index bc59a23..debcba0 100644
--- a/libs/WindowManager/Shell/res/values/ids.xml
+++ b/libs/WindowManager/Shell/res/values/ids.xml
@@ -42,6 +42,8 @@
<item type="id" name="action_move_top_right"/>
<item type="id" name="action_move_bottom_left"/>
<item type="id" name="action_move_bottom_right"/>
+ <item type="id" name="action_move_bubble_bar_left"/>
+ <item type="id" name="action_move_bubble_bar_right"/>
<item type="id" name="dismiss_view"/>
</resources>
diff --git a/libs/WindowManager/Shell/res/values/strings.xml b/libs/WindowManager/Shell/res/values/strings.xml
index 0a8166f..6a62d7a3 100644
--- a/libs/WindowManager/Shell/res/values/strings.xml
+++ b/libs/WindowManager/Shell/res/values/strings.xml
@@ -139,6 +139,14 @@
<string name="bubble_accessibility_action_move_bottom_left">Move bottom left</string>
<!-- Action in accessibility menu to move the stack of bubbles to the bottom right of the screen. [CHAR LIMIT=30]-->
<string name="bubble_accessibility_action_move_bottom_right">Move bottom right</string>
+ <!-- Click action label for bubbles to expand menu. [CHAR LIMIT=30]-->
+ <string name="bubble_accessibility_action_expand_menu">expand menu</string>
+ <!-- Click action label for bubbles to collapse menu. [CHAR LIMIT=30]-->
+ <string name="bubble_accessibility_action_collapse_menu">collapse menu</string>
+ <!-- Action in accessibility menu to move the bubble bar to the left side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_accessibility_action_move_bar_left">Move left</string>
+ <!-- Action in accessibility menu to move the bubble bar to the right side of the screen. [CHAR_LIMIT=30] -->
+ <string name="bubble_accessibility_action_move_bar_right">Move right</string>
<!-- Accessibility announcement when the stack of bubbles expands. [CHAR LIMIT=NONE]-->
<string name="bubble_accessibility_announce_expand">expand <xliff:g id="bubble_title" example="Messages">%1$s</xliff:g></string>
<!-- Accessibility announcement when the stack of bubbles collapses. [CHAR LIMIT=NONE]-->
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
index f9a1d94..dc511be 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BadgedImageView.java
@@ -357,7 +357,9 @@
void showBadge() {
Bitmap appBadgeBitmap = mBubble.getAppBadge();
- if (appBadgeBitmap == null) {
+ final boolean isAppLaunchIntent = (mBubble instanceof Bubble)
+ && ((Bubble) mBubble).isAppLaunchIntent();
+ if (appBadgeBitmap == null || isAppLaunchIntent) {
mAppIcon.setVisibility(GONE);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
index 7dbbb04..5cd2cb7 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubble.java
@@ -50,6 +50,7 @@
import com.android.internal.logging.InstanceId;
import com.android.internal.protolog.ProtoLog;
import com.android.launcher3.icons.BubbleIconFactory;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.bubbles.bar.BubbleBarExpandedView;
import com.android.wm.shell.bubbles.bar.BubbleBarLayerView;
import com.android.wm.shell.common.bubbles.BubbleInfo;
@@ -246,7 +247,23 @@
mAppIntent = intent;
mDesiredHeight = Integer.MAX_VALUE;
mPackageName = intent.getPackage();
+ }
+ private Bubble(ShortcutInfo info, Executor mainExecutor) {
+ mGroupKey = null;
+ mLocusId = null;
+ mFlags = 0;
+ mUser = info.getUserHandle();
+ mIcon = info.getIcon();
+ mIsAppBubble = false;
+ mKey = getBubbleKeyForShortcut(info);
+ mShowBubbleUpdateDot = false;
+ mMainExecutor = mainExecutor;
+ mTaskId = INVALID_TASK_ID;
+ mAppIntent = null;
+ mDesiredHeight = Integer.MAX_VALUE;
+ mPackageName = info.getPackage();
+ mShortcutInfo = info;
}
/** Creates an app bubble. */
@@ -263,6 +280,13 @@
mainExecutor);
}
+ /** Creates a shortcut bubble. */
+ public static Bubble createShortcutBubble(
+ ShortcutInfo info,
+ Executor mainExecutor) {
+ return new Bubble(info, mainExecutor);
+ }
+
/**
* Returns the key for an app bubble from an app with package name, {@code packageName} on an
* Android user, {@code user}.
@@ -273,6 +297,14 @@
return KEY_APP_BUBBLE + ":" + user.getIdentifier() + ":" + packageName;
}
+ /**
+ * Returns the key for a shortcut bubble using {@code packageName}, {@code user}, and the
+ * {@code shortcutInfo} id.
+ */
+ public static String getBubbleKeyForShortcut(ShortcutInfo info) {
+ return info.getPackage() + ":" + info.getUserId() + ":" + info.getId();
+ }
+
@VisibleForTesting(visibility = PRIVATE)
public Bubble(@NonNull final BubbleEntry entry,
final Bubbles.BubbleMetadataFlagListener listener,
@@ -888,6 +920,17 @@
return mIntent;
}
+ /**
+ * Whether this bubble represents the full app, i.e. the intent used is the launch
+ * intent for an app. In this case we don't show a badge on the icon.
+ */
+ public boolean isAppLaunchIntent() {
+ if (Flags.enableBubbleAnything() && mAppIntent != null) {
+ return mAppIntent.hasCategory("android.intent.category.LAUNCHER");
+ }
+ return false;
+ }
+
@Nullable
PendingIntent getDeleteIntent() {
return mDeleteIntent;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
index 949a723..29520ef 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleController.java
@@ -1335,6 +1335,40 @@
}
/**
+ * Expands and selects a bubble created or found via the provided shortcut info.
+ *
+ * @param info the shortcut info for the bubble.
+ */
+ public void expandStackAndSelectBubble(ShortcutInfo info) {
+ if (!Flags.enableBubbleAnything()) return;
+ Bubble b = mBubbleData.getOrCreateBubble(info); // Removes from overflow
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - shortcut=%s", info);
+ if (b.isInflated()) {
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
+ } else {
+ b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ }
+ }
+
+ /**
+ * Expands and selects a bubble created or found for this app.
+ *
+ * @param intent the intent for the bubble.
+ */
+ public void expandStackAndSelectBubble(Intent intent) {
+ if (!Flags.enableBubbleAnything()) return;
+ Bubble b = mBubbleData.getOrCreateBubble(intent); // Removes from overflow
+ ProtoLog.v(WM_SHELL_BUBBLES, "expandStackAndSelectBubble - intent=%s", intent);
+ if (b.isInflated()) {
+ mBubbleData.setSelectedBubbleAndExpandStack(b);
+ } else {
+ b.enable(Notification.BubbleMetadata.FLAG_AUTO_EXPAND_BUBBLE);
+ inflateAndAdd(b, /* suppressFlyout= */ true, /* showInShade= */ false);
+ }
+ }
+
+ /**
* Expands and selects a bubble based on the provided {@link BubbleEntry}. If no bubble
* exists for this entry, and it is able to bubble, a new bubble will be created.
*
@@ -2323,6 +2357,7 @@
* @param entry the entry to bubble.
*/
static boolean canLaunchInTaskView(Context context, BubbleEntry entry) {
+ if (Flags.enableBubbleAnything()) return true;
PendingIntent intent = entry.getBubbleMetadata() != null
? entry.getBubbleMetadata().getIntent()
: null;
@@ -2439,6 +2474,16 @@
}
@Override
+ public void showShortcutBubble(ShortcutInfo info) {
+ mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(info));
+ }
+
+ @Override
+ public void showAppBubble(Intent intent) {
+ mMainExecutor.execute(() -> mController.expandStackAndSelectBubble(intent));
+ }
+
+ @Override
public void showBubble(String key, int topOnScreen) {
mMainExecutor.execute(
() -> mController.expandStackAndSelectBubbleFromLauncher(key, topOnScreen));
@@ -2634,6 +2679,13 @@
}
@Override
+ public void expandStackAndSelectBubble(ShortcutInfo info) {
+ mMainExecutor.execute(() -> {
+ BubbleController.this.expandStackAndSelectBubble(info);
+ });
+ }
+
+ @Override
public void expandStackAndSelectBubble(Bubble bubble) {
mMainExecutor.execute(() -> {
BubbleController.this.expandStackAndSelectBubble(bubble);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
index b6da761..3c6c6fa 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleData.java
@@ -23,8 +23,10 @@
import android.annotation.NonNull;
import android.app.PendingIntent;
import android.content.Context;
+import android.content.Intent;
import android.content.LocusId;
import android.content.pm.ShortcutInfo;
+import android.os.UserHandle;
import android.text.TextUtils;
import android.util.ArrayMap;
import android.util.ArraySet;
@@ -421,23 +423,19 @@
Bubble bubbleToReturn = getBubbleInStackWithKey(key);
if (bubbleToReturn == null) {
- bubbleToReturn = getOverflowBubbleWithKey(key);
- if (bubbleToReturn != null) {
- // Promoting from overflow
- mOverflowBubbles.remove(bubbleToReturn);
- if (mOverflowBubbles.isEmpty()) {
- mStateChange.showOverflowChanged = true;
+ // Check if it's in the overflow
+ bubbleToReturn = findAndRemoveBubbleFromOverflow(key);
+ if (bubbleToReturn == null) {
+ if (entry != null) {
+ // Not in the overflow, have an entry, so it's a new bubble
+ bubbleToReturn = new Bubble(entry,
+ mBubbleMetadataFlagListener,
+ mCancelledListener,
+ mMainExecutor);
+ } else {
+ // If there's no entry it must be a persisted bubble
+ bubbleToReturn = persistedBubble;
}
- } else if (mPendingBubbles.containsKey(key)) {
- // Update while it was pending
- bubbleToReturn = mPendingBubbles.get(key);
- } else if (entry != null) {
- // New bubble
- bubbleToReturn = new Bubble(entry, mBubbleMetadataFlagListener, mCancelledListener,
- mMainExecutor);
- } else {
- // Persisted bubble being promoted
- bubbleToReturn = persistedBubble;
}
}
@@ -448,6 +446,46 @@
return bubbleToReturn;
}
+ Bubble getOrCreateBubble(ShortcutInfo info) {
+ String bubbleKey = Bubble.getBubbleKeyForShortcut(info);
+ Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+ if (bubbleToReturn == null) {
+ bubbleToReturn = Bubble.createShortcutBubble(info, mMainExecutor);
+ }
+ return bubbleToReturn;
+ }
+
+ Bubble getOrCreateBubble(Intent intent) {
+ UserHandle user = UserHandle.of(mCurrentUserId);
+ String bubbleKey = Bubble.getAppBubbleKeyForApp(intent.getPackage(),
+ user);
+ Bubble bubbleToReturn = findAndRemoveBubbleFromOverflow(bubbleKey);
+ if (bubbleToReturn == null) {
+ bubbleToReturn = Bubble.createAppBubble(intent, user, null, mMainExecutor);
+ }
+ return bubbleToReturn;
+ }
+
+ @Nullable
+ private Bubble findAndRemoveBubbleFromOverflow(String key) {
+ Bubble bubbleToReturn = getBubbleInStackWithKey(key);
+ if (bubbleToReturn != null) {
+ return bubbleToReturn;
+ }
+ bubbleToReturn = getOverflowBubbleWithKey(key);
+ if (bubbleToReturn != null) {
+ mOverflowBubbles.remove(bubbleToReturn);
+ // Promoting from overflow
+ mOverflowBubbles.remove(bubbleToReturn);
+ if (mOverflowBubbles.isEmpty()) {
+ mStateChange.showOverflowChanged = true;
+ }
+ } else if (mPendingBubbles.containsKey(key)) {
+ bubbleToReturn = mPendingBubbles.get(key);
+ }
+ return bubbleToReturn;
+ }
+
/**
* When this method is called it is expected that all info in the bubble has completed loading.
* @see Bubble#inflate(BubbleViewInfoTask.Callback, Context, BubbleExpandedViewManager,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
index fdb4523..a0c0a25 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedView.java
@@ -232,6 +232,9 @@
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId()
+ || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything()));
+
if (mBubble.isAppBubble()) {
Context context =
mContext.createContextAsUser(
@@ -246,7 +249,8 @@
/* options= */ null);
mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
launchBounds);
- } else if (!mIsOverflow && mBubble.hasMetadataShortcutId()) {
+ } else if (!mIsOverflow && isShortcutBubble) {
+ ProtoLog.v(WM_SHELL_BUBBLES, "startingShortcutBubble=%s", getBubbleKey());
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
index 3d9bf03..4e80e90 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleExpandedViewManager.kt
@@ -16,6 +16,8 @@
package com.android.wm.shell.bubbles
+import com.android.wm.shell.common.bubbles.BubbleBarLocation
+
/** Manager interface for bubble expanded views. */
interface BubbleExpandedViewManager {
@@ -30,6 +32,7 @@
fun isStackExpanded(): Boolean
fun isShowingAsBubbleBar(): Boolean
fun hideCurrentInputMethod()
+ fun updateBubbleBarLocation(location: BubbleBarLocation)
companion object {
/**
@@ -78,6 +81,10 @@
override fun hideCurrentInputMethod() {
controller.hideCurrentInputMethod()
}
+
+ override fun updateBubbleBarLocation(location: BubbleBarLocation) {
+ controller.bubbleBarLocation = location
+ }
}
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
index 5e2141a..5f8f0fd 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/BubbleTaskViewHelper.java
@@ -36,6 +36,7 @@
import androidx.annotation.Nullable;
import com.android.internal.protolog.ProtoLog;
+import com.android.wm.shell.Flags;
import com.android.wm.shell.taskview.TaskView;
/**
@@ -110,6 +111,8 @@
fillInIntent.addFlags(FLAG_ACTIVITY_NEW_DOCUMENT);
fillInIntent.addFlags(FLAG_ACTIVITY_MULTIPLE_TASK);
+ final boolean isShortcutBubble = (mBubble.hasMetadataShortcutId()
+ || (mBubble.getShortcutInfo() != null && Flags.enableBubbleAnything()));
if (mBubble.isAppBubble()) {
Context context =
mContext.createContextAsUser(
@@ -124,7 +127,7 @@
/* options= */ null);
mTaskView.startActivity(pi, /* fillInIntent= */ null, options,
launchBounds);
- } else if (mBubble.hasMetadataShortcutId()) {
+ } else if (isShortcutBubble) {
options.setApplyActivityFlagsForBubbles(true);
mTaskView.startShortcutActivity(mBubble.getShortcutInfo(),
options, launchBounds);
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
index 589dfd2..9a27fb6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/Bubbles.java
@@ -23,6 +23,7 @@
import android.app.NotificationChannel;
import android.content.Intent;
+import android.content.pm.ShortcutInfo;
import android.content.pm.UserInfo;
import android.graphics.drawable.Icon;
import android.hardware.HardwareBuffer;
@@ -118,6 +119,14 @@
/**
* Request the stack expand if needed, then select the specified Bubble as current.
+ * If no bubble exists for this entry, one is created.
+ *
+ * @param info the shortcut info to use to create the bubble.
+ */
+ void expandStackAndSelectBubble(ShortcutInfo info);
+
+ /**
+ * Request the stack expand if needed, then select the specified Bubble as current.
*
* @param bubble the bubble to be selected
*/
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
index 0907ddd..5c78974 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/IBubbles.aidl
@@ -18,6 +18,7 @@
import android.content.Intent;
import android.graphics.Rect;
+import android.content.pm.ShortcutInfo;
import com.android.wm.shell.bubbles.IBubblesListener;
import com.android.wm.shell.common.bubbles.BubbleBarLocation;
@@ -48,4 +49,8 @@
oneway void updateBubbleBarTopOnScreen(in int topOnScreen) = 10;
oneway void stopBubbleDrag(in BubbleBarLocation location, in int topOnScreen) = 11;
+
+ oneway void showShortcutBubble(in ShortcutInfo info) = 12;
+
+ oneway void showAppBubble(in Intent intent) = 13;
}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
index 24c568c..6d868d2 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarExpandedView.java
@@ -26,14 +26,18 @@
import android.graphics.Insets;
import android.graphics.Outline;
import android.graphics.Rect;
+import android.os.Bundle;
import android.util.AttributeSet;
import android.util.FloatProperty;
import android.view.LayoutInflater;
import android.view.View;
import android.view.ViewGroup;
import android.view.ViewOutlineProvider;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.FrameLayout;
+import androidx.annotation.NonNull;
+
import com.android.wm.shell.R;
import com.android.wm.shell.bubbles.Bubble;
import com.android.wm.shell.bubbles.BubbleExpandedViewManager;
@@ -42,6 +46,7 @@
import com.android.wm.shell.bubbles.BubbleTaskView;
import com.android.wm.shell.bubbles.BubbleTaskViewHelper;
import com.android.wm.shell.bubbles.Bubbles;
+import com.android.wm.shell.common.bubbles.BubbleBarLocation;
import com.android.wm.shell.taskview.TaskView;
import java.util.function.Supplier;
@@ -81,6 +86,7 @@
private static final String TAG = BubbleBarExpandedView.class.getSimpleName();
private static final int INVALID_TASK_ID = -1;
+ private Bubble mBubble;
private BubbleExpandedViewManager mManager;
private BubblePositioner mPositioner;
private boolean mIsOverflow;
@@ -188,12 +194,21 @@
// Handle view needs to draw on top of task view.
bringChildToFront(mHandleView);
+
+ mHandleView.setAccessibilityDelegate(new HandleViewAccessibilityDelegate());
}
mMenuViewController = new BubbleBarMenuViewController(mContext, this);
mMenuViewController.setListener(new BubbleBarMenuViewController.Listener() {
@Override
public void onMenuVisibilityChanged(boolean visible) {
setObscured(visible);
+ if (visible) {
+ mHandleView.setFocusable(false);
+ mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_NO);
+ } else {
+ mHandleView.setFocusable(true);
+ mHandleView.setImportantForAccessibility(IMPORTANT_FOR_ACCESSIBILITY_AUTO);
+ }
}
@Override
@@ -319,6 +334,7 @@
/** Updates the bubble shown in the expanded view. */
public void update(Bubble bubble) {
+ mBubble = bubble;
mBubbleTaskViewHelper.update(bubble);
mMenuViewController.updateMenu(bubble);
}
@@ -457,4 +473,51 @@
invalidateOutline();
}
}
+
+ private class HandleViewAccessibilityDelegate extends AccessibilityDelegate {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(@NonNull View host,
+ @NonNull AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK, getResources().getString(
+ R.string.bubble_accessibility_action_expand_menu)));
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_COLLAPSE);
+ info.addAction(AccessibilityNodeInfo.AccessibilityAction.ACTION_DISMISS);
+ if (mPositioner.isBubbleBarOnLeft()) {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ R.id.action_move_bubble_bar_right, getResources().getString(
+ R.string.bubble_accessibility_action_move_bar_right)));
+ } else {
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ R.id.action_move_bubble_bar_left, getResources().getString(
+ R.string.bubble_accessibility_action_move_bar_left)));
+ }
+ }
+
+ @Override
+ public boolean performAccessibilityAction(@NonNull View host, int action,
+ @Nullable Bundle args) {
+ if (super.performAccessibilityAction(host, action, args)) {
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_COLLAPSE) {
+ mManager.collapseStack();
+ return true;
+ }
+ if (action == AccessibilityNodeInfo.ACTION_DISMISS) {
+ mManager.dismissBubble(mBubble, Bubbles.DISMISS_USER_GESTURE);
+ return true;
+ }
+ if (action == R.id.action_move_bubble_bar_left) {
+ mManager.updateBubbleBarLocation(BubbleBarLocation.LEFT);
+ return true;
+ }
+ if (action == R.id.action_move_bubble_bar_right) {
+ mManager.updateBubbleBarLocation(BubbleBarLocation.RIGHT);
+ return true;
+ }
+ return false;
+ }
+ }
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
index 8389c81..0300869 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/bubbles/bar/BubbleBarMenuView.java
@@ -23,7 +23,9 @@
import android.graphics.drawable.Icon;
import android.util.AttributeSet;
import android.view.LayoutInflater;
+import android.view.View;
import android.view.ViewGroup;
+import android.view.accessibility.AccessibilityNodeInfo;
import android.widget.ImageView;
import android.widget.LinearLayout;
import android.widget.TextView;
@@ -71,6 +73,16 @@
mBubbleTitleView = findViewById(R.id.bubble_bar_manage_menu_bubble_title);
mBubbleDismissIconView = findViewById(R.id.bubble_bar_manage_menu_dismiss_icon);
updateThemeColors();
+
+ mBubbleSectionView.setAccessibilityDelegate(new AccessibilityDelegate() {
+ @Override
+ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfo info) {
+ super.onInitializeAccessibilityNodeInfo(host, info);
+ info.addAction(new AccessibilityNodeInfo.AccessibilityAction(
+ AccessibilityNodeInfo.ACTION_CLICK, getResources().getString(
+ R.string.bubble_accessibility_action_collapse_menu)));
+ }
+ });
}
private void updateThemeColors() {
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS
new file mode 100644
index 0000000..1875675
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/OWNERS
@@ -0,0 +1,4 @@
+# WM shell sub-module compat ui owners
+mariiasand@google.com
+gracielawputri@google.com
+mcarli@google.com
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt
index a520d5e..022906c 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/compatui/api/CompatUISpec.kt
@@ -16,6 +16,9 @@
package com.android.wm.shell.compatui.api
+import com.android.internal.protolog.ProtoLog
+import com.android.wm.shell.protolog.ShellProtoLogGroup
+
/**
* Defines the predicates to invoke for understanding if a component can be created or destroyed.
*/
@@ -39,6 +42,7 @@
* Describes each compat ui component to the framework.
*/
class CompatUISpec(
+ val log: (String) -> Unit = { str -> ProtoLog.v(ShellProtoLogGroup.WM_SHELL_COMPAT_UI, str) },
// Unique name for the component. It's used for debug and for generating the
// unique component identifier in the system.
val name: String,
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
index b8b62a7..e787a3d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/dagger/WMShellModule.java
@@ -72,6 +72,7 @@
import com.android.wm.shell.desktopmode.ReturnToDragStartAnimator;
import com.android.wm.shell.desktopmode.SpringDragToDesktopTransitionHandler;
import com.android.wm.shell.desktopmode.ToggleResizeDesktopTaskTransitionHandler;
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository;
import com.android.wm.shell.draganddrop.DragAndDropController;
import com.android.wm.shell.draganddrop.GlobalDragListener;
import com.android.wm.shell.freeform.FreeformComponents;
@@ -679,6 +680,13 @@
return new DesktopModeEventLogger();
}
+ @WMSingleton
+ @Provides
+ static AppHandleEducationDatastoreRepository provideAppHandleEducationDatastoreRepository(
+ Context context) {
+ return new AppHandleEducationDatastoreRepository(context);
+ }
+
//
// Drag and drop
//
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 400882a..05c9d02 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
@@ -73,25 +73,9 @@
sessionId,
taskUpdate.instanceId
)
- FrameworkStatsLog.write(
- DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
- /* task_event */
+ logTaskUpdate(
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_ADDED,
- /* instance_id */
- taskUpdate.instanceId,
- /* uid */
- taskUpdate.uid,
- /* task_height */
- taskUpdate.taskHeight,
- /* task_width */
- taskUpdate.taskWidth,
- /* task_x */
- taskUpdate.taskX,
- /* task_y */
- taskUpdate.taskY,
- /* session_id */
- sessionId
- )
+ sessionId, taskUpdate)
}
/**
@@ -105,25 +89,9 @@
sessionId,
taskUpdate.instanceId
)
- FrameworkStatsLog.write(
- DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
- /* task_event */
+ logTaskUpdate(
FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_REMOVED,
- /* instance_id */
- taskUpdate.instanceId,
- /* uid */
- taskUpdate.uid,
- /* task_height */
- taskUpdate.taskHeight,
- /* task_width */
- taskUpdate.taskWidth,
- /* task_x */
- taskUpdate.taskX,
- /* task_y */
- taskUpdate.taskY,
- /* session_id */
- sessionId
- )
+ sessionId, taskUpdate)
}
/**
@@ -137,10 +105,16 @@
sessionId,
taskUpdate.instanceId
)
+ logTaskUpdate(
+ FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+ sessionId, taskUpdate)
+ }
+
+ private fun logTaskUpdate(taskEvent: Int, sessionId: Int, taskUpdate: TaskUpdate) {
FrameworkStatsLog.write(
DESKTOP_MODE_TASK_UPDATE_ATOM_ID,
/* task_event */
- FrameworkStatsLog.DESKTOP_MODE_SESSION_TASK_UPDATE__TASK_EVENT__TASK_INFO_CHANGED,
+ taskEvent,
/* instance_id */
taskUpdate.instanceId,
/* uid */
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
index 38675129..597637d 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/DesktopTasksLimiter.kt
@@ -70,11 +70,11 @@
// TODO(b/333018485): replace this observer when implementing the minimize-animation
private inner class MinimizeTransitionObserver : TransitionObserver {
- private val mPendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
- private val mActiveTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
+ private val pendingTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
+ private val activeTransitionTokensAndTasks = mutableMapOf<IBinder, TaskDetails>()
fun addPendingTransitionToken(transition: IBinder, taskDetails: TaskDetails) {
- mPendingTransitionTokensAndTasks[transition] = taskDetails
+ pendingTransitionTokensAndTasks[transition] = taskDetails
}
override fun onTransitionReady(
@@ -83,9 +83,7 @@
startTransaction: SurfaceControl.Transaction,
finishTransaction: SurfaceControl.Transaction
) {
- val taskToMinimize = mPendingTransitionTokensAndTasks.remove(transition) ?: return
- taskToMinimize.transitionInfo = info
- mActiveTransitionTokensAndTasks[transition] = taskToMinimize
+ val taskToMinimize = pendingTransitionTokensAndTasks.remove(transition) ?: return
if (!taskRepository.isActiveTask(taskToMinimize.taskId)) return
@@ -97,6 +95,8 @@
return
}
+ taskToMinimize.transitionInfo = info
+ activeTransitionTokensAndTasks[transition] = taskToMinimize
this@DesktopTasksLimiter.markTaskMinimized(
taskToMinimize.displayId, taskToMinimize.taskId)
}
@@ -121,7 +121,7 @@
}
override fun onTransitionStarting(transition: IBinder) {
- val mActiveTaskDetails = mActiveTransitionTokensAndTasks[transition]
+ val mActiveTaskDetails = activeTransitionTokensAndTasks[transition]
if (mActiveTaskDetails != null && mActiveTaskDetails.transitionInfo != null) {
// Begin minimize window CUJ instrumentation.
interactionJankMonitor.begin(
@@ -132,11 +132,11 @@
}
override fun onTransitionMerged(merged: IBinder, playing: IBinder) {
- if (mActiveTransitionTokensAndTasks.remove(merged) != null) {
+ if (activeTransitionTokensAndTasks.remove(merged) != null) {
interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)
}
- mPendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
- mPendingTransitionTokensAndTasks[playing] = taskToTransfer
+ pendingTransitionTokensAndTasks.remove(merged)?.let { taskToTransfer ->
+ pendingTransitionTokensAndTasks[playing] = taskToTransfer
}
}
@@ -144,14 +144,14 @@
ProtoLog.v(
ShellProtoLogGroup.WM_SHELL_DESKTOP_MODE,
"DesktopTasksLimiter: transition %s finished", transition)
- if (mActiveTransitionTokensAndTasks.remove(transition) != null) {
+ if (activeTransitionTokensAndTasks.remove(transition) != null) {
if (aborted) {
interactionJankMonitor.cancel(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)
} else {
interactionJankMonitor.end(CUJ_DESKTOP_MODE_MINIMIZE_WINDOW)
}
}
- mPendingTransitionTokensAndTasks.remove(transition)
+ pendingTransitionTokensAndTasks.remove(transition)
}
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
new file mode 100644
index 0000000..bf4a2ab
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/AppHandleEducationDatastoreRepository.kt
@@ -0,0 +1,81 @@
+/*
+ * 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.wm.shell.desktopmode.education.data
+
+import android.content.Context
+import android.util.Log
+import androidx.datastore.core.CorruptionException
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.core.Serializer
+import androidx.datastore.dataStore
+import androidx.datastore.dataStoreFile
+import com.android.framework.protobuf.InvalidProtocolBufferException
+import com.android.internal.annotations.VisibleForTesting
+import java.io.InputStream
+import java.io.OutputStream
+import kotlinx.coroutines.flow.first
+
+/**
+ * Manages interactions with the App Handle education datastore.
+ *
+ * This class provides a layer of abstraction between the UI/business logic and the underlying
+ * DataStore.
+ */
+class AppHandleEducationDatastoreRepository
+@VisibleForTesting
+constructor(private val dataStore: DataStore<WindowingEducationProto>) {
+ constructor(
+ context: Context
+ ) : this(
+ DataStoreFactory.create(
+ serializer = WindowingEducationProtoSerializer,
+ produceFile = { context.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_FILEPATH) }))
+
+ /**
+ * Reads and returns the [WindowingEducationProto] Proto object from the DataStore. If the
+ * DataStore is empty or there's an error reading, it returns the default value of Proto.
+ */
+ suspend fun windowingEducationProto(): WindowingEducationProto =
+ try {
+ dataStore.data.first()
+ } catch (e: Exception) {
+ Log.e(TAG, "Unable to read from datastore")
+ WindowingEducationProto.getDefaultInstance()
+ }
+
+ companion object {
+ private const val TAG = "AppHandleEducationDatastoreRepository"
+ private const val APP_HANDLE_EDUCATION_DATASTORE_FILEPATH = "app_handle_education.pb"
+
+ object WindowingEducationProtoSerializer : Serializer<WindowingEducationProto> {
+
+ override val defaultValue: WindowingEducationProto =
+ WindowingEducationProto.getDefaultInstance()
+
+ override suspend fun readFrom(input: InputStream): WindowingEducationProto =
+ try {
+ WindowingEducationProto.parseFrom(input)
+ } catch (exception: InvalidProtocolBufferException) {
+ throw CorruptionException("Cannot read proto.", exception)
+ }
+
+ override suspend fun writeTo(windowingProto: WindowingEducationProto, output: OutputStream) =
+ windowingProto.writeTo(output)
+ }
+ }
+}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
new file mode 100644
index 0000000..d29ec53
--- /dev/null
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/desktopmode/education/data/proto/windowing_education.proto
@@ -0,0 +1,39 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+syntax = "proto2";
+
+option java_package = "com.android.wm.shell.desktopmode.education.data";
+option java_multiple_files = true;
+
+// Desktop Windowing education data
+message WindowingEducationProto {
+ // Timestamp in milliseconds of when the education was last viewed.
+ optional int64 education_viewed_timestamp_millis = 1;
+ // Timestamp in milliseconds of when the feature was last used.
+ optional int64 feature_used_timestamp_millis = 2;
+ oneof education_data {
+ // Fields specific to app handle education
+ AppHandleEducation app_handle_education = 3;
+ }
+
+ message AppHandleEducation {
+ // Map that stores app launch count for corresponding package
+ map<string, int32> app_usage_stats = 1;
+ // Timestamp of when app_usage_stats was last cached
+ optional int64 app_usage_stats_last_update_timestamp_millis = 2;
+ }
+}
\ No newline at end of file
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
index 723a531..428cc91 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/PipTaskOrganizer.java
@@ -693,16 +693,6 @@
return;
}
- if (mSplitScreenOptional.isPresent()) {
- // If pip activity will reparent to origin task case and if the origin task still
- // under split root, apply exit split transaction to make it expand to fullscreen.
- SplitScreenController split = mSplitScreenOptional.get();
- if (split.isTaskInSplitScreen(mTaskInfo.lastParentTaskIdBeforePip)) {
- split.prepareExitSplitScreen(wct, split.getStageOfTask(
- mTaskInfo.lastParentTaskIdBeforePip),
- SplitScreenController.EXIT_REASON_APP_FINISHED);
- }
- }
mPipTransitionController.startExitTransition(TRANSIT_EXIT_PIP, wct, destinationBounds);
return;
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
index c189642..0d7f7f6 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip/phone/PipMenuView.java
@@ -34,11 +34,13 @@
import android.annotation.IntDef;
import android.annotation.NonNull;
import android.annotation.Nullable;
+import android.app.ActivityManager;
import android.app.PendingIntent;
import android.app.RemoteAction;
import android.content.ComponentName;
import android.content.Context;
import android.content.Intent;
+import android.content.pm.PackageManager;
import android.graphics.Color;
import android.graphics.Rect;
import android.graphics.drawable.Drawable;
@@ -151,6 +153,10 @@
// How long the shell will wait for the app to close the PiP if a custom action is set.
private final int mPipForceCloseDelay;
+ // Context for the currently active user. This may differ from the regular systemui Context
+ // in cases such as secondary users or HSUM.
+ private Context mContextForUser;
+
public PipMenuView(Context context, PhonePipMenuController controller,
ShellExecutor mainExecutor, Handler mainHandler,
PipUiEventLogger pipUiEventLogger) {
@@ -202,6 +208,7 @@
.getInteger(R.integer.config_pipExitAnimationDuration);
initAccessibility();
+ setContextForUser();
}
private void initAccessibility() {
@@ -476,7 +483,7 @@
actionView.setImageDrawable(null);
} else {
// TODO: Check if the action drawable has changed before we reload it
- action.getIcon().loadDrawableAsync(mContext, d -> {
+ action.getIcon().loadDrawableAsync(mContextForUser, d -> {
if (d != null) {
d.setTint(Color.WHITE);
actionView.setImageDrawable(d);
@@ -510,6 +517,33 @@
expandContainer.requestLayout();
}
+ /**
+ * Sets the Context for the current user. If the user is the same as systemui, then simply
+ * use systemui Context.
+ */
+ private void setContextForUser() {
+ int userId = ActivityManager.getCurrentUser();
+
+ if (mContext.getUserId() != userId) {
+ try {
+ mContextForUser = mContext.createPackageContextAsUser(mContext.getPackageName(),
+ Context.CONTEXT_RESTRICTED, new UserHandle(userId));
+ } catch (PackageManager.NameNotFoundException e) {
+ // Shouldn't happen, use systemui context as backup
+ ProtoLog.w(ShellProtoLogGroup.WM_SHELL_PICTURE_IN_PICTURE,
+ "%s: Failed to get context for user. Sysui userid=%d,"
+ + " current userid=%d, error=%s",
+ TAG,
+ mContext.getUserId(),
+ userId,
+ e);
+ mContextForUser = mContext;
+ }
+ } else {
+ mContextForUser = mContext;
+ }
+ }
+
private void notifyMenuStateChangeStart(int menuState, boolean resize, Runnable callback) {
mController.onMenuStateChangeStart(menuState, resize, callback);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
index 7774384..dc21f82 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipController.java
@@ -31,6 +31,8 @@
import android.os.Bundle;
import android.view.InsetsState;
import android.view.SurfaceControl;
+import android.window.DisplayAreaInfo;
+import android.window.WindowContainerTransaction;
import androidx.annotation.BinderThread;
import androidx.annotation.Nullable;
@@ -40,6 +42,7 @@
import com.android.internal.util.Preconditions;
import com.android.wm.shell.R;
import com.android.wm.shell.ShellTaskOrganizer;
+import com.android.wm.shell.common.DisplayChangeController;
import com.android.wm.shell.common.DisplayController;
import com.android.wm.shell.common.DisplayInsetsController;
import com.android.wm.shell.common.DisplayLayout;
@@ -71,7 +74,8 @@
*/
public class PipController implements ConfigurationChangeListener,
PipTransitionState.PipTransitionStateChangedListener,
- DisplayController.OnDisplaysChangedListener, RemoteCallable<PipController> {
+ DisplayController.OnDisplaysChangedListener,
+ DisplayChangeController.OnDisplayChangingListener, RemoteCallable<PipController> {
private static final String TAG = PipController.class.getSimpleName();
private static final String SWIPE_TO_PIP_APP_BOUNDS = "pip_app_bounds";
private static final String SWIPE_TO_PIP_OVERLAY = "swipe_to_pip_overlay";
@@ -197,11 +201,12 @@
mPipDisplayLayoutState.setDisplayLayout(layout);
mDisplayController.addDisplayWindowListener(this);
+ mDisplayController.addDisplayChangingController(this);
mDisplayInsetsController.addInsetsChangedListener(mPipDisplayLayoutState.getDisplayId(),
new DisplayInsetsController.OnInsetsChangedListener() {
@Override
public void insetsChanged(InsetsState insetsState) {
- onDisplayChanged(mDisplayController
+ setDisplayLayout(mDisplayController
.getDisplayLayout(mPipDisplayLayoutState.getDisplayId()));
}
});
@@ -264,11 +269,12 @@
@Override
public void onThemeChanged() {
- onDisplayChanged(new DisplayLayout(mContext, mContext.getDisplay()));
+ setDisplayLayout(new DisplayLayout(mContext, mContext.getDisplay()));
}
//
- // DisplayController.OnDisplaysChangedListener implementations
+ // DisplayController.OnDisplaysChangedListener and
+ // DisplayChangeController.OnDisplayChangingListener implementations
//
@Override
@@ -276,7 +282,7 @@
if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
- onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
}
@Override
@@ -284,10 +290,35 @@
if (displayId != mPipDisplayLayoutState.getDisplayId()) {
return;
}
- onDisplayChanged(mDisplayController.getDisplayLayout(displayId));
+ setDisplayLayout(mDisplayController.getDisplayLayout(displayId));
}
- private void onDisplayChanged(DisplayLayout layout) {
+ /**
+ * A callback for any observed transition that contains a display change in its
+ * {@link android.window.TransitionRequestInfo} with a non-zero rotation delta.
+ */
+ @Override
+ public void onDisplayChange(int displayId, int fromRotation, int toRotation,
+ @Nullable DisplayAreaInfo newDisplayAreaInfo, WindowContainerTransaction t) {
+ if (!mPipTransitionState.isInPip()) {
+ return;
+ }
+
+ // Calculate the snap fraction pre-rotation.
+ float snapFraction = mPipBoundsAlgorithm.getSnapFraction(mPipBoundsState.getBounds());
+
+ // Update the caches to reflect the new display layout and movement bounds.
+ mPipDisplayLayoutState.rotateTo(toRotation);
+ mPipTouchHandler.updateMovementBounds();
+
+ // The policy is to keep PiP width, height and snap fraction invariant.
+ Rect toBounds = mPipBoundsState.getBounds();
+ mPipBoundsAlgorithm.applySnapFraction(toBounds, snapFraction);
+ mPipBoundsState.setBounds(toBounds);
+ t.setBounds(mPipTransitionState.mPipTaskToken, toBounds);
+ }
+
+ private void setDisplayLayout(DisplayLayout layout) {
mPipDisplayLayoutState.setDisplayLayout(layout);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
index d7c225b..d75fa00 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/pip2/phone/PipTouchHandler.java
@@ -1081,7 +1081,7 @@
* Updates the current movement bounds based on whether the menu is currently visible and
* resized.
*/
- private void updateMovementBounds() {
+ void updateMovementBounds() {
Rect insetBounds = new Rect();
mPipBoundsAlgorithm.getInsetBounds(insetBounds);
mPipBoundsAlgorithm.getMovementBounds(mPipBoundsState.getBounds(),
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
index 497c3f7..f739d65 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/protolog/ShellProtoLogGroup.java
@@ -61,6 +61,8 @@
Consts.TAG_WM_SHELL),
WM_SHELL_BUBBLES(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
"Bubbles"),
+ WM_SHELL_COMPAT_UI(Consts.ENABLE_DEBUG, Consts.ENABLE_LOG_TO_PROTO_DEBUG, false,
+ Consts.TAG_WM_COMPAT_UI),
TEST_GROUP(true, true, false, "WindowManagerShellProtoLogTest");
private final boolean mEnabled;
@@ -128,6 +130,7 @@
private static final String TAG_WM_STARTING_WINDOW = "ShellStartingWindow";
private static final String TAG_WM_SPLIT_SCREEN = "ShellSplitScreen";
private static final String TAG_WM_DESKTOP_MODE = "ShellDesktopMode";
+ private static final String TAG_WM_COMPAT_UI = "CompatUi";
private static final boolean ENABLE_DEBUG = true;
private static final boolean ENABLE_LOG_TO_PROTO_DEBUG = true;
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
index 48d17ec6..c11a112 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/SplitScreenTransitions.java
@@ -439,9 +439,9 @@
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "setResizeTransition: hasPendingResize=%b",
mPendingResize != null);
if (mPendingResize != null) {
+ mPendingResize.cancel(null);
mainDecor.cancelRunningAnimations();
sideDecor.cancelRunningAnimations();
- mPendingResize.cancel(null);
mAnimations.clear();
onFinish(null /* wct */);
}
diff --git a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
index 87dc16a..9bf5159 100644
--- a/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
+++ b/libs/WindowManager/Shell/src/com/android/wm/shell/splitscreen/StageCoordinator.java
@@ -2243,6 +2243,7 @@
final @WindowManager.TransitionType int type = request.getType();
final boolean isOpening = isOpeningType(type);
final boolean inFullscreen = triggerTask.getWindowingMode() == WINDOWING_MODE_FULLSCREEN;
+ final StageTaskListener stage = getStageOfTask(triggerTask);
if (isOpening && inFullscreen) {
// One task is opening into fullscreen mode, remove the corresponding split record.
@@ -2258,7 +2259,6 @@
+ " sideChildren=%d", triggerTask.taskId, transitTypeToString(type),
mMainStage.getChildCount(), mSideStage.getChildCount());
out = new WindowContainerTransaction();
- final StageTaskListener stage = getStageOfTask(triggerTask);
if (stage != null) {
if (isClosingType(type) && stage.getChildCount() == 1) {
// Dismiss split if the last task in one of the stages is going away
@@ -2331,16 +2331,8 @@
// Don't intercept the transition if we are not handling it as a part of one of the
// cases above and it is not already visible
return null;
- } else {
- if (triggerTask.parentTaskId == mMainStage.mRootTaskInfo.taskId
- || triggerTask.parentTaskId == mSideStage.mRootTaskInfo.taskId) {
- ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d "
- + "restoring to split", request.getDebugId());
- out = new WindowContainerTransaction();
- mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
- TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */);
- }
- if (isOpening && getStageOfTask(triggerTask) != null) {
+ } else if (stage != null) {
+ if (isOpening) {
ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d enter split",
request.getDebugId());
// One task is appearing into split, prepare to enter split screen.
@@ -2348,9 +2340,15 @@
prepareEnterSplitScreen(out);
mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
TRANSIT_SPLIT_SCREEN_PAIR_OPEN, !mIsDropEntering);
+ return out;
}
- return out;
+ ProtoLog.d(WM_SHELL_SPLIT_SCREEN, "handleRequest: transition=%d "
+ + "restoring to split", request.getDebugId());
+ out = new WindowContainerTransaction();
+ mSplitTransitions.setEnterTransition(transition, request.getRemoteTransition(),
+ TRANSIT_SPLIT_SCREEN_OPEN_TO_SIDE, false /* resizeAnim */);
}
+ return out;
}
/**
diff --git a/libs/WindowManager/Shell/tests/unittest/Android.bp b/libs/WindowManager/Shell/tests/unittest/Android.bp
index a040865..4d761e1 100644
--- a/libs/WindowManager/Shell/tests/unittest/Android.bp
+++ b/libs/WindowManager/Shell/tests/unittest/Android.bp
@@ -44,6 +44,8 @@
"androidx.test.runner",
"androidx.test.rules",
"androidx.test.ext.junit",
+ "androidx.datastore_datastore",
+ "kotlinx_coroutines_test",
"androidx.dynamicanimation_dynamicanimation",
"dagger2",
"frameworks-base-testutils",
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
new file mode 100644
index 0000000..4d40738
--- /dev/null
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/desktopmode/education/AppHandleEducationDatastoreRepositoryTest.kt
@@ -0,0 +1,125 @@
+/*
+ * 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.wm.shell.desktopmode.education
+
+import android.content.Context
+import android.testing.AndroidTestingRunner
+import androidx.datastore.core.DataStore
+import androidx.datastore.core.DataStoreFactory
+import androidx.datastore.dataStoreFile
+import androidx.test.core.app.ApplicationProvider
+import androidx.test.filters.SmallTest
+import androidx.test.platform.app.InstrumentationRegistry
+import com.android.wm.shell.desktopmode.education.data.AppHandleEducationDatastoreRepository
+import com.android.wm.shell.desktopmode.education.data.WindowingEducationProto
+import com.google.common.truth.Truth.assertThat
+import java.io.File
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.Dispatchers
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.SupervisorJob
+import kotlinx.coroutines.cancel
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.runTest
+import kotlinx.coroutines.test.setMain
+import org.junit.After
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidTestingRunner::class)
+@ExperimentalCoroutinesApi
+class AppHandleEducationDatastoreRepositoryTest {
+ private val testContext: Context = InstrumentationRegistry.getInstrumentation().targetContext
+ private lateinit var testDatastore: DataStore<WindowingEducationProto>
+ private lateinit var datastoreRepository: AppHandleEducationDatastoreRepository
+ private lateinit var datastoreScope: CoroutineScope
+
+ @Before
+ fun setUp() {
+ Dispatchers.setMain(StandardTestDispatcher())
+ datastoreScope = CoroutineScope(Dispatchers.Unconfined + SupervisorJob())
+ testDatastore =
+ DataStoreFactory.create(
+ serializer =
+ AppHandleEducationDatastoreRepository.Companion.WindowingEducationProtoSerializer,
+ scope = datastoreScope) {
+ testContext.dataStoreFile(APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE)
+ }
+ datastoreRepository = AppHandleEducationDatastoreRepository(testDatastore)
+ }
+
+ @After
+ fun tearDown() {
+ File(ApplicationProvider.getApplicationContext<Context>().filesDir, "datastore")
+ .deleteRecursively()
+
+ datastoreScope.cancel()
+ }
+
+ @Test
+ fun getWindowingEducationProto_returnsCorrectProto() =
+ runTest(StandardTestDispatcher()) {
+ val windowingEducationProto =
+ createWindowingEducationProto(
+ educationViewedTimestampMillis = 123L,
+ featureUsedTimestampMillis = 124L,
+ appUsageStats = mapOf(GMAIL_PACKAGE_NAME to 2),
+ appUsageStatsLastUpdateTimestampMillis = 125L)
+ testDatastore.updateData { windowingEducationProto }
+
+ val resultProto = datastoreRepository.windowingEducationProto()
+
+ assertThat(resultProto).isEqualTo(windowingEducationProto)
+ }
+
+ private fun createWindowingEducationProto(
+ educationViewedTimestampMillis: Long? = null,
+ featureUsedTimestampMillis: Long? = null,
+ appUsageStats: Map<String, Int>? = null,
+ appUsageStatsLastUpdateTimestampMillis: Long? = null
+ ): WindowingEducationProto =
+ WindowingEducationProto.newBuilder()
+ .apply {
+ if (educationViewedTimestampMillis != null)
+ setEducationViewedTimestampMillis(educationViewedTimestampMillis)
+ if (featureUsedTimestampMillis != null)
+ setFeatureUsedTimestampMillis(featureUsedTimestampMillis)
+ setAppHandleEducation(
+ createAppHandleEducationProto(
+ appUsageStats, appUsageStatsLastUpdateTimestampMillis))
+ }
+ .build()
+
+ private fun createAppHandleEducationProto(
+ appUsageStats: Map<String, Int>? = null,
+ appUsageStatsLastUpdateTimestampMillis: Long? = null
+ ): WindowingEducationProto.AppHandleEducation =
+ WindowingEducationProto.AppHandleEducation.newBuilder()
+ .apply {
+ if (appUsageStats != null) putAllAppUsageStats(appUsageStats)
+ if (appUsageStatsLastUpdateTimestampMillis != null)
+ setAppUsageStatsLastUpdateTimestampMillis(appUsageStatsLastUpdateTimestampMillis)
+ }
+ .build()
+
+ companion object {
+ private const val GMAIL_PACKAGE_NAME = "com.google.android.gm"
+ private const val APP_HANDLE_EDUCATION_DATASTORE_TEST_FILE = "app_handle_education_test.pb"
+ }
+}
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
index dd19d76..571bdd4 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/shared/desktopmode/DesktopModeFlagsTest.kt
@@ -33,7 +33,7 @@
import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.ToggleOverride.OVERRIDE_UNSET
import com.android.wm.shell.shared.desktopmode.DesktopModeFlags.WALLPAPER_ACTIVITY
import com.google.common.truth.Truth.assertThat
-import org.junit.Before
+import org.junit.After
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
@@ -49,9 +49,9 @@
@JvmField @Rule val setFlagsRule = SetFlagsRule()
- @Before
- fun setUp() {
- resetCache()
+ @After
+ fun tearDown() {
+ resetToggleOverrideCache()
}
// TODO(b/348193756): Add tests
@@ -338,7 +338,7 @@
}
}
- private fun resetCache() {
+ private fun resetToggleOverrideCache() {
val cachedToggleOverride =
DesktopModeFlags::class.java.getDeclaredField("cachedToggleOverride")
cachedToggleOverride.isAccessible = true
diff --git a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
index 6d68797..fa905e2 100644
--- a/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
+++ b/libs/WindowManager/Shell/tests/unittest/src/com/android/wm/shell/windowdecor/DesktopModeWindowDecorViewModelTests.kt
@@ -57,6 +57,7 @@
import android.view.View
import android.view.WindowInsets.Type.statusBars
import android.window.WindowContainerTransaction
+import android.window.WindowContainerTransaction.HierarchyOp
import androidx.test.filters.SmallTest
import com.android.dx.mockito.inline.extended.ExtendedMockito.doReturn
import com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession
@@ -356,6 +357,36 @@
}
@Test
+ fun testCloseButtonInFreeform() {
+ val task = createTask(windowingMode = WINDOWING_MODE_FREEFORM)
+ val windowDecor = setUpMockDecorationForTask(task)
+
+ onTaskOpening(task)
+ val onClickListenerCaptor = argumentCaptor<View.OnClickListener>()
+ verify(windowDecor).setCaptionListeners(
+ onClickListenerCaptor.capture(), any(), any(), any())
+
+ val onClickListener = onClickListenerCaptor.firstValue
+ val view = mock(View::class.java)
+ whenever(view.id).thenReturn(R.id.close_window)
+
+ val freeformTaskTransitionStarter = mock(FreeformTaskTransitionStarter::class.java)
+ desktopModeWindowDecorViewModel
+ .setFreeformTaskTransitionStarter(freeformTaskTransitionStarter)
+
+ onClickListener.onClick(view)
+
+ val transactionCaptor = argumentCaptor<WindowContainerTransaction>()
+ verify(freeformTaskTransitionStarter).startRemoveTransition(transactionCaptor.capture())
+ val wct = transactionCaptor.firstValue
+
+ assertEquals(1, wct.getHierarchyOps().size)
+ assertEquals(HierarchyOp.HIERARCHY_OP_TYPE_REMOVE_TASK,
+ wct.getHierarchyOps().get(0).getType())
+ assertEquals(task.token.asBinder(), wct.getHierarchyOps().get(0).getContainer())
+ }
+
+ @Test
@EnableFlags(Flags.FLAG_ENABLE_DESKTOP_WINDOWING_MODALS_POLICY)
fun testDecorationIsCreatedForTopTranslucentActivitiesWithStyleFloating() {
val task = createTask(windowingMode = WINDOWING_MODE_FULLSCREEN, focused = true).apply {
diff --git a/libs/androidfw/BigBuffer.cpp b/libs/androidfw/BigBuffer.cpp
index bedfc49..43b56c3 100644
--- a/libs/androidfw/BigBuffer.cpp
+++ b/libs/androidfw/BigBuffer.cpp
@@ -17,8 +17,8 @@
#include <androidfw/BigBuffer.h>
#include <algorithm>
+#include <iterator>
#include <memory>
-#include <vector>
#include "android-base/logging.h"
@@ -78,10 +78,27 @@
std::string BigBuffer::to_string() const {
std::string result;
+ result.reserve(size_);
for (const Block& block : blocks_) {
result.append(block.buffer.get(), block.buffer.get() + block.size);
}
return result;
}
+void BigBuffer::AppendBuffer(BigBuffer&& buffer) {
+ std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_));
+ size_ += buffer.size_;
+ buffer.blocks_.clear();
+ buffer.size_ = 0;
+}
+
+void BigBuffer::BackUp(size_t count) {
+ Block& block = blocks_.back();
+ block.size -= count;
+ size_ -= count;
+ // BigBuffer is supposed to always give zeroed memory, but backing up usually means
+ // something has been already written into the block. Erase it.
+ std::fill_n(block.buffer.get() + block.size, count, 0);
+}
+
} // namespace android
diff --git a/libs/androidfw/include/androidfw/BigBuffer.h b/libs/androidfw/include/androidfw/BigBuffer.h
index b99a4ed..c4cd7c5 100644
--- a/libs/androidfw/include/androidfw/BigBuffer.h
+++ b/libs/androidfw/include/androidfw/BigBuffer.h
@@ -14,13 +14,12 @@
* limitations under the License.
*/
-#ifndef _ANDROID_BIG_BUFFER_H
-#define _ANDROID_BIG_BUFFER_H
+#pragma once
-#include <cstring>
#include <memory>
#include <string>
#include <type_traits>
+#include <utility>
#include <vector>
#include "android-base/logging.h"
@@ -150,24 +149,11 @@
template <typename T>
inline T* BigBuffer::NextBlock(size_t count) {
- static_assert(std::is_standard_layout<T>::value, "T must be standard_layout type");
+ static_assert(std::is_standard_layout_v<T>, "T must be standard_layout type");
CHECK(count != 0);
return reinterpret_cast<T*>(NextBlockImpl(sizeof(T) * count));
}
-inline void BigBuffer::BackUp(size_t count) {
- Block& block = blocks_.back();
- block.size -= count;
- size_ -= count;
-}
-
-inline void BigBuffer::AppendBuffer(BigBuffer&& buffer) {
- std::move(buffer.blocks_.begin(), buffer.blocks_.end(), std::back_inserter(blocks_));
- size_ += buffer.size_;
- buffer.blocks_.clear();
- buffer.size_ = 0;
-}
-
inline void BigBuffer::Pad(size_t bytes) {
NextBlock<char>(bytes);
}
@@ -188,5 +174,3 @@
}
} // namespace android
-
-#endif // _ANDROID_BIG_BUFFER_H
diff --git a/libs/androidfw/tests/BigBuffer_test.cpp b/libs/androidfw/tests/BigBuffer_test.cpp
index 382d21e..7e38f17 100644
--- a/libs/androidfw/tests/BigBuffer_test.cpp
+++ b/libs/androidfw/tests/BigBuffer_test.cpp
@@ -98,4 +98,20 @@
ASSERT_EQ(8u, buffer.size());
}
+TEST(BigBufferTest, BackUpZeroed) {
+ BigBuffer buffer(16);
+
+ auto block = buffer.NextBlock<char>(2);
+ ASSERT_TRUE(block != nullptr);
+ ASSERT_EQ(2u, buffer.size());
+ block[0] = 0x01;
+ block[1] = 0x02;
+ buffer.BackUp(1);
+ ASSERT_EQ(1u, buffer.size());
+ auto new_block = buffer.NextBlock<char>(1);
+ ASSERT_TRUE(new_block != nullptr);
+ ASSERT_EQ(2u, buffer.size());
+ ASSERT_EQ(0, *new_block);
+}
+
} // namespace android
diff --git a/libs/hwui/Properties.cpp b/libs/hwui/Properties.cpp
index d184f64..1217b47 100644
--- a/libs/hwui/Properties.cpp
+++ b/libs/hwui/Properties.cpp
@@ -42,6 +42,9 @@
constexpr bool initialize_gl_always() {
return false;
}
+constexpr bool resample_gainmap_regions() {
+ return false;
+}
} // namespace hwui_flags
#endif
@@ -100,6 +103,7 @@
bool Properties::clipSurfaceViews = false;
bool Properties::hdr10bitPlus = false;
+bool Properties::resampleGainmapRegions = false;
int Properties::timeoutMultiplier = 1;
@@ -175,6 +179,8 @@
clipSurfaceViews =
base::GetBoolProperty("debug.hwui.clip_surfaceviews", hwui_flags::clip_surfaceviews());
hdr10bitPlus = hwui_flags::hdr_10bit_plus();
+ resampleGainmapRegions = base::GetBoolProperty("debug.hwui.resample_gainmap_regions",
+ hwui_flags::resample_gainmap_regions());
timeoutMultiplier = android::base::GetIntProperty("ro.hw_timeout_multiplier", 1);
diff --git a/libs/hwui/Properties.h b/libs/hwui/Properties.h
index e264642..73e80ce 100644
--- a/libs/hwui/Properties.h
+++ b/libs/hwui/Properties.h
@@ -342,6 +342,7 @@
static bool clipSurfaceViews;
static bool hdr10bitPlus;
+ static bool resampleGainmapRegions;
static int timeoutMultiplier;
diff --git a/libs/hwui/aconfig/hwui_flags.aconfig b/libs/hwui/aconfig/hwui_flags.aconfig
index cd3ae53..13c0b00 100644
--- a/libs/hwui/aconfig/hwui_flags.aconfig
+++ b/libs/hwui/aconfig/hwui_flags.aconfig
@@ -97,3 +97,13 @@
description: "Initialize GL even when HWUI is set to use Vulkan. This improves app startup time for apps using GL."
bug: "335172671"
}
+
+flag {
+ name: "resample_gainmap_regions"
+ namespace: "core_graphics"
+ description: "Resample gainmaps when decoding regions, to improve visual quality"
+ bug: "352847821"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/libs/hwui/jni/BitmapRegionDecoder.cpp b/libs/hwui/jni/BitmapRegionDecoder.cpp
index ea5c144..6a65b82 100644
--- a/libs/hwui/jni/BitmapRegionDecoder.cpp
+++ b/libs/hwui/jni/BitmapRegionDecoder.cpp
@@ -87,8 +87,17 @@
requireUnpremul, prefColorSpace);
}
- bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, int outWidth, int outHeight,
- const SkIRect& desiredSubset, int sampleSize, bool requireUnpremul) {
+ // Decodes the gainmap region. If decoding succeeded, returns true and
+ // populate outGainmap with the decoded gainmap. Otherwise, returns false.
+ //
+ // Note that the desiredSubset is the logical region within the source
+ // gainmap that we want to decode. This is used for scaling into the final
+ // bitmap, since we do not want to include portions of the gainmap outside
+ // of this region. desiredSubset is also _not_ guaranteed to be
+ // pixel-aligned, so it's not possible to simply resize the resulting
+ // bitmap to accomplish this.
+ bool decodeGainmapRegion(sp<uirenderer::Gainmap>* outGainmap, SkISize bitmapDimensions,
+ const SkRect& desiredSubset, int sampleSize, bool requireUnpremul) {
SkColorType decodeColorType = mGainmapBRD->computeOutputColorType(kN32_SkColorType);
sk_sp<SkColorSpace> decodeColorSpace =
mGainmapBRD->computeOutputColorSpace(decodeColorType, nullptr);
@@ -107,13 +116,30 @@
// allocation type. RecyclingClippingPixelAllocator will populate this with the
// actual alpha type in either allocPixelRef() or copyIfNecessary()
sk_sp<Bitmap> nativeBitmap = Bitmap::allocateHeapBitmap(SkImageInfo::Make(
- outWidth, outHeight, decodeColorType, kPremul_SkAlphaType, decodeColorSpace));
+ bitmapDimensions, decodeColorType, kPremul_SkAlphaType, decodeColorSpace));
if (!nativeBitmap) {
ALOGE("OOM allocating Bitmap for Gainmap");
return false;
}
- RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false);
- if (!mGainmapBRD->decodeRegion(&bm, &allocator, desiredSubset, sampleSize, decodeColorType,
+
+ // Round out the subset so that we decode a slightly larger region, in
+ // case the subset has fractional components.
+ SkIRect roundedSubset = desiredSubset.roundOut();
+
+ // Map the desired subset to the space of the decoded gainmap. The
+ // subset is repositioned relative to the resulting bitmap, and then
+ // scaled to respect the sampleSize.
+ // This assumes that the subset will not be modified by the decoder, which is true
+ // for existing gainmap formats.
+ SkRect logicalSubset = desiredSubset.makeOffset(-std::floorf(desiredSubset.left()),
+ -std::floorf(desiredSubset.top()));
+ logicalSubset.fLeft /= sampleSize;
+ logicalSubset.fTop /= sampleSize;
+ logicalSubset.fRight /= sampleSize;
+ logicalSubset.fBottom /= sampleSize;
+
+ RecyclingClippingPixelAllocator allocator(nativeBitmap.get(), false, logicalSubset);
+ if (!mGainmapBRD->decodeRegion(&bm, &allocator, roundedSubset, sampleSize, decodeColorType,
requireUnpremul, decodeColorSpace)) {
ALOGE("Error decoding Gainmap region");
return false;
@@ -130,16 +156,31 @@
return true;
}
- SkIRect calculateGainmapRegion(const SkIRect& mainImageRegion, int* inOutWidth,
- int* inOutHeight) {
+ struct Projection {
+ SkRect srcRect;
+ SkISize destSize;
+ };
+ Projection calculateGainmapRegion(const SkIRect& mainImageRegion, SkISize dimensions) {
const float scaleX = ((float)mGainmapBRD->width()) / mMainImageBRD->width();
const float scaleY = ((float)mGainmapBRD->height()) / mMainImageBRD->height();
- *inOutWidth *= scaleX;
- *inOutHeight *= scaleY;
- // TODO: Account for rounding error?
- return SkIRect::MakeLTRB(mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
- mainImageRegion.right() * scaleX,
- mainImageRegion.bottom() * scaleY);
+
+ if (uirenderer::Properties::resampleGainmapRegions) {
+ const auto srcRect = SkRect::MakeLTRB(
+ mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+ mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY);
+ // Request a slightly larger destination size so that the gainmap
+ // subset we want fits entirely in this size.
+ const auto destSize = SkISize::Make(std::ceil(dimensions.width() * scaleX),
+ std::ceil(dimensions.height() * scaleY));
+ return Projection{.srcRect = srcRect, .destSize = destSize};
+ } else {
+ const auto srcRect = SkRect::Make(SkIRect::MakeLTRB(
+ mainImageRegion.left() * scaleX, mainImageRegion.top() * scaleY,
+ mainImageRegion.right() * scaleX, mainImageRegion.bottom() * scaleY));
+ const auto destSize =
+ SkISize::Make(dimensions.width() * scaleX, dimensions.height() * scaleY);
+ return Projection{.srcRect = srcRect, .destSize = destSize};
+ }
}
bool hasGainmap() { return mGainmapBRD != nullptr; }
@@ -327,16 +368,16 @@
sp<uirenderer::Gainmap> gainmap;
bool hasGainmap = brd->hasGainmap();
if (hasGainmap) {
- int gainmapWidth = bitmap.width();
- int gainmapHeight = bitmap.height();
+ SkISize gainmapDims = SkISize::Make(bitmap.width(), bitmap.height());
if (javaBitmap) {
// If we are recycling we must match the inBitmap's relative dimensions
- gainmapWidth = recycledBitmap->width();
- gainmapHeight = recycledBitmap->height();
+ gainmapDims.fWidth = recycledBitmap->width();
+ gainmapDims.fHeight = recycledBitmap->height();
}
- SkIRect gainmapSubset = brd->calculateGainmapRegion(subset, &gainmapWidth, &gainmapHeight);
- if (!brd->decodeGainmapRegion(&gainmap, gainmapWidth, gainmapHeight, gainmapSubset,
- sampleSize, requireUnpremul)) {
+ BitmapRegionDecoderWrapper::Projection gainmapProjection =
+ brd->calculateGainmapRegion(subset, gainmapDims);
+ if (!brd->decodeGainmapRegion(&gainmap, gainmapProjection.destSize,
+ gainmapProjection.srcRect, sampleSize, requireUnpremul)) {
// If there is an error decoding Gainmap - we don't fail. We just don't provide Gainmap
hasGainmap = false;
}
diff --git a/libs/hwui/jni/Graphics.cpp b/libs/hwui/jni/Graphics.cpp
index a88139d..258bf91 100644
--- a/libs/hwui/jni/Graphics.cpp
+++ b/libs/hwui/jni/Graphics.cpp
@@ -1,12 +1,14 @@
#include <assert.h>
+#include <cutils/ashmem.h>
+#include <hwui/Canvas.h>
+#include <log/log.h>
+#include <nativehelper/JNIHelp.h>
#include <unistd.h>
-#include "jni.h"
-#include <nativehelper/JNIHelp.h>
#include "GraphicsJNI.h"
-
#include "SkBitmap.h"
#include "SkCanvas.h"
+#include "SkColor.h"
#include "SkColorSpace.h"
#include "SkFontMetrics.h"
#include "SkImageInfo.h"
@@ -14,10 +16,9 @@
#include "SkPoint.h"
#include "SkRect.h"
#include "SkRegion.h"
+#include "SkSamplingOptions.h"
#include "SkTypes.h"
-#include <cutils/ashmem.h>
-#include <hwui/Canvas.h>
-#include <log/log.h>
+#include "jni.h"
using namespace android;
@@ -630,13 +631,15 @@
////////////////////////////////////////////////////////////////////////////////
-RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
- bool mustMatchColorType)
+RecyclingClippingPixelAllocator::RecyclingClippingPixelAllocator(
+ android::Bitmap* recycledBitmap, bool mustMatchColorType,
+ std::optional<SkRect> desiredSubset)
: mRecycledBitmap(recycledBitmap)
, mRecycledBytes(recycledBitmap ? recycledBitmap->getAllocationByteCount() : 0)
, mSkiaBitmap(nullptr)
, mNeedsCopy(false)
- , mMustMatchColorType(mustMatchColorType) {}
+ , mMustMatchColorType(mustMatchColorType)
+ , mDesiredSubset(getSourceBoundsForUpsample(desiredSubset)) {}
RecyclingClippingPixelAllocator::~RecyclingClippingPixelAllocator() {}
@@ -668,7 +671,8 @@
const SkImageInfo maxInfo = bitmap->info().makeWH(maxWidth, maxHeight);
const size_t rowBytes = maxInfo.minRowBytes();
const size_t bytesNeeded = maxInfo.computeByteSize(rowBytes);
- if (bytesNeeded <= mRecycledBytes) {
+
+ if (!mDesiredSubset && bytesNeeded <= mRecycledBytes) {
// Here we take advantage of reconfigure() to reset the rowBytes
// of mRecycledBitmap. It is very important that we pass in
// mRecycledBitmap->info() for the SkImageInfo. According to the
@@ -712,20 +716,31 @@
if (mNeedsCopy) {
mRecycledBitmap->ref();
android::Bitmap* recycledPixels = mRecycledBitmap;
- void* dst = recycledPixels->pixels();
- const size_t dstRowBytes = mRecycledBitmap->rowBytes();
- const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
- mSkiaBitmap->info().minRowBytes());
- const int rowsToCopy = std::min(mRecycledBitmap->info().height(),
- mSkiaBitmap->info().height());
- for (int y = 0; y < rowsToCopy; y++) {
- memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
- // Cast to bytes in order to apply the dstRowBytes offset correctly.
- dst = reinterpret_cast<void*>(
- reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
+ if (mDesiredSubset) {
+ recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+ recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
+
+ auto canvas = SkCanvas(recycledPixels->getSkBitmap());
+ SkRect destination = SkRect::Make(recycledPixels->info().bounds());
+ destination.intersect(SkRect::Make(mSkiaBitmap->info().bounds()));
+ canvas.drawImageRect(mSkiaBitmap->asImage(), *mDesiredSubset, destination,
+ SkSamplingOptions(SkFilterMode::kLinear), nullptr,
+ SkCanvas::kFast_SrcRectConstraint);
+ } else {
+ void* dst = recycledPixels->pixels();
+ const size_t dstRowBytes = mRecycledBitmap->rowBytes();
+ const size_t bytesToCopy = std::min(mRecycledBitmap->info().minRowBytes(),
+ mSkiaBitmap->info().minRowBytes());
+ const int rowsToCopy =
+ std::min(mRecycledBitmap->info().height(), mSkiaBitmap->info().height());
+ for (int y = 0; y < rowsToCopy; y++) {
+ memcpy(dst, mSkiaBitmap->getAddr(0, y), bytesToCopy);
+ // Cast to bytes in order to apply the dstRowBytes offset correctly.
+ dst = reinterpret_cast<void*>(reinterpret_cast<uint8_t*>(dst) + dstRowBytes);
+ }
+ recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
+ recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
}
- recycledPixels->setAlphaType(mSkiaBitmap->alphaType());
- recycledPixels->setColorSpace(mSkiaBitmap->refColorSpace());
recycledPixels->notifyPixelsChanged();
recycledPixels->unref();
}
@@ -733,6 +748,20 @@
mSkiaBitmap = nullptr;
}
+std::optional<SkRect> RecyclingClippingPixelAllocator::getSourceBoundsForUpsample(
+ std::optional<SkRect> subset) {
+ if (!uirenderer::Properties::resampleGainmapRegions || !subset || subset->isEmpty()) {
+ return std::nullopt;
+ }
+
+ if (subset->left() == floor(subset->left()) && subset->top() == floor(subset->top()) &&
+ subset->right() == floor(subset->right()) && subset->bottom() == floor(subset->bottom())) {
+ return std::nullopt;
+ }
+
+ return subset;
+}
+
////////////////////////////////////////////////////////////////////////////////
AshmemPixelAllocator::AshmemPixelAllocator(JNIEnv *env) {
diff --git a/libs/hwui/jni/GraphicsJNI.h b/libs/hwui/jni/GraphicsJNI.h
index b0a1074..4b08f8d 100644
--- a/libs/hwui/jni/GraphicsJNI.h
+++ b/libs/hwui/jni/GraphicsJNI.h
@@ -216,8 +216,8 @@
*/
class RecyclingClippingPixelAllocator : public android::skia::BRDAllocator {
public:
- RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap,
- bool mustMatchColorType = true);
+ RecyclingClippingPixelAllocator(android::Bitmap* recycledBitmap, bool mustMatchColorType = true,
+ std::optional<SkRect> desiredSubset = std::nullopt);
~RecyclingClippingPixelAllocator();
@@ -241,11 +241,24 @@
SkCodec::ZeroInitialized zeroInit() const override { return SkCodec::kNo_ZeroInitialized; }
private:
+ /**
+ * Optionally returns a subset rectangle that we need to upsample from.
+ * E.g., a gainmap subset may be decoded in a slightly larger rectangle
+ * than is needed (in order to correctly preserve gainmap alignment when
+ * rendering at display time), so we need to re-sample the "intended"
+ * gainmap back up to the bitmap dimensions.
+ *
+ * If we don't need to upsample from a subregion, then returns an empty
+ * optional
+ */
+ static std::optional<SkRect> getSourceBoundsForUpsample(std::optional<SkRect> subset);
+
android::Bitmap* mRecycledBitmap;
const size_t mRecycledBytes;
SkBitmap* mSkiaBitmap;
bool mNeedsCopy;
const bool mMustMatchColorType;
+ const std::optional<SkRect> mDesiredSubset;
};
class AshmemPixelAllocator : public SkBitmap::Allocator {
diff --git a/nfc/java/android/nfc/flags.aconfig b/nfc/java/android/nfc/flags.aconfig
index 95945d7..f16fa80 100644
--- a/nfc/java/android/nfc/flags.aconfig
+++ b/nfc/java/android/nfc/flags.aconfig
@@ -118,3 +118,10 @@
bug: "321310044"
}
+flag {
+ name: "nfc_action_manage_services_settings"
+ is_exported: true
+ namespace: "nfc"
+ description: "Add Settings.ACTION_MANAGE_OTHER_NFC_SERVICES_SETTINGS"
+ bug: "358129872"
+}
diff --git a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
index e2ab316..c61a2ac 100644
--- a/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
+++ b/packages/PackageInstaller/src/com/android/packageinstaller/v2/ui/InstallLaunch.kt
@@ -16,7 +16,9 @@
package com.android.packageinstaller.v2.ui
-import android.app.Activity
+import android.app.Activity.RESULT_CANCELED
+import android.app.Activity.RESULT_FIRST_USER
+import android.app.Activity.RESULT_OK
import android.app.AppOpsManager
import android.content.ActivityNotFoundException
import android.content.Intent
@@ -135,7 +137,7 @@
}
InstallAborted.ABORT_REASON_POLICY -> showPolicyRestrictionDialog(aborted)
- else -> setResult(Activity.RESULT_CANCELED, null, true)
+ else -> setResult(RESULT_CANCELED, null, true)
}
}
@@ -169,7 +171,7 @@
val success = installStage as InstallSuccess
if (success.shouldReturnResult) {
val successIntent = success.resultIntent
- setResult(Activity.RESULT_OK, successIntent, true)
+ setResult(RESULT_OK, successIntent, true)
} else {
val successDialog = InstallSuccessFragment(success)
showDialogInner(successDialog)
@@ -180,7 +182,7 @@
val failed = installStage as InstallFailed
if (failed.shouldReturnResult) {
val failureIntent = failed.resultIntent
- setResult(Activity.RESULT_FIRST_USER, failureIntent, true)
+ setResult(RESULT_FIRST_USER, failureIntent, true)
} else {
val failureDialog = InstallFailedFragment(failed)
showDialogInner(failureDialog)
@@ -219,7 +221,7 @@
shouldFinish = blockedByPolicyDialog == null
showDialogInner(blockedByPolicyDialog)
}
- setResult(Activity.RESULT_CANCELED, null, shouldFinish)
+ setResult(RESULT_CANCELED, null, shouldFinish)
}
/**
@@ -257,6 +259,10 @@
fun setResult(resultCode: Int, data: Intent?, shouldFinish: Boolean) {
super.setResult(resultCode, data)
+ if (resultCode != RESULT_OK) {
+ // Let callers know that the install was cancelled
+ installViewModel!!.cleanupInstall()
+ }
if (shouldFinish) {
finish()
}
@@ -282,7 +288,7 @@
if (stageCode == InstallStage.STAGE_USER_ACTION_REQUIRED) {
installViewModel!!.cleanupInstall()
}
- setResult(Activity.RESULT_CANCELED, null, true)
+ setResult(RESULT_CANCELED, null, true)
}
override fun onNegativeResponse(resultCode: Int, data: Intent?) {
@@ -318,7 +324,7 @@
if (localLogv) {
Log.d(LOG_TAG, "Opening $intent")
}
- setResult(Activity.RESULT_OK, intent, true)
+ setResult(RESULT_OK, intent, true)
if (intent != null && intent.hasCategory(Intent.CATEGORY_LAUNCHER)) {
startActivity(intent)
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
index 7124ed2..c6eb9fd 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/CsipDeviceManager.java
@@ -178,7 +178,7 @@
}
log("updateRelationshipOfGroupDevices: mCachedDevices list =" + mCachedDevices.toString());
- // Get the preferred main device by getPreferredMainDeviceWithoutConectionState
+ // Get the preferred main device by getPreferredMainDeviceWithoutConnectionState
List<CachedBluetoothDevice> groupDevicesList = getGroupDevicesFromAllOfDevicesList(groupId);
CachedBluetoothDevice preferredMainDevice =
getPreferredMainDevice(groupId, groupDevicesList);
@@ -373,6 +373,7 @@
preferredMainDevice.addMemberDevice(deviceItem);
mCachedDevices.remove(deviceItem);
mBtManager.getEventManager().dispatchDeviceRemoved(deviceItem);
+ preferredMainDevice.refresh();
hasChanged = true;
}
}
diff --git a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
index 27fcdbe..26905b1 100644
--- a/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
+++ b/packages/SettingsLib/src/com/android/settingslib/bluetooth/LocalBluetoothLeBroadcast.java
@@ -80,6 +80,7 @@
"com.android.settings.action.BLUETOOTH_LE_AUDIO_SHARING_STATE_CHANGE";
public static final String EXTRA_LE_AUDIO_SHARING_STATE = "BLUETOOTH_LE_AUDIO_SHARING_STATE";
public static final String EXTRA_BLUETOOTH_DEVICE = "BLUETOOTH_DEVICE";
+ public static final String EXTRA_START_LE_AUDIO_SHARING = "START_LE_AUDIO_SHARING";
public static final int BROADCAST_STATE_UNKNOWN = 0;
public static final int BROADCAST_STATE_ON = 1;
public static final int BROADCAST_STATE_OFF = 2;
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
index 2c982d6..f7492cf 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/TestModeBuilder.java
@@ -40,18 +40,17 @@
private ZenModeConfig.ZenRule mConfigZenRule;
public static final ZenMode EXAMPLE = new TestModeBuilder().build();
- public static final ZenMode MANUAL_DND_ACTIVE = ZenMode.manualDndMode(
- new AutomaticZenRule.Builder("Do Not Disturb", Uri.parse("rule://dnd"))
- .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
- .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
- .build(),
- /* isActive= */ true);
- public static final ZenMode MANUAL_DND_INACTIVE = ZenMode.manualDndMode(
- new AutomaticZenRule.Builder("Do Not Disturb", Uri.parse("rule://dnd"))
- .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
- .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
- .build(),
- /* isActive= */ false);
+ public static final ZenMode MANUAL_DND_ACTIVE = manualDnd(Uri.EMPTY, true);
+ public static final ZenMode MANUAL_DND_INACTIVE = manualDnd(Uri.EMPTY, false);
+
+ public static ZenMode manualDnd(Uri conditionId, boolean isActive) {
+ return ZenMode.manualDndMode(
+ new AutomaticZenRule.Builder("Do Not Disturb", conditionId)
+ .setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
+ .setZenPolicy(new ZenPolicy.Builder().disallowAllSounds().build())
+ .build(),
+ isActive);
+ }
public TestModeBuilder() {
// Reasonable defaults
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
index 88497a3..2f4b2ef 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenMode.java
@@ -22,6 +22,7 @@
import static android.app.NotificationManager.INTERRUPTION_FILTER_PRIORITY;
import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleEvent;
import static android.service.notification.SystemZenRules.getTriggerDescriptionForScheduleTime;
+import static android.service.notification.ZenModeConfig.tryParseCountdownConditionId;
import static android.service.notification.ZenModeConfig.tryParseEventConditionId;
import static android.service.notification.ZenModeConfig.tryParseScheduleConditionId;
@@ -188,11 +189,37 @@
return mRule.getType();
}
+ /** Returns the trigger description of the mode. */
@Nullable
public String getTriggerDescription() {
return mRule.getTriggerDescription();
}
+ /**
+ * Returns a "dynamic" trigger description. For some modes (such as manual Do Not Disturb)
+ * when activated, we know when (and if) the mode is expected to end on its own; this dynamic
+ * description reflects that. In other cases, returns {@link #getTriggerDescription}.
+ */
+ @Nullable
+ public String getDynamicDescription(Context context) {
+ if (isManualDnd() && isActive()) {
+ long countdownEndTime = tryParseCountdownConditionId(mRule.getConditionId());
+ if (countdownEndTime > 0) {
+ CharSequence formattedTime = ZenModeConfig.getFormattedTime(context,
+ countdownEndTime, ZenModeConfig.isToday(countdownEndTime),
+ context.getUserId());
+ return context.getString(com.android.internal.R.string.zen_mode_until,
+ formattedTime);
+ }
+ }
+ // TODO: b/333527800 - For TYPE_SCHEDULE_TIME rules we could do the same; however
+ // according to the snoozing discussions the mode may or may not end at the scheduled
+ // time if manually activated. When we resolve that point, we could calculate end time
+ // for these modes as well.
+
+ return getTriggerDescription();
+ }
+
@NonNull
public ListenableFuture<Drawable> getIcon(@NonNull Context context,
@NonNull ZenIconLoader iconLoader) {
diff --git a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
index f533c95..64e503b32 100644
--- a/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
+++ b/packages/SettingsLib/src/com/android/settingslib/notification/modes/ZenModesBackend.java
@@ -116,7 +116,6 @@
private ZenMode getManualDndMode(ZenModeConfig config) {
ZenModeConfig.ZenRule manualRule = config.manualRule;
- // TODO: b/333682392 - Replace with final strings for name & trigger description
AutomaticZenRule manualDndRule = new AutomaticZenRule.Builder(
mContext.getString(R.string.zen_mode_settings_title), manualRule.conditionId)
.setType(manualRule.type)
@@ -127,7 +126,7 @@
.setInterruptionFilter(NotificationManager.INTERRUPTION_FILTER_PRIORITY)
.build();
- return ZenMode.manualDndMode(manualDndRule, config != null && config.isManualActive());
+ return ZenMode.manualDndMode(manualDndRule, config.isManualActive());
}
public void updateMode(ZenMode mode) {
@@ -175,7 +174,6 @@
mNotificationManager.setZenMode(Settings.Global.ZEN_MODE_OFF, null, TAG,
/* fromUser= */ true);
} else {
- // TODO: b/333527800 - This should (potentially) snooze the rule if it was active.
mNotificationManager.setAutomaticZenRuleState(mode.getId(),
new Condition(mode.getRule().getConditionId(), "", Condition.STATE_FALSE,
Condition.SOURCE_USER_ACTION));
diff --git a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
index 69c7410..6198d80 100644
--- a/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
+++ b/packages/SettingsLib/src/com/android/settingslib/users/CreateUserDialogController.java
@@ -181,14 +181,14 @@
* admin status.
*/
public Dialog createDialog(Activity activity,
- ActivityStarter activityStarter, boolean isMultipleAdminEnabled,
+ ActivityStarter activityStarter, boolean canCreateAdminUser,
NewUserData successCallback, Runnable cancelCallback) {
mActivity = activity;
mCustomDialogHelper = new CustomDialogHelper(activity);
mSuccessCallback = successCallback;
mCancelCallback = cancelCallback;
mActivityStarter = activityStarter;
- addCustomViews(isMultipleAdminEnabled);
+ addCustomViews(canCreateAdminUser);
mUserCreationDialog = mCustomDialogHelper.getDialog();
updateLayout();
mUserCreationDialog.setOnDismissListener(view -> finish());
@@ -197,19 +197,19 @@
return mUserCreationDialog;
}
- private void addCustomViews(boolean isMultipleAdminEnabled) {
+ private void addCustomViews(boolean canCreateAdminUser) {
addGrantAdminView();
addUserInfoEditView();
mCustomDialogHelper.setPositiveButton(R.string.next, view -> {
mCurrentState++;
- if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
+ if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) {
mCurrentState++;
}
updateLayout();
});
mCustomDialogHelper.setNegativeButton(R.string.back, view -> {
mCurrentState--;
- if (mCurrentState == GRANT_ADMIN_DIALOG && !isMultipleAdminEnabled) {
+ if (mCurrentState == GRANT_ADMIN_DIALOG && !canCreateAdminUser) {
mCurrentState--;
}
updateLayout();
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
index f94f21f..698eb81 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/bluetooth/CsipDeviceManagerTest.java
@@ -19,7 +19,9 @@
import static com.google.common.truth.Truth.assertThat;
import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.Mockito.atLeastOnce;
import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
import android.bluetooth.BluetoothClass;
@@ -352,4 +354,34 @@
assertThat(mCachedDevice1.getMemberDevice()).contains(mCachedDevice3);
assertThat(mCachedDevice1.getDevice()).isEqualTo(expectedMainBluetoothDevice);
}
+
+ @Test
+ public void onProfileConnectionStateChangedIfProcessed_addMemberDevice_refreshUI() {
+ mCachedDevice3.setGroupId(GROUP1);
+
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_CONNECTED);
+
+ verify(mCachedDevice1).refresh();
+ }
+
+ @Test
+ public void onProfileConnectionStateChangedIfProcessed_switchMainDevice_refreshUI() {
+ when(mDevice3.isConnected()).thenReturn(true);
+ when(mDevice2.isConnected()).thenReturn(false);
+ when(mDevice1.isConnected()).thenReturn(false);
+ mCachedDevice3.setGroupId(GROUP1);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_CONNECTED);
+
+ when(mDevice3.isConnected()).thenReturn(false);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice3,
+ BluetoothProfile.STATE_DISCONNECTED);
+ when(mDevice1.isConnected()).thenReturn(true);
+ mCsipDeviceManager.onProfileConnectionStateChangedIfProcessed(mCachedDevice1,
+ BluetoothProfile.STATE_CONNECTED);
+
+ verify(mCachedDevice3).switchMemberDeviceContent(mCachedDevice1);
+ verify(mCachedDevice3, atLeastOnce()).refresh();
+ }
}
diff --git a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
index 00c7ae3..539519b 100644
--- a/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
+++ b/packages/SettingsLib/tests/robotests/src/com/android/settingslib/notification/modes/ZenModesBackendTest.java
@@ -123,7 +123,6 @@
zenRule.id = id;
zenRule.pkg = "package";
zenRule.enabled = azr.isEnabled();
- zenRule.snoozing = false;
zenRule.conditionId = azr.getConditionId();
zenRule.condition = new Condition(azr.getConditionId(), "",
active ? Condition.STATE_TRUE : Condition.STATE_FALSE,
diff --git a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
index 03c2a83..65937ea 100644
--- a/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
+++ b/packages/SettingsProvider/src/android/provider/settings/validators/GlobalSettingsValidators.java
@@ -445,7 +445,6 @@
String.valueOf(Global.Wearable.TETHERED_CONFIG_TETHERED),
String.valueOf(Global.Wearable.TETHERED_CONFIG_RESTRICTED)
}));
- VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_SUPPORTED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.Wearable.WEAR_LAUNCHER_UI_MODE, ANY_INTEGER_VALIDATOR);
VALIDATORS.put(Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.Wearable.CONNECTIVITY_KEEP_DATA_ON, BOOLEAN_VALIDATOR);
@@ -457,5 +456,10 @@
VALIDATORS.put(Global.ADD_USERS_WHEN_LOCKED, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.REMOVE_GUEST_ON_EXIT, BOOLEAN_VALIDATOR);
VALIDATORS.put(Global.USER_SWITCHER_ENABLED, BOOLEAN_VALIDATOR);
+ VALIDATORS.put(Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE,
+ new InclusiveIntegerRangeValidator(
+ Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE_NONE,
+ Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE_COMPANION
+ ));
}
}
diff --git a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
index e66bacf..d39b564 100644
--- a/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
+++ b/packages/SettingsProvider/test/src/android/provider/SettingsBackupTest.java
@@ -629,11 +629,11 @@
Settings.Global.Wearable.CUSTOM_COLOR_BACKGROUND,
Settings.Global.Wearable.PHONE_SWITCHING_STATUS,
Settings.Global.Wearable.TETHER_CONFIG_STATE,
- Settings.Global.Wearable.PHONE_SWITCHING_SUPPORTED,
Settings.Global.Wearable.WEAR_MEDIA_CONTROLS_PACKAGE,
Settings.Global.Wearable.WEAR_MEDIA_SESSIONS_PACKAGE,
Settings.Global.Wearable.WEAR_POWER_ANOMALY_SERVICE_ENABLED,
- Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON);
+ Settings.Global.Wearable.CONNECTIVITY_KEEP_DATA_ON,
+ Settings.Global.Wearable.PHONE_SWITCHING_REQUEST_SOURCE);
private static final Set<String> BACKUP_DENY_LIST_SECURE_SETTINGS =
newHashSet(
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
index 66a2fae..c698d18 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/model/A11yMenuShortcut.java
@@ -19,7 +19,6 @@
import com.android.systemui.accessibility.accessibilitymenu.R;
-import java.util.HashMap;
import java.util.Map;
/**
@@ -52,80 +51,80 @@
private static final int LABEL_TEXT_INDEX = 3;
/** Map stores all shortcut resource IDs that is in matching order of defined shortcut. */
- private static final Map<ShortcutId, int[]> sShortcutResource = new HashMap<>() {{
- put(ShortcutId.ID_ASSISTANT_VALUE, new int[] {
+ private static final Map<ShortcutId, int[]> sShortcutResource = Map.ofEntries(
+ Map.entry(ShortcutId.ID_ASSISTANT_VALUE, new int[] {
R.drawable.ic_logo_a11y_assistant_24dp,
R.color.assistant_color,
R.string.assistant_utterance,
R.string.assistant_label,
- });
- put(ShortcutId.ID_A11YSETTING_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_A11YSETTING_VALUE, new int[] {
R.drawable.ic_logo_a11y_settings_24dp,
R.color.a11y_settings_color,
R.string.a11y_settings_label,
R.string.a11y_settings_label,
- });
- put(ShortcutId.ID_POWER_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_POWER_VALUE, new int[] {
R.drawable.ic_logo_a11y_power_24dp,
R.color.power_color,
R.string.power_utterance,
R.string.power_label,
- });
- put(ShortcutId.ID_RECENT_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_RECENT_VALUE, new int[] {
R.drawable.ic_logo_a11y_recent_apps_24dp,
R.color.recent_apps_color,
R.string.recent_apps_label,
R.string.recent_apps_label,
- });
- put(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_LOCKSCREEN_VALUE, new int[] {
R.drawable.ic_logo_a11y_lock_24dp,
R.color.lockscreen_color,
R.string.lockscreen_label,
R.string.lockscreen_label,
- });
- put(ShortcutId.ID_QUICKSETTING_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_QUICKSETTING_VALUE, new int[] {
R.drawable.ic_logo_a11y_quick_settings_24dp,
R.color.quick_settings_color,
R.string.quick_settings_label,
R.string.quick_settings_label,
- });
- put(ShortcutId.ID_NOTIFICATION_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_NOTIFICATION_VALUE, new int[] {
R.drawable.ic_logo_a11y_notifications_24dp,
R.color.notifications_color,
R.string.notifications_label,
R.string.notifications_label,
- });
- put(ShortcutId.ID_SCREENSHOT_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_SCREENSHOT_VALUE, new int[] {
R.drawable.ic_logo_a11y_screenshot_24dp,
R.color.screenshot_color,
R.string.screenshot_utterance,
R.string.screenshot_label,
- });
- put(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_BRIGHTNESS_UP_VALUE, new int[] {
R.drawable.ic_logo_a11y_brightness_up_24dp,
R.color.brightness_color,
R.string.brightness_up_label,
R.string.brightness_up_label,
- });
- put(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_BRIGHTNESS_DOWN_VALUE, new int[] {
R.drawable.ic_logo_a11y_brightness_down_24dp,
R.color.brightness_color,
R.string.brightness_down_label,
R.string.brightness_down_label,
- });
- put(ShortcutId.ID_VOLUME_UP_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_VOLUME_UP_VALUE, new int[] {
R.drawable.ic_logo_a11y_volume_up_24dp,
R.color.volume_color,
R.string.volume_up_label,
R.string.volume_up_label,
- });
- put(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] {
+ }),
+ Map.entry(ShortcutId.ID_VOLUME_DOWN_VALUE, new int[] {
R.drawable.ic_logo_a11y_volume_down_24dp,
R.color.volume_color,
R.string.volume_down_label,
R.string.volume_down_label,
- });
- }};
+ })
+ );
/** Shortcut id used to identify. */
private int mShortcutId = ShortcutId.UNSPECIFIED_ID_VALUE.ordinal();
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
index 2e036e6..6bea30f 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/src/com/android/systemui/accessibility/accessibilitymenu/view/A11yMenuOverlayLayout.java
@@ -348,7 +348,17 @@
/** Toggles a11y menu layout visibility. */
public void toggleVisibility() {
- mLayout.setVisibility((mLayout.getVisibility() == View.VISIBLE) ? View.GONE : View.VISIBLE);
+ if (mLayout.getVisibility() == View.VISIBLE) {
+ mLayout.setVisibility(View.GONE);
+ } else {
+ if (Flags.hideRestrictedActions()) {
+ // Reconfigure the shortcut list in case the set of restricted actions has changed.
+ mA11yMenuViewPager.configureViewPagerAndFooter(
+ mLayout, createShortcutList(), getPageIndex());
+ updateViewLayout();
+ }
+ mLayout.setVisibility(View.VISIBLE);
+ }
}
/** Shows hint text on a minimal Snackbar-like text view. */
diff --git a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
index d16617f..4ab771b 100644
--- a/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
+++ b/packages/SystemUI/accessibility/accessibilitymenu/tests/src/com/android/systemui/accessibility/accessibilitymenu/tests/AccessibilityMenuServiceTest.java
@@ -542,8 +542,6 @@
final Context context = sInstrumentation.getTargetContext();
final UserManager userManager = context.getSystemService(UserManager.class);
userManager.setUserRestriction(restriction, isRestricted);
- // Re-enable the service for the restriction to take effect.
- enableA11yMenuService(context);
}
private static void unlockSignal() throws IOException {
diff --git a/packages/SystemUI/aconfig/biometrics_framework.aconfig b/packages/SystemUI/aconfig/biometrics_framework.aconfig
index e81d5d5..95e4b59 100644
--- a/packages/SystemUI/aconfig/biometrics_framework.aconfig
+++ b/packages/SystemUI/aconfig/biometrics_framework.aconfig
@@ -3,9 +3,3 @@
# NOTE: Keep alphabetized to help limit merge conflicts from multiple simultaneous editors.
-flag {
- name: "constraint_bp"
- namespace: "biometrics_framework"
- description: "Refactors Biometric Prompt to use a ConstraintLayout"
- bug: "288175072"
-}
diff --git a/packages/SystemUI/aconfig/systemui.aconfig b/packages/SystemUI/aconfig/systemui.aconfig
index 19dced5..1ff9a80 100644
--- a/packages/SystemUI/aconfig/systemui.aconfig
+++ b/packages/SystemUI/aconfig/systemui.aconfig
@@ -156,17 +156,6 @@
}
flag {
- name: "pss_app_selector_abrupt_exit_fix"
- namespace: "systemui"
- description: "Fixes the app selector abruptly disappearing without an animation, when the"
- "selected task is the foreground task."
- bug: "314385883"
- metadata {
- purpose: PURPOSE_BUGFIX
- }
-}
-
-flag {
name: "pss_app_selector_recents_split_screen"
namespace: "systemui"
description: "Allows recent apps selected for partial screenshare to be launched in split screen mode"
@@ -1197,6 +1186,17 @@
}
flag {
+ name: "hubmode_fullscreen_vertical_swipe_fix"
+ namespace: "systemui"
+ description: "Bug fix that enables fullscreen vertical swiping in hub mode to bring up and down the bouncer and shade"
+ bug: "340177049"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
+
+
+flag {
namespace: "systemui"
name: "remove_update_listener_in_qs_icon_view_impl"
description: "Remove update listeners in QsIconViewImpl class to avoid memory leak."
@@ -1263,6 +1263,13 @@
}
flag {
+ name: "new_picker_ui"
+ namespace: "systemui"
+ description: "Enables the BC25 design of the customization picker UI."
+ bug: "339081035"
+}
+
+flag {
namespace: "systemui"
name: "settings_ext_register_content_observer_on_bg_thread"
description: "Register content observer in callback flow APIs on background thread in SettingsProxyExt."
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt
new file mode 100644
index 0000000..8f5cdbf
--- /dev/null
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetector.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.detector.api.Category
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Implementation
+import com.android.tools.lint.detector.api.Issue
+import com.android.tools.lint.detector.api.JavaContext
+import com.android.tools.lint.detector.api.Scope
+import com.android.tools.lint.detector.api.Severity
+import com.android.tools.lint.detector.api.SourceCodeScanner
+import com.intellij.psi.PsiMethod
+import org.jetbrains.uast.UCallExpression
+import org.jetbrains.uast.UClass
+import org.jetbrains.uast.getParentOfType
+
+/**
+ * Checks if registerContentObserver/registerContentObserverAsUser/unregisterContentObserver is
+ * called on a ContentResolver (or subclasses), and directs the caller to using
+ * com.android.systemui.util.settings.SettingsProxy or its sub-classes.
+ */
+@Suppress("UnstableApiUsage")
+class RegisterContentObserverViaContentResolverDetector : Detector(), SourceCodeScanner {
+
+ override fun getApplicableMethodNames(): List<String> {
+ return CONTENT_RESOLVER_METHOD_LIST
+ }
+
+ override fun visitMethodCall(context: JavaContext, node: UCallExpression, method: PsiMethod) {
+ val classQualifiedName = node.getParentOfType(UClass::class.java)?.qualifiedName
+ if (classQualifiedName in CLASSNAME_ALLOWLIST) {
+ // Don't warn for class we want the developers to use.
+ return
+ }
+
+ val evaluator = context.evaluator
+ if (evaluator.isMemberInSubClassOf(method, "android.content.ContentResolver")) {
+ context.report(
+ issue = CONTENT_RESOLVER_ERROR,
+ location = context.getNameLocation(node),
+ message =
+ "`ContentResolver.${method.name}()` should be replaced with " +
+ "an appropriate interface API call, for eg. " +
+ "`<SettingsProxy>/<UserSettingsProxy>.${method.name}()`"
+ )
+ }
+ }
+
+ companion object {
+ @JvmField
+ val CONTENT_RESOLVER_ERROR: Issue =
+ Issue.create(
+ id = "RegisterContentObserverViaContentResolver",
+ briefDescription =
+ "Content observer registration done via `ContentResolver`" +
+ "instead of `SettingsProxy or child interfaces.`",
+ // lint trims indents and converts \ to line continuations
+ explanation =
+ """
+ Use registerContentObserver/unregisterContentObserver methods in \
+ `SettingsProxy`, `UserSettingsProxy` or `GlobalSettings` class instead of \
+ using `ContentResolver.registerContentObserver` or \
+ `ContentResolver.unregisterContentObserver`.""",
+ category = Category.PERFORMANCE,
+ priority = 10,
+ severity = Severity.ERROR,
+ implementation =
+ Implementation(
+ RegisterContentObserverViaContentResolverDetector::class.java,
+ Scope.JAVA_FILE_SCOPE
+ )
+ )
+
+ private val CLASSNAME_ALLOWLIST =
+ listOf(
+ "com.android.systemui.util.settings.SettingsProxy",
+ "com.android.systemui.util.settings.UserSettingsProxy",
+ "com.android.systemui.util.settings.GlobalSettings",
+ "com.android.systemui.util.settings.SecureSettings",
+ "com.android.systemui.util.settings.SystemSettings"
+ )
+
+ private val CONTENT_RESOLVER_METHOD_LIST =
+ listOf(
+ "registerContentObserver",
+ "registerContentObserverAsUser",
+ "unregisterContentObserver"
+ )
+ }
+}
diff --git a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
index 5206b05..a1f4f55 100644
--- a/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
+++ b/packages/SystemUI/checks/src/com/android/internal/systemui/lint/SystemUIIssueRegistry.kt
@@ -46,7 +46,8 @@
DemotingTestWithoutBugDetector.ISSUE,
TestFunctionNameViolationDetector.ISSUE,
MissingApacheLicenseDetector.ISSUE,
- RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING
+ RegisterContentObserverSyncViaSettingsProxyDetector.SYNC_WARNING,
+ RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR
)
override val api: Int
diff --git a/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt
new file mode 100644
index 0000000..1d33bce
--- /dev/null
+++ b/packages/SystemUI/checks/tests/com/android/internal/systemui/lint/RegisterContentObserverViaContentResolverDetectorTest.kt
@@ -0,0 +1,207 @@
+/*
+ * 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.internal.systemui.lint
+
+import com.android.tools.lint.checks.infrastructure.TestFiles
+import com.android.tools.lint.detector.api.Detector
+import com.android.tools.lint.detector.api.Issue
+import org.junit.Test
+
+class RegisterContentObserverViaContentResolverDetectorTest : SystemUILintDetectorTest() {
+
+ override fun getDetector(): Detector = RegisterContentObserverViaContentResolverDetector()
+
+ override fun getIssues(): List<Issue> =
+ listOf(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+
+ @Test
+ fun testRegisterContentObserver_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserver() [RegisterContentObserverViaContentResolver]
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ ~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testRegisterContentObserverForUser_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.registerContentObserverAsUser() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.registerContentObserverAsUser() [RegisterContentObserverViaContentResolver]
+ registerContentObserverAsUser(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+
+ @Test
+ fun testSuppressRegisterContentObserver() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ @SuppressWarnings("RegisterContentObserverViaContentResolver")
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testRegisterContentObserverInSettingsProxy_allowed() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package com.android.systemui.util.settings;
+ import android.content.Context;
+
+ public class SettingsProxy {
+ public void register(Context context) {
+ context.getContentResolver().
+ registerContentObserver(Settings.Global.getUriFor(Settings.Global.DEVICE_PROVISIONED),
+ false, mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testNoopIfNoCall() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class SettingsProxy {
+ public void register(Context context) {
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expectClean()
+ }
+
+ @Test
+ fun testUnRegisterContentObserver_throwError() {
+ lint()
+ .files(
+ TestFiles.java(
+ """
+ package test.pkg;
+ import android.content.Context;
+
+ public class TestClass {
+ public void register(Context context) {
+ context.getContentResolver().
+ unregisterContentObserver(mSettingObserver);
+ }
+ }
+ """
+ )
+ .indented(),
+ *androidStubs
+ )
+ .issues(RegisterContentObserverViaContentResolverDetector.CONTENT_RESOLVER_ERROR)
+ .run()
+ .expect(
+ """
+ src/test/pkg/TestClass.java:7: Error: ContentResolver.unregisterContentObserver() should be replaced with an appropriate interface API call, for eg. <SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver() [RegisterContentObserverViaContentResolver]
+ unregisterContentObserver(mSettingObserver);
+ ~~~~~~~~~~~~~~~~~~~~~~~~~
+ 1 errors, 0 warnings
+ """
+ .trimIndent()
+ )
+ }
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
index 69f1174..b65b471 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/communal/ui/compose/CommunalHub.kt
@@ -160,6 +160,7 @@
import com.android.compose.theme.LocalAndroidColorScheme
import com.android.compose.ui.graphics.painter.rememberDrawablePainter
import com.android.internal.R.dimen.system_app_widget_background_radius
+import com.android.systemui.Flags
import com.android.systemui.Flags.communalTimerFlickerFix
import com.android.systemui.communal.domain.model.CommunalContentModel
import com.android.systemui.communal.shared.model.CommunalContentSize
@@ -269,7 +270,7 @@
}
}
// Nested scroll for full screen swipe to get to shade and bouncer
- .thenIf(!viewModel.isEditMode) {
+ .thenIf(!viewModel.isEditMode && Flags.hubmodeFullscreenVerticalSwipeFix()) {
Modifier.nestedScroll(nestedScrollConnection).pointerInput(viewModel) {
awaitPointerEventScope {
while (true) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt
new file mode 100644
index 0000000..04bcc36
--- /dev/null
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/AlternateBouncer.kt
@@ -0,0 +1,198 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.keyguard.ui.composable
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.animation.AnimatedVisibility
+import androidx.compose.animation.Crossfade
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.foundation.gestures.detectTapGestures
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxHeight
+import androidx.compose.foundation.layout.offset
+import androidx.compose.foundation.layout.padding
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Alignment
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.input.pointer.pointerInput
+import androidx.compose.ui.text.style.TextAlign
+import androidx.compose.ui.text.style.TextOverflow
+import androidx.compose.ui.unit.IntOffset
+import androidx.compose.ui.unit.dp
+import androidx.compose.ui.unit.sp
+import androidx.compose.ui.viewinterop.AndroidView
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
+import com.android.compose.modifiers.background
+import com.android.compose.modifiers.height
+import com.android.compose.modifiers.width
+import com.android.systemui.deviceentry.shared.model.BiometricMessage
+import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
+import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
+import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
+import com.android.systemui.keyguard.ui.binder.AlternateBouncerUdfpsViewBinder
+import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
+import com.android.systemui.res.R
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+@ExperimentalCoroutinesApi
+@Composable
+fun AlternateBouncer(
+ alternateBouncerDependencies: AlternateBouncerDependencies,
+ modifier: Modifier = Modifier,
+) {
+
+ val isVisible by
+ alternateBouncerDependencies.viewModel.isVisible.collectAsStateWithLifecycle(
+ initialValue = false
+ )
+
+ val udfpsIconLocation by
+ alternateBouncerDependencies.udfpsIconViewModel.iconLocation.collectAsStateWithLifecycle(
+ initialValue = null
+ )
+
+ // TODO (b/353955910): back handling doesn't work
+ BackHandler { alternateBouncerDependencies.viewModel.onBackRequested() }
+
+ AnimatedVisibility(
+ visible = isVisible,
+ enter = fadeIn(),
+ exit = fadeOut(),
+ modifier = modifier,
+ ) {
+ Box(
+ contentAlignment = Alignment.TopCenter,
+ modifier =
+ Modifier.background(color = Colors.AlternateBouncerBackgroundColor, alpha = { 1f })
+ .pointerInput(Unit) {
+ detectTapGestures(
+ onTap = { alternateBouncerDependencies.viewModel.onTapped() }
+ )
+ },
+ ) {
+ StatusMessage(
+ viewModel = alternateBouncerDependencies.messageAreaViewModel,
+ )
+ }
+
+ udfpsIconLocation?.let { udfpsLocation ->
+ Box {
+ DeviceEntryIcon(
+ viewModel = alternateBouncerDependencies.udfpsIconViewModel,
+ modifier =
+ Modifier.width { udfpsLocation.width }
+ .height { udfpsLocation.height }
+ .fillMaxHeight()
+ .offset {
+ IntOffset(
+ x = udfpsLocation.left,
+ y = udfpsLocation.top,
+ )
+ },
+ )
+ }
+
+ UdfpsA11yOverlay(
+ viewModel = alternateBouncerDependencies.udfpsAccessibilityOverlayViewModel.get(),
+ modifier = Modifier.fillMaxHeight(),
+ )
+ }
+ }
+}
+
+@ExperimentalCoroutinesApi
+@Composable
+private fun StatusMessage(
+ viewModel: AlternateBouncerMessageAreaViewModel,
+ modifier: Modifier = Modifier,
+) {
+ val message: BiometricMessage? by
+ viewModel.message.collectAsStateWithLifecycle(initialValue = null)
+
+ Crossfade(
+ targetState = message,
+ label = "Alternate Bouncer message",
+ animationSpec = tween(),
+ modifier = modifier,
+ ) { biometricMessage ->
+ biometricMessage?.let {
+ Text(
+ textAlign = TextAlign.Center,
+ text = it.message ?: "",
+ color = Colors.AlternateBouncerTextColor,
+ fontSize = 18.sp,
+ lineHeight = 24.sp,
+ overflow = TextOverflow.Ellipsis,
+ modifier = Modifier.padding(top = 92.dp),
+ )
+ }
+ }
+}
+
+@ExperimentalCoroutinesApi
+@Composable
+private fun DeviceEntryIcon(
+ viewModel: AlternateBouncerUdfpsIconViewModel,
+ modifier: Modifier = Modifier,
+) {
+ AndroidView(
+ modifier = modifier,
+ factory = { context ->
+ val view =
+ DeviceEntryIconView(context, null).apply {
+ id = R.id.alternate_bouncer_udfps_icon_view
+ contentDescription =
+ context.resources.getString(R.string.accessibility_fingerprint_label)
+ }
+ AlternateBouncerUdfpsViewBinder.bind(view, viewModel)
+ view
+ },
+ )
+}
+
+/** TODO (b/353955910): Validate accessibility CUJs */
+@ExperimentalCoroutinesApi
+@Composable
+private fun UdfpsA11yOverlay(
+ viewModel: AlternateBouncerUdfpsAccessibilityOverlayViewModel,
+ modifier: Modifier = Modifier,
+) {
+ AndroidView(
+ factory = { context ->
+ val view =
+ UdfpsAccessibilityOverlay(context).apply {
+ id = R.id.alternate_bouncer_udfps_accessibility_overlay
+ }
+ UdfpsAccessibilityOverlayBinder.bind(view, viewModel)
+ view
+ },
+ modifier = modifier,
+ )
+}
+
+private object Colors {
+ val AlternateBouncerBackgroundColor: Color = Color.Black.copy(alpha = .66f)
+ val AlternateBouncerTextColor: Color = Color.White
+}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
index 9afb4d5..a78c038 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/LockscreenSceneBlueprintModule.kt
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard.ui.composable
import com.android.systemui.keyguard.ui.composable.blueprint.CommunalBlueprintModule
-import com.android.systemui.keyguard.ui.composable.blueprint.ShortcutsBesideUdfpsBlueprintModule
import com.android.systemui.keyguard.ui.composable.section.OptionalSectionModule
import dagger.Module
@@ -26,7 +25,6 @@
[
CommunalBlueprintModule::class,
OptionalSectionModule::class,
- ShortcutsBesideUdfpsBlueprintModule::class,
],
)
interface LockscreenSceneBlueprintModule
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
deleted file mode 100644
index a5e120c..0000000
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/blueprint/ShortcutsBesideUdfpsBlueprint.kt
+++ /dev/null
@@ -1,259 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.keyguard.ui.composable.blueprint
-
-import androidx.compose.foundation.layout.Box
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.fillMaxHeight
-import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.runtime.Composable
-import androidx.compose.runtime.getValue
-import androidx.compose.ui.Alignment
-import androidx.compose.ui.Modifier
-import androidx.compose.ui.graphics.graphicsLayer
-import androidx.compose.ui.layout.Layout
-import androidx.compose.ui.unit.IntRect
-import androidx.lifecycle.compose.collectAsStateWithLifecycle
-import com.android.compose.animation.scene.SceneScope
-import com.android.compose.modifiers.padding
-import com.android.systemui.keyguard.ui.composable.LockscreenLongPress
-import com.android.systemui.keyguard.ui.composable.section.AmbientIndicationSection
-import com.android.systemui.keyguard.ui.composable.section.BottomAreaSection
-import com.android.systemui.keyguard.ui.composable.section.LockSection
-import com.android.systemui.keyguard.ui.composable.section.NotificationSection
-import com.android.systemui.keyguard.ui.composable.section.SettingsMenuSection
-import com.android.systemui.keyguard.ui.composable.section.StatusBarSection
-import com.android.systemui.keyguard.ui.composable.section.TopAreaSection
-import com.android.systemui.keyguard.ui.viewmodel.LockscreenContentViewModel
-import dagger.Binds
-import dagger.Module
-import dagger.multibindings.IntoSet
-import java.util.Optional
-import javax.inject.Inject
-import kotlin.math.roundToInt
-
-/**
- * Renders the lockscreen scene when showing with the default layout (e.g. vertical phone form
- * factor).
- */
-class ShortcutsBesideUdfpsBlueprint
-@Inject
-constructor(
- private val statusBarSection: StatusBarSection,
- private val lockSection: LockSection,
- private val ambientIndicationSectionOptional: Optional<AmbientIndicationSection>,
- private val bottomAreaSection: BottomAreaSection,
- private val settingsMenuSection: SettingsMenuSection,
- private val topAreaSection: TopAreaSection,
- private val notificationSection: NotificationSection,
-) : ComposableLockscreenSceneBlueprint {
-
- override val id: String = "shortcuts-besides-udfps"
-
- @Composable
- override fun SceneScope.Content(
- viewModel: LockscreenContentViewModel,
- modifier: Modifier,
- ) {
- val isUdfpsVisible = viewModel.isUdfpsVisible
- val isShadeLayoutWide by viewModel.isShadeLayoutWide.collectAsStateWithLifecycle()
- val unfoldTranslations by viewModel.unfoldTranslations.collectAsStateWithLifecycle()
- val areNotificationsVisible by
- viewModel
- .areNotificationsVisible(contentKey)
- .collectAsStateWithLifecycle(initialValue = false)
-
- LockscreenLongPress(
- viewModel = viewModel.touchHandling,
- modifier = modifier,
- ) { onSettingsMenuPlaced ->
- Layout(
- content = {
- // Constrained to above the lock icon.
- Column(
- modifier = Modifier.fillMaxSize(),
- ) {
- with(statusBarSection) {
- StatusBar(
- modifier =
- Modifier.fillMaxWidth()
- .padding(
- horizontal = { unfoldTranslations.start.roundToInt() },
- )
- )
- }
-
- Box {
- with(topAreaSection) {
- DefaultClockLayout(
- smartSpacePaddingTop = viewModel::getSmartSpacePaddingTop,
- modifier =
- Modifier.graphicsLayer {
- translationX = unfoldTranslations.start
- },
- )
- }
- if (isShadeLayoutWide) {
- with(notificationSection) {
- Notifications(
- areNotificationsVisible = areNotificationsVisible,
- isShadeLayoutWide = isShadeLayoutWide,
- burnInParams = null,
- modifier =
- Modifier.fillMaxWidth(0.5f)
- .fillMaxHeight()
- .align(alignment = Alignment.TopEnd)
- )
- }
- }
- }
- if (!isShadeLayoutWide) {
- with(notificationSection) {
- Notifications(
- areNotificationsVisible = areNotificationsVisible,
- isShadeLayoutWide = isShadeLayoutWide,
- burnInParams = null,
- modifier = Modifier.weight(weight = 1f)
- )
- }
- }
- if (!isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
- with(ambientIndicationSectionOptional.get()) {
- AmbientIndication(modifier = Modifier.fillMaxWidth())
- }
- }
- }
-
- // Constrained to the left of the lock icon (in left-to-right layouts).
- with(bottomAreaSection) {
- Shortcut(
- isStart = true,
- applyPadding = false,
- modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.start },
- )
- }
-
- with(lockSection) { LockIcon() }
-
- // Constrained to the right of the lock icon (in left-to-right layouts).
- with(bottomAreaSection) {
- Shortcut(
- isStart = false,
- applyPadding = false,
- modifier =
- Modifier.graphicsLayer { translationX = unfoldTranslations.end },
- )
- }
-
- // Aligned to bottom and constrained to below the lock icon.
- Column(modifier = Modifier.fillMaxWidth()) {
- if (isUdfpsVisible && ambientIndicationSectionOptional.isPresent) {
- with(ambientIndicationSectionOptional.get()) {
- AmbientIndication(modifier = Modifier.fillMaxWidth())
- }
- }
-
- with(bottomAreaSection) {
- IndicationArea(modifier = Modifier.fillMaxWidth())
- }
- }
-
- // Aligned to bottom and NOT constrained by the lock icon.
- with(settingsMenuSection) { SettingsMenu(onSettingsMenuPlaced) }
- },
- modifier = Modifier.fillMaxSize(),
- ) { measurables, constraints ->
- check(measurables.size == 6)
- val aboveLockIconMeasurable = measurables[0]
- val startSideShortcutMeasurable = measurables[1]
- val lockIconMeasurable = measurables[2]
- val endSideShortcutMeasurable = measurables[3]
- val belowLockIconMeasurable = measurables[4]
- val settingsMenuMeasurable = measurables[5]
-
- val noMinConstraints =
- constraints.copy(
- minWidth = 0,
- minHeight = 0,
- )
-
- val lockIconPlaceable = lockIconMeasurable.measure(noMinConstraints)
- val lockIconBounds =
- IntRect(
- left = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Left],
- top = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Top],
- right = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Right],
- bottom = lockIconPlaceable[BlueprintAlignmentLines.LockIcon.Bottom],
- )
-
- val aboveLockIconPlaceable =
- aboveLockIconMeasurable.measure(
- noMinConstraints.copy(maxHeight = lockIconBounds.top)
- )
- val startSideShortcutPlaceable =
- startSideShortcutMeasurable.measure(noMinConstraints)
- val endSideShortcutPlaceable = endSideShortcutMeasurable.measure(noMinConstraints)
- val belowLockIconPlaceable =
- belowLockIconMeasurable.measure(
- noMinConstraints.copy(
- maxHeight = constraints.maxHeight - lockIconBounds.bottom
- )
- )
- val settingsMenuPlaceable = settingsMenuMeasurable.measure(noMinConstraints)
-
- layout(constraints.maxWidth, constraints.maxHeight) {
- aboveLockIconPlaceable.place(
- x = 0,
- y = 0,
- )
- startSideShortcutPlaceable.placeRelative(
- x = lockIconBounds.left / 2 - startSideShortcutPlaceable.width / 2,
- y = lockIconBounds.center.y - startSideShortcutPlaceable.height / 2,
- )
- lockIconPlaceable.place(
- x = lockIconBounds.left,
- y = lockIconBounds.top,
- )
- endSideShortcutPlaceable.placeRelative(
- x =
- lockIconBounds.right +
- (constraints.maxWidth - lockIconBounds.right) / 2 -
- endSideShortcutPlaceable.width / 2,
- y = lockIconBounds.center.y - endSideShortcutPlaceable.height / 2,
- )
- belowLockIconPlaceable.place(
- x = 0,
- y = constraints.maxHeight - belowLockIconPlaceable.height,
- )
- settingsMenuPlaceable.place(
- x = (constraints.maxWidth - settingsMenuPlaceable.width) / 2,
- y = constraints.maxHeight - settingsMenuPlaceable.height,
- )
- }
- }
- }
- }
-}
-
-@Module
-interface ShortcutsBesideUdfpsBlueprintModule {
- @Binds
- @IntoSet
- fun blueprint(blueprint: ShortcutsBesideUdfpsBlueprint): ComposableLockscreenSceneBlueprint
-}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
index 9c72d93..364adca 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/keyguard/ui/composable/section/BottomAreaSection.kt
@@ -32,7 +32,6 @@
import androidx.core.content.res.ResourcesCompat
import com.android.compose.animation.scene.ElementKey
import com.android.compose.animation.scene.SceneScope
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.keyguard.ui.binder.KeyguardIndicationAreaBinder
import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
@@ -40,10 +39,8 @@
import com.android.systemui.keyguard.ui.viewmodel.KeyguardIndicationAreaViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import javax.inject.Inject
import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.flow.Flow
@@ -52,11 +49,9 @@
@Inject
constructor(
private val viewModel: KeyguardQuickAffordancesCombinedViewModel,
- private val falsingManager: FalsingManager,
- private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val indicationAreaViewModel: KeyguardIndicationAreaViewModel,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) {
/**
* Renders a single lockscreen shortcut.
@@ -80,9 +75,8 @@
viewId = if (isStart) R.id.start_button else R.id.end_button,
viewModel = if (isStart) viewModel.startButton else viewModel.endButton,
transitionAlpha = viewModel.transitionAlpha,
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
indicationController = indicationController,
+ binder = keyguardQuickAffordanceViewBinder,
modifier =
if (applyPadding) {
Modifier.shortcutPadding()
@@ -124,9 +118,8 @@
@IdRes viewId: Int,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
transitionAlpha: Flow<Float>,
- falsingManager: FalsingManager,
- vibratorHelper: VibratorHelper,
indicationController: KeyguardIndicationController,
+ binder: KeyguardQuickAffordanceViewBinder,
modifier: Modifier = Modifier,
) {
val (binding, setBinding) = mutableStateOf<KeyguardQuickAffordanceViewBinder.Binding?>(null)
@@ -158,13 +151,10 @@
}
setBinding(
- KeyguardQuickAffordanceViewBinder.bind(
+ binder.bind(
view,
viewModel,
transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
index 84782fd..0bef05dc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/Notifications.kt
@@ -276,6 +276,7 @@
shouldReserveSpaceForNavBar: Boolean = true,
shouldIncludeHeadsUpSpace: Boolean = true,
shadeMode: ShadeMode,
+ onEmptySpaceClick: (() -> Unit)? = null,
modifier: Modifier = Modifier,
) {
val coroutineScope = rememberCoroutineScope()
@@ -328,8 +329,6 @@
// The height of the scrim visible on screen when it is in its resting (collapsed) state.
val minVisibleScrimHeight: () -> Float = { screenHeight - maxScrimTop() }
- val isClickable by viewModel.isClickable.collectAsStateWithLifecycle()
-
// we are not scrolled to the top unless the scrim is at its maximum offset.
LaunchedEffect(viewModel, scrimOffset) {
snapshotFlow { scrimOffset.value >= 0f }
@@ -437,8 +436,8 @@
)
)
}
- .thenIf(isClickable) {
- Modifier.clickable(onClick = { viewModel.onEmptySpaceClicked() })
+ .thenIf(onEmptySpaceClick != null) {
+ Modifier.clickable(onClick = { onEmptySpaceClick?.invoke() })
}
) {
// Creates a cutout in the background scrim in the shape of the notifications scrim.
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
index 7159def..66be7bc 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/notifications/ui/composable/NotificationsShadeScene.kt
@@ -20,15 +20,19 @@
import androidx.compose.foundation.layout.fillMaxWidth
import androidx.compose.foundation.layout.padding
import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
import androidx.compose.ui.Modifier
import androidx.compose.ui.unit.dp
+import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.animation.scene.SceneScope
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
import com.android.systemui.battery.BatteryMeterViewController
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.composable.LockscreenContent
-import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.lifecycle.rememberViewModel
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneContentViewModel
import com.android.systemui.scene.session.ui.composable.SaveableSession
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
@@ -50,9 +54,10 @@
class NotificationsShadeScene
@Inject
constructor(
- sceneViewModel: NotificationsShadeSceneViewModel,
- private val overlayShadeViewModel: OverlayShadeViewModel,
- private val shadeHeaderViewModel: ShadeHeaderViewModel,
+ private val contentViewModelFactory: NotificationsShadeSceneContentViewModel.Factory,
+ private val actionsViewModelFactory: NotificationsShadeSceneActionsViewModel.Factory,
+ private val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory,
+ private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
private val tintedIconManagerFactory: TintedIconManager.Factory,
private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
@@ -64,21 +69,32 @@
override val key = Scenes.NotificationsShade
+ private val actionsViewModel: NotificationsShadeSceneActionsViewModel by lazy {
+ actionsViewModelFactory.create()
+ }
+
override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- sceneViewModel.destinationScenes
+ actionsViewModel.actions
+
+ override suspend fun activate() {
+ actionsViewModel.activate()
+ }
@Composable
override fun SceneScope.Content(
modifier: Modifier,
) {
+ val viewModel = rememberViewModel { contentViewModelFactory.create() }
+ val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle()
+
OverlayShade(
modifier = modifier,
- viewModel = overlayShadeViewModel,
+ viewModelFactory = overlayShadeViewModelFactory,
lockscreenContent = lockscreenContent,
) {
Column {
ExpandedShadeHeader(
- viewModel = shadeHeaderViewModel,
+ viewModelFactory = shadeHeaderViewModelFactory,
createTintedIconManager = tintedIconManagerFactory::create,
createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
statusBarIconController = statusBarIconController,
@@ -94,6 +110,8 @@
shouldFillMaxSize = false,
shouldReserveSpaceForNavBar = false,
shadeMode = ShadeMode.Dual,
+ onEmptySpaceClick =
+ viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
modifier = Modifier.fillMaxWidth(),
)
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
index cdcd840..8bba0f4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsScene.kt
@@ -81,6 +81,7 @@
import com.android.systemui.common.ui.compose.windowinsets.LocalRawScreenHeight
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.controller.MediaCarouselController
import com.android.systemui.media.controls.ui.view.MediaHost
@@ -166,19 +167,23 @@
) {
val cutoutLocation = LocalDisplayCutout.current.location
- val brightnessMirrorShowing by
- viewModel.brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
+ val brightnessMirrorViewModel = rememberViewModel {
+ viewModel.brightnessMirrorViewModelFactory.create()
+ }
+ val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
val contentAlpha by
animateFloatAsState(
targetValue = if (brightnessMirrorShowing) 0f else 1f,
label = "alphaAnimationBrightnessMirrorContentHiding",
)
- viewModel.notifications.setAlphaForBrightnessMirror(contentAlpha)
- DisposableEffect(Unit) { onDispose { viewModel.notifications.setAlphaForBrightnessMirror(1f) } }
+ notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(contentAlpha)
+ DisposableEffect(Unit) {
+ onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) }
+ }
BrightnessMirror(
- viewModel = viewModel.brightnessMirrorViewModel,
+ viewModel = brightnessMirrorViewModel,
qsSceneAdapter = viewModel.qsSceneAdapter,
modifier =
Modifier.thenIf(cutoutLocation != CutoutLocation.CENTER) {
@@ -337,7 +342,7 @@
fadeOut(tween(customizingAnimationDuration)),
) {
ExpandedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
+ viewModelFactory = viewModel.shadeHeaderViewModelFactory,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController =
createBatteryMeterViewController,
@@ -347,7 +352,7 @@
}
else ->
CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
+ viewModelFactory = viewModel.shadeHeaderViewModelFactory,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
@@ -417,7 +422,7 @@
)
NotificationStackCutoffGuideline(
stackScrollView = notificationStackScrollView,
- viewModel = viewModel.notifications,
+ viewModel = notificationsPlaceholderViewModel,
modifier =
Modifier.align(Alignment.BottomCenter).navigationBarsPadding().offset {
IntOffset(x = 0, y = screenHeight.roundToInt())
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
index f6d1283..eea00c4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/qs/ui/composable/QuickSettingsShadeScene.kt
@@ -44,12 +44,14 @@
import com.android.systemui.brightness.ui.compose.BrightnessSliderContainer
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.keyguard.ui.composable.LockscreenContent
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.qs.panels.ui.compose.EditMode
import com.android.systemui.qs.panels.ui.compose.TileGrid
import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutEnter
import com.android.systemui.qs.ui.composable.QuickSettingsShade.Transitions.QuickSettingsLayoutExit
import com.android.systemui.qs.ui.viewmodel.QuickSettingsContainerViewModel
-import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneViewModel
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneActionsViewModel
+import com.android.systemui.qs.ui.viewmodel.QuickSettingsShadeSceneContentViewModel
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.ui.composable.ExpandedShadeHeader
@@ -66,9 +68,10 @@
class QuickSettingsShadeScene
@Inject
constructor(
- private val viewModel: QuickSettingsShadeSceneViewModel,
+ private val actionsViewModelFactory: QuickSettingsShadeSceneActionsViewModel.Factory,
+ private val contentViewModelFactory: QuickSettingsShadeSceneContentViewModel.Factory,
private val lockscreenContent: Lazy<Optional<LockscreenContent>>,
- private val shadeHeaderViewModel: ShadeHeaderViewModel,
+ private val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
private val tintedIconManagerFactory: TintedIconManager.Factory,
private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
private val statusBarIconController: StatusBarIconController,
@@ -76,21 +79,26 @@
override val key = Scenes.QuickSettingsShade
+ private val actionsViewModel: QuickSettingsShadeSceneActionsViewModel by lazy {
+ actionsViewModelFactory.create()
+ }
+
override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- viewModel.destinationScenes
+ actionsViewModel.actions
@Composable
override fun SceneScope.Content(
modifier: Modifier,
) {
+ val viewModel = rememberViewModel { contentViewModelFactory.create() }
OverlayShade(
- viewModel = viewModel.overlayShadeViewModel,
+ viewModelFactory = viewModel.overlayShadeViewModelFactory,
lockscreenContent = lockscreenContent,
modifier = modifier,
) {
Column {
ExpandedShadeHeader(
- viewModel = shadeHeaderViewModel,
+ viewModelFactory = shadeHeaderViewModelFactory,
createTintedIconManager = tintedIconManagerFactory::create,
createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
statusBarIconController = statusBarIconController,
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
index facbcaf..445ffcb 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/OverlayShade.kt
@@ -53,6 +53,7 @@
import com.android.compose.animation.scene.SceneScope
import com.android.compose.windowsizeclass.LocalWindowSizeClass
import com.android.systemui.keyguard.ui.composable.LockscreenContent
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.shared.model.ShadeAlignment
import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
@@ -63,11 +64,12 @@
/** The overlay shade renders a lightweight shade UI container on top of a background scene. */
@Composable
fun SceneScope.OverlayShade(
- viewModel: OverlayShadeViewModel,
+ viewModelFactory: OverlayShadeViewModel.Factory,
lockscreenContent: Lazy<Optional<LockscreenContent>>,
modifier: Modifier = Modifier,
content: @Composable () -> Unit,
) {
+ val viewModel = rememberViewModel { viewModelFactory.create() }
val backgroundScene by viewModel.backgroundScene.collectAsStateWithLifecycle()
Box(modifier) {
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
index 1cd48bf..8c53740 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeHeader.kt
@@ -73,6 +73,7 @@
import com.android.systemui.common.ui.compose.windowinsets.LocalDisplayCutout
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.compose.modifiers.sysuiResTag
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.model.Scenes
@@ -122,12 +123,13 @@
@Composable
fun SceneScope.CollapsedShadeHeader(
- viewModel: ShadeHeaderViewModel,
+ viewModelFactory: ShadeHeaderViewModel.Factory,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
+ val viewModel = rememberViewModel { viewModelFactory.create() }
val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle()
if (isDisabled) {
return
@@ -279,12 +281,13 @@
@Composable
fun SceneScope.ExpandedShadeHeader(
- viewModel: ShadeHeaderViewModel,
+ viewModelFactory: ShadeHeaderViewModel.Factory,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
statusBarIconController: StatusBarIconController,
modifier: Modifier = Modifier,
) {
+ val viewModel = rememberViewModel { viewModelFactory.create() }
val isDisabled by viewModel.isDisabled.collectAsStateWithLifecycle()
if (isDisabled) {
return
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
index 77b48d3..0e3fcf4 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/shade/ui/composable/ShadeScene.kt
@@ -80,6 +80,7 @@
import com.android.systemui.common.ui.compose.windowinsets.LocalScreenCornerRadius
import com.android.systemui.compose.modifiers.sysuiResTag
import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.lifecycle.rememberViewModel
import com.android.systemui.media.controls.ui.composable.MediaCarousel
import com.android.systemui.media.controls.ui.composable.MediaContentPicker
import com.android.systemui.media.controls.ui.composable.shouldElevateMedia
@@ -102,7 +103,8 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.scene.ui.composable.ComposableScene
import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import com.android.systemui.statusbar.phone.StatusBarLocation
@@ -145,7 +147,8 @@
constructor(
private val shadeSession: SaveableSession,
private val notificationStackScrollView: Lazy<NotificationScrollView>,
- private val viewModel: ShadeSceneViewModel,
+ private val actionsViewModelFactory: ShadeSceneActionsViewModel.Factory,
+ private val contentViewModelFactory: ShadeSceneContentViewModel.Factory,
private val notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
private val tintedIconManagerFactory: TintedIconManager.Factory,
private val batteryMeterViewControllerFactory: BatteryMeterViewController.Factory,
@@ -157,12 +160,16 @@
override val key = Scenes.Shade
+ private val actionsViewModel: ShadeSceneActionsViewModel by lazy {
+ actionsViewModelFactory.create()
+ }
+
override suspend fun activate() {
- viewModel.activate()
+ actionsViewModel.activate()
}
override val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- viewModel.destinationScenes
+ actionsViewModel.actions
@Composable
override fun SceneScope.Content(
@@ -170,7 +177,7 @@
) =
ShadeScene(
notificationStackScrollView.get(),
- viewModel = viewModel,
+ viewModel = rememberViewModel { contentViewModelFactory.create() },
notificationsPlaceholderViewModel = notificationsPlaceholderViewModel,
createTintedIconManager = tintedIconManagerFactory::create,
createBatteryMeterViewController = batteryMeterViewControllerFactory::create,
@@ -196,7 +203,7 @@
@Composable
private fun SceneScope.ShadeScene(
notificationStackScrollView: NotificationScrollView,
- viewModel: ShadeSceneViewModel,
+ viewModel: ShadeSceneContentViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -242,7 +249,7 @@
@Composable
private fun SceneScope.SingleShade(
notificationStackScrollView: NotificationScrollView,
- viewModel: ShadeSceneViewModel,
+ viewModel: ShadeSceneContentViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -261,7 +268,7 @@
key = QuickSettings.SharedValues.TilesSquishiness,
canOverflow = false
)
- val isClickable by viewModel.isClickable.collectAsStateWithLifecycle()
+ val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle()
val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle()
val shouldPunchHoleBehindScrim =
@@ -299,9 +306,9 @@
horizontalAlignment = Alignment.CenterHorizontally,
modifier =
Modifier.fillMaxWidth()
- .thenIf(isClickable) {
+ .thenIf(isEmptySpaceClickable) {
Modifier.clickable(
- onClick = { viewModel.onContentClicked() }
+ onClick = { viewModel.onEmptySpaceClicked() }
)
}
.thenIf(cutoutLocation != CutoutLocation.CENTER) {
@@ -309,7 +316,7 @@
},
) {
CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
+ viewModelFactory = viewModel.shadeHeaderViewModelFactory,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
@@ -361,6 +368,8 @@
maxScrimTop = { maxNotifScrimTop.value },
shadeMode = ShadeMode.Single,
shouldPunchHoleBehindScrim = shouldPunchHoleBehindScrim,
+ onEmptySpaceClick =
+ viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
)
},
)
@@ -407,7 +416,7 @@
@Composable
private fun SceneScope.SplitShade(
notificationStackScrollView: NotificationScrollView,
- viewModel: ShadeSceneViewModel,
+ viewModel: ShadeSceneContentViewModel,
notificationsPlaceholderViewModel: NotificationsPlaceholderViewModel,
createTintedIconManager: (ViewGroup, StatusBarLocation) -> TintedIconManager,
createBatteryMeterViewController: (ViewGroup, StatusBarLocation) -> BatteryMeterViewController,
@@ -468,8 +477,10 @@
}
}
- val brightnessMirrorShowing by
- viewModel.brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
+ val brightnessMirrorViewModel = rememberViewModel {
+ viewModel.brightnessMirrorViewModelFactory.create()
+ }
+ val brightnessMirrorShowing by brightnessMirrorViewModel.isShowing.collectAsStateWithLifecycle()
val contentAlpha by
animateFloatAsState(
targetValue = if (brightnessMirrorShowing) 0f else 1f,
@@ -481,6 +492,7 @@
onDispose { notificationsPlaceholderViewModel.setAlphaForBrightnessMirror(1f) }
}
+ val isEmptySpaceClickable by viewModel.isEmptySpaceClickable.collectAsStateWithLifecycle()
val isMediaVisible by viewModel.isMediaVisible.collectAsStateWithLifecycle()
val brightnessMirrorShowingModifier = Modifier.graphicsLayer { alpha = contentAlpha }
@@ -503,7 +515,7 @@
modifier = Modifier.fillMaxSize(),
) {
CollapsedShadeHeader(
- viewModel = viewModel.shadeHeaderViewModel,
+ viewModelFactory = viewModel.shadeHeaderViewModelFactory,
createTintedIconManager = createTintedIconManager,
createBatteryMeterViewController = createBatteryMeterViewController,
statusBarIconController = statusBarIconController,
@@ -522,7 +534,7 @@
.graphicsLayer { translationX = unfoldTranslationXForStartSide },
) {
BrightnessMirror(
- viewModel = viewModel.brightnessMirrorViewModel,
+ viewModel = brightnessMirrorViewModel,
qsSceneAdapter = viewModel.qsSceneAdapter,
// Need to use the offset measured from the container as the header
// has to be accounted for
@@ -591,6 +603,8 @@
shouldPunchHoleBehindScrim = false,
shouldReserveSpaceForNavBar = false,
shadeMode = ShadeMode.Split,
+ onEmptySpaceClick =
+ viewModel::onEmptySpaceClicked.takeIf { isEmptySpaceClickable },
modifier =
Modifier.weight(1f)
.fillMaxHeight()
diff --git a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
index 9da2a1b..5ffb6f8 100644
--- a/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
+++ b/packages/SystemUI/compose/features/src/com/android/systemui/volume/panel/ui/composable/VolumePanelRoot.kt
@@ -50,7 +50,7 @@
) {
val accessibilityTitle = stringResource(R.string.accessibility_volume_settings)
val state: VolumePanelState by viewModel.volumePanelState.collectAsStateWithLifecycle()
- val components by viewModel.componentsLayout.collectAsStateWithLifecycle(null)
+ val components by viewModel.componentsLayout.collectAsStateWithLifecycle()
with(VolumePanelComposeScope(state)) {
components?.let { componentsState ->
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
new file mode 100644
index 0000000..5eabd22
--- /dev/null
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateContent.kt
@@ -0,0 +1,91 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.compose.animation.scene
+
+import androidx.compose.animation.core.Animatable
+import androidx.compose.animation.core.AnimationVector1D
+import androidx.compose.animation.core.SpringSpec
+import com.android.compose.animation.scene.content.state.ContentState
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.CoroutineStart
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.launch
+
+internal fun CoroutineScope.animateContent(
+ transition: ContentState.Transition<*>,
+ oneOffAnimation: OneOffAnimation,
+ targetProgress: Float,
+ startTransition: () -> Unit,
+ finishTransition: () -> Unit,
+) {
+ // Start the transition. This will compute the TransformationSpec associated to [transition],
+ // which we need to initialize the Animatable that will actually animate it.
+ startTransition()
+
+ // The transition now contains the transformation spec that we should use to instantiate the
+ // Animatable.
+ val animationSpec = transition.transformationSpec.progressSpec
+ val visibilityThreshold =
+ (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
+ val replacedTransition = transition.replacedTransition
+ val initialProgress = replacedTransition?.progress ?: 0f
+ val initialVelocity = replacedTransition?.progressVelocity ?: 0f
+ val animatable =
+ Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
+ oneOffAnimation.animatable = it
+ }
+
+ // Animate the progress to its target value.
+ //
+ // Important: We start atomically to make sure that we start the coroutine even if it is
+ // cancelled right after it is launched, so that finishTransition() is correctly called.
+ // Otherwise, this transition will never be stopped and we will never settle to Idle.
+ oneOffAnimation.job =
+ launch(start = CoroutineStart.ATOMIC) {
+ try {
+ animatable.animateTo(targetProgress, animationSpec, initialVelocity)
+ } finally {
+ finishTransition()
+ }
+ }
+}
+
+internal class OneOffAnimation {
+ /**
+ * The animatable used to animate this transition.
+ *
+ * Note: This is lateinit because we need to first create this object so that
+ * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
+ * the transition, which is needed to initialize this Animatable.
+ */
+ lateinit var animatable: Animatable<Float, AnimationVector1D>
+
+ /** The job that is animating [animatable]. */
+ lateinit var job: Job
+
+ val progress: Float
+ get() = animatable.value
+
+ val progressVelocity: Float
+ get() = animatable.velocity
+
+ fun finish(): Job = job
+}
+
+// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
+// and screen density.
+internal const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
index 1fc1f98..68a6c98 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/AnimateToScene.kt
@@ -16,15 +16,10 @@
package com.android.compose.animation.scene
-import androidx.compose.animation.core.Animatable
-import androidx.compose.animation.core.AnimationVector1D
-import androidx.compose.animation.core.SpringSpec
import com.android.compose.animation.scene.content.state.TransitionState
import kotlin.math.absoluteValue
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.CoroutineStart
import kotlinx.coroutines.Job
-import kotlinx.coroutines.launch
/**
* Transition to [target] using a canned animation. This function will try to be smart and take over
@@ -50,7 +45,7 @@
return when (transitionState) {
is TransitionState.Idle -> {
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
@@ -80,13 +75,11 @@
} else {
// The transition is in progress: start the canned animation at the same
// progress as it was in.
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
isInitiatedByUserInput,
- initialProgress = progress,
- initialVelocity = transitionState.progressVelocity,
replacedTransition = transitionState,
)
}
@@ -102,13 +95,11 @@
layoutState.finishTransition(transitionState, target)
null
} else {
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
isInitiatedByUserInput,
- initialProgress = progress,
- initialVelocity = transitionState.progressVelocity,
reversed = true,
replacedTransition = transitionState,
)
@@ -140,7 +131,7 @@
animateToScene(layoutState, animateFrom, transitionKey = null)
}
- animate(
+ animateToScene(
layoutState,
target,
transitionKey,
@@ -154,103 +145,68 @@
}
}
-private fun CoroutineScope.animate(
+private fun CoroutineScope.animateToScene(
layoutState: MutableSceneTransitionLayoutStateImpl,
targetScene: SceneKey,
transitionKey: TransitionKey?,
isInitiatedByUserInput: Boolean,
replacedTransition: TransitionState.Transition?,
- initialProgress: Float = 0f,
- initialVelocity: Float = 0f,
reversed: Boolean = false,
fromScene: SceneKey = layoutState.transitionState.currentScene,
chain: Boolean = true,
): TransitionState.Transition {
+ val oneOffAnimation = OneOffAnimation()
val targetProgress = if (reversed) 0f else 1f
val transition =
if (reversed) {
- OneOffTransition(
+ OneOffSceneTransition(
key = transitionKey,
fromScene = targetScene,
toScene = fromScene,
currentScene = targetScene,
isInitiatedByUserInput = isInitiatedByUserInput,
- isUserInputOngoing = false,
replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
)
} else {
- OneOffTransition(
+ OneOffSceneTransition(
key = transitionKey,
fromScene = fromScene,
toScene = targetScene,
currentScene = targetScene,
isInitiatedByUserInput = isInitiatedByUserInput,
- isUserInputOngoing = false,
replacedTransition = replacedTransition,
+ oneOffAnimation = oneOffAnimation,
)
}
- // Change the current layout state to start this new transition. This will compute the
- // TransformationSpec associated to this transition, which we need to initialize the Animatable
- // that will actually animate it.
- layoutState.startTransition(transition, chain)
-
- // The transition now contains the transformation spec that we should use to instantiate the
- // Animatable.
- val animationSpec = transition.transformationSpec.progressSpec
- val visibilityThreshold =
- (animationSpec as? SpringSpec)?.visibilityThreshold ?: ProgressVisibilityThreshold
- val animatable =
- Animatable(initialProgress, visibilityThreshold = visibilityThreshold).also {
- transition.animatable = it
- }
-
- // Animate the progress to its target value.
- // Important: We start atomically to make sure that we start the coroutine even if it is
- // cancelled right after it is launched, so that finishTransition() is correctly called.
- // Otherwise, this transition will never be stopped and we will never settle to Idle.
- transition.job =
- launch(start = CoroutineStart.ATOMIC) {
- try {
- animatable.animateTo(targetProgress, animationSpec, initialVelocity)
- } finally {
- layoutState.finishTransition(transition, targetScene)
- }
- }
+ animateContent(
+ transition = transition,
+ oneOffAnimation = oneOffAnimation,
+ targetProgress = targetProgress,
+ startTransition = { layoutState.startTransition(transition, chain) },
+ finishTransition = { layoutState.finishTransition(transition, targetScene) },
+ )
return transition
}
-private class OneOffTransition(
+private class OneOffSceneTransition(
override val key: TransitionKey?,
fromScene: SceneKey,
toScene: SceneKey,
override val currentScene: SceneKey,
override val isInitiatedByUserInput: Boolean,
- override val isUserInputOngoing: Boolean,
replacedTransition: TransitionState.Transition?,
+ private val oneOffAnimation: OneOffAnimation,
) : TransitionState.Transition(fromScene, toScene, replacedTransition) {
- /**
- * The animatable used to animate this transition.
- *
- * Note: This is lateinit because we need to first create this Transition object so that
- * [SceneTransitionLayoutState] can compute the transformations and animation spec associated to
- * it, which is need to initialize this Animatable.
- */
- lateinit var animatable: Animatable<Float, AnimationVector1D>
-
- /** The job that is animating [animatable]. */
- lateinit var job: Job
-
override val progress: Float
- get() = animatable.value
+ get() = oneOffAnimation.progress
override val progressVelocity: Float
- get() = animatable.velocity
+ get() = oneOffAnimation.progressVelocity
- override fun finish(): Job = job
+ override val isUserInputOngoing: Boolean = false
+
+ override fun finish(): Job = oneOffAnimation.finish()
}
-
-// TODO(b/290184746): Compute a good default visibility threshold that depends on the layout size
-// and screen density.
-internal const val ProgressVisibilityThreshold = 1e-3f
diff --git a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
index 0105af3..fe16ef751 100644
--- a/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
+++ b/packages/SystemUI/compose/scene/src/com/android/compose/animation/scene/PredictiveBackHandler.kt
@@ -127,16 +127,7 @@
return coroutineScope
.launch(start = CoroutineStart.ATOMIC) {
try {
- if (currentScene == toScene) {
- animatable.animateTo(targetProgress, transformationSpec.progressSpec)
- } else {
- // If the back gesture is cancelled, the progress is animated back to 0f by
- // the system. But we need this animate call anyways because
- // PredictiveBackHandler doesn't guarantee that it ends at 0f. Since the
- // remaining change in progress is usually very small, the progressSpec is
- // omitted and the default spring spec used instead.
- animatable.animateTo(targetProgress)
- }
+ animatable.animateTo(targetProgress)
} finally {
state.finishTransition(this@PredictiveBackTransition, scene)
}
diff --git a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
index c414fbe..0eaecb0 100644
--- a/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
+++ b/packages/SystemUI/compose/scene/tests/src/com/android/compose/animation/scene/PredictiveBackHandlerTest.kt
@@ -18,8 +18,6 @@
import androidx.activity.BackEventCompat
import androidx.activity.ComponentActivity
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.tween
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.runtime.rememberCoroutineScope
@@ -61,23 +59,7 @@
@Test
fun testPredictiveBack() {
- val transitionFrames = 2
- val layoutState =
- rule.runOnUiThread {
- MutableSceneTransitionLayoutState(
- SceneA,
- transitions =
- transitions {
- from(SceneA, to = SceneB) {
- spec =
- tween(
- durationMillis = transitionFrames * 16,
- easing = LinearEasing
- )
- }
- }
- )
- }
+ val layoutState = rule.runOnUiThread { MutableSceneTransitionLayoutState(SceneA) }
rule.setContent {
SceneTransitionLayout(layoutState) {
scene(SceneA, mapOf(Back to SceneB)) { Box(Modifier.fillMaxSize()) }
@@ -106,27 +88,12 @@
assertThat(layoutState.transitionState).hasCurrentScene(SceneA)
assertThat(layoutState.transitionState).isIdle()
- rule.mainClock.autoAdvance = false
-
// Start again and commit it.
rule.runOnUiThread {
dispatcher.dispatchOnBackStarted(backEvent())
dispatcher.dispatchOnBackProgressed(backEvent(progress = 0.4f))
dispatcher.onBackPressed()
}
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
- val transition2 = assertThat(layoutState.transitionState).isTransition()
- // verify that transition picks up progress from preview
- assertThat(transition2).hasProgress(0.4f, tolerance = 0.0001f)
-
- rule.mainClock.advanceTimeByFrame()
- rule.waitForIdle()
- // verify that transition is half way between preview-end-state (0.4f) and target-state (1f)
- // after one frame
- assertThat(transition2).hasProgress(0.7f, tolerance = 0.0001f)
-
- rule.mainClock.autoAdvance = true
rule.waitForIdle()
assertThat(layoutState.transitionState).hasCurrentScene(SceneB)
assertThat(layoutState.transitionState).isIdle()
diff --git a/packages/SystemUI/lint-baseline.xml b/packages/SystemUI/lint-baseline.xml
index b4c839f..7577147 100644
--- a/packages/SystemUI/lint-baseline.xml
+++ b/packages/SystemUI/lint-baseline.xml
@@ -32157,4 +32157,631 @@
column="6"/>
</issue>
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/ActiveUnlockConfig.kt"
+ line="154"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(ALWAYS_ON_DISPLAY_CONSTANTS_URI,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/doze/AlwaysOnDisplayPolicy.java"
+ line="164"
+ column="22"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt"
+ line="61"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" awaitClose { resolver.unregisterContentObserver(observer) }"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/util/animation/data/repository/AnimationStatusRepository.kt"
+ line="67"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="123"
+ column="38"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="180"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="211"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/battery/BatteryMeterViewController.java"
+ line="219"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="46"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="48"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" resolver.unregisterContentObserver(allowedObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="54"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" resolver.unregisterContentObserver(onObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/demomode/DemoModeAvailabilityTracker.kt"
+ line="55"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mQuickPickupGesture, false, this,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="511"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mPickupGesture, false, this, UserHandle.USER_ALL);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="513"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(mAlwaysOnEnabled, false, this,"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/phone/DozeParameters.java"
+ line="514"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="2470"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3077"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3168"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeviceProvisionedObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3957"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mTimeFormatChangeObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/keyguard/KeyguardUpdateMonitor.java"
+ line="3961"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="486"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="492"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/lockscreen/LockscreenSmartspaceController.kt"
+ line="543"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt"
+ line="82"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" contentResolver.unregisterContentObserver(settingsObserver)"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/smartspace/filters/LockscreenTargetFilter.kt"
+ line="100"
+ column="25"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuTargetFeaturesContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="275"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuSizeContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="276"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mMenuFadeOutContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/floatingmenu/MenuInfoRepository.java"
+ line="277"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor(Global.MOBILE_DATA),"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="200"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(Global.getUriFor("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="202"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/connectivity/MobileSignalController.java"
+ line="212"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" context.contentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/NaturalScrollingSettingObserver.kt"
+ line="54"
+ column="33"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="253"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="256"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="259"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="262"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mAssistContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/navigationbar/NavBarHelper.java"
+ line="295"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java"
+ line="395"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/NotificationLockscreenUserManagerImpl.java"
+ line="400"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java"
+ line="3675"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mSettingsChangeObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/shade/NotificationPanelViewController.java"
+ line="4705"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver(Settings.Global.getUriFor("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="191"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="205"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" resolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/power/PowerUI.java"
+ line="216"
+ column="18"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java"
+ line="178"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(mDeveloperSettingsObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/qs/QSFooterView.java"
+ line="186"
+ column="39"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java"
+ line="83"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mContentObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/accessibility/SecureSettingsContentObserver.java"
+ line="100"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="219"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContentResolver.unregisterContentObserver(mObserver);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="241"
+ column="26"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContentResolver.registerContentObserver(uri, false, mObserver, mCurrentUser);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/tuner/TunerServiceImpl.java"
+ line="243"
+ column="30"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_URI, false, this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1235"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mContext.getContentResolver().registerContentObserver(ZEN_MODE_CONFIG_URI, false, this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1236"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mContext.getContentResolver().unregisterContentObserver(this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/volume/VolumeDialogControllerImpl.java"
+ line="1240"
+ column="43"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.unregisterContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.unregisterContentObserver()`"
+ errorLine1=" mResolver.unregisterContentObserver(this);"
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="377"
+ column="27"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="379"
+ column="23"/>
+ </issue>
+
+ <issue
+ id="RegisterContentObserverViaContentResolver"
+ message="`ContentResolver.registerContentObserver()` should be replaced with an appropriate interface API call, for eg.`<SettingsProxy>/<UserSettingsProxy>.registerContentObserver()`"
+ errorLine1=" mResolver.registerContentObserver("
+ errorLine2=" ~~~~~~~~~~~~~~~~~~~~~~~">
+ <location
+ file="frameworks/base/packages/SystemUI/src/com/android/systemui/statusbar/policy/ZenModeControllerImpl.java"
+ line="381"
+ column="23"/>
+ </issue>
+
</issues>
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
index 4850085..d244482 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerFullscreenSwipeTouchHandlerTest.java
@@ -62,7 +62,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
-@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+@EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@DisableFlags(Flags.FLAG_COMMUNAL_BOUNCER_DO_NOT_MODIFY_PLUGIN_OPEN)
public class BouncerFullscreenSwipeTouchHandlerTest extends SysuiTestCase {
private KosmosJavaAdapter mKosmos;
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
index 0e98b84..b85e32b 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandlerTest.java
@@ -74,7 +74,7 @@
@SmallTest
@RunWith(AndroidJUnit4.class)
-@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+@DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
public class BouncerSwipeTouchHandlerTest extends SysuiTestCase {
private KosmosJavaAdapter mKosmos;
@Mock
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
index 204d4b0..38ea4497 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/ambient/touch/ShadeTouchHandlerTest.kt
@@ -79,7 +79,7 @@
// Verifies that a swipe down in the gesture region is captured by the shade touch handler.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testSwipeDown_captured() {
val captured = swipe(Direction.DOWN)
Truth.assertThat(captured).isTrue()
@@ -87,7 +87,7 @@
// Verifies that a swipe in the upward direction is not captured.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testSwipeUp_notCaptured() {
val captured = swipe(Direction.UP)
@@ -97,7 +97,7 @@
// Verifies that a swipe down forwards captured touches to central surfaces for handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeDown_communalEnabled_sentToCentralSurfaces() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -110,7 +110,7 @@
// Verifies that a swipe down forwards captured touches to the shade view for handling.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testSwipeDown_communalDisabled_sentToShadeView() {
swipe(Direction.DOWN)
@@ -121,7 +121,7 @@
// Verifies that a swipe down while dreaming forwards captured touches to the shade view for
// handling.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testSwipeDown_dreaming_sentToShadeView() {
whenever(mDreamManager.isDreaming).thenReturn(true)
swipe(Direction.DOWN)
@@ -132,7 +132,7 @@
// Verifies that a swipe up is not forwarded to central surfaces.
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
@EnableFlags(Flags.FLAG_COMMUNAL_HUB)
fun testSwipeUp_communalEnabled_touchesNotSent() {
kosmos.fakeFeatureFlagsClassic.set(COMMUNAL_SERVICE_ENABLED, true)
@@ -146,7 +146,7 @@
// Verifies that a swipe up is not forwarded to the shade view.
@Test
- @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_COMMUNAL_HUB, Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testSwipeUp_communalDisabled_touchesNotSent() {
swipe(Direction.UP)
@@ -156,7 +156,7 @@
}
@Test
- @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @DisableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testCancelMotionEvent_popsTouchSession() {
swipe(Direction.DOWN)
val event = MotionEvent.obtain(0, 0, MotionEvent.ACTION_CANCEL, 0f, 0f, 0)
@@ -165,7 +165,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testFullVerticalSwipe_initiatedWhenAvailable() {
// Indicate touches are available
mTouchHandler.onGlanceableTouchAvailable(true)
@@ -176,7 +176,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testFullVerticalSwipe_notInitiatedWhenNotAvailable() {
// Indicate touches aren't available
mTouchHandler.onGlanceableTouchAvailable(false)
@@ -187,7 +187,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testFullVerticalSwipe_resetsTouchStateOnUp() {
// Indicate touches are available
mTouchHandler.onGlanceableTouchAvailable(true)
@@ -203,7 +203,7 @@
}
@Test
- @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE)
+ @EnableFlags(Flags.FLAG_HUBMODE_FULLSCREEN_VERTICAL_SWIPE_FIX)
fun testFullVerticalSwipe_resetsTouchStateOnCancel() {
// Indicate touches are available
mTouchHandler.onGlanceableTouchAvailable(true)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
index 9b1d4ec..752c93e 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/AuthControllerTest.java
@@ -148,8 +148,6 @@
@Mock
private WakefulnessLifecycle mWakefulnessLifecycle;
@Mock
- private AuthDialogPanelInteractionDetector mPanelInteractionDetector;
- @Mock
private UserManager mUserManager;
@Mock
private LockPatternUtils mLockPatternUtils;
@@ -1059,10 +1057,9 @@
super(context, null /* applicationCoroutineScope */,
mExecution, mCommandQueue, mActivityTaskManager, mWindowManager,
mFingerprintManager, mFaceManager, () -> mUdfpsController, mDisplayManager,
- mWakefulnessLifecycle, mPanelInteractionDetector, mUserManager,
- mLockPatternUtils, () -> mUdfpsLogger, () -> mLogContextInteractor,
- () -> mPromptSelectionInteractor, () -> mCredentialViewModel,
- () -> mPromptViewModel, mInteractionJankMonitor,
+ mWakefulnessLifecycle, mUserManager, mLockPatternUtils, () -> mUdfpsLogger,
+ () -> mLogContextInteractor, () -> mPromptSelectionInteractor,
+ () -> mCredentialViewModel, () -> mPromptViewModel, mInteractionJankMonitor,
mHandler, mBackgroundExecutor, mUdfpsUtils, mVibratorHelper);
}
@@ -1071,7 +1068,6 @@
boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
WakefulnessLifecycle wakefulnessLifecycle,
- AuthDialogPanelInteractionDetector panelInteractionDetector,
UserManager userManager,
LockPatternUtils lockPatternUtils, PromptViewModel viewModel) {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
deleted file mode 100644
index cd9189b..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapterTest.java
+++ /dev/null
@@ -1,141 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import static org.junit.Assert.assertEquals;
-
-import android.hardware.biometrics.ComponentInfoInternal;
-import android.hardware.biometrics.SensorLocationInternal;
-import android.hardware.biometrics.SensorProperties;
-import android.hardware.fingerprint.FingerprintSensorProperties;
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
-
-import androidx.test.ext.junit.runners.AndroidJUnit4;
-import androidx.test.filters.SmallTest;
-
-import com.android.systemui.SysuiTestCase;
-
-import org.junit.Test;
-import org.junit.runner.RunWith;
-
-import java.util.ArrayList;
-import java.util.List;
-
-@RunWith(AndroidJUnit4.class)
-@SmallTest
-public class UdfpsDialogMeasureAdapterTest extends SysuiTestCase {
- @Test
- public void testUdfpsBottomSpacerHeightForPortrait() {
- final int displayHeightPx = 3000;
- final int navbarHeightPx = 10;
- final int dialogBottomMarginPx = 20;
- final int buttonBarHeightPx = 100;
- final int textIndicatorHeightPx = 200;
-
- final int sensorLocationX = 540;
- final int sensorLocationY = 1600;
- final int sensorRadius = 100;
-
- final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
- componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */,
- "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
- "00000001" /* serialNumber */, "" /* softwareVersion */));
- componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */,
- "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */));
-
- final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal(
- 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
- componentInfo,
- FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
- true /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- List.of(new SensorLocationInternal("" /* displayId */,
- sensorLocationX, sensorLocationY, sensorRadius)));
-
- assertEquals(970,
- UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForPortrait(
- props, displayHeightPx, textIndicatorHeightPx, buttonBarHeightPx,
- dialogBottomMarginPx, navbarHeightPx, 1.0f /* resolutionScale */
- ));
- }
-
- @Test
- public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceAboveIcon() {
- final int titleHeightPx = 320;
- final int subtitleHeightPx = 240;
- final int descriptionHeightPx = 200;
- final int topSpacerHeightPx = 550;
- final int textIndicatorHeightPx = 190;
- final int buttonBarHeightPx = 160;
- final int navbarBottomInsetPx = 75;
-
- assertEquals(885,
- UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape(
- titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx,
- textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx));
- }
-
- @Test
- public void testUdfpsBottomSpacerHeightForLandscape_whenMoreSpaceBelowIcon() {
- final int titleHeightPx = 315;
- final int subtitleHeightPx = 160;
- final int descriptionHeightPx = 75;
- final int topSpacerHeightPx = 220;
- final int textIndicatorHeightPx = 290;
- final int buttonBarHeightPx = 360;
- final int navbarBottomInsetPx = 205;
-
- assertEquals(-85,
- UdfpsDialogMeasureAdapter.calculateBottomSpacerHeightForLandscape(
- titleHeightPx, subtitleHeightPx, descriptionHeightPx, topSpacerHeightPx,
- textIndicatorHeightPx, buttonBarHeightPx, navbarBottomInsetPx));
- }
-
- @Test
- public void testUdfpsHorizontalSpacerWidthForLandscape() {
- final int displayWidthPx = 3000;
- final int dialogMarginPx = 20;
- final int navbarHorizontalInsetPx = 75;
-
- final int sensorLocationX = 540;
- final int sensorLocationY = 1600;
- final int sensorRadius = 100;
-
- final List<ComponentInfoInternal> componentInfo = new ArrayList<>();
- componentInfo.add(new ComponentInfoInternal("faceSensor" /* componentId */,
- "vendor/model/revision" /* hardwareVersion */, "1.01" /* firmwareVersion */,
- "00000001" /* serialNumber */, "" /* softwareVersion */));
- componentInfo.add(new ComponentInfoInternal("matchingAlgorithm" /* componentId */,
- "" /* hardwareVersion */, "" /* firmwareVersion */, "" /* serialNumber */,
- "vendor/version/revision" /* softwareVersion */));
-
- final FingerprintSensorPropertiesInternal props = new FingerprintSensorPropertiesInternal(
- 0 /* sensorId */, SensorProperties.STRENGTH_STRONG, 5 /* maxEnrollmentsPerUser */,
- componentInfo,
- FingerprintSensorProperties.TYPE_UDFPS_OPTICAL,
- true /* halControlsIllumination */,
- true /* resetLockoutRequiresHardwareAuthToken */,
- List.of(new SensorLocationInternal("" /* displayId */,
- sensorLocationX, sensorLocationY, sensorRadius)));
-
- assertEquals(1205,
- UdfpsDialogMeasureAdapter.calculateHorizontalSpacerWidthForLandscape(
- props, displayWidthPx, dialogMarginPx, navbarHorizontalInsetPx,
- 1.0f /* resolutionScale */));
- }
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
index ad73853..d6712f0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/domain/interactor/CommunalSceneTransitionInteractorTest.kt
@@ -225,7 +225,7 @@
kosmos.fakeKeyguardRepository.setKeyguardOccluded(true)
kosmos.fakeKeyguardRepository.setDreaming(true)
kosmos.fakeKeyguardRepository.setDreamingWithOverlay(true)
- advanceTimeBy(100L)
+ advanceTimeBy(600L)
sceneTransitions.value = hubToBlank
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
index 1255248..cc945d6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/communal/view/viewmodel/CommunalViewModelTest.kt
@@ -533,6 +533,8 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
+ advanceTimeBy(600L)
+
keyguardRepository.setDreaming(true)
keyguardRepository.setDreamingWithOverlay(true)
advanceTimeBy(60L)
@@ -641,6 +643,7 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
+ advanceTimeBy(600L)
keyguardRepository.setDreaming(true)
keyguardRepository.setDreamingWithOverlay(true)
advanceTimeBy(60L)
@@ -699,6 +702,7 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
+ advanceTimeBy(600L)
keyguardRepository.setDreaming(true)
keyguardRepository.setDreamingWithOverlay(true)
advanceTimeBy(60L)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
index 6412276..3895595 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayContainerViewControllerTest.java
@@ -62,6 +62,7 @@
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.ArgumentMatchers;
import org.mockito.Mock;
import org.mockito.MockitoAnnotations;
@@ -324,4 +325,13 @@
// enabled.
mController.onViewAttached();
}
+
+ @Test
+ public void destroy_cleansUpState() {
+ mController.destroy();
+ verify(mStateController).removeCallback(any());
+ verify(mAmbientStatusBarViewController).destroy();
+ verify(mComplicationHostViewController).destroy();
+ verify(mLowLightTransitionCoordinator).setLowLightEnterListener(ArgumentMatchers.isNull());
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
index 5c09777..7a86e57 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/dreams/DreamOverlayServiceTest.kt
@@ -596,6 +596,9 @@
// are created.
verify(mDreamOverlayComponent).getDreamOverlayContainerViewController()
verify(mAmbientTouchComponent).getTouchMonitor()
+
+ // Verify DreamOverlayContainerViewController is destroyed.
+ verify(mDreamOverlayContainerViewController).destroy()
}
@Test
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
new file mode 100644
index 0000000..1f73347
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/education/domain/ui/view/ContextualEduUiCoordinatorTest.kt
@@ -0,0 +1,82 @@
+/*
+ * Copyright 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.education.domain.ui.view
+
+import android.content.applicationContext
+import android.widget.Toast
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.contextualeducation.GestureType.BACK
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
+import com.android.systemui.education.domain.interactor.contextualEducationInteractor
+import com.android.systemui.education.domain.interactor.keyboardTouchpadEduInteractor
+import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
+import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
+import com.android.systemui.kosmos.applicationCoroutineScope
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.testKosmos
+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.kotlin.verify
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class ContextualEduUiCoordinatorTest : SysuiTestCase() {
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val interactor = kosmos.contextualEducationInteractor
+ private lateinit var underTest: ContextualEduUiCoordinator
+ @Mock private lateinit var toast: Toast
+
+ @get:Rule val mockitoRule = MockitoJUnit.rule()
+
+ @Before
+ fun setUp() {
+ val viewModel =
+ ContextualEduViewModel(
+ kosmos.applicationContext.resources,
+ kosmos.keyboardTouchpadEduInteractor
+ )
+ underTest =
+ ContextualEduUiCoordinator(kosmos.applicationCoroutineScope, viewModel) { _ -> toast }
+ underTest.start()
+ kosmos.keyboardTouchpadEduInteractor.start()
+ }
+
+ @Test
+ @OptIn(kotlinx.coroutines.ExperimentalCoroutinesApi::class)
+ fun showToastOnNewEdu() =
+ testScope.runTest {
+ triggerEducation(BACK)
+ runCurrent()
+ verify(toast).show()
+ }
+
+ private suspend fun triggerEducation(gestureType: GestureType) {
+ for (i in 1..KeyboardTouchpadEduInteractor.MAX_SIGNAL_COUNT) {
+ interactor.incrementSignalCount(gestureType)
+ }
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
similarity index 83%
rename from packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
index 032794c..638c957 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractorTest.kt
@@ -14,22 +14,6 @@
* limitations under the License.
*/
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
package com.android.systemui.keyguard.domain.interactor
import android.platform.test.annotations.EnableFlags
@@ -46,17 +30,18 @@
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.util.KeyguardTransitionRepositorySpySubject.Companion.assertThat
import com.android.systemui.kosmos.testScope
-import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAsleepForTest
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.statusbar.domain.interactor.keyguardOcclusionInteractor
import com.android.systemui.testKosmos
-import kotlin.test.Test
+import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.runBlocking
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
import org.junit.Ignore
+import org.junit.Test
import org.junit.runner.RunWith
import org.mockito.Mockito.reset
import org.mockito.Mockito.spy
@@ -79,21 +64,6 @@
@Before
fun setup() {
underTest.start()
-
- kosmos.fakeKeyguardRepository.setDreaming(true)
- kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true)
-
- // Transition to DOZING and set the power interactor asleep.
- powerInteractor.setAsleepForTest()
- runBlocking {
- transitionRepository.sendTransitionSteps(
- from = KeyguardState.LOCKSCREEN,
- to = KeyguardState.DREAMING,
- testScope
- )
- kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.NONE)
- reset(transitionRepository)
- }
}
@Test
@@ -146,20 +116,27 @@
@EnableFlags(Flags.FLAG_KEYGUARD_WM_STATE_REFACTOR)
fun testTransitionsToLockscreen_whenOccludingActivityEnds() =
testScope.runTest {
+ runCurrent()
+
kosmos.fakeKeyguardRepository.setDreaming(true)
- kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = true)
+ kosmos.keyguardOcclusionInteractor.setWmNotifiedShowWhenLockedActivityOnTop(true)
+ // Transition to DREAMING and set the power interactor awake
+ powerInteractor.setAwakeForTest()
+
transitionRepository.sendTransitionSteps(
from = KeyguardState.LOCKSCREEN,
to = KeyguardState.DREAMING,
- testScope,
+ testScope
)
- runCurrent()
+ kosmos.fakeKeyguardRepository.setBiometricUnlockState(BiometricUnlockMode.NONE)
+ // Get past initial setup
+ advanceTimeBy(600L)
reset(transitionRepository)
kosmos.keyguardOcclusionRepository.setShowWhenLockedActivityInfo(onTop = false)
kosmos.fakeKeyguardRepository.setDreaming(false)
- runCurrent()
+ advanceTimeBy(60L)
assertThat(transitionRepository)
.startedTransition(
@@ -171,6 +148,13 @@
@Test
fun testTransitionToAlternateBouncer() =
testScope.runTest {
+ transitionRepository.sendTransitionSteps(
+ from = KeyguardState.LOCKSCREEN,
+ to = KeyguardState.DREAMING,
+ testScope,
+ )
+ reset(transitionRepository)
+
kosmos.fakeKeyguardBouncerRepository.setAlternateVisible(true)
runCurrent()
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
index fc827a14..ebefb4d 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractorTest.kt
@@ -33,11 +33,15 @@
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.data.repository.fakeKeyguardTransitionRepository
import com.android.systemui.keyguard.shared.model.CameraLaunchType
+import com.android.systemui.keyguard.shared.model.DozeStateModel
+import com.android.systemui.keyguard.shared.model.DozeTransitionModel
import com.android.systemui.keyguard.shared.model.KeyguardState
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.keyguard.shared.model.TransitionState
import com.android.systemui.keyguard.shared.model.TransitionStep
import com.android.systemui.kosmos.testScope
+import com.android.systemui.power.domain.interactor.PowerInteractor.Companion.setAwakeForTest
+import com.android.systemui.power.domain.interactor.powerInteractor
import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -47,6 +51,7 @@
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
+import kotlinx.coroutines.test.advanceTimeBy
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Before
@@ -67,6 +72,7 @@
private val configRepository by lazy { kosmos.fakeConfigurationRepository }
private val bouncerRepository by lazy { kosmos.keyguardBouncerRepository }
private val shadeRepository by lazy { kosmos.shadeRepository }
+ private val powerInteractor by lazy { kosmos.powerInteractor }
private val keyguardTransitionRepository by lazy { kosmos.fakeKeyguardTransitionRepository }
private val transitionState: MutableStateFlow<ObservableTransitionState> =
@@ -350,6 +356,59 @@
}
@Test
+ fun isAbleToDream_falseWhenDozing() =
+ testScope.runTest {
+ val isAbleToDream by collectLastValue(underTest.isAbleToDream)
+
+ repository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.INITIALIZED, to = DozeStateModel.DOZE_AOD)
+ )
+
+ assertThat(isAbleToDream).isEqualTo(false)
+ }
+
+ @Test
+ fun isAbleToDream_falseWhenNotDozingAndNotDreaming() =
+ testScope.runTest {
+ val isAbleToDream by collectLastValue(underTest.isAbleToDream)
+
+ repository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ powerInteractor.setAwakeForTest()
+ advanceTimeBy(1000L)
+
+ assertThat(isAbleToDream).isEqualTo(false)
+ }
+
+ @Test
+ fun isAbleToDream_trueWhenNotDozingAndIsDreaming_afterDelay() =
+ testScope.runTest {
+ val isAbleToDream by collectLastValue(underTest.isAbleToDream)
+ runCurrent()
+
+ repository.setDreaming(true)
+ repository.setDozeTransitionModel(
+ DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
+ )
+ powerInteractor.setAwakeForTest()
+ runCurrent()
+
+ // After some delay, still false
+ advanceTimeBy(300L)
+ assertThat(isAbleToDream).isEqualTo(false)
+
+ // After more delay, is true
+ advanceTimeBy(300L)
+ assertThat(isAbleToDream).isEqualTo(true)
+
+ // Also changes back after the minimal debounce
+ repository.setDreaming(false)
+ advanceTimeBy(55L)
+ assertThat(isAbleToDream).isEqualTo(false)
+ }
+
+ @Test
@EnableSceneContainer
fun animationDozingTransitions() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
index 9762fd8..8c1e8de 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionScenariosTest.kt
@@ -258,7 +258,7 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- runCurrent()
+ advanceTimeBy(600L)
// GIVEN a prior transition has run to LOCKSCREEN
runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
@@ -287,7 +287,7 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- runCurrent()
+ advanceTimeBy(600L)
// GIVEN a prior transition has run to LOCKSCREEN
runTransitionAndSetWakefulness(KeyguardState.GONE, KeyguardState.LOCKSCREEN)
@@ -625,6 +625,9 @@
@DisableSceneContainer
fun dreamingToGoneWithKeyguardNotShowing() =
testScope.runTest {
+ // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream]
+ advanceTimeBy(600L)
+
// GIVEN a prior transition has run to DREAMING
keyguardRepository.setDreamingWithOverlay(true)
runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.DREAMING)
@@ -754,15 +757,35 @@
}
@Test
+ @BrokenWithSceneContainer(339465026)
+ fun goneToOccluded() =
+ testScope.runTest {
+ // GIVEN a prior transition has run to GONE
+ runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
+
+ // WHEN an occluding app is running and showDismissibleKeyguard is called
+ keyguardRepository.setKeyguardOccluded(true)
+ keyguardRepository.showDismissibleKeyguard()
+ runCurrent()
+
+ assertThat(transitionRepository)
+ .startedTransition(
+ from = KeyguardState.GONE,
+ to = KeyguardState.OCCLUDED,
+ ownerName =
+ "FromGoneTransitionInteractor" + "(Dismissible keyguard with occlusion)",
+ animatorAssertion = { it.isNotNull() }
+ )
+
+ coroutineContext.cancelChildren()
+ }
+
+ @Test
@DisableSceneContainer
fun goneToDreaming() =
testScope.runTest {
- // GIVEN a device that is not dreaming or dozing
- keyguardRepository.setDreamingWithOverlay(false)
- keyguardRepository.setDozeTransitionModel(
- DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
- )
- runCurrent()
+ // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream]
+ advanceTimeBy(600L)
// GIVEN a prior transition has run to GONE
runTransitionAndSetWakefulness(KeyguardState.LOCKSCREEN, KeyguardState.GONE)
@@ -1130,6 +1153,9 @@
@DisableSceneContainer
fun primaryBouncerToGlanceableHubWhileDreaming() =
testScope.runTest {
+ // Setup - Move past initial delay with [KeyguardInteractor#isAbleToDream]
+ advanceTimeBy(600L)
+
// GIVEN the device is idle on the glanceable hub
communalSceneInteractor.changeScene(CommunalScenes.Communal)
runCurrent()
@@ -1144,6 +1170,7 @@
// GIVEN that we are dreaming and occluded
keyguardRepository.setDreaming(true)
keyguardRepository.setKeyguardOccluded(true)
+ advanceTimeBy(60L)
// WHEN the primaryBouncer stops showing
bouncerRepository.setPrimaryShow(false)
@@ -2181,12 +2208,14 @@
@DisableFlags(FLAG_COMMUNAL_SCENE_KTF_REFACTOR)
fun glanceableHubToDreaming() =
testScope.runTest {
+ runCurrent()
+
// GIVEN that we are dreaming and not dozing
keyguardRepository.setDreaming(true)
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- runCurrent()
+ advanceTimeBy(600L)
// GIVEN a prior transition has run to GLANCEABLE_HUB
runTransitionAndSetWakefulness(KeyguardState.DREAMING, KeyguardState.GLANCEABLE_HUB)
@@ -2233,7 +2262,7 @@
keyguardRepository.setDozeTransitionModel(
DozeTransitionModel(from = DozeStateModel.DOZE, to = DozeStateModel.FINISH)
)
- advanceTimeBy(100L)
+ advanceTimeBy(600L)
// GIVEN a prior transition has run to GLANCEABLE_HUB
communalSceneInteractor.changeScene(CommunalScenes.Communal)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
index 3f6e229..df8afdb 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModelTest.kt
@@ -23,6 +23,7 @@
import com.android.systemui.biometrics.data.repository.fingerprintPropertyRepository
import com.android.systemui.biometrics.shared.model.FingerprintSensorType
import com.android.systemui.biometrics.shared.model.SensorStrength
+import com.android.systemui.coroutines.collectLastValue
import com.android.systemui.coroutines.collectValues
import com.android.systemui.keyguard.data.repository.FakeBiometricSettingsRepository
import com.android.systemui.keyguard.data.repository.FakeKeyguardTransitionRepository
@@ -117,6 +118,24 @@
}
@Test
+ fun lockscreenAlphaStartsFromViewStateAccessorAlpha() =
+ testScope.runTest {
+ val viewState = ViewStateAccessor(alpha = { 0.5f })
+ val alpha by collectLastValue(underTest.lockscreenAlpha(viewState))
+
+ keyguardTransitionRepository.sendTransitionStep(step(0f, TransitionState.STARTED))
+
+ keyguardTransitionRepository.sendTransitionStep(step(0f))
+ assertThat(alpha).isEqualTo(0.5f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(0.5f))
+ assertThat(alpha).isEqualTo(0.75f)
+
+ keyguardTransitionRepository.sendTransitionStep(step(1f))
+ assertThat(alpha).isEqualTo(1f)
+ }
+
+ @Test
fun deviceEntryBackgroundViewDisappear() =
testScope.runTest {
val values by collectValues(underTest.deviceEntryBackgroundViewAlpha)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
index d1f908d..46b370f 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/lifecycle/SysUiViewModelTest.kt
@@ -16,16 +16,27 @@
package com.android.systemui.lifecycle
+import android.view.View
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.ui.test.junit4.createComposeRule
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.SysuiTestCase
+import com.android.systemui.util.Assert
import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.awaitCancellation
+import kotlinx.coroutines.launch
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
import org.junit.Rule
import org.junit.Test
import org.junit.runner.RunWith
+import org.mockito.kotlin.argumentCaptor
+import org.mockito.kotlin.doReturn
+import org.mockito.kotlin.mock
+import org.mockito.kotlin.stub
+import org.mockito.kotlin.verify
@SmallTest
@RunWith(AndroidJUnit4::class)
@@ -110,4 +121,45 @@
assertThat(isActive).isFalse()
}
+
+ @Test
+ fun viewModel_viewBinder() = runTest {
+ Assert.setTestThread(Thread.currentThread())
+
+ val view: View = mock { on { isAttachedToWindow } doReturn false }
+ val viewModel = FakeViewModel()
+ backgroundScope.launch {
+ view.viewModel(
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = { viewModel },
+ ) {
+ awaitCancellation()
+ }
+ }
+ runCurrent()
+
+ assertThat(viewModel.isActivated).isFalse()
+
+ view.stub { on { isAttachedToWindow } doReturn true }
+ argumentCaptor<View.OnAttachStateChangeListener>()
+ .apply { verify(view).addOnAttachStateChangeListener(capture()) }
+ .allValues
+ .forEach { it.onViewAttachedToWindow(view) }
+ runCurrent()
+
+ assertThat(viewModel.isActivated).isTrue()
+ }
+}
+
+private class FakeViewModel : SysUiViewModel() {
+ var isActivated = false
+
+ override suspend fun onActivated() {
+ isActivated = true
+ try {
+ awaitCancellation()
+ } finally {
+ isActivated = false
+ }
+ }
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt
similarity index 78%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt
index 16c7090..8f925d5 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModelTest.kt
@@ -31,12 +31,13 @@
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.fakeShadeRepository
-import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneViewModel
+import com.android.systemui.shade.ui.viewmodel.notificationsShadeSceneActionsViewModel
import com.android.systemui.testKosmos
import com.google.common.truth.Truth.assertThat
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -51,23 +52,24 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
-class NotificationsShadeSceneViewModelTest : SysuiTestCase() {
+class NotificationsShadeSceneActionsViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val sceneInteractor by lazy { kosmos.sceneInteractor }
private val deviceUnlockedInteractor by lazy { kosmos.deviceUnlockedInteractor }
- private val underTest by lazy { kosmos.notificationsShadeSceneViewModel }
+ private val underTest by lazy { kosmos.notificationsShadeSceneActionsViewModel }
@Test
fun upTransitionSceneKey_deviceLocked_lockscreen() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
lockDevice()
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Down)).isNull()
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
.isEqualTo(Scenes.Lockscreen)
}
@@ -75,23 +77,25 @@
@Test
fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
lockDevice()
kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone)
}
@Test
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
lockDevice()
unlockDevice()
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Down)).isNull()
assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
}
@@ -99,11 +103,12 @@
fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() =
testScope.runTest {
kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
lockDevice()
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)).isNull()
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
.isEqualTo(Scenes.Lockscreen)
}
@@ -112,26 +117,28 @@
fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() =
testScope.runTest {
kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
lockDevice()
unlockDevice()
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)).isNull()
assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
}
@Test
fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.None
)
sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value)
.isEqualTo(Scenes.Lockscreen)
}
@@ -139,7 +146,7 @@
@Test
fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
AuthenticationMethodModel.None
@@ -147,8 +154,9 @@
sceneInteractor // force the lazy; this will kick off StateFlows
runCurrent()
sceneInteractor.changeScene(Scenes.Gone, "reason")
+ underTest.activateIn(this)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(kosmos.homeSceneFamilyResolver.resolvedScene.value).isEqualTo(Scenes.Gone)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.kt
new file mode 100644
index 0000000..5a73fe2
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/AirplaneModeMapperTest.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.qs.tiles.impl
+
+import android.graphics.drawable.TestStubDrawable
+import android.service.quicksettings.Tile
+import android.widget.Switch
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.systemui.SysuiTestCase
+import com.android.systemui.common.shared.model.Icon
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.tiles.impl.airplane.domain.AirplaneModeMapper
+import com.android.systemui.qs.tiles.impl.airplane.domain.model.AirplaneModeTileModel
+import com.android.systemui.qs.tiles.impl.airplane.qsAirplaneModeTileConfig
+import com.android.systemui.qs.tiles.impl.custom.QSTileStateSubject
+import com.android.systemui.qs.tiles.viewmodel.QSTileState
+import com.android.systemui.res.R
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+class AirplaneModeMapperTest : SysuiTestCase() {
+ private val kosmos = Kosmos()
+ private val airplaneModeConfig = kosmos.qsAirplaneModeTileConfig
+
+ private lateinit var mapper: AirplaneModeMapper
+
+ @Before
+ fun setup() {
+ mapper =
+ AirplaneModeMapper(
+ context.orCreateTestableResources
+ .apply {
+ addOverride(R.drawable.qs_airplane_icon_off, TestStubDrawable())
+ addOverride(R.drawable.qs_airplane_icon_on, TestStubDrawable())
+ }
+ .resources,
+ context.theme,
+ )
+ }
+
+ @Test
+ fun enabledModel_mapsCorrectly() {
+ val inputModel = AirplaneModeTileModel(true)
+
+ val outputState = mapper.map(airplaneModeConfig, inputModel)
+
+ val expectedState =
+ createAirplaneModeState(
+ QSTileState.ActivationState.ACTIVE,
+ context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_ACTIVE],
+ R.drawable.qs_airplane_icon_on
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ @Test
+ fun disabledModel_mapsCorrectly() {
+ val inputModel = AirplaneModeTileModel(false)
+
+ val outputState = mapper.map(airplaneModeConfig, inputModel)
+
+ val expectedState =
+ createAirplaneModeState(
+ QSTileState.ActivationState.INACTIVE,
+ context.resources.getStringArray(R.array.tile_states_airplane)[Tile.STATE_INACTIVE],
+ R.drawable.qs_airplane_icon_off
+ )
+ QSTileStateSubject.assertThat(outputState).isEqualTo(expectedState)
+ }
+
+ private fun createAirplaneModeState(
+ activationState: QSTileState.ActivationState,
+ secondaryLabel: String,
+ iconRes: Int
+ ): QSTileState {
+ val label = context.getString(R.string.airplane_mode)
+ return QSTileState(
+ { Icon.Loaded(context.getDrawable(iconRes)!!, null) },
+ iconRes,
+ label,
+ activationState,
+ secondaryLabel,
+ setOf(QSTileState.UserAction.CLICK, QSTileState.UserAction.LONG_CLICK),
+ label,
+ null,
+ QSTileState.SideViewIcon.None,
+ QSTileState.EnabledState.ENABLED,
+ Switch::class.qualifiedName
+ )
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
index b4ff565..f1d08c0 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/custom/domain/interactor/CustomTileMapperTest.kt
@@ -18,6 +18,7 @@
import android.app.IUriGrantsManager
import android.content.ComponentName
+import android.content.Context
import android.graphics.drawable.Drawable
import android.graphics.drawable.Icon
import android.graphics.drawable.TestStubDrawable
@@ -51,11 +52,13 @@
class CustomTileMapperTest : SysuiTestCase() {
private val uriGrantsManager: IUriGrantsManager = mock {}
+ private val mockContext =
+ mock<Context> { whenever(createContextAsUser(any(), any())).thenReturn(context) }
private val kosmos =
testKosmos().apply { customTileSpec = TileSpec.Companion.create(TEST_COMPONENT) }
private val underTest by lazy {
CustomTileMapper(
- context = mock { whenever(createContextAsUser(any(), any())).thenReturn(context) },
+ context = mockContext,
uriGrantsManager = uriGrantsManager,
)
}
@@ -164,7 +167,7 @@
)
val expected =
createTileState(
- activationState = QSTileState.ActivationState.INACTIVE,
+ activationState = QSTileState.ActivationState.UNAVAILABLE,
icon = DEFAULT_DRAWABLE,
)
@@ -173,7 +176,7 @@
}
@Test
- fun failedToLoadIconTileIsInactive() =
+ fun failedToLoadIconTileIsUnavailable() =
with(kosmos) {
testScope.runTest {
val actual =
@@ -187,13 +190,32 @@
val expected =
createTileState(
icon = null,
- activationState = QSTileState.ActivationState.INACTIVE,
+ activationState = QSTileState.ActivationState.UNAVAILABLE,
)
assertThat(actual).isEqualTo(expected)
}
}
+ @Test
+ fun nullUserContextDoesNotCauseExceptionReturnsNullIconAndUnavailableState() =
+ with(kosmos) {
+ testScope.runTest {
+ // map() will catch this exception
+ whenever(mockContext.createContextAsUser(any(), any()))
+ .thenThrow(IllegalStateException("Unable to create userContext"))
+
+ val actual = underTest.map(customTileQsTileConfig, createModel())
+
+ val expected =
+ createTileState(
+ icon = null,
+ activationState = QSTileState.ActivationState.UNAVAILABLE,
+ )
+ assertThat(actual).isEqualTo(expected)
+ }
+ }
+
private fun Kosmos.createModel(
tileState: Int = Tile.STATE_ACTIVE,
tileIcon: Icon = createIcon(DRAWABLE, false),
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
index 13d6411..1ea8abc 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractorTest.kt
@@ -204,7 +204,7 @@
val actualIcon = latest?.icon
assertThat(actualIcon).isEqualTo(expectedIcon)
- assertThat(latest?.iconId).isNull()
+ assertThat(latest?.iconId).isEqualTo(WifiIcons.WIFI_NO_INTERNET_ICONS[4])
assertThat(latest?.contentDescription.loadContentDescription(context))
.isEqualTo("$internet,test ssid")
val expectedSd = wifiIcon.contentDescription
@@ -443,15 +443,15 @@
* on the mentioned context. Since that context does not have a looper assigned to it, the
* handler instantiation will throw a RuntimeException.
*
- * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception
- * So either we should make Robolectric behvase similar to the device test, or change this
- * test to look for a different signal than the exception, when run by Robolectric. For now
- * we just assume the test is not Robolectric.
+ * TODO(b/338068066): Robolectric behavior differs in that it does not throw the exception So
+ * either we should make Robolectric behave similar to the device test, or change this test to
+ * look for a different signal than the exception, when run by Robolectric. For now we just
+ * assume the test is not Robolectric.
*/
@Test(expected = java.lang.RuntimeException::class)
fun mobileDefault_usesNetworkNameAndIcon_throwsRunTimeException() =
testScope.runTest {
- assumeFalse(isRobolectricTest());
+ assumeFalse(isRobolectricTest())
collectLastValue(underTest.tileData(testUser, flowOf(DataUpdateTrigger.InitialRequest)))
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
index 3ca802e..0363808 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModelTest.kt
@@ -49,9 +49,8 @@
import com.android.systemui.scene.domain.startable.sceneContainerStartable
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
-import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModel
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.notificationsPlaceholderViewModel
+import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory
+import com.android.systemui.shade.ui.viewmodel.shadeHeaderViewModelFactory
import com.android.systemui.testKosmos
import com.android.systemui.util.mockito.any
import com.android.systemui.util.mockito.mock
@@ -93,10 +92,9 @@
sceneContainerStartable.start()
underTest =
QuickSettingsSceneViewModel(
- brightnessMirrorViewModel = kosmos.brightnessMirrorViewModel,
- shadeHeaderViewModel = kosmos.shadeHeaderViewModel,
+ brightnessMirrorViewModelFactory = kosmos.brightnessMirrorViewModelFactory,
+ shadeHeaderViewModelFactory = kosmos.shadeHeaderViewModelFactory,
qsSceneAdapter = qsFlexiglassAdapter,
- notifications = kosmos.notificationsPlaceholderViewModel,
footerActionsViewModelFactory = footerActionsViewModelFactory,
footerActionsController = footerActionsController,
sceneBackInteractor = sceneBackInteractor,
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt
similarity index 77%
rename from packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
rename to packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt
index a7a3a0f..647fdf6 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelTest.kt
@@ -32,6 +32,7 @@
import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.qs.panels.ui.viewmodel.editModeViewModel
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
@@ -44,6 +45,7 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -52,49 +54,54 @@
@RunWith(AndroidJUnit4::class)
@TestableLooper.RunWithLooper
@EnableSceneContainer
-class QuickSettingsShadeSceneViewModelTest : SysuiTestCase() {
+class QuickSettingsShadeSceneActionsViewModelTest : SysuiTestCase() {
private val kosmos = testKosmos()
private val testScope = kosmos.testScope
private val sceneInteractor = kosmos.sceneInteractor
private val deviceUnlockedInteractor = kosmos.deviceUnlockedInteractor
- private val underTest by lazy { kosmos.quickSettingsShadeSceneViewModel }
+ private val underTest by lazy { kosmos.quickSettingsShadeSceneActionsViewModel }
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
@Test
fun upTransitionSceneKey_deviceLocked_lockscreen() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
lockDevice()
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Down)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
}
@Test
fun upTransitionSceneKey_deviceLocked_keyguardDisabled_gone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
lockDevice()
kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(homeScene).isEqualTo(Scenes.Gone)
}
@Test
fun upTransitionSceneKey_deviceUnlocked_gone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
lockDevice()
unlockDevice()
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Down)).isNull()
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Down)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Gone)
}
@@ -102,12 +109,12 @@
fun downTransitionSceneKey_deviceLocked_bottomAligned_lockscreen() =
testScope.runTest {
kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
lockDevice()
- assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
}
@@ -115,20 +122,20 @@
fun downTransitionSceneKey_deviceUnlocked_bottomAligned_gone() =
testScope.runTest {
kosmos.fakeShadeRepository.setDualShadeAlignedToBottom(true)
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
lockDevice()
unlockDevice()
- assertThat(destinationScenes?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
- assertThat(destinationScenes?.get(Swipe.Up)).isNull()
+ assertThat(actions?.get(Swipe.Down)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)).isNull()
assertThat(homeScene).isEqualTo(Scenes.Gone)
}
@Test
fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -136,14 +143,14 @@
)
sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
}
@Test
fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
@@ -152,26 +159,26 @@
runCurrent()
sceneInteractor.changeScene(Scenes.Gone, "reason")
- assertThat(destinationScenes?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Swipe.Up)?.toScene).isEqualTo(SceneFamilies.Home)
assertThat(homeScene).isEqualTo(Scenes.Gone)
}
@Test
fun backTransitionSceneKey_notEditing_Home() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
- assertThat(destinationScenes?.get(Back)?.toScene).isEqualTo(SceneFamilies.Home)
+ assertThat(actions?.get(Back)?.toScene).isEqualTo(SceneFamilies.Home)
}
@Test
fun backTransition_editing_noDestination() =
testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
+ val actions by collectLastValue(underTest.actions)
kosmos.editModeViewModel.startEditing()
- assertThat(destinationScenes!!).isNotEmpty()
- assertThat(destinationScenes?.get(Back)).isNull()
+ assertThat(actions!!).isNotEmpty()
+ assertThat(actions?.get(Back)).isNull()
}
private fun TestScope.lockDevice() {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS
new file mode 100644
index 0000000..e322e38
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/OWNERS
@@ -0,0 +1 @@
+file:/packages/SystemUI/src/com/android/systemui/scene/OWNERS
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
index 72a5cd1..66e45ab 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/scene/SceneFrameworkIntegrationTest.kt
@@ -63,8 +63,10 @@
import com.android.systemui.scene.shared.model.fakeSceneDataSource
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
-import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneActionsViewModel
+import com.android.systemui.shade.ui.viewmodel.ShadeSceneContentViewModel
+import com.android.systemui.shade.ui.viewmodel.shadeSceneActionsViewModel
+import com.android.systemui.shade.ui.viewmodel.shadeSceneContentViewModel
import com.android.systemui.statusbar.pipeline.mobile.data.repository.FakeMobileConnectionsRepository
import com.android.systemui.statusbar.pipeline.mobile.data.repository.fakeMobileConnectionsRepository
import com.android.systemui.telephony.data.repository.fakeTelephonyRepository
@@ -147,7 +149,8 @@
)
}
- private lateinit var shadeSceneViewModel: ShadeSceneViewModel
+ private lateinit var shadeSceneContentViewModel: ShadeSceneContentViewModel
+ private lateinit var shadeSceneActionsViewModel: ShadeSceneActionsViewModel
private val powerInteractor by lazy { kosmos.powerInteractor }
@@ -186,12 +189,15 @@
bouncerActionButtonInteractor = kosmos.bouncerActionButtonInteractor
bouncerViewModel = kosmos.bouncerViewModel
- shadeSceneViewModel = kosmos.shadeSceneViewModel
+ shadeSceneContentViewModel = kosmos.shadeSceneContentViewModel
+ shadeSceneActionsViewModel = kosmos.shadeSceneActionsViewModel
val startable = kosmos.sceneContainerStartable
startable.start()
lockscreenSceneActionsViewModel.activateIn(testScope)
+ shadeSceneContentViewModel.activateIn(testScope)
+ shadeSceneActionsViewModel.activateIn(testScope)
assertWithMessage("Initial scene key mismatch!")
.that(sceneContainerViewModel.currentScene.value)
@@ -220,8 +226,8 @@
@Test
fun swipeUpOnLockscreen_enterCorrectPin_unlocksDevice() =
testScope.runTest {
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(
to = upDestinationSceneKey,
@@ -240,8 +246,8 @@
testScope.runTest {
setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
emulateUserDrivenTransition(
to = upDestinationSceneKey,
@@ -251,7 +257,7 @@
@Test
fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
testScope.runTest {
- val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes)
+ val actions by collectLastValue(shadeSceneActionsViewModel.actions)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
setAuthMethod(AuthenticationMethodModel.None, enableLockscreen = true)
assertCurrentScene(Scenes.Lockscreen)
@@ -260,7 +266,7 @@
emulateUserDrivenTransition(to = Scenes.Shade)
assertCurrentScene(Scenes.Shade)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
emulateUserDrivenTransition(
@@ -271,7 +277,7 @@
@Test
fun swipeUpOnShadeScene_withAuthMethodSwipe_lockscreenDismissed_goesToGone() =
testScope.runTest {
- val destinationScenes by collectLastValue(shadeSceneViewModel.destinationScenes)
+ val actions by collectLastValue(shadeSceneActionsViewModel.actions)
val canSwipeToEnter by collectLastValue(deviceEntryInteractor.canSwipeToEnter)
val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
@@ -288,7 +294,7 @@
emulateUserDrivenTransition(to = Scenes.Shade)
assertCurrentScene(Scenes.Shade)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(SceneFamilies.Home)
assertThat(homeScene).isEqualTo(Scenes.Gone)
emulateUserDrivenTransition(
@@ -346,8 +352,8 @@
fun swipeUpOnLockscreenWhileUnlocked_dismissesLockscreen() =
testScope.runTest {
unlockDevice()
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Gone)
}
@@ -368,8 +374,8 @@
fun dismissingIme_whileOnPasswordBouncer_navigatesToLockscreen() =
testScope.runTest {
setAuthMethod(AuthenticationMethodModel.Password)
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(
to = upDestinationSceneKey,
@@ -386,8 +392,8 @@
fun bouncerActionButtonClick_opensEmergencyServicesDialer() =
testScope.runTest {
setAuthMethod(AuthenticationMethodModel.Password)
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(to = upDestinationSceneKey)
@@ -406,8 +412,8 @@
testScope.runTest {
setAuthMethod(AuthenticationMethodModel.Password)
startPhoneCall()
- val destinationScenes by collectLastValue(lockscreenSceneActionsViewModel.actions)
- val upDestinationSceneKey = destinationScenes?.get(Swipe.Up)?.toScene
+ val actions by collectLastValue(lockscreenSceneActionsViewModel.actions)
+ val upDestinationSceneKey = actions?.get(Swipe.Up)?.toScene
assertThat(upDestinationSceneKey).isEqualTo(Scenes.Bouncer)
emulateUserDrivenTransition(to = upDestinationSceneKey)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
index 8a43198..fadb1d7 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImplTest.kt
@@ -22,13 +22,13 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.common.ui.data.repository.fakeConfigurationRepository
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.flags.EnableSceneContainer
import com.android.systemui.keyguard.data.repository.fakeKeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.kosmos.testScope
-import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.testKosmos
import com.google.common.truth.Truth
import com.google.common.truth.Truth.assertThat
@@ -43,6 +43,7 @@
@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
@RunWith(AndroidJUnit4::class)
+@EnableSceneContainer
class ShadeInteractorSceneContainerImplTest : SysuiTestCase() {
private val kosmos = testKosmos()
@@ -50,7 +51,7 @@
private val configurationRepository = kosmos.fakeConfigurationRepository
private val keyguardRepository = kosmos.fakeKeyguardRepository
private val sceneInteractor = kosmos.sceneInteractor
- private val shadeRepository = kosmos.shadeRepository
+ private val shadeTestUtil = kosmos.shadeTestUtil
private val underTest = kosmos.shadeInteractorSceneContainerImpl
@@ -60,7 +61,7 @@
val actual by collectLastValue(underTest.qsExpansion)
// WHEN split shade is enabled and QS is expanded
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val transitionState =
@@ -89,7 +90,7 @@
// WHEN split shade is not enabled and QS is expanded
keyguardRepository.setStatusBarState(StatusBarState.SHADE)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
runCurrent()
val progress = MutableStateFlow(.3f)
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt
index 0ffabd8..3f087b4 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.testKosmos
@@ -37,6 +38,7 @@
import kotlinx.coroutines.test.TestScope
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
+import org.junit.Before
import org.junit.Test
import org.junit.runner.RunWith
@@ -54,6 +56,11 @@
private val underTest = kosmos.overlayShadeViewModel
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
+
@Test
fun backgroundScene_deviceLocked_lockscreen() =
testScope.runTest {
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
index 3ded8a3..f6fe667ff 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelTest.kt
@@ -15,6 +15,7 @@
import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.scene.shared.model.Scenes
@@ -52,6 +53,7 @@
@Before
fun setUp() {
MockitoAnnotations.initMocks(this)
+ underTest.activateIn(testScope)
}
@Test
@@ -107,15 +109,15 @@
@Test
fun onSystemIconContainerClicked_unlocked_collapsesShadeToGone() =
- testScope.runTest {
- setDeviceEntered(true)
- setScene(Scenes.Shade)
+ testScope.runTest {
+ setDeviceEntered(true)
+ setScene(Scenes.Shade)
- underTest.onSystemIconContainerClicked()
- runCurrent()
+ underTest.onSystemIconContainerClicked()
+ runCurrent()
- assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
- }
+ assertThat(sceneInteractor.currentScene.value).isEqualTo(Scenes.Gone)
+ }
companion object {
private val SUB_1 =
@@ -137,7 +139,7 @@
private fun setScene(key: SceneKey) {
sceneInteractor.changeScene(key, "test")
sceneInteractor.setTransitionState(
- MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
)
testScope.runCurrent()
}
@@ -146,16 +148,16 @@
if (isEntered) {
// Unlock the device marking the device has entered.
kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
- SuccessFingerprintAuthenticationStatus(0, true)
+ SuccessFingerprintAuthenticationStatus(0, true)
)
runCurrent()
}
setScene(
- if (isEntered) {
- Scenes.Gone
- } else {
- Scenes.Lockscreen
- }
+ if (isEntered) {
+ Scenes.Gone
+ } else {
+ Scenes.Lockscreen
+ }
)
assertThat(deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt
new file mode 100644
index 0000000..06a02c6
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelTest.kt
@@ -0,0 +1,237 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+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.coroutines.collectLastValue
+import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter
+import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.domain.startable.shadeStartable
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.testKosmos
+import com.google.common.truth.Truth.assertThat
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+@DisableFlags(DualShade.FLAG_NAME)
+class ShadeSceneActionsViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val shadeRepository by lazy { kosmos.shadeRepository }
+ private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter }
+
+ private val underTest: ShadeSceneActionsViewModel by lazy { kosmos.shadeSceneActionsViewModel }
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun upTransitionSceneKey_deviceLocked_lockScreen() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_deviceUnlocked_gone() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ setDeviceEntered(true)
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun upTransitionSceneKey_keyguardDisabled_gone() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
+ kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.None
+ )
+ runCurrent()
+ sceneInteractor.changeScene(Scenes.Gone, "reason")
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.toScene)
+ .isEqualTo(SceneFamilies.Home)
+ assertThat(homeScene).isEqualTo(Scenes.Gone)
+ }
+
+ @Test
+ fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ shadeRepository.setShadeLayoutWide(true)
+ runCurrent()
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.transitionKey)
+ .isEqualTo(ToSplitShade)
+ }
+
+ @Test
+ fun upTransitionKey_splitShadeDisable_isNull() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+ shadeRepository.setShadeLayoutWide(false)
+ runCurrent()
+
+ assertThat(actions?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull()
+ }
+
+ @Test
+ fun downTransitionSceneKey_inSplitShade_null() =
+ testScope.runTest {
+ overrideResource(R.bool.config_use_split_notification_shade, true)
+ kosmos.shadeStartable.start()
+ val actions by collectLastValue(underTest.actions)
+ assertThat(actions?.get(Swipe(SwipeDirection.Down))?.toScene).isNull()
+ }
+
+ @Test
+ fun downTransitionSceneKey_notSplitShade_quickSettings() =
+ testScope.runTest {
+ overrideResource(R.bool.config_use_split_notification_shade, false)
+ kosmos.shadeStartable.start()
+ val actions by collectLastValue(underTest.actions)
+ assertThat(actions?.get(Swipe(SwipeDirection.Down))?.toScene)
+ .isEqualTo(Scenes.QuickSettings)
+ }
+
+ @Test
+ fun upTransitionSceneKey_customizing_noTransition() =
+ testScope.runTest {
+ val actions by collectLastValue(underTest.actions)
+
+ qsSceneAdapter.setCustomizing(true)
+ assertThat(
+ actions!!.keys.filterIsInstance<Swipe>().filter {
+ it.direction == SwipeDirection.Up
+ }
+ )
+ .isEmpty()
+ }
+
+ private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+ if (isEntered) {
+ // Unlock the device marking the device has entered.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ }
+ setScene(
+ if (isEntered) {
+ Scenes.Gone
+ } else {
+ Scenes.Lockscreen
+ }
+ )
+ assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+ }
+
+ private fun TestScope.setScene(key: SceneKey) {
+ sceneInteractor.changeScene(key, "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ runCurrent()
+ }
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt
new file mode 100644
index 0000000..558606f
--- /dev/null
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelTest.kt
@@ -0,0 +1,231 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import android.platform.test.annotations.DisableFlags
+import android.testing.TestableLooper
+import androidx.test.ext.junit.runners.AndroidJUnit4
+import androidx.test.filters.SmallTest
+import com.android.compose.animation.scene.ObservableTransitionState
+import com.android.compose.animation.scene.SceneKey
+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.common.ui.data.repository.fakeConfigurationRepository
+import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.flags.EnableSceneContainer
+import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
+import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
+import com.android.systemui.kosmos.testScope
+import com.android.systemui.lifecycle.activateIn
+import com.android.systemui.media.controls.data.repository.mediaFilterRepository
+import com.android.systemui.media.controls.shared.model.MediaData
+import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter
+import com.android.systemui.res.R
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.shade.data.repository.shadeRepository
+import com.android.systemui.shade.shared.flag.DualShade
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.testKosmos
+import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
+import com.google.common.truth.Truth.assertThat
+import java.util.Locale
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.test.TestScope
+import kotlinx.coroutines.test.runCurrent
+import kotlinx.coroutines.test.runTest
+import org.junit.Before
+import org.junit.Test
+import org.junit.runner.RunWith
+
+@OptIn(ExperimentalCoroutinesApi::class)
+@SmallTest
+@RunWith(AndroidJUnit4::class)
+@TestableLooper.RunWithLooper
+@EnableSceneContainer
+@DisableFlags(DualShade.FLAG_NAME)
+class ShadeSceneContentViewModelTest : SysuiTestCase() {
+
+ private val kosmos = testKosmos()
+ private val testScope = kosmos.testScope
+ private val sceneInteractor by lazy { kosmos.sceneInteractor }
+ private val shadeRepository by lazy { kosmos.shadeRepository }
+ private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter }
+
+ private val underTest: ShadeSceneContentViewModel by lazy { kosmos.shadeSceneContentViewModel }
+
+ @Before
+ fun setUp() {
+ underTest.activateIn(testScope)
+ }
+
+ @Test
+ fun isEmptySpaceClickable_deviceUnlocked_false() =
+ testScope.runTest {
+ val isEmptySpaceClickable by collectLastValue(underTest.isEmptySpaceClickable)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ setDeviceEntered(true)
+ runCurrent()
+
+ assertThat(isEmptySpaceClickable).isFalse()
+ }
+
+ @Test
+ fun isEmptySpaceClickable_deviceLockedSecurely_true() =
+ testScope.runTest {
+ val isEmptySpaceClickable by collectLastValue(underTest.isEmptySpaceClickable)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ runCurrent()
+
+ assertThat(isEmptySpaceClickable).isTrue()
+ }
+
+ @Test
+ fun onEmptySpaceClicked_deviceLockedSecurely_switchesToLockscreen() =
+ testScope.runTest {
+ val currentScene by collectLastValue(sceneInteractor.currentScene)
+ kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
+ AuthenticationMethodModel.Pin
+ )
+ runCurrent()
+
+ underTest.onEmptySpaceClicked()
+
+ assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
+ }
+
+ @Test
+ fun addAndRemoveMedia_mediaVisibilityisUpdated() =
+ testScope.runTest {
+ val isMediaVisible by collectLastValue(underTest.isMediaVisible)
+ val userMedia = MediaData(active = true)
+
+ assertThat(isMediaVisible).isFalse()
+
+ kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
+
+ assertThat(isMediaVisible).isTrue()
+
+ kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId)
+
+ assertThat(isMediaVisible).isFalse()
+ }
+
+ @Test
+ fun shadeMode() =
+ testScope.runTest {
+ val shadeMode by collectLastValue(underTest.shadeMode)
+
+ shadeRepository.setShadeLayoutWide(true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+
+ shadeRepository.setShadeLayoutWide(false)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Single)
+
+ shadeRepository.setShadeLayoutWide(true)
+ assertThat(shadeMode).isEqualTo(ShadeMode.Split)
+ }
+
+ @Test
+ fun unfoldTransitionProgress() =
+ testScope.runTest {
+ val maxTranslation = prepareConfiguration()
+ val translations by
+ collectLastValue(
+ combine(
+ underTest.unfoldTranslationX(isOnStartSide = true),
+ underTest.unfoldTranslationX(isOnStartSide = false),
+ ) { start, end ->
+ Translations(
+ start = start,
+ end = end,
+ )
+ }
+ )
+
+ val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
+ unfoldProvider.onTransitionStarted()
+ assertThat(translations?.start).isEqualTo(0f)
+ assertThat(translations?.end).isEqualTo(-0f)
+
+ repeat(10) { repetition ->
+ val transitionProgress = 0.1f * (repetition + 1)
+ unfoldProvider.onTransitionProgress(transitionProgress)
+ assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation)
+ assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation)
+ }
+
+ unfoldProvider.onTransitionFinishing()
+ assertThat(translations?.start).isEqualTo(0f)
+ assertThat(translations?.end).isEqualTo(-0f)
+
+ unfoldProvider.onTransitionFinished()
+ assertThat(translations?.start).isEqualTo(0f)
+ assertThat(translations?.end).isEqualTo(-0f)
+ }
+
+ private fun prepareConfiguration(): Int {
+ val configuration = context.resources.configuration
+ configuration.setLayoutDirection(Locale.US)
+ kosmos.fakeConfigurationRepository.onConfigurationChange(configuration)
+ val maxTranslation = 10
+ kosmos.fakeConfigurationRepository.setDimensionPixelSize(
+ R.dimen.notification_side_paddings,
+ maxTranslation
+ )
+ return maxTranslation
+ }
+
+ private fun TestScope.setDeviceEntered(isEntered: Boolean) {
+ if (isEntered) {
+ // Unlock the device marking the device has entered.
+ kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
+ SuccessFingerprintAuthenticationStatus(0, true)
+ )
+ runCurrent()
+ }
+ setScene(
+ if (isEntered) {
+ Scenes.Gone
+ } else {
+ Scenes.Lockscreen
+ }
+ )
+ assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
+ }
+
+ private fun TestScope.setScene(key: SceneKey) {
+ sceneInteractor.changeScene(key, "test")
+ sceneInteractor.setTransitionState(
+ MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
+ )
+ runCurrent()
+ }
+
+ private data class Translations(
+ val start: Float,
+ val end: Float,
+ )
+}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
deleted file mode 100644
index 3b2c981..0000000
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelTest.kt
+++ /dev/null
@@ -1,370 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.ui.viewmodel
-
-import android.platform.test.annotations.DisableFlags
-import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
-import androidx.test.filters.SmallTest
-import com.android.compose.animation.scene.ObservableTransitionState
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-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.common.ui.data.repository.fakeConfigurationRepository
-import com.android.systemui.coroutines.collectLastValue
-import com.android.systemui.deviceentry.data.repository.fakeDeviceEntryRepository
-import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
-import com.android.systemui.flags.EnableSceneContainer
-import com.android.systemui.keyguard.data.repository.fakeDeviceEntryFingerprintAuthRepository
-import com.android.systemui.keyguard.domain.interactor.keyguardEnabledInteractor
-import com.android.systemui.keyguard.shared.model.SuccessFingerprintAuthenticationStatus
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.lifecycle.activateIn
-import com.android.systemui.media.controls.data.repository.mediaFilterRepository
-import com.android.systemui.media.controls.shared.model.MediaData
-import com.android.systemui.qs.ui.adapter.fakeQSSceneAdapter
-import com.android.systemui.res.R
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.scene.domain.resolver.homeSceneFamilyResolver
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.shade.data.repository.shadeRepository
-import com.android.systemui.shade.domain.startable.shadeStartable
-import com.android.systemui.shade.shared.flag.DualShade
-import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.testKosmos
-import com.android.systemui.unfold.fakeUnfoldTransitionProgressProvider
-import com.google.common.truth.Truth.assertThat
-import java.util.Locale
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.test.TestScope
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-
-@OptIn(ExperimentalCoroutinesApi::class)
-@SmallTest
-@RunWith(AndroidJUnit4::class)
-@TestableLooper.RunWithLooper
-@EnableSceneContainer
-@DisableFlags(DualShade.FLAG_NAME)
-class ShadeSceneViewModelTest : SysuiTestCase() {
-
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
- private val sceneInteractor by lazy { kosmos.sceneInteractor }
- private val shadeRepository by lazy { kosmos.shadeRepository }
- private val qsSceneAdapter by lazy { kosmos.fakeQSSceneAdapter }
-
- private val underTest: ShadeSceneViewModel by lazy { kosmos.shadeSceneViewModel }
-
- @Before
- fun setUp() {
- underTest.activateIn(testScope)
- }
-
- @Test
- fun upTransitionSceneKey_deviceLocked_lockScreen() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
- }
-
- @Test
- fun upTransitionSceneKey_deviceUnlocked_gone() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- setDeviceEntered(true)
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Gone)
- }
-
- @Test
- fun upTransitionSceneKey_keyguardDisabled_gone() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- kosmos.keyguardEnabledInteractor.notifyKeyguardEnabled(false)
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Gone)
- }
-
- @Test
- fun upTransitionSceneKey_authMethodSwipe_lockscreenNotDismissed_goesToLockscreen() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
- kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.None
- )
- sceneInteractor.changeScene(Scenes.Lockscreen, "reason")
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Lockscreen)
- }
-
- @Test
- fun upTransitionSceneKey_authMethodSwipe_lockscreenDismissed_goesToGone() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- val homeScene by collectLastValue(kosmos.homeSceneFamilyResolver.resolvedScene)
- kosmos.fakeDeviceEntryRepository.setLockscreenEnabled(true)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.None
- )
- runCurrent()
- sceneInteractor.changeScene(Scenes.Gone, "reason")
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.toScene)
- .isEqualTo(SceneFamilies.Home)
- assertThat(homeScene).isEqualTo(Scenes.Gone)
- }
-
- @Test
- fun upTransitionKey_splitShadeEnabled_isGoneToSplitShade() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- shadeRepository.setShadeLayoutWide(true)
- runCurrent()
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey)
- .isEqualTo(ToSplitShade)
- }
-
- @Test
- fun upTransitionKey_splitShadeDisable_isNull() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- shadeRepository.setShadeLayoutWide(false)
- runCurrent()
-
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Up))?.transitionKey).isNull()
- }
-
- @Test
- fun isClickable_deviceUnlocked_false() =
- testScope.runTest {
- val isClickable by collectLastValue(underTest.isClickable)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- setDeviceEntered(true)
- runCurrent()
-
- assertThat(isClickable).isFalse()
- }
-
- @Test
- fun isClickable_deviceLockedSecurely_true() =
- testScope.runTest {
- val isClickable by collectLastValue(underTest.isClickable)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- runCurrent()
-
- assertThat(isClickable).isTrue()
- }
-
- @Test
- fun onContentClicked_deviceLockedSecurely_switchesToLockscreen() =
- testScope.runTest {
- val currentScene by collectLastValue(sceneInteractor.currentScene)
- kosmos.fakeAuthenticationRepository.setAuthenticationMethod(
- AuthenticationMethodModel.Pin
- )
- runCurrent()
-
- underTest.onContentClicked()
-
- assertThat(currentScene).isEqualTo(Scenes.Lockscreen)
- }
-
- @Test
- fun addAndRemoveMedia_mediaVisibilityisUpdated() =
- testScope.runTest {
- val isMediaVisible by collectLastValue(underTest.isMediaVisible)
- val userMedia = MediaData(active = true)
-
- assertThat(isMediaVisible).isFalse()
-
- kosmos.mediaFilterRepository.addSelectedUserMediaEntry(userMedia)
-
- assertThat(isMediaVisible).isTrue()
-
- kosmos.mediaFilterRepository.removeSelectedUserMediaEntry(userMedia.instanceId)
-
- assertThat(isMediaVisible).isFalse()
- }
-
- @Test
- fun downTransitionSceneKey_inSplitShade_null() =
- testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, true)
- kosmos.shadeStartable.start()
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene).isNull()
- }
-
- @Test
- fun downTransitionSceneKey_notSplitShade_quickSettings() =
- testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, false)
- kosmos.shadeStartable.start()
- val destinationScenes by collectLastValue(underTest.destinationScenes)
- assertThat(destinationScenes?.get(Swipe(SwipeDirection.Down))?.toScene)
- .isEqualTo(Scenes.QuickSettings)
- }
-
- @Test
- fun upTransitionSceneKey_customizing_noTransition() =
- testScope.runTest {
- val destinationScenes by collectLastValue(underTest.destinationScenes)
-
- qsSceneAdapter.setCustomizing(true)
- assertThat(
- destinationScenes!!.keys.filterIsInstance<Swipe>().filter {
- it.direction == SwipeDirection.Up
- }
- )
- .isEmpty()
- }
-
- @Test
- fun shadeMode() =
- testScope.runTest {
- val shadeMode by collectLastValue(underTest.shadeMode)
-
- shadeRepository.setShadeLayoutWide(true)
- assertThat(shadeMode).isEqualTo(ShadeMode.Split)
-
- shadeRepository.setShadeLayoutWide(false)
- assertThat(shadeMode).isEqualTo(ShadeMode.Single)
-
- shadeRepository.setShadeLayoutWide(true)
- assertThat(shadeMode).isEqualTo(ShadeMode.Split)
- }
-
- @Test
- fun unfoldTransitionProgress() =
- testScope.runTest {
- val maxTranslation = prepareConfiguration()
- val translations by
- collectLastValue(
- combine(
- underTest.unfoldTranslationX(isOnStartSide = true),
- underTest.unfoldTranslationX(isOnStartSide = false),
- ) { start, end ->
- Translations(
- start = start,
- end = end,
- )
- }
- )
-
- val unfoldProvider = kosmos.fakeUnfoldTransitionProgressProvider
- unfoldProvider.onTransitionStarted()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
-
- repeat(10) { repetition ->
- val transitionProgress = 0.1f * (repetition + 1)
- unfoldProvider.onTransitionProgress(transitionProgress)
- assertThat(translations?.start).isEqualTo((1 - transitionProgress) * maxTranslation)
- assertThat(translations?.end).isEqualTo(-(1 - transitionProgress) * maxTranslation)
- }
-
- unfoldProvider.onTransitionFinishing()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
-
- unfoldProvider.onTransitionFinished()
- assertThat(translations?.start).isEqualTo(0f)
- assertThat(translations?.end).isEqualTo(-0f)
- }
-
- private fun prepareConfiguration(): Int {
- val configuration = context.resources.configuration
- configuration.setLayoutDirection(Locale.US)
- kosmos.fakeConfigurationRepository.onConfigurationChange(configuration)
- val maxTranslation = 10
- kosmos.fakeConfigurationRepository.setDimensionPixelSize(
- R.dimen.notification_side_paddings,
- maxTranslation
- )
- return maxTranslation
- }
-
- private fun TestScope.setDeviceEntered(isEntered: Boolean) {
- if (isEntered) {
- // Unlock the device marking the device has entered.
- kosmos.fakeDeviceEntryFingerprintAuthRepository.setAuthenticationStatus(
- SuccessFingerprintAuthenticationStatus(0, true)
- )
- runCurrent()
- }
- setScene(
- if (isEntered) {
- Scenes.Gone
- } else {
- Scenes.Lockscreen
- }
- )
- assertThat(kosmos.deviceEntryInteractor.isDeviceEntered.value).isEqualTo(isEntered)
- }
-
- private fun TestScope.setScene(key: SceneKey) {
- sceneInteractor.changeScene(key, "test")
- sceneInteractor.setTransitionState(
- MutableStateFlow<ObservableTransitionState>(ObservableTransitionState.Idle(key))
- )
- runCurrent()
- }
-
- private data class Translations(
- val start: Float,
- val end: Float,
- )
-}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
index 2fb9e1e0..733cac9 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/SharedNotificationContainerViewModelTest.kt
@@ -56,6 +56,7 @@
import com.android.systemui.keyguard.ui.viewmodel.keyguardRootViewModel
import com.android.systemui.kosmos.testScope
import com.android.systemui.res.R
+import com.android.systemui.shade.data.repository.fakeShadeRepository
import com.android.systemui.shade.mockLargeScreenHeaderHelper
import com.android.systemui.shade.shadeTestUtil
import com.android.systemui.statusbar.notification.stack.domain.interactor.sharedNotificationContainerInteractor
@@ -135,11 +136,14 @@
val communalSceneRepository
get() = kosmos.communalSceneRepository
+ val shadeRepository
+ get() = kosmos.fakeShadeRepository
+
lateinit var underTest: SharedNotificationContainerViewModel
@Before
fun setUp() {
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
movementFlow = MutableStateFlow(BurnInModel())
whenever(aodBurnInViewModel.movement(any())).thenReturn(movementFlow)
underTest = kosmos.sharedNotificationContainerViewModel
@@ -148,7 +152,7 @@
@Test
fun validateMarginStartInSplitShade() =
testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens by collectLastValue(underTest.configurationBasedDimensions)
@@ -161,7 +165,7 @@
@Test
fun validateMarginStart() =
testScope.runTest {
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.dimen.notification_panel_margin_horizontal, 20)
val dimens by collectLastValue(underTest.configurationBasedDimensions)
@@ -175,7 +179,7 @@
fun validatePaddingTopInSplitShade_usesLargeHeaderHelper() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -191,7 +195,7 @@
fun validatePaddingTopInNonSplitShade_usesLargeScreenHeader() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -207,7 +211,7 @@
fun validatePaddingTopInNonSplitShade_doesNotUseLargeScreenHeader() =
testScope.runTest {
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(10)
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
overrideResource(R.bool.config_use_large_screen_shade_header, false)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -508,7 +512,7 @@
val bounds by collectLastValue(underTest.bounds)
// When not in split shade
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
runCurrent()
@@ -567,7 +571,7 @@
// When in split shade
whenever(largeScreenHeaderHelper.getLargeScreenHeaderHeight()).thenReturn(5)
- overrideResource(R.bool.config_use_split_notification_shade, true)
+ shadeTestUtil.setSplitShade(true)
overrideResource(R.bool.config_use_large_screen_shade_header, true)
overrideResource(R.dimen.large_screen_shade_header_height, 10)
overrideResource(R.dimen.keyguard_split_shade_top_margin, 50)
@@ -628,7 +632,7 @@
advanceTimeBy(50L)
showLockscreen()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
assertThat(maxNotifications).isEqualTo(10)
@@ -656,7 +660,7 @@
advanceTimeBy(50L)
showLockscreen()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
assertThat(maxNotifications).isEqualTo(10)
@@ -690,7 +694,7 @@
// Show lockscreen with shade expanded
showLockscreenWithShadeExpanded()
- overrideResource(R.bool.config_use_split_notification_shade, false)
+ shadeTestUtil.setSplitShade(false)
configurationRepository.onAnyConfigurationChange()
// -1 means No Limit
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt
index ab184ab..f232d52 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorImplTest.kt
@@ -28,6 +28,7 @@
import com.android.systemui.volume.panel.domain.unavailableCriteria
import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import com.android.systemui.volume.panel.ui.composable.enabledComponents
+import com.android.systemui.volume.shared.volumePanelLogger
import com.google.common.truth.Truth.assertThat
import javax.inject.Provider
import kotlinx.coroutines.test.runTest
@@ -49,6 +50,7 @@
enabledComponents,
{ defaultCriteria },
testScope.backgroundScope,
+ volumePanelLogger,
criteriaByKey,
)
}
diff --git a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
index 420b955..51a70bd 100644
--- a/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
+++ b/packages/SystemUI/multivalentTests/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelTest.kt
@@ -24,21 +24,30 @@
import com.android.systemui.SysuiTestCase
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.coroutines.collectLastValue
+import com.android.systemui.dump.DumpManager
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.kosmos.testScope
+import com.android.systemui.statusbar.policy.configurationController
import com.android.systemui.statusbar.policy.fakeConfigurationController
import com.android.systemui.testKosmos
+import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory
import com.android.systemui.volume.panel.data.repository.volumePanelGlobalStateRepository
import com.android.systemui.volume.panel.domain.interactor.criteriaByKey
+import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
import com.android.systemui.volume.panel.domain.unavailableCriteria
import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import com.android.systemui.volume.panel.shared.model.mockVolumePanelUiComponentProvider
import com.android.systemui.volume.panel.ui.composable.componentByKey
import com.android.systemui.volume.panel.ui.layout.DefaultComponentsLayoutManager
import com.android.systemui.volume.panel.ui.layout.componentsLayoutManager
+import com.android.systemui.volume.shared.volumePanelLogger
import com.google.common.truth.Truth.assertThat
+import java.io.PrintWriter
+import java.io.StringWriter
import javax.inject.Provider
import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.launch
import kotlinx.coroutines.test.runCurrent
import kotlinx.coroutines.test.runTest
import org.junit.Test
@@ -55,6 +64,7 @@
volumePanelGlobalStateRepository.updateVolumePanelState { it.copy(isVisible = true) }
}
+ private val realDumpManager = DumpManager()
private val testableResources = context.orCreateTestableResources
private lateinit var underTest: VolumePanelViewModel
@@ -124,6 +134,60 @@
}
@Test
+ fun testDumpableRegister_unregister() =
+ with(kosmos) {
+ testScope.runTest {
+ val job = launch {
+ applicationCoroutineScope = this
+ underTest = createViewModel()
+
+ runCurrent()
+
+ assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME })
+ .isTrue()
+ }
+
+ runCurrent()
+ job.cancel()
+
+ assertThat(realDumpManager.getDumpables().any { it.name == DUMPABLE_NAME }).isTrue()
+ }
+ }
+
+ @Test
+ fun testDumpingState() =
+ test({
+ componentByKey =
+ mapOf(
+ COMPONENT_1 to mockVolumePanelUiComponentProvider,
+ COMPONENT_2 to mockVolumePanelUiComponentProvider,
+ BOTTOM_BAR to mockVolumePanelUiComponentProvider,
+ )
+ criteriaByKey = mapOf(COMPONENT_2 to Provider { unavailableCriteria })
+ }) {
+ testScope.runTest {
+ runCurrent()
+
+ StringWriter().use {
+ underTest.dump(PrintWriter(it), emptyArray())
+
+ assertThat(it.buffer.toString())
+ .isEqualTo(
+ "volumePanelState=" +
+ "VolumePanelState(orientation=1, isLargeScreen=false)\n" +
+ "componentsLayout=( " +
+ "headerComponents= " +
+ "contentComponents=" +
+ "test_component:1:visible=true, " +
+ "test_component:2:visible=false " +
+ "footerComponents= " +
+ "bottomBarComponent=test_bottom_bar:visible=true )\n"
+ )
+ }
+ }
+ }
+
+ @Test
fun dismissBroadcast_dismissesPanel() = test {
testScope.runTest {
runCurrent() // run the flows to let allow the receiver to be registered
@@ -140,11 +204,26 @@
private fun test(setup: Kosmos.() -> Unit = {}, test: Kosmos.() -> Unit) =
with(kosmos) {
setup()
- underTest = volumePanelViewModel
+ underTest = createViewModel()
+
test()
}
+ private fun Kosmos.createViewModel(): VolumePanelViewModel =
+ VolumePanelViewModel(
+ context.orCreateTestableResources.resources,
+ applicationCoroutineScope,
+ volumePanelComponentFactory,
+ configurationController,
+ broadcastDispatcher,
+ realDumpManager,
+ volumePanelLogger,
+ volumePanelGlobalStateInteractor,
+ )
+
private companion object {
+ const val DUMPABLE_NAME = "VolumePanelViewModel"
+
const val BOTTOM_BAR: VolumePanelComponentKey = "test_bottom_bar"
const val COMPONENT_1: VolumePanelComponentKey = "test_component:1"
const val COMPONENT_2: VolumePanelComponentKey = "test_component:2"
diff --git a/packages/SystemUI/res/drawable/ic_bugreport.xml b/packages/SystemUI/res/drawable/ic_bugreport.xml
index ed1c6c7..badbd88 100644
--- a/packages/SystemUI/res/drawable/ic_bugreport.xml
+++ b/packages/SystemUI/res/drawable/ic_bugreport.xml
@@ -19,14 +19,14 @@
android:height="24.0dp"
android:viewportWidth="24.0"
android:viewportHeight="24.0"
- android:tint="?attr/colorControlNormal">
+ android:tint="?android:attr/colorControlNormal">
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M20,10V8h-2.81c-0.45,-0.78 -1.07,-1.46 -1.82,-1.96L17,4.41L15.59,3l-2.17,2.17c-0.03,-0.01 -0.05,-0.01 -0.08,-0.01c-0.16,-0.04 -0.32,-0.06 -0.49,-0.09c-0.06,-0.01 -0.11,-0.02 -0.17,-0.03C12.46,5.02 12.23,5 12,5h0c-0.49,0 -0.97,0.07 -1.42,0.18l0.02,-0.01L8.41,3L7,4.41l1.62,1.63l0.01,0C7.88,6.54 7.26,7.22 6.81,8H4v2h2.09C6.03,10.33 6,10.66 6,11v1H4v2h2v1c0,0.34 0.04,0.67 0.09,1H4v2h2.81c1.04,1.79 2.97,3 5.19,3h0c2.22,0 4.15,-1.21 5.19,-3H20v-2h-2.09l0,0c0.05,-0.33 0.09,-0.66 0.09,-1v-1h2v-2h-2v-1c0,-0.34 -0.04,-0.67 -0.09,-1l0,0H20zM16,15c0,2.21 -1.79,4 -4,4c-2.21,0 -4,-1.79 -4,-4v-4c0,-2.21 1.79,-4 4,-4h0c2.21,0 4,1.79 4,4V15z"/>
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M10,14h4v2h-4z"/>
<path
- android:fillColor="#FF000000"
+ android:fillColor="#FFFFFFFF"
android:pathData="M10,10h4v2h-4z"/>
</vector>
\ No newline at end of file
diff --git a/packages/SystemUI/res/layout/biometric_prompt_layout.xml b/packages/SystemUI/res/layout/biometric_prompt_layout.xml
deleted file mode 100644
index ff89ed9..0000000
--- a/packages/SystemUI/res/layout/biometric_prompt_layout.xml
+++ /dev/null
@@ -1,208 +0,0 @@
-<!--
- ~ Copyright (C) 2023 The Android Open Source Project
- ~
- ~ Licensed under the Apache License, Version 2.0 (the "License");
- ~ you may not use this file except in compliance with the License.
- ~ You may obtain a copy of the License at
- ~
- ~ http://www.apache.org/licenses/LICENSE-2.0
- ~
- ~ Unless required by applicable law or agreed to in writing, software
- ~ distributed under the License is distributed on an "AS IS" BASIS,
- ~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- ~ See the License for the specific language governing permissions and
- ~ limitations under the License.
- -->
-<com.android.systemui.biometrics.ui.BiometricPromptLayout
- xmlns:android="http://schemas.android.com/apk/res/android"
- android:id="@+id/contents"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:orientation="vertical">
-
- <ImageView
- android:id="@+id/logo"
- android:layout_width="@dimen/biometric_auth_icon_size"
- android:layout_height="@dimen/biometric_auth_icon_size"
- android:layout_gravity="center"
- android:scaleType="fitXY"/>
-
- <TextView
- android:id="@+id/logo_description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="@integer/biometric_dialog_text_gravity"
- android:singleLine="true"
- android:marqueeRepeatLimit="1"
- android:ellipsize="marquee"/>
-
- <TextView
- android:id="@+id/title"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="@integer/biometric_dialog_text_gravity"
- android:singleLine="true"
- android:marqueeRepeatLimit="1"
- android:ellipsize="marquee"
- style="@style/TextAppearance.AuthCredential.OldTitle"/>
-
- <TextView
- android:id="@+id/subtitle"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="@integer/biometric_dialog_text_gravity"
- android:singleLine="true"
- android:marqueeRepeatLimit="1"
- android:ellipsize="marquee"
- style="@style/TextAppearance.AuthCredential.OldSubtitle"/>
-
- <TextView
- android:id="@+id/description"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:gravity="@integer/biometric_dialog_text_gravity"
- android:scrollbars ="vertical"
- android:importantForAccessibility="no"
- style="@style/TextAppearance.AuthCredential.OldDescription"/>
-
- <Space
- android:id="@+id/space_above_content"
- android:layout_width="match_parent"
- android:layout_height="24dp"
- android:visibility="gone" />
-
- <LinearLayout
- android:id="@+id/customized_view_container"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:fadeScrollbars="false"
- android:gravity="center_vertical"
- android:orientation="vertical"
- android:scrollbars="vertical"
- android:visibility="gone" />
-
- <Space android:id="@+id/space_above_icon"
- android:layout_width="match_parent"
- android:layout_height="48dp" />
-
- <FrameLayout
- android:id="@+id/biometric_icon_frame"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center">
-
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY" />
-
- <com.android.systemui.biometrics.BiometricPromptLottieViewWrapper
- android:id="@+id/biometric_icon_overlay"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- android:layout_gravity="center"
- android:contentDescription="@null"
- android:scaleType="fitXY" />
- </FrameLayout>
-
- <!-- For sensors such as UDFPS, this view is used during custom measurement/layout to add extra
- padding so that the biometric icon is always in the right physical position. -->
- <Space android:id="@+id/space_below_icon"
- android:layout_width="match_parent"
- android:layout_height="12dp" />
-
- <TextView
- android:id="@+id/indicator"
- android:layout_width="match_parent"
- android:layout_height="wrap_content"
- android:paddingHorizontal="24dp"
- android:textSize="12sp"
- android:gravity="center_horizontal"
- android:accessibilityLiveRegion="polite"
- android:singleLine="true"
- android:ellipsize="marquee"
- android:marqueeRepeatLimit="marquee_forever"
- android:scrollHorizontally="true"
- android:fadingEdge="horizontal"
- android:textColor="@color/biometric_dialog_gray"/>
-
- <LinearLayout
- android:id="@+id/button_bar"
- android:layout_width="match_parent"
- android:layout_height="88dp"
- style="?android:attr/buttonBarStyle"
- android:orientation="horizontal"
- android:paddingTop="24dp">
-
- <Space android:id="@+id/leftSpacer"
- android:layout_width="8dp"
- android:layout_height="match_parent"
- android:visibility="visible" />
-
- <!-- Negative Button, reserved for app -->
- <Button android:id="@+id/button_negative"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
- android:layout_gravity="center_vertical"
- android:ellipsize="end"
- android:maxLines="2"
- android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
- android:visibility="gone"/>
- <!-- Cancel Button, replaces negative button when biometric is accepted -->
- <Button android:id="@+id/button_cancel"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
- android:layout_gravity="center_vertical"
- android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
- android:text="@string/cancel"
- android:visibility="gone"/>
- <!-- "Use Credential" Button, replaces if device credential is allowed -->
- <Button android:id="@+id/button_use_credential"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@*android:style/Widget.DeviceDefault.Button.Borderless.Colored"
- android:layout_gravity="center_vertical"
- android:maxWidth="@dimen/biometric_dialog_button_negative_max_width"
- android:visibility="gone"/>
-
- <Space android:id="@+id/middleSpacer"
- android:layout_width="0dp"
- android:layout_height="match_parent"
- android:layout_weight="1"
- android:visibility="visible"/>
-
- <!-- Positive Button -->
- <Button android:id="@+id/button_confirm"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@*android:style/Widget.DeviceDefault.Button.Colored"
- android:layout_gravity="center_vertical"
- android:ellipsize="end"
- android:maxLines="2"
- android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
- android:text="@string/biometric_dialog_confirm"
- android:visibility="gone"/>
- <!-- Try Again Button -->
- <Button android:id="@+id/button_try_again"
- android:layout_width="wrap_content"
- android:layout_height="wrap_content"
- style="@*android:style/Widget.DeviceDefault.Button.Colored"
- android:layout_gravity="center_vertical"
- android:ellipsize="end"
- android:maxLines="2"
- android:maxWidth="@dimen/biometric_dialog_button_positive_max_width"
- android:text="@string/biometric_dialog_try_again"
- android:visibility="gone"/>
-
- <Space android:id="@+id/rightSpacer"
- android:layout_width="8dp"
- android:layout_height="match_parent"
- android:visibility="visible" />
- </LinearLayout>
-
-</com.android.systemui.biometrics.ui.BiometricPromptLayout>
diff --git a/packages/SystemUI/res/raw/action_key_edu.json b/packages/SystemUI/res/raw/action_key_edu.json
new file mode 100644
index 0000000..014d837
--- /dev/null
+++ b/packages/SystemUI/res/raw/action_key_edu.json
@@ -0,0 +1 @@
+{"v":"5.12.1","fr":60,"ip":0,"op":181,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-EDU","ddd":0,"assets":[{"id":"comp_0","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":34,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":37,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":40,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":46,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.33],"y":[0.52]},"t":124,"s":[0]},{"i":{"x":[0.64],"y":[1]},"o":{"x":[0.36],"y":[0]},"t":127,"s":[100]},{"i":{"x":[0.64],"y":[0.48]},"o":{"x":[0.36],"y":[0]},"t":130,"s":[100]},{"t":136,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.072,"y":0.635},"o":{"x":0.424,"y":0.112},"t":27,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":39,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.167,"y":0.167},"t":57,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.07,"y":0.63},"o":{"x":0.42,"y":0.11},"t":117,"s":[40,39.79,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0,"y":1},"o":{"x":0.313,"y":0.131},"t":129,"s":[40,49.21,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[40,39.79,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"AllApps_Tray_themed","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-8.859,0]},"a":{"a":0,"k":[277,256.562,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,330.391]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 4 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,281.172]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 3 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,231.953]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 2 App - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Safety app","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[400.047,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Settings","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[350.828,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"SoundCloud","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[301.609,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Starbucks","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252.391,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Snapchat","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[203.172,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[29.531,29.531]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Spotify","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[153.953,182.734]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Row 1 App - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-129.938,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[275.625,25.594]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"560x52","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":4,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-151.594,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[15.75,1.969]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":118.125},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"handle","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":1,"k":[{"i":{"x":0.1,"y":1},"o":{"x":0.05,"y":0.7},"t":45,"s":[277,516,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.833,"y":0.833},"o":{"x":0.214,"y":0.214},"t":75,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"i":{"x":0.999,"y":1},"o":{"x":0.3,"y":0},"t":135,"s":[277,265.422,0],"to":[0,0,0],"ti":[0,0,0]},{"t":147,"s":[277,516,0]}]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[316.969,320.906]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":13.78},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"all apps","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[50]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[50]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":51,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[70],"t":135,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[100],"t":144,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"BlankButton","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"actionKey_themed","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":4,"nm":"matte","td":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.32549020648,0.270588248968,0.164705887437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"AllApps_Tray_themed","tt":1,"tp":5,"refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,282,0]},"a":{"a":0,"k":[277,282,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":554,"h":564,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":451,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":180,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":451,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file
diff --git a/packages/SystemUI/res/raw/action_key_success.json b/packages/SystemUI/res/raw/action_key_success.json
new file mode 100644
index 0000000..cae7344
--- /dev/null
+++ b/packages/SystemUI/res/raw/action_key_success.json
@@ -0,0 +1 @@
+{"v":"5.12.1","fr":60,"ip":0,"op":50,"w":554,"h":564,"nm":"Trackpad-JSON_ActionKey-Success","ddd":0,"assets":[{"id":"comp_0","nm":"TrackpadAK_Success_Checkmark","fr":60,"layers":[{"ddd":0,"ind":1,"ty":3,"nm":"Check Rotate","parent":2,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":2,"s":[-16]},{"t":20,"s":[6]}]},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[95.049,95.049,100]}},"ao":0,"ip":0,"op":228,"st":-72,"bm":0},{"ddd":0,"ind":2,"ty":3,"nm":"Bounce","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":1,"k":[{"i":{"x":[0.12],"y":[1]},"o":{"x":[0.44],"y":[0]},"t":12,"s":[0]},{"t":36,"s":[-6]}]},"p":{"a":0,"k":[81,127,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.263,0.263,0.833],"y":[1.126,1.126,1]},"o":{"x":[0.05,0.05,0.05],"y":[0.958,0.958,0]},"t":1,"s":[80,80,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.45,0.45,0.167],"y":[0.325,0.325,0]},"t":20,"s":[105,105,100]},{"t":36,"s":[100,100,100]}]}},"ao":0,"ip":0,"op":300,"st":0,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","parent":1,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":-0.289},"p":{"a":0,"k":[14.364,-33.591,0]},"a":{"a":0,"k":[-0.125,0,0]},"s":{"a":0,"k":[104.744,104.744,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[-1.401,-0.007],[-10.033,11.235]],"o":[[5.954,7.288],[1.401,0.007],[0,0]],"v":[[-28.591,4.149],[-10.73,26.013],[31.482,-21.255]],"c":false}},"nm":"Path 1","hd":false},{"ty":"tm","s":{"a":0,"k":0},"e":{"a":1,"k":[{"i":{"x":[0.2],"y":[1]},"o":{"x":[0.4],"y":[0]},"t":3,"s":[0]},{"i":{"x":[0.22],"y":[1]},"o":{"x":[0.001],"y":[0.149]},"t":10,"s":[29]},{"t":27,"s":[100]}]},"o":{"a":0,"k":0},"m":1,"nm":"Trim Paths 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":11},"lc":2,"lj":2,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Shape 1","bm":0,"hd":false}],"ip":5,"op":44,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[95,95,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.275,0.275,0.21],"y":[1.102,1.102,1]},"o":{"x":[0.037,0.037,0.05],"y":[0.476,0.476,0]},"t":0,"s":[0,0,100]},{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.252,0.252,0.47],"y":[0.159,0.159,0]},"t":16,"s":[120,120,100]},{"t":28,"s":[100,100,100]}]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":1,"k":[{"i":{"x":[0.1,0.1],"y":[1,1]},"o":{"x":[0.32,0.32],"y":[0.11,0.11]},"t":16,"s":[148,148]},{"t":28,"s":[136,136]}]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":88},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"Checkbox - Widget","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_1","nm":"actionKey_themed-static","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","parent":3,"sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[0.288,-0.035,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[0.579,-0.158],[0.605,0],[1.21,1.21],[0,1.684],[-1.184,1.184],[-1.684,0],[-1.21,-1.21],[0,-1.71],[0.184,-0.553],[0.316,-0.474],[0,0],[0,0]],"o":[[-0.474,0.316],[-0.553,0.158],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.184],[0,0.605],[-0.158,0.553],[0,0],[0,0],[0,0]],"v":[[10.241,12.155],[8.663,12.866],[6.926,13.103],[2.585,11.287],[0.809,6.946],[2.585,2.605],[6.926,0.789],[11.307,2.605],[13.122,6.946],[12.846,8.682],[12.136,10.222],[16.911,14.997],[15.017,16.891]],"c":true}},"nm":"Path 1","hd":false},{"ind":1,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.184],[-1.684,0],[-1.184,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.736],[1.21,-1.21],[1.736,0],[1.21,1.184],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,11.327],[-16.911,6.985],[-15.096,2.605],[-10.754,0.789],[-6.374,2.605],[-4.558,6.985],[-6.374,11.327],[-10.754,13.142]],"c":true}},"nm":"Path 2","hd":false},{"ind":2,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[-8.268,9.432],[-7.242,6.985],[-8.268,4.499],[-10.754,3.473],[-13.201,4.499],[-14.188,6.985],[-13.201,9.432],[-10.754,10.419]],"c":true}},"nm":"Path 3","hd":false},{"ind":3,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.658],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.658],[0.973,0]],"v":[[9.413,9.432],[10.439,6.985],[9.413,4.499],[6.926,3.473],[4.479,4.499],[3.453,6.985],[4.479,9.432],[6.926,10.419]],"c":true}},"nm":"Path 4","hd":false},{"ind":4,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.184,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.736,0],[1.21,1.21],[0,1.684],[-1.184,1.21],[-1.684,0]],"v":[[-15.096,-6.354],[-16.911,-10.695],[-15.096,-15.076],[-10.754,-16.891],[-6.374,-15.076],[-4.558,-10.695],[-6.374,-6.354],[-10.754,-4.539]],"c":true}},"nm":"Path 5","hd":false},{"ind":5,"ty":"sh","ks":{"a":0,"k":{"i":[[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0],[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0]],"o":[[-1.21,-1.21],[0,-1.71],[1.21,-1.21],[1.71,0],[1.21,1.21],[0,1.684],[-1.21,1.21],[-1.684,0]],"v":[[2.585,-6.354],[0.77,-10.695],[2.585,-15.076],[6.926,-16.891],[11.307,-15.076],[13.122,-10.695],[11.307,-6.354],[6.926,-4.539]],"c":true}},"nm":"Path 6","hd":false},{"ind":6,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.658,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.658,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[-8.268,-8.248],[-7.242,-10.695],[-8.268,-13.182],[-10.754,-14.208],[-13.201,-13.182],[-14.188,-10.695],[-13.201,-8.248],[-10.754,-7.222]],"c":true}},"nm":"Path 7","hd":false},{"ind":7,"ty":"sh","ks":{"a":0,"k":{"i":[[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0],[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0]],"o":[[0.684,-0.684],[0,-0.973],[-0.684,-0.684],[-0.947,0],[-0.684,0.684],[0,0.947],[0.684,0.684],[0.973,0]],"v":[[9.413,-8.248],[10.439,-10.695],[9.413,-13.182],[6.926,-14.208],[4.479,-13.182],[3.453,-10.695],[4.479,-8.248],[6.926,-7.222]],"c":true}},"nm":"Path 8","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098045468,0.101960785687,0.015686275437,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"icon","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".primaryFixedDim","cl":"primaryFixedDim","parent":3,"sr":1,"ks":{"o":{"a":0,"k":0},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,0,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.92549020052,0.752941191196,0.423529416323,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_2","nm":"BlankButton","fr":60,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,39.79,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shortcut symbols","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":80},"r":{"a":0,"k":0},"p":{"a":0,"k":[40,49.21,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[80,79.581]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":14.032},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.364705890417,0.258823543787,0,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"shadow","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]},{"id":"comp_3","nm":"AK_LofiLauncher","fr":60,"pfr":1,"layers":[{"ddd":0,"ind":1,"ty":4,"nm":"Scale Down","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":45,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":51,"s":[70]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":135,"s":[70]},{"t":144,"s":[100]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[252,157.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":1,"k":[{"i":{"x":[0.1,0.1,0.1],"y":[1,1,1]},"o":{"x":[0.153,0.153,0.153],"y":[0.074,0.074,0]},"t":45,"s":[100,100,100]},{"i":{"x":[0.841,0.841,0.841],"y":[1,1,1]},"o":{"x":[0.161,0.161,0.161],"y":[0,0,0]},"t":75,"s":[95,95,100]},{"i":{"x":[0,0,0],"y":[1,1,1]},"o":{"x":[0.2,0.2,0.2],"y":[0,0,0]},"t":135,"s":[95,95,100]},{"t":165,"s":[100,100,100]}]}},"ao":0,"ef":[{"ty":5,"nm":"Void","np":19,"mn":"Pseudo/250958","ix":1,"en":1,"ef":[{"ty":0,"nm":"Width","mn":"Pseudo/250958-0001","ix":1,"v":{"a":0,"k":100}},{"ty":0,"nm":"Height","mn":"Pseudo/250958-0002","ix":2,"v":{"a":0,"k":100}},{"ty":0,"nm":"Offset X","mn":"Pseudo/250958-0003","ix":3,"v":{"a":0,"k":0}},{"ty":0,"nm":"Offset Y","mn":"Pseudo/250958-0004","ix":4,"v":{"a":0,"k":0}},{"ty":0,"nm":"Roundness","mn":"Pseudo/250958-0005","ix":5,"v":{"a":0,"k":0}},{"ty":6,"nm":"About","mn":"Pseudo/250958-0006","ix":6,"v":0},{"ty":6,"nm":"Plague of null layers.","mn":"Pseudo/250958-0007","ix":7,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0008","ix":8,"v":0},{"ty":6,"nm":"Following projects","mn":"Pseudo/250958-0009","ix":9,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0010","ix":10,"v":0},{"ty":6,"nm":"through time.","mn":"Pseudo/250958-0011","ix":11,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0012","ix":12,"v":0},{"ty":6,"nm":"Be free of the past.","mn":"Pseudo/250958-0013","ix":13,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0014","ix":14,"v":0},{"ty":6,"nm":"Copyright 2023 Battle Axe Inc","mn":"Pseudo/250958-0015","ix":15,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0016","ix":16,"v":0},{"ty":6,"nm":"Void","mn":"Pseudo/250958-0017","ix":17,"v":0}]}],"shapes":[],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,117.5,0]},"a":{"a":0,"k":[252,275,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[300,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"hotseat - 1","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[168,20]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":15},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[132,275]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"qsb","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[20.144,20.144],[20.144,-20.144],[0,0],[-20.144,-20.144],[-20.144,20.144],[0,0]],"o":[[-20.144,-20.144],[0,0],[-20.144,20.144],[20.144,20.144],[0,0],[20.144,-20.144]],"v":[[44.892,-44.892],[-28.057,-44.892],[-44.892,-28.057],[-44.892,44.892],[28.057,44.892],[44.892,28.057]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[108,152.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets weather","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"ind":0,"ty":"sh","ks":{"a":0,"k":{"i":[[0,0],[4.782,-2.684],[0,0],[2.63,-0.033],[0,0],[2.807,-4.716],[0,0],[2.263,-1.343],[0,0],[0.066,-5.485],[0,0],[1.292,-2.295],[0,0],[-2.683,-4.784],[0,0],[-0.033,-2.63],[0,0],[-4.716,-2.807],[0,0],[-1.338,-2.263],[0,0],[-5.483,-0.066],[0,0],[-2.296,-1.292],[0,0],[-4.782,2.683],[0,0],[-2.63,0.033],[0,0],[-2.807,4.716],[0,0],[-2.263,1.338],[0,0],[-0.066,5.483],[0,0],[-1.292,2.295],[0,0],[2.683,4.784],[0,0],[0.033,2.631],[0,0],[4.716,2.801],[0,0],[1.338,2.262],[0,0],[5.483,0.068],[0,0],[2.296,1.287]],"o":[[-4.782,-2.684],[0,0],[-2.296,1.287],[0,0],[-5.483,0.068],[0,0],[-1.338,2.262],[0,0],[-4.716,2.801],[0,0],[-0.033,2.631],[0,0],[-2.683,4.784],[0,0],[1.292,2.295],[0,0],[0.066,5.483],[0,0],[2.263,1.338],[0,0],[2.807,4.716],[0,0],[2.63,0.033],[0,0],[4.782,2.683],[0,0],[2.296,-1.292],[0,0],[5.483,-0.066],[0,0],[1.338,-2.263],[0,0],[4.716,-2.807],[0,0],[0.033,-2.63],[0,0],[2.683,-4.784],[0,0],[-1.292,-2.295],[0,0],[-0.066,-5.485],[0,0],[-2.263,-1.343],[0,0],[-2.807,-4.716],[0,0],[-2.63,-0.033],[0,0]],"v":[[7.7,-57.989],[-7.7,-57.989],[-11.019,-56.128],[-18.523,-54.117],[-22.327,-54.07],[-35.668,-46.369],[-37.609,-43.1],[-43.099,-37.605],[-46.372,-35.663],[-54.072,-22.324],[-54.118,-18.522],[-56.132,-11.016],[-57.988,-7.7],[-57.988,7.703],[-56.132,11.019],[-54.118,18.524],[-54.072,22.328],[-46.372,35.669],[-43.099,37.611],[-37.609,43.101],[-35.668,46.373],[-22.327,54.074],[-18.523,54.12],[-11.019,56.133],[-7.7,57.99],[7.7,57.99],[11.019,56.133],[18.523,54.12],[22.327,54.074],[35.668,46.373],[37.609,43.101],[43.099,37.611],[46.372,35.669],[54.072,22.328],[54.118,18.524],[56.132,11.019],[57.988,7.703],[57.988,-7.7],[56.132,-11.016],[54.118,-18.522],[54.072,-22.324],[46.372,-35.663],[43.099,-37.605],[37.609,-43.1],[35.668,-46.369],[22.327,-54.07],[18.523,-54.117],[11.019,-56.128]],"c":true}},"nm":"Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[396,104.003]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"widgets clock","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","parent":1,"sr":1,"ks":{"o":{"k":[{"s":[100],"t":45,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[80],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[0,-29.497,0]},"a":{"a":0,"k":[252,128.003,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[444,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 7","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[348,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 6","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,200.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 5","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,128.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 4","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[252,56.002]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 3","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[156,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 2","bm":0,"hd":false},{"ty":"gr","it":[{"ty":"gr","it":[{"d":1,"ty":"el","s":{"a":0,"k":[24,24]},"p":{"a":0,"k":[0,0]},"nm":"Ellipse Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"apps","bm":0,"hd":false},{"ty":"tr","p":{"a":0,"k":[60,56.004]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"app - 1","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0}]}],"layers":[{"ddd":0,"ind":1,"ty":0,"nm":"TrackpadAK_Success_Checkmark","refId":"comp_0","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,198.5,0]},"a":{"a":0,"k":[95,95,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":190,"h":190,"ip":6,"op":50,"st":6,"ct":1,"bm":0},{"ddd":0,"ind":2,"ty":4,"nm":".onSecondaryFixed","cl":"onSecondaryFixed","sr":1,"ks":{"o":{"a":1,"k":[{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":1,"s":[0]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":7,"s":[100]},{"i":{"x":[0.833],"y":[0.833]},"o":{"x":[0.167],"y":[0.167]},"t":389,"s":[100]},{"t":392,"s":[0]}]},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.145098039216,0.101960784314,0.01568627451,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":3,"ty":0,"nm":"actionKey_themed-static","refId":"comp_1","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[325,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":4,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[421,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":5,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[229,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":6,"ty":0,"nm":"BlankButton","refId":"comp_2","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[133,455.5,0]},"a":{"a":0,"k":[40,44.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":80,"h":89,"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":7,"ty":0,"nm":"AK_LofiLauncher","refId":"comp_3","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[252,157.5,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"w":504,"h":315,"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":8,"ty":4,"nm":".onSecondaryFixedVariant","cl":"onSecondaryFixedVariant","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"ef":[{"ty":5,"nm":"Global Position","np":4,"mn":"Pseudo/88900","ix":1,"en":1,"ef":[{"ty":10,"nm":"Master Parent","mn":"Pseudo/88900-0001","ix":1,"v":{"a":0,"k":2}},{"ty":3,"nm":"Global Position","mn":"Pseudo/88900-0002","ix":2,"v":{"k":[{"s":[277,197.5],"t":0,"i":{"x":1,"y":1},"o":{"x":0,"y":0}},{"s":[277,197.5],"t":49,"i":{"x":1,"y":1},"o":{"x":0,"y":0}}]}}]}],"shapes":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.325490196078,0.270588235294,0.164705882353,1]},"o":{"a":0,"k":50},"r":1,"bm":0,"nm":"Fill 1","hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":9,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980401039,0.768627464771,0.627451002598,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"illustrations: action key","bm":0,"hd":false}],"ip":0,"op":600,"st":0,"ct":1,"bm":0},{"ddd":0,"ind":10,"ty":4,"nm":".secondaryFixedDim","cl":"secondaryFixedDim","sr":1,"ks":{"o":{"a":0,"k":100},"r":{"a":0,"k":0},"p":{"a":0,"k":[277,197.5,0]},"a":{"a":0,"k":[0,0,0]},"s":{"a":0,"k":[100,100,100]}},"ao":0,"shapes":[{"ty":"gr","it":[{"ty":"rc","d":1,"s":{"a":0,"k":[504,315]},"p":{"a":0,"k":[0,0]},"r":{"a":0,"k":28},"nm":"Rectangle Path 1","hd":false},{"ty":"st","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"w":{"a":0,"k":14},"lc":1,"lj":1,"ml":4,"bm":0,"nm":"Stroke 1","hd":false},{"ty":"op","nm":"Stroke align: Outside","a":{"k":[{"s":[7],"t":0,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}},{"s":[7],"t":49,"i":{"x":[1],"y":[1]},"o":{"x":[0],"y":[0]}}]},"lj":1,"ml":{"a":0,"k":4},"hd":false},{"ty":"fl","c":{"a":0,"k":[0.850980392157,0.76862745098,0.627450980392,1]},"o":{"a":0,"k":100},"r":1,"bm":0,"nm":"Fill 1","hd":false},{"ty":"tr","p":{"a":0,"k":[0,0]},"a":{"a":0,"k":[0,0]},"s":{"a":0,"k":[100,100]},"r":{"a":0,"k":0},"o":{"a":0,"k":100},"sk":{"a":0,"k":0},"sa":{"a":0,"k":0},"nm":"Transform"}],"nm":"frame","bm":0,"hd":false}],"ip":0,"op":50,"st":0,"ct":1,"bm":0}],"markers":[],"props":{}}
\ No newline at end of file
diff --git a/packages/SystemUI/res/values/strings.xml b/packages/SystemUI/res/values/strings.xml
index e49f8c8..2ad6b6a 100644
--- a/packages/SystemUI/res/values/strings.xml
+++ b/packages/SystemUI/res/values/strings.xml
@@ -280,13 +280,17 @@
<!-- For updated Screen Recording permission dialog (i.e. with PSS)-->
<!-- Title for the screen prompting the user to begin recording their screen [CHAR LIMIT=NONE]-->
- <string name="screenrecord_permission_dialog_title">Start Recording?</string>
+ <string name="screenrecord_permission_dialog_title">Record your screen?</string>
+ <!-- Screen recording permission option for recording just a single app [CHAR LIMIT=50] -->
+ <string name="screenrecord_permission_dialog_option_text_single_app">Record one app</string>
+ <!-- Screen recording permission option for recording the whole screen [CHAR LIMIT=50] -->
+ <string name="screenrecord_permission_dialog_option_text_entire_screen">Record entire screen</string>
<!-- Message reminding the user that sensitive information may be captured during a full screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
- <string name="screenrecord_permission_dialog_warning_entire_screen">While you’re recording, Android has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+ <string name="screenrecord_permission_dialog_warning_entire_screen">When you’re recording your entire screen, anything shown on your screen is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
<!-- Message reminding the user that sensitive information may be captured during a single app screen recording for the updated dialog that includes partial screen sharing option [CHAR_LIMIT=350]-->
- <string name="screenrecord_permission_dialog_warning_single_app">While you’re recording an app, Android has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
- <!-- Button to start a screen recording in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]-->
- <string name="screenrecord_permission_dialog_continue">Start recording</string>
+ <string name="screenrecord_permission_dialog_warning_single_app">When you’re recording an app, anything shown or played in that app is recorded. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+ <!-- Button to start a screen recording of the entire screen in the updated screen record dialog that allows to select an app to record [CHAR LIMIT=50]-->
+ <string name="screenrecord_permission_dialog_continue_entire_screen">Record screen</string>
<!-- Label for a switch to enable recording audio [CHAR LIMIT=NONE]-->
<string name="screenrecord_audio_label">Record audio</string>
@@ -971,8 +975,8 @@
<string name="hearing_devices_presets_error">Couldn\'t update preset</string>
<!-- QuickSettings: Title for hearing aids presets. Preset is a set of hearing aid settings. User can apply different settings in different environments (e.g. Outdoor, Restaurant, Home) [CHAR LIMIT=40]-->
<string name="hearing_devices_preset_label">Preset</string>
- <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40]-->
- <string name="live_caption_title">Live Caption</string>
+ <!-- QuickSettings: Tool name for hearing devices dialog related tools [CHAR LIMIT=40] [BACKUP_MESSAGE_ID=8916875614623730005]-->
+ <string name="quick_settings_hearing_devices_live_caption_title">Live Caption</string>
<!--- Title of dialog triggered if the microphone is disabled but an app tried to access it. [CHAR LIMIT=150] -->
<string name="sensor_privacy_start_use_mic_dialog_title">Unblock device microphone?</string>
@@ -1358,22 +1362,23 @@
<!-- Media projection permission dialog warning text for system services. [CHAR LIMIT=NONE] -->
<string name="media_projection_sys_service_dialog_warning">The service providing this function will have access to all of the information that is visible on your screen or played from your device while recording or casting. This includes information such as passwords, payment details, photos, messages, and audio that you play.</string>
- <!-- Permission dropdown option for sharing or recording the whole screen. [CHAR LIMIT=30] -->
- <string name="screen_share_permission_dialog_option_entire_screen">Entire screen</string>
- <!-- Permission dropdown option for sharing or recording single app. [CHAR LIMIT=30] -->
- <string name="screen_share_permission_dialog_option_single_app">A single app</string>
<!-- Title of the dialog that allows to select an app to share or record [CHAR LIMIT=NONE] -->
<string name="screen_share_permission_app_selector_title">Share or record an app</string>
<!-- Media projection that launched from 1P/3P apps -->
<!-- 1P/3P app media projection permission dialog title. [CHAR LIMIT=NONE] -->
- <string name="media_projection_entry_app_permission_dialog_title">Start recording or casting with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string>
+ <string name="media_projection_entry_app_permission_dialog_title">Share your screen with <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>?</string>
+
+ <!-- 1P/3P app media projection permission option for capturing just a single app [CHAR LIMIT=50] -->
+ <string name="media_projection_entry_app_permission_dialog_option_text_single_app">Share one app</string>
+ <!-- 1P/3P app media projection permission option for capturing the whole screen [CHAR LIMIT=50] -->
+ <string name="media_projection_entry_app_permission_dialog_option_text_entire_screen">Share entire screen</string>
<!-- 1P/3P app media projection permission warning for capturing the whole screen. [CHAR LIMIT=350] -->
- <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing, recording, or casting, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything visible on your screen or played on your device. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+ <string name="media_projection_entry_app_permission_dialog_warning_entire_screen">When you’re sharing your entire screen, anything on your screen is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
<!-- 1P/3P app media projection permission warning for capturing an app. [CHAR LIMIT=350] -->
- <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing, recording, or casting an app, <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g> has access to anything shown or played on that app. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
+ <string name="media_projection_entry_app_permission_dialog_warning_single_app">When you’re sharing an app, anything shown or played in that app is visible to <xliff:g id="app_seeking_permission" example="Meet">%s</xliff:g>. So be careful with things like passwords, payment details, messages, photos, and audio and video.</string>
<!-- 1P/3P apps media projection permission button to continue with app selection or recording [CHAR LIMIT=60] -->
- <string name="media_projection_entry_app_permission_dialog_continue">Start</string>
+ <string name="media_projection_entry_app_permission_dialog_continue_entire_screen">Share screen</string>
<!-- 1P/3P apps disabled the single app projection option. [CHAR LIMIT=NONE] -->
<string name="media_projection_entry_app_permission_dialog_single_app_disabled"><xliff:g id="app_name" example="Meet">%1$s</xliff:g> has disabled this option</string>
@@ -3674,6 +3679,7 @@
-->
<string name="shortcut_helper_key_combinations_or_separator">or</string>
+ <!-- TOUCHPAD TUTORIAL-->
<!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
<string name="touchpad_tutorial_back_gesture_button">Back gesture</string>
<!-- Label for button opening tutorial for back gesture on touchpad [CHAR LIMIT=NONE] -->
@@ -3688,19 +3694,29 @@
<!-- Touchpad back gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_back_gesture_guidance">To go back, swipe left or right using three fingers anywhere on the touchpad.\n\nYou can also use the keyboard shortcut
Action + ESC for this.</string>
- <!-- Screen title after gesture was done successfully [CHAR LIMIT=NONE] -->
- <string name="touchpad_tutorial_gesture_done">Great job!</string>
+ <!-- Screen title after back gesture was done successfully [CHAR LIMIT=NONE] -->
+ <string name="touchpad_back_gesture_success_title">Great job!</string>
<!-- Text shown to the user after they complete back gesture tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_back_gesture_finished">You completed the go back gesture.</string>
+ <string name="touchpad_back_gesture_success_body">You completed the go back gesture.</string>
<!-- HOME GESTURE -->
<!-- Touchpad home gesture action name in tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_home_gesture_action_title">Go home</string>
<!-- Touchpad home gesture guidance in gestures tutorial [CHAR LIMIT=NONE] -->
<string name="touchpad_home_gesture_guidance">To go to your home screen at any time, swipe up with three fingers from the bottom of your screen.</string>
<!-- Screen title after home gesture was done successfully [CHAR LIMIT=NONE] -->
- <string name="touchpad_home_gesture_done">Nice!</string>
+ <string name="touchpad_home_gesture_success_title">Nice!</string>
<!-- Text shown to the user after they complete home gesture tutorial [CHAR LIMIT=NONE] -->
- <string name="touchpad_home_gesture_finished">You completed the go home gesture.</string>
+ <string name="touchpad_home_gesture_success_body">You completed the go home gesture.</string>
+
+ <!-- KEYBOARD TUTORIAL-->
+ <!-- Action key tutorial title [CHAR LIMIT=NONE] -->
+ <string name="tutorial_action_key_title">Action key</string>
+ <!-- Action key tutorial guidance[CHAR LIMIT=NONE] -->
+ <string name="tutorial_action_key_guidance">To access your apps, press the action key on your keyboard.</string>
+ <!-- Screen title after action key pressed successfully [CHAR LIMIT=NONE] -->
+ <string name="tutorial_action_key_success_title">Congratulations!</string>
+ <!-- Text shown to the user after they complete action key tutorial [CHAR LIMIT=NONE] -->
+ <string name="tutorial_action_key_success_body">You completed the action key gesture.\n\nAction + / shows all the shortcuts you have available.</string>
<!-- Content description for keyboard backlight brightness dialog [CHAR LIMIT=NONE] -->
<string name="keyboard_backlight_dialog_title">Keyboard backlight</string>
diff --git a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
index 12d881b..c0b6acf 100644
--- a/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
+++ b/packages/SystemUI/shared/biometrics/src/com/android/systemui/biometrics/Utils.kt
@@ -16,6 +16,7 @@
package com.android.systemui.biometrics
import android.Manifest
+import android.app.ActivityTaskManager
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHABETIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_ALPHANUMERIC
import android.app.admin.DevicePolicyManager.PASSWORD_QUALITY_COMPLEX
@@ -35,6 +36,7 @@
import android.hardware.biometrics.SensorPropertiesInternal
import android.os.UserManager
import android.util.DisplayMetrics
+import android.util.Log
import android.view.ViewGroup
import android.view.WindowInsets
import android.view.WindowManager
@@ -45,6 +47,8 @@
import com.android.systemui.biometrics.shared.model.PromptKind
object Utils {
+ private const val TAG = "SysUIBiometricUtils"
+
/** Base set of layout flags for fingerprint overlay widgets. */
const val FINGERPRINT_OVERLAY_LAYOUT_PARAM_FLAGS =
(WindowManager.LayoutParams.FLAG_LAYOUT_IN_SCREEN or
@@ -148,4 +152,39 @@
draw(canvas)
return bitmap
}
+
+ // LINT.IfChange
+ @JvmStatic
+ /**
+ * Checks if a client package is running in the background or it's a system app.
+ *
+ * @param clientPackage The name of the package to be checked.
+ * @param clientClassNameIfItIsConfirmDeviceCredentialActivity The class name of
+ * ConfirmDeviceCredentialActivity.
+ * @return Whether the client package is running in background
+ */
+ fun ActivityTaskManager.isSystemAppOrInBackground(
+ context: Context,
+ clientPackage: String,
+ clientClassNameIfItIsConfirmDeviceCredentialActivity: String?
+ ): Boolean {
+ Log.v(TAG, "Checking if the authenticating is in background, clientPackage:$clientPackage")
+ val tasks = getTasks(Int.MAX_VALUE)
+ if (tasks == null || tasks.isEmpty()) {
+ Log.w(TAG, "No running tasks reported")
+ return false
+ }
+
+ val topActivity = tasks[0].topActivity
+ val isSystemApp = isSystem(context, clientPackage)
+ val topPackageEqualsToClient = topActivity!!.packageName == clientPackage
+ val isClientConfirmDeviceCredentialActivity =
+ clientClassNameIfItIsConfirmDeviceCredentialActivity != null
+ // b/339532378: If it's ConfirmDeviceCredentialActivity, we need to check further on
+ // class name.
+ return !(isSystemApp || topPackageEqualsToClient) ||
+ (isClientConfirmDeviceCredentialActivity &&
+ topActivity.className != clientClassNameIfItIsConfirmDeviceCredentialActivity)
+ }
+ // LINT.ThenChange(frameworks/base/services/core/java/com/android/server/biometrics/Utils.java)
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
index 317201d..f358ba2 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButton.java
@@ -125,6 +125,7 @@
taskbarMarginLeft, taskbarMarginBottom, floatingRotationButtonPositionLeft);
final int diameter = res.getDimensionPixelSize(mButtonDiameterResource);
+ mKeyButtonView.setDiameter(diameter);
mContainerSize = diameter + Math.max(defaultMargin, Math.max(taskbarMarginLeft,
taskbarMarginBottom));
}
@@ -195,6 +196,7 @@
public void updateIcon(int lightIconColor, int darkIconColor) {
mAnimatedDrawable = (AnimatedVectorDrawable) mKeyButtonView.getContext()
.getDrawable(mRotationButtonController.getIconResId());
+ mAnimatedDrawable.setBounds(0, 0, mKeyButtonView.getWidth(), mKeyButtonView.getHeight());
mKeyButtonView.setImageDrawable(mAnimatedDrawable);
mKeyButtonView.setColors(lightIconColor, darkIconColor);
}
@@ -248,8 +250,14 @@
updateDimensionResources();
if (mIsShowing) {
+ updateIcon(mRotationButtonController.getLightIconColor(),
+ mRotationButtonController.getDarkIconColor());
final LayoutParams layoutParams = adjustViewPositionAndCreateLayoutParams();
mWindowManager.updateViewLayout(mKeyButtonContainer, layoutParams);
+ if (mAnimatedDrawable != null) {
+ mAnimatedDrawable.reset();
+ mAnimatedDrawable.start();
+ }
}
}
diff --git a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
index 2145166..75412f9 100644
--- a/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
+++ b/packages/SystemUI/shared/src/com/android/systemui/shared/rotation/FloatingRotationButtonView.java
@@ -37,6 +37,7 @@
private static final float BACKGROUND_ALPHA = 0.92f;
private KeyButtonRipple mRipple;
+ private int mDiameter;
private final Paint mOvalBgPaint = new Paint(Paint.ANTI_ALIAS_FLAG | Paint.FILTER_BITMAP_FLAG);
private final Configuration mLastConfiguration;
@@ -93,10 +94,25 @@
mRipple.setDarkIntensity(darkIntensity);
}
+ /**
+ * Sets the view's diameter.
+ *
+ * @param diameter the diameter value for the view
+ */
+ void setDiameter(int diameter) {
+ mDiameter = diameter;
+ }
+
@Override
public void draw(Canvas canvas) {
int d = Math.min(getWidth(), getHeight());
canvas.drawOval(0, 0, d, d, mOvalBgPaint);
super.draw(canvas);
}
+
+ @Override
+ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
+ super.onMeasure(widthMeasureSpec, heightMeasureSpec);
+ setMeasuredDimension(mDiameter, mDiameter);
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
index 083f1db..d08653c3 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesDialogDelegate.java
@@ -228,7 +228,7 @@
mHearingDeviceItemList = getHearingDevicesList();
if (mPresetsController != null) {
activeHearingDevice = getActiveHearingDevice(mHearingDeviceItemList);
- mPresetsController.setActiveHearingDevice(activeHearingDevice);
+ mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
} else {
activeHearingDevice = null;
}
@@ -336,7 +336,7 @@
}
final CachedBluetoothDevice activeHearingDevice = getActiveHearingDevice(
mHearingDeviceItemList);
- mPresetsController.setActiveHearingDevice(activeHearingDevice);
+ mPresetsController.setHearingDeviceIfSupportHap(activeHearingDevice);
mPresetInfoAdapter = new ArrayAdapter<>(dialog.getContext(),
R.layout.hearing_devices_preset_spinner_selected,
@@ -499,7 +499,8 @@
final List<ResolveInfo> resolved = packageManager.queryIntentActivities(LIVE_CAPTION_INTENT,
/* flags= */ 0);
if (!resolved.isEmpty()) {
- return new ToolItem(context.getString(R.string.live_caption_title),
+ return new ToolItem(
+ context.getString(R.string.quick_settings_hearing_devices_live_caption_title),
context.getDrawable(R.drawable.ic_volume_odi_captions),
LIVE_CAPTION_INTENT);
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
index b46b8fe..664f3f8 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesListAdapter.java
@@ -27,6 +27,7 @@
import androidx.annotation.NonNull;
import androidx.recyclerview.widget.RecyclerView;
+import com.android.settingslib.Utils;
import com.android.systemui.bluetooth.qsdialog.DeviceItem;
import com.android.systemui.res.R;
@@ -105,6 +106,7 @@
private final TextView mNameView;
private final TextView mSummaryView;
private final ImageView mIconView;
+ private final ImageView mGearIcon;
private final View mGearView;
DeviceItemViewHolder(@NonNull View itemView, Context context) {
@@ -114,6 +116,7 @@
mNameView = itemView.requireViewById(R.id.bluetooth_device_name);
mSummaryView = itemView.requireViewById(R.id.bluetooth_device_summary);
mIconView = itemView.requireViewById(R.id.bluetooth_device_icon);
+ mGearIcon = itemView.requireViewById(R.id.gear_icon_image);
mGearView = itemView.requireViewById(R.id.gear_icon);
}
@@ -124,13 +127,31 @@
if (backgroundResId != null) {
mContainer.setBackground(mContext.getDrawable(item.getBackground()));
}
- mNameView.setText(item.getDeviceName());
- mSummaryView.setText(item.getConnectionSummary());
+
+ // tint different color in different state for bad color contrast problem
+ int tintColor = item.isActive() ? Utils.getColorAttr(mContext,
+ com.android.internal.R.attr.materialColorOnPrimaryContainer).getDefaultColor()
+ : Utils.getColorAttr(mContext,
+ com.android.internal.R.attr.materialColorOnSurface).getDefaultColor();
+
Pair<Drawable, String> iconPair = item.getIconWithDescription();
if (iconPair != null) {
- mIconView.setImageDrawable(iconPair.getFirst());
+ Drawable drawable = iconPair.getFirst().mutate();
+ drawable.setTint(tintColor);
+ mIconView.setImageDrawable(drawable);
mIconView.setContentDescription(iconPair.getSecond());
}
+
+ mNameView.setTextAppearance(
+ item.isActive() ? R.style.BluetoothTileDialog_DeviceName_Active
+ : R.style.BluetoothTileDialog_DeviceName);
+ mNameView.setText(item.getDeviceName());
+ mSummaryView.setTextAppearance(
+ item.isActive() ? R.style.BluetoothTileDialog_DeviceSummary_Active
+ : R.style.BluetoothTileDialog_DeviceSummary);
+ mSummaryView.setText(item.getConnectionSummary());
+
+ mGearIcon.getDrawable().mutate().setTint(tintColor);
mGearView.setOnClickListener(view -> callback.onDeviceItemGearClicked(item, view));
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
index f81124e..aa95fd0 100644
--- a/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
+++ b/packages/SystemUI/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsController.java
@@ -113,7 +113,7 @@
@Override
public void onPresetSelectionForGroupFailed(int hapGroupId, int reason) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -137,7 +137,7 @@
@Override
public void onSetPresetNameForGroupFailed(int hapGroupId, int reason) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (hapGroupId == mHapClientProfile.getHapGroup(mActiveHearingDevice.getDevice())) {
@@ -177,22 +177,33 @@
}
/**
- * Sets the hearing device for this controller to control the preset.
+ * Sets the hearing device for this controller to control the preset if it supports
+ * {@link HapClientProfile}.
*
* @param activeHearingDevice the {@link CachedBluetoothDevice} need to be hearing aid device
+ * and support {@link HapClientProfile}.
*/
- public void setActiveHearingDevice(CachedBluetoothDevice activeHearingDevice) {
- mActiveHearingDevice = activeHearingDevice;
+ public void setHearingDeviceIfSupportHap(CachedBluetoothDevice activeHearingDevice) {
+ if (mHapClientProfile == null || activeHearingDevice == null) {
+ mActiveHearingDevice = null;
+ return;
+ }
+ if (activeHearingDevice.getProfiles().stream().anyMatch(
+ profile -> profile instanceof HapClientProfile)) {
+ mActiveHearingDevice = activeHearingDevice;
+ } else {
+ mActiveHearingDevice = null;
+ }
}
/**
* Selects the currently active preset for {@code mActiveHearingDevice} individual device or
- * the device group accoridng to whether it supports synchronized presets or not.
+ * the device group according to whether it supports synchronized presets or not.
*
* @param presetIndex an index of one of the available presets
*/
public void selectPreset(int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
mSelectedPresetIndex = presetIndex;
@@ -217,7 +228,7 @@
* @return a list of all known preset info
*/
public List<BluetoothHapPresetInfo> getAllPresetInfo() {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return emptyList();
}
return mHapClientProfile.getAllPresetInfo(mActiveHearingDevice.getDevice()).stream().filter(
@@ -230,14 +241,14 @@
* @return active preset index
*/
public int getActivePresetIndex() {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return BluetoothHapClient.PRESET_INDEX_UNAVAILABLE;
}
return mHapClientProfile.getActivePresetIndex(mActiveHearingDevice.getDevice());
}
private void selectPresetSynchronously(int groupId, int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (DEBUG) {
@@ -250,7 +261,7 @@
}
private void selectPresetIndependently(int presetIndex) {
- if (mActiveHearingDevice == null) {
+ if (mActiveHearingDevice == null || mHapClientProfile == null) {
return;
}
if (DEBUG) {
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
index d5790a4..a093f58 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/BouncerSwipeTouchHandler.kt
@@ -118,7 +118,8 @@
if (Flags.dreamOverlayBouncerSwipeDirectionFiltering()) {
(abs(distanceY.toDouble()) > abs(distanceX.toDouble()) &&
distanceY > 0) &&
- if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable
+ else true
} else {
// If the user scrolling favors a vertical direction, begin capturing
// scrolls.
@@ -175,7 +176,7 @@
}
init {
- if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
scope.launch {
communalViewModel.glanceableTouchAvailable.collect {
onGlanceableTouchAvailable(it)
@@ -218,7 +219,7 @@
val normalRegion =
Rect(0, Math.round(height * (1 - bouncerZoneScreenPercentage)), width, height)
- if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
region.op(bounds, Region.Op.UNION)
exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) }
}
@@ -265,7 +266,7 @@
when (motionEvent.action) {
MotionEvent.ACTION_CANCEL,
MotionEvent.ACTION_UP -> {
- if (Flags.hubmodeFullscreenVerticalSwipe() && capture == true) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix() && capture == true) {
communalViewModel.onResetTouchState()
}
touchSession?.apply { pop() }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
index 06b41de..9da9a3a 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/ShadeTouchHandler.kt
@@ -61,7 +61,7 @@
private var touchAvailable = false
init {
- if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
scope.launch {
communalViewModel.glanceableTouchAvailable.collect {
onGlanceableTouchAvailable(it)
@@ -107,7 +107,8 @@
capture =
abs(distanceY.toDouble()) > abs(distanceX.toDouble()) &&
distanceY < 0 &&
- if (Flags.hubmodeFullscreenVerticalSwipe()) touchAvailable else true
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) touchAvailable
+ else true
if (capture == true) {
// Send the initial touches over, as the input listener has already
// processed these touches.
@@ -144,7 +145,7 @@
override fun getTouchInitiationRegion(bounds: Rect, region: Region, exclusionRect: Rect?) {
// If fullscreen swipe, use entire space minus exclusion region
- if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
region.op(bounds, Region.Op.UNION)
exclusionRect?.apply { region.op(this, Region.Op.DIFFERENCE) }
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
index 190bc15..d27e72a 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchHandler.java
@@ -122,4 +122,9 @@
* @param session
*/
void onSessionStart(TouchSession session);
+
+ /**
+ * Called when the handler is being torn down.
+ */
+ default void onDestroy() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
index efa55e9..1be6f9e 100644
--- a/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
+++ b/packages/SystemUI/src/com/android/systemui/ambient/touch/TouchMonitor.java
@@ -581,6 +581,10 @@
mBoundsFlow.cancel(new CancellationException());
}
+ for (TouchHandler handler : mHandlers) {
+ handler.onDestroy();
+ }
+
mInitialized = false;
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
index 9521be1..723587e 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthContainerView.java
@@ -17,11 +17,9 @@
package com.android.systemui.biometrics;
import static android.hardware.biometrics.BiometricAuthenticator.TYPE_FACE;
-import static android.hardware.fingerprint.FingerprintSensorProperties.TYPE_POWER_BUTTON;
import static android.view.WindowManager.LayoutParams.LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS;
import static com.android.internal.jank.InteractionJankMonitor.CUJ_BIOMETRIC_PROMPT_TRANSITION;
-import static com.android.systemui.Flags.constraintBp;
import android.animation.Animator;
import android.annotation.IntDef;
@@ -30,8 +28,6 @@
import android.app.AlertDialog;
import android.content.Context;
import android.content.res.Configuration;
-import android.content.res.TypedArray;
-import android.graphics.Color;
import android.graphics.PixelFormat;
import android.hardware.biometrics.BiometricAuthenticator.Modality;
import android.hardware.biometrics.BiometricConstants;
@@ -41,17 +37,11 @@
import android.hardware.face.FaceSensorPropertiesInternal;
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
import android.os.Binder;
-import android.os.Handler;
import android.os.IBinder;
-import android.os.Looper;
import android.os.UserManager;
import android.util.Log;
-import android.view.Display;
-import android.view.DisplayInfo;
-import android.view.Gravity;
import android.view.KeyEvent;
import android.view.LayoutInflater;
-import android.view.Surface;
import android.view.View;
import android.view.ViewGroup;
import android.view.WindowInsets;
@@ -60,7 +50,6 @@
import android.widget.FrameLayout;
import android.widget.ImageView;
import android.widget.LinearLayout;
-import android.widget.ScrollView;
import android.window.OnBackInvokedCallback;
import android.window.OnBackInvokedDispatcher;
@@ -74,7 +63,6 @@
import com.android.systemui.biometrics.domain.interactor.PromptSelectorInteractor;
import com.android.systemui.biometrics.shared.model.BiometricModalities;
import com.android.systemui.biometrics.shared.model.PromptKind;
-import com.android.systemui.biometrics.ui.BiometricPromptLayout;
import com.android.systemui.biometrics.ui.CredentialView;
import com.android.systemui.biometrics.ui.binder.BiometricViewBinder;
import com.android.systemui.biometrics.ui.binder.BiometricViewSizeBinder;
@@ -111,7 +99,6 @@
private static final int ANIMATION_DURATION_SHOW_MS = 250;
private static final int ANIMATION_DURATION_AWAY_MS = 350;
- private static final int ANIMATE_CREDENTIAL_START_DELAY_MS = 300;
private static final int STATE_UNKNOWN = 0;
private static final int STATE_ANIMATING_IN = 1;
@@ -136,13 +123,11 @@
private final Config mConfig;
private final int mEffectiveUserId;
- private final Handler mHandler;
private final IBinder mWindowToken = new Binder();
private final WindowManager mWindowManager;
private final Interpolator mLinearOutSlowIn;
private final LockPatternUtils mLockPatternUtils;
private final WakefulnessLifecycle mWakefulnessLifecycle;
- private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private final InteractionJankMonitor mInteractionJankMonitor;
private final CoroutineScope mApplicationCoroutineScope;
@@ -159,10 +144,7 @@
private final AuthPanelController mPanelController;
private final ViewGroup mLayout;
private final ImageView mBackgroundView;
- private final ScrollView mBiometricScrollView;
private final View mPanelView;
- private final List<FingerprintSensorPropertiesInternal> mFpProps;
- private final List<FaceSensorPropertiesInternal> mFaceProps;
private final float mTranslationY;
@VisibleForTesting @ContainerState int mContainerState = STATE_UNKNOWN;
private final Set<Integer> mFailedModalities = new HashSet<Integer>();
@@ -229,13 +211,7 @@
@Override
public void onUseDeviceCredential() {
mConfig.mCallback.onDeviceCredentialPressed(getRequestId());
- if (constraintBp()) {
- addCredentialView(false /* animatePanel */, true /* animateContents */);
- } else {
- mHandler.postDelayed(() -> {
- addCredentialView(false /* animatePanel */, true /* animateContents */);
- }, mConfig.mSkipAnimation ? 0 : ANIMATE_CREDENTIAL_START_DELAY_MS);
- }
+ addCredentialView(false /* animatePanel */, true /* animateContents */);
// TODO(b/313469218): Remove Config
mConfig.mPromptInfo.setAuthenticators(Authenticators.DEVICE_CREDENTIAL);
@@ -303,36 +279,12 @@
@Nullable List<FingerprintSensorPropertiesInternal> fpProps,
@Nullable List<FaceSensorPropertiesInternal> faceProps,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
- @NonNull UserManager userManager,
- @NonNull LockPatternUtils lockPatternUtils,
- @NonNull InteractionJankMonitor jankMonitor,
- @NonNull Provider<PromptSelectorInteractor> promptSelectorInteractor,
- @NonNull PromptViewModel promptViewModel,
- @NonNull Provider<CredentialViewModel> credentialViewModelProvider,
- @NonNull @Background DelayableExecutor bgExecutor,
- @NonNull VibratorHelper vibratorHelper) {
- this(config, applicationCoroutineScope, fpProps, faceProps,
- wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
- jankMonitor, promptSelectorInteractor, promptViewModel,
- credentialViewModelProvider, new Handler(Looper.getMainLooper()), bgExecutor,
- vibratorHelper);
- }
-
- @VisibleForTesting
- AuthContainerView(@NonNull Config config,
- @NonNull CoroutineScope applicationCoroutineScope,
- @Nullable List<FingerprintSensorPropertiesInternal> fpProps,
- @Nullable List<FaceSensorPropertiesInternal> faceProps,
- @NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull InteractionJankMonitor jankMonitor,
@NonNull Provider<PromptSelectorInteractor> promptSelectorInteractorProvider,
@NonNull PromptViewModel promptViewModel,
@NonNull Provider<CredentialViewModel> credentialViewModelProvider,
- @NonNull Handler mainHandler,
@NonNull @Background DelayableExecutor bgExecutor,
@NonNull VibratorHelper vibratorHelper) {
super(config.mContext);
@@ -340,10 +292,8 @@
mConfig = config;
mLockPatternUtils = lockPatternUtils;
mEffectiveUserId = userManager.getCredentialOwnerProfile(mConfig.mUserId);
- mHandler = mainHandler;
mWindowManager = mContext.getSystemService(WindowManager.class);
mWakefulnessLifecycle = wakefulnessLifecycle;
- mPanelInteractionDetector = panelInteractionDetector;
mApplicationCoroutineScope = applicationCoroutineScope;
mPromptViewModel = promptViewModel;
@@ -352,8 +302,6 @@
mLinearOutSlowIn = Interpolators.LINEAR_OUT_SLOW_IN;
mBiometricCallback = new BiometricCallback();
- mFpProps = fpProps;
- mFaceProps = faceProps;
final BiometricModalities biometricModalities = new BiometricModalities(
Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds));
@@ -367,7 +315,7 @@
final LayoutInflater layoutInflater = LayoutInflater.from(mContext);
final PromptKind kind = mPromptViewModel.getPromptKind().getValue();
- if (constraintBp() && kind.isBiometric()) {
+ if (kind.isBiometric()) {
if (kind.isTwoPaneLandscapeBiometric()) {
mLayout = (ConstraintLayout) layoutInflater.inflate(
R.layout.biometric_prompt_two_pane_layout, this, false /* attachToRoot */);
@@ -379,26 +327,16 @@
mLayout = (FrameLayout) layoutInflater.inflate(
R.layout.auth_container_view, this, false /* attachToRoot */);
}
- mBiometricScrollView = mLayout.findViewById(R.id.biometric_scrollview);
addView(mLayout);
mBackgroundView = mLayout.findViewById(R.id.background);
mPanelView = mLayout.findViewById(R.id.panel);
- if (!constraintBp()) {
- final TypedArray ta = mContext.obtainStyledAttributes(new int[]{
- android.R.attr.colorBackgroundFloating});
- mPanelView.setBackgroundColor(ta.getColor(0, Color.WHITE));
- ta.recycle();
- }
mPanelController = new AuthPanelController(mContext, mPanelView);
mBackgroundExecutor = bgExecutor;
mInteractionJankMonitor = jankMonitor;
mCredentialViewModelProvider = credentialViewModelProvider;
- showPrompt(config, layoutInflater, promptViewModel,
- Utils.findFirstSensorProperties(fpProps, mConfig.mSensorIds),
- Utils.findFirstSensorProperties(faceProps, mConfig.mSensorIds),
- vibratorHelper);
+ showPrompt(promptViewModel, vibratorHelper);
// TODO: De-dupe the logic with AuthCredentialPasswordView
setOnKeyListener((v, keyCode, event) -> {
@@ -415,52 +353,25 @@
requestFocus();
}
- private void showPrompt(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
- @NonNull PromptViewModel viewModel,
- @Nullable FingerprintSensorPropertiesInternal fpProps,
- @Nullable FaceSensorPropertiesInternal faceProps,
- @NonNull VibratorHelper vibratorHelper
- ) {
+ private void showPrompt(@NonNull PromptViewModel viewModel,
+ @NonNull VibratorHelper vibratorHelper) {
if (mPromptViewModel.getPromptKind().getValue().isBiometric()) {
- addBiometricView(config, layoutInflater, viewModel, fpProps, faceProps, vibratorHelper);
+ addBiometricView(viewModel, vibratorHelper);
} else if (mPromptViewModel.getPromptKind().getValue().isCredential()) {
- if (constraintBp()) {
- addCredentialView(true, false);
- }
+ addCredentialView(true, false);
} else {
mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
}
}
- private void addBiometricView(@NonNull Config config, @NonNull LayoutInflater layoutInflater,
- @NonNull PromptViewModel viewModel,
- @Nullable FingerprintSensorPropertiesInternal fpProps,
- @Nullable FaceSensorPropertiesInternal faceProps,
+ private void addBiometricView(@NonNull PromptViewModel viewModel,
@NonNull VibratorHelper vibratorHelper) {
-
- if (constraintBp()) {
- mBiometricView = BiometricViewBinder.bind(mLayout, viewModel, null,
- // TODO(b/201510778): This uses the wrong timeout in some cases
- getJankListener(mLayout, TRANSIT,
- BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
- mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
- vibratorHelper);
- } else {
- final BiometricPromptLayout view = (BiometricPromptLayout) layoutInflater.inflate(
- R.layout.biometric_prompt_layout, null, false);
- mBiometricView = BiometricViewBinder.bind(view, viewModel, mPanelController,
- // TODO(b/201510778): This uses the wrong timeout in some cases
- getJankListener(view, TRANSIT,
- BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
- mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
- vibratorHelper);
-
- // TODO(b/251476085): migrate these dependencies
- if (fpProps != null && fpProps.isAnyUdfpsType()) {
- view.setUdfpsAdapter(new UdfpsDialogMeasureAdapter(view, fpProps),
- config.mScaleProvider);
- }
- }
+ mBiometricView = BiometricViewBinder.bind(mLayout, viewModel,
+ // TODO(b/201510778): This uses the wrong timeout in some cases
+ getJankListener(mLayout, TRANSIT,
+ BiometricViewSizeBinder.ANIMATE_MEDIUM_TO_LARGE_DURATION_MS),
+ mBackgroundView, mBiometricCallback, mApplicationCoroutineScope,
+ vibratorHelper);
}
@VisibleForTesting
@@ -524,9 +435,6 @@
@Override
public void onOrientationChanged() {
- if (!constraintBp()) {
- updatePositionByCapability(true /* invalidate */);
- }
}
@Override
@@ -538,23 +446,6 @@
}
mWakefulnessLifecycle.addObserver(this);
- if (constraintBp()) {
- // Do nothing on attachment with constraintLayout
- } else if (mPromptViewModel.getPromptKind().getValue().isBiometric()) {
- mBiometricScrollView.addView(mBiometricView.asView());
- } else if (mPromptViewModel.getPromptKind().getValue().isCredential()) {
- addCredentialView(true /* animatePanel */, false /* animateContents */);
- } else {
- throw new IllegalStateException("Unknown configuration: "
- + mConfig.mPromptInfo.getAuthenticators());
- }
-
- if (!constraintBp()) {
- mPanelInteractionDetector.enable(
- () -> animateAway(AuthDialogCallback.DISMISSED_USER_CANCELED));
- updatePositionByCapability(false /* invalidate */);
- }
-
if (mConfig.mSkipIntro) {
mContainerState = STATE_SHOWING;
} else {
@@ -618,120 +509,8 @@
};
}
- private void updatePositionByCapability(boolean forceInvalidate) {
- final FingerprintSensorPropertiesInternal fpProp = Utils.findFirstSensorProperties(
- mFpProps, mConfig.mSensorIds);
- final FaceSensorPropertiesInternal faceProp = Utils.findFirstSensorProperties(
- mFaceProps, mConfig.mSensorIds);
- if (fpProp != null && fpProp.isAnyUdfpsType()) {
- maybeUpdatePositionForUdfps(forceInvalidate /* invalidate */);
- }
- if (faceProp != null && mBiometricView != null && mBiometricView.isFaceOnly()) {
- alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */);
- }
- if (fpProp != null && fpProp.sensorType == TYPE_POWER_BUTTON) {
- alwaysUpdatePositionAtScreenBottom(forceInvalidate /* invalidate */);
- }
- }
-
- private static boolean shouldUpdatePositionForUdfps(@NonNull View view) {
- if (view instanceof BiometricPromptLayout) {
- // this will force the prompt to align itself on the edge of the screen
- // instead of centering (temporary workaround to prevent small implicit view
- // from breaking due to the way gravity / margins are set in the legacy
- // AuthPanelController
- return true;
- }
-
- return false;
- }
-
- private boolean maybeUpdatePositionForUdfps(boolean invalidate) {
- final Display display = getDisplay();
- if (display == null) {
- return false;
- }
-
- final DisplayInfo cachedDisplayInfo = new DisplayInfo();
- display.getDisplayInfo(cachedDisplayInfo);
- if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
- return false;
- }
-
- final int displayRotation = cachedDisplayInfo.rotation;
- switch (displayRotation) {
- case Surface.ROTATION_0:
- mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
- setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
- break;
-
- case Surface.ROTATION_90:
- mPanelController.setPosition(AuthPanelController.POSITION_RIGHT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.RIGHT);
- break;
-
- case Surface.ROTATION_270:
- mPanelController.setPosition(AuthPanelController.POSITION_LEFT);
- setScrollViewGravity(Gravity.CENTER_VERTICAL | Gravity.LEFT);
- break;
-
- case Surface.ROTATION_180:
- default:
- Log.e(TAG, "Unsupported display rotation: " + displayRotation);
- mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
- setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
- break;
- }
-
- if (invalidate) {
- mPanelView.invalidateOutline();
- }
-
- return true;
- }
-
- private boolean alwaysUpdatePositionAtScreenBottom(boolean invalidate) {
- final Display display = getDisplay();
- if (display == null) {
- return false;
- }
- if (mBiometricView == null || !shouldUpdatePositionForUdfps(mBiometricView.asView())) {
- return false;
- }
-
- final int displayRotation = display.getRotation();
- switch (displayRotation) {
- case Surface.ROTATION_0:
- case Surface.ROTATION_90:
- case Surface.ROTATION_270:
- case Surface.ROTATION_180:
- mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
- setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
- break;
- default:
- Log.e(TAG, "Unsupported display rotation: " + displayRotation);
- mPanelController.setPosition(AuthPanelController.POSITION_BOTTOM);
- setScrollViewGravity(Gravity.CENTER_HORIZONTAL | Gravity.BOTTOM);
- break;
- }
-
- if (invalidate) {
- mPanelView.invalidateOutline();
- }
-
- return true;
- }
-
- private void setScrollViewGravity(int gravity) {
- final FrameLayout.LayoutParams params =
- (FrameLayout.LayoutParams) mBiometricScrollView.getLayoutParams();
- params.gravity = gravity;
- mBiometricScrollView.setLayoutParams(params);
- }
-
@Override
public void onDetachedFromWindow() {
- mPanelInteractionDetector.disable();
OnBackInvokedDispatcher dispatcher = findOnBackInvokedDispatcher();
if (dispatcher != null) {
findOnBackInvokedDispatcher().unregisterOnBackInvokedCallback(mBackCallback);
@@ -834,6 +613,11 @@
}
@Override
+ public String getClassNameIfItIsConfirmDeviceCredentialActivity() {
+ return mConfig.mPromptInfo.getClassNameIfItIsConfirmDeviceCredentialActivity();
+ }
+
+ @Override
public long getRequestId() {
return mConfig.mRequestId;
}
@@ -878,7 +662,7 @@
final Runnable endActionRunnable = () -> {
setVisibility(View.INVISIBLE);
- if (Flags.customBiometricPrompt() && constraintBp()) {
+ if (Flags.customBiometricPrompt()) {
// TODO(b/288175645): resetPrompt calls should be lifecycle aware
mPromptSelectorInteractorProvider.get().resetPrompt(getRequestId());
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
index b466f31..037f5b7 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthController.java
@@ -22,7 +22,6 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityManager;
import android.app.ActivityTaskManager;
import android.app.TaskStackListener;
import android.content.BroadcastReceiver;
@@ -173,7 +172,6 @@
@NonNull private final SparseBooleanArray mSfpsEnrolledForUser;
@NonNull private final SensorPrivacyManager mSensorPrivacyManager;
private final WakefulnessLifecycle mWakefulnessLifecycle;
- private final AuthDialogPanelInteractionDetector mPanelInteractionDetector;
private boolean mAllFingerprintAuthenticatorsRegistered;
@NonNull private final UserManager mUserManager;
@NonNull private final LockPatternUtils mLockPatternUtils;
@@ -187,7 +185,7 @@
final TaskStackListener mTaskStackListener = new TaskStackListener() {
@Override
public void onTaskStackChanged() {
- if (!isOwnerInForeground()) {
+ if (isOwnerInBackground()) {
mHandler.post(AuthController.this::cancelIfOwnerIsNotInForeground);
}
}
@@ -227,21 +225,20 @@
}
}
- private boolean isOwnerInForeground() {
+ private boolean isOwnerInBackground() {
if (mCurrentDialog != null) {
final String clientPackage = mCurrentDialog.getOpPackageName();
- final List<ActivityManager.RunningTaskInfo> runningTasks =
- mActivityTaskManager.getTasks(1);
- if (!runningTasks.isEmpty()) {
- final String topPackage = runningTasks.get(0).topActivity.getPackageName();
- if (!topPackage.contentEquals(clientPackage)
- && !Utils.isSystem(mContext, clientPackage)) {
- Log.w(TAG, "Evicting client due to: " + topPackage);
- return false;
- }
+ final String clientClassNameIfItIsConfirmDeviceCredentialActivity =
+ mCurrentDialog.getClassNameIfItIsConfirmDeviceCredentialActivity();
+ final boolean isInBackground = Utils.isSystemAppOrInBackground(mActivityTaskManager,
+ mContext, clientPackage,
+ clientClassNameIfItIsConfirmDeviceCredentialActivity);
+ if (isInBackground) {
+ Log.w(TAG, "Evicting client due to top activity is not : " + clientPackage);
}
+ return isInBackground;
}
- return true;
+ return false;
}
private void cancelIfOwnerIsNotInForeground() {
@@ -728,7 +725,6 @@
Provider<UdfpsController> udfpsControllerFactory,
@NonNull DisplayManager displayManager,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull Lazy<UdfpsLogger> udfpsLogger,
@@ -779,7 +775,6 @@
});
mWakefulnessLifecycle = wakefulnessLifecycle;
- mPanelInteractionDetector = panelInteractionDetector;
mFaceProps = mFaceManager != null ? mFaceManager.getSensorPropertiesInternal() : null;
@@ -1229,7 +1224,6 @@
operationId,
requestId,
mWakefulnessLifecycle,
- mPanelInteractionDetector,
mUserManager,
mLockPatternUtils,
viewModel);
@@ -1259,9 +1253,9 @@
}
mCurrentDialog = newDialog;
- // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be
+ // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be
// removed.
- if (!promptInfo.isAllowBackgroundAuthentication() && !isOwnerInForeground()) {
+ if (!promptInfo.isAllowBackgroundAuthentication() && isOwnerInBackground()) {
cancelIfOwnerIsNotInForeground();
} else {
mCurrentDialog.show(mWindowManager);
@@ -1306,7 +1300,6 @@
PromptInfo promptInfo, boolean requireConfirmation, int userId, int[] sensorIds,
String opPackageName, boolean skipIntro, long operationId, long requestId,
@NonNull WakefulnessLifecycle wakefulnessLifecycle,
- @NonNull AuthDialogPanelInteractionDetector panelInteractionDetector,
@NonNull UserManager userManager,
@NonNull LockPatternUtils lockPatternUtils,
@NonNull PromptViewModel viewModel) {
@@ -1323,7 +1316,7 @@
config.mSensorIds = sensorIds;
config.mScaleProvider = this::getScaleFactor;
return new AuthContainerView(config, mApplicationCoroutineScope, mFpProps, mFaceProps,
- wakefulnessLifecycle, panelInteractionDetector, userManager, lockPatternUtils,
+ wakefulnessLifecycle, userManager, lockPatternUtils,
mInteractionJankMonitor, mPromptSelectorInteractor, viewModel,
mCredentialViewModelProvider, bgExecutor, mVibratorHelper);
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
index 3fd488c..8611916 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialog.java
@@ -94,6 +94,12 @@
*/
String getOpPackageName();
+ /**
+ * Get the class name of ConfirmDeviceCredentialActivity. Returns null if the direct caller is
+ * not ConfirmDeviceCredentialActivity.
+ */
+ String getClassNameIfItIsConfirmDeviceCredentialActivity();
+
/** The requestId of the underlying operation within the framework. */
long getRequestId();
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt b/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
deleted file mode 100644
index 04c2351..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetector.kt
+++ /dev/null
@@ -1,68 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics
-
-import android.annotation.MainThread
-import android.util.Log
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import dagger.Lazy
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.Job
-import kotlinx.coroutines.flow.first
-import kotlinx.coroutines.launch
-
-class AuthDialogPanelInteractionDetector
-@Inject
-constructor(
- @Application private val scope: CoroutineScope,
- private val shadeInteractorLazy: Lazy<ShadeInteractor>,
-) {
- private var shadeExpansionCollectorJob: Job? = null
-
- @MainThread
- fun enable(onShadeInteraction: Runnable) {
- if (shadeExpansionCollectorJob != null) {
- Log.e(TAG, "Already enabled")
- return
- }
- //TODO(b/313957306) delete this check
- if (shadeInteractorLazy.get().isUserInteracting.value) {
- // Workaround for b/311266890. This flow is in an error state that breaks this.
- Log.e(TAG, "isUserInteracting already true, skipping enable")
- return
- }
- shadeExpansionCollectorJob =
- scope.launch {
- Log.i(TAG, "Enable detector")
- // wait for it to emit true once
- shadeInteractorLazy.get().isUserInteracting.first { it }
- Log.i(TAG, "Detector detected shade interaction")
- onShadeInteraction.run()
- }
- shadeExpansionCollectorJob?.invokeOnCompletion { shadeExpansionCollectorJob = null }
- }
-
- @MainThread
- fun disable() {
- Log.i(TAG, "Disable detector")
- shadeExpansionCollectorJob?.cancel()
- }
-}
-
-private const val TAG = "AuthDialogPanelInteractionDetector"
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java b/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
deleted file mode 100644
index 02eae9ced..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/UdfpsDialogMeasureAdapter.java
+++ /dev/null
@@ -1,386 +0,0 @@
-/*
- * Copyright (C) 2021 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics;
-
-import android.annotation.IdRes;
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.graphics.Insets;
-import android.graphics.Rect;
-import android.hardware.biometrics.SensorLocationInternal;
-import android.hardware.fingerprint.FingerprintSensorPropertiesInternal;
-import android.os.Build;
-import android.util.Log;
-import android.view.Surface;
-import android.view.View;
-import android.view.View.MeasureSpec;
-import android.view.ViewGroup;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.view.WindowMetrics;
-import android.widget.FrameLayout;
-
-import com.android.internal.annotations.VisibleForTesting;
-import com.android.systemui.res.R;
-
-/**
- * Adapter that remeasures an auth dialog view to ensure that it matches the location of a physical
- * under-display fingerprint sensor (UDFPS).
- */
-public class UdfpsDialogMeasureAdapter {
- private static final String TAG = "UdfpsDialogMeasurementAdapter";
- private static final boolean DEBUG = Build.IS_USERDEBUG || Build.IS_ENG;
-
- @NonNull private final ViewGroup mView;
- @NonNull private final FingerprintSensorPropertiesInternal mSensorProps;
- @Nullable private WindowManager mWindowManager;
- private int mBottomSpacerHeight;
-
- public UdfpsDialogMeasureAdapter(
- @NonNull ViewGroup view, @NonNull FingerprintSensorPropertiesInternal sensorProps) {
- mView = view;
- mSensorProps = sensorProps;
- mWindowManager = mView.getContext().getSystemService(WindowManager.class);
- }
-
- @NonNull
- FingerprintSensorPropertiesInternal getSensorProps() {
- return mSensorProps;
- }
-
- @NonNull
- public AuthDialog.LayoutParams onMeasureInternal(
- int width, int height, @NonNull AuthDialog.LayoutParams layoutParams,
- float scaleFactor) {
-
- final int displayRotation = mView.getDisplay().getRotation();
- switch (displayRotation) {
- case Surface.ROTATION_0:
- return onMeasureInternalPortrait(width, height, scaleFactor);
- case Surface.ROTATION_90:
- case Surface.ROTATION_270:
- return onMeasureInternalLandscape(width, height, scaleFactor);
- default:
- Log.e(TAG, "Unsupported display rotation: " + displayRotation);
- return layoutParams;
- }
- }
-
- /**
- * @return the actual (and possibly negative) bottom spacer height. If negative, this indicates
- * that the UDFPS sensor is too low. Our current xml and custom measurement logic is very hard
- * too cleanly support this case. So, let's have the onLayout code translate the sensor location
- * instead.
- */
- public int getBottomSpacerHeight() {
- return mBottomSpacerHeight;
- }
-
- /**
- * @return sensor diameter size as scaleFactor
- */
- public int getSensorDiameter(float scaleFactor) {
- return (int) (scaleFactor * mSensorProps.getLocation().sensorRadius * 2);
- }
-
- @NonNull
- private AuthDialog.LayoutParams onMeasureInternalPortrait(int width, int height,
- float scaleFactor) {
- final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();
-
- // Figure out where the bottom of the sensor anim should be.
- final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
- final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
- final int dialogMargin = getDialogMarginPx();
- final int displayHeight = getMaximumWindowBounds(windowMetrics).height();
- final Insets navbarInsets = getNavbarInsets(windowMetrics);
- mBottomSpacerHeight = calculateBottomSpacerHeightForPortrait(
- mSensorProps, displayHeight, textIndicatorHeight, buttonBarHeight,
- dialogMargin, navbarInsets.bottom, scaleFactor);
-
- // Go through each of the children and do the custom measurement.
- int totalHeight = 0;
- final int numChildren = mView.getChildCount();
- final int sensorDiameter = getSensorDiameter(scaleFactor);
- for (int i = 0; i < numChildren; i++) {
- final View child = mView.getChildAt(i);
- if (child.getId() == R.id.biometric_icon_frame) {
- final FrameLayout iconFrame = (FrameLayout) child;
- final View icon = iconFrame.getChildAt(0);
- // Create a frame that's exactly the height of the sensor circle.
- iconFrame.measure(
- MeasureSpec.makeMeasureSpec(
- child.getLayoutParams().width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));
-
- // Ensure that the icon is never larger than the sensor.
- icon.measure(
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
- } else if (child.getId() == R.id.space_above_icon
- || child.getId() == R.id.space_above_content
- || child.getId() == R.id.button_bar) {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(
- child.getLayoutParams().height, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.space_below_icon) {
- // Set the spacer height so the fingerprint icon is on the physical sensor area
- final int clampedSpacerHeight = Math.max(mBottomSpacerHeight, 0);
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(clampedSpacerHeight, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.description
- || child.getId() == R.id.customized_view_container) {
- //skip description view and compute later
- continue;
- } else if (child.getId() == R.id.logo) {
- child.measure(
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().width,
- MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
- MeasureSpec.EXACTLY));
- } else {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
- }
-
- if (child.getVisibility() != View.GONE) {
- totalHeight += child.getMeasuredHeight();
- }
- }
-
- //re-calculate the height of body content
- View description = mView.findViewById(R.id.description);
- View contentView = mView.findViewById(R.id.customized_view_container);
- if (description != null && description.getVisibility() != View.GONE) {
- totalHeight += measureDescription(description, displayHeight, width, totalHeight);
- } else if (contentView != null && contentView.getVisibility() != View.GONE) {
- totalHeight += measureDescription(contentView, displayHeight, width, totalHeight);
- }
-
- return new AuthDialog.LayoutParams(width, totalHeight);
- }
-
- private int measureDescription(View bodyContent, int displayHeight, int currWidth,
- int currHeight) {
- int newHeight = bodyContent.getMeasuredHeight() + currHeight;
- int limit = (int) (displayHeight * 0.75);
- if (newHeight > limit) {
- bodyContent.measure(
- MeasureSpec.makeMeasureSpec(currWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(limit - currHeight, MeasureSpec.EXACTLY));
- }
- return bodyContent.getMeasuredHeight();
- }
-
- @NonNull
- private AuthDialog.LayoutParams onMeasureInternalLandscape(int width, int height,
- float scaleFactor) {
- final WindowMetrics windowMetrics = mWindowManager.getMaximumWindowMetrics();
-
- // Find the spacer height needed to vertically align the icon with the sensor.
- final int titleHeight = getViewHeightPx(R.id.title);
- final int subtitleHeight = getViewHeightPx(R.id.subtitle);
- final int descriptionHeight = getViewHeightPx(R.id.description);
- final int topSpacerHeight = getViewHeightPx(R.id.space_above_icon);
- final int textIndicatorHeight = getViewHeightPx(R.id.indicator);
- final int buttonBarHeight = getViewHeightPx(R.id.button_bar);
-
- final Insets navbarInsets = getNavbarInsets(windowMetrics);
- final int bottomSpacerHeight = calculateBottomSpacerHeightForLandscape(titleHeight,
- subtitleHeight, descriptionHeight, topSpacerHeight, textIndicatorHeight,
- buttonBarHeight, navbarInsets.bottom);
-
- // Find the spacer width needed to horizontally align the icon with the sensor.
- final int displayWidth = getMaximumWindowBounds(windowMetrics).width();
- final int dialogMargin = getDialogMarginPx();
- final int horizontalInset = navbarInsets.left + navbarInsets.right;
- final int horizontalSpacerWidth = calculateHorizontalSpacerWidthForLandscape(
- mSensorProps, displayWidth, dialogMargin, horizontalInset, scaleFactor);
-
- final int sensorDiameter = getSensorDiameter(scaleFactor);
- final int remeasuredWidth = sensorDiameter + 2 * horizontalSpacerWidth;
-
- int remeasuredHeight = 0;
- final int numChildren = mView.getChildCount();
- for (int i = 0; i < numChildren; i++) {
- final View child = mView.getChildAt(i);
- if (child.getId() == R.id.biometric_icon_frame) {
- final FrameLayout iconFrame = (FrameLayout) child;
- final View icon = iconFrame.getChildAt(0);
- // Create a frame that's exactly the height of the sensor circle.
- iconFrame.measure(
- MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.EXACTLY));
-
- // Ensure that the icon is never larger than the sensor.
- icon.measure(
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(sensorDiameter, MeasureSpec.AT_MOST));
- } else if (child.getId() == R.id.space_above_icon) {
- // Adjust the width and height of the top spacer if necessary.
- final int newTopSpacerHeight = child.getLayoutParams().height
- - Math.min(bottomSpacerHeight, 0);
- child.measure(
- MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(newTopSpacerHeight, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.button_bar) {
- // Adjust the width of the button bar while preserving its height.
- child.measure(
- MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(
- child.getLayoutParams().height, MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.space_below_icon) {
- // Adjust the bottom spacer height to align the fingerprint icon with the sensor.
- final int newBottomSpacerHeight = Math.max(bottomSpacerHeight, 0);
- child.measure(
- MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(newBottomSpacerHeight, MeasureSpec.EXACTLY));
- } else {
- // Use the remeasured width for all other child views.
- child.measure(
- MeasureSpec.makeMeasureSpec(remeasuredWidth, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
- }
-
- if (child.getVisibility() != View.GONE) {
- remeasuredHeight += child.getMeasuredHeight();
- }
- }
-
- return new AuthDialog.LayoutParams(remeasuredWidth, remeasuredHeight);
- }
-
- private int getViewHeightPx(@IdRes int viewId) {
- final View view = mView.findViewById(viewId);
- return view != null && view.getVisibility() != View.GONE ? view.getMeasuredHeight() : 0;
- }
-
- private int getDialogMarginPx() {
- return mView.getResources().getDimensionPixelSize(R.dimen.biometric_dialog_border_padding);
- }
-
- @NonNull
- private static Insets getNavbarInsets(@Nullable WindowMetrics windowMetrics) {
- return windowMetrics != null
- ? windowMetrics.getWindowInsets().getInsets(WindowInsets.Type.navigationBars())
- : Insets.NONE;
- }
-
- @NonNull
- private static Rect getMaximumWindowBounds(@Nullable WindowMetrics windowMetrics) {
- return windowMetrics != null ? windowMetrics.getBounds() : new Rect();
- }
-
- /**
- * For devices in portrait orientation where the sensor is too high up, calculates the amount of
- * padding necessary to center the biometric icon within the sensor's physical location.
- */
- @VisibleForTesting
- static int calculateBottomSpacerHeightForPortrait(
- @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayHeightPx,
- int textIndicatorHeightPx, int buttonBarHeightPx, int dialogMarginPx,
- int navbarBottomInsetPx, float scaleFactor) {
- final SensorLocationInternal location = sensorProperties.getLocation();
- final int sensorDistanceFromBottom = displayHeightPx
- - (int) (scaleFactor * location.sensorLocationY)
- - (int) (scaleFactor * location.sensorRadius);
-
- final int spacerHeight = sensorDistanceFromBottom
- - textIndicatorHeightPx
- - buttonBarHeightPx
- - dialogMarginPx
- - navbarBottomInsetPx;
-
- if (DEBUG) {
- Log.d(TAG, "Display height: " + displayHeightPx
- + ", Distance from bottom: " + sensorDistanceFromBottom
- + ", Bottom margin: " + dialogMarginPx
- + ", Navbar bottom inset: " + navbarBottomInsetPx
- + ", Bottom spacer height (portrait): " + spacerHeight
- + ", Scale Factor: " + scaleFactor);
- }
-
- return spacerHeight;
- }
-
- /**
- * For devices in landscape orientation where the sensor is too high up, calculates the amount
- * of padding necessary to center the biometric icon within the sensor's physical location.
- */
- @VisibleForTesting
- static int calculateBottomSpacerHeightForLandscape(int titleHeightPx, int subtitleHeightPx,
- int descriptionHeightPx, int topSpacerHeightPx, int textIndicatorHeightPx,
- int buttonBarHeightPx, int navbarBottomInsetPx) {
-
- final int dialogHeightAboveIcon = titleHeightPx
- + subtitleHeightPx
- + descriptionHeightPx
- + topSpacerHeightPx;
-
- final int dialogHeightBelowIcon = textIndicatorHeightPx + buttonBarHeightPx;
-
- final int bottomSpacerHeight = dialogHeightAboveIcon
- - dialogHeightBelowIcon
- - navbarBottomInsetPx;
-
- if (DEBUG) {
- Log.d(TAG, "Title height: " + titleHeightPx
- + ", Subtitle height: " + subtitleHeightPx
- + ", Description height: " + descriptionHeightPx
- + ", Top spacer height: " + topSpacerHeightPx
- + ", Text indicator height: " + textIndicatorHeightPx
- + ", Button bar height: " + buttonBarHeightPx
- + ", Navbar bottom inset: " + navbarBottomInsetPx
- + ", Bottom spacer height (landscape): " + bottomSpacerHeight);
- }
-
- return bottomSpacerHeight;
- }
-
- /**
- * For devices in landscape orientation where the sensor is too left/right, calculates the
- * amount of padding necessary to center the biometric icon within the sensor's physical
- * location.
- */
- @VisibleForTesting
- static int calculateHorizontalSpacerWidthForLandscape(
- @NonNull FingerprintSensorPropertiesInternal sensorProperties, int displayWidthPx,
- int dialogMarginPx, int navbarHorizontalInsetPx, float scaleFactor) {
- final SensorLocationInternal location = sensorProperties.getLocation();
- final int sensorDistanceFromEdge = displayWidthPx
- - (int) (scaleFactor * location.sensorLocationY)
- - (int) (scaleFactor * location.sensorRadius);
-
- final int horizontalPadding = sensorDistanceFromEdge
- - dialogMarginPx
- - navbarHorizontalInsetPx;
-
- if (DEBUG) {
- Log.d(TAG, "Display width: " + displayWidthPx
- + ", Distance from edge: " + sensorDistanceFromEdge
- + ", Dialog margin: " + dialogMarginPx
- + ", Navbar horizontal inset: " + navbarHorizontalInsetPx
- + ", Horizontal spacer width (landscape): " + horizontalPadding
- + ", Scale Factor: " + scaleFactor);
- }
-
- return horizontalPadding;
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
index 5e2b5ff..6da5e42 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractor.kt
@@ -188,7 +188,6 @@
val hasCredentialViewShown = promptKind.value.isCredential()
val showBpForCredential =
Flags.customBiometricPrompt() &&
- com.android.systemui.Flags.constraintBp() &&
!Utils.isBiometricAllowed(promptInfo) &&
isDeviceCredentialAllowed(promptInfo) &&
promptInfo.contentView != null &&
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
index 348b423..695707d 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/domain/model/BiometricPromptRequest.kt
@@ -44,7 +44,7 @@
val logoDescription: String? = info.logoDescription
val negativeButtonText: String = info.negativeButtonText?.toString() ?: ""
val componentNameForConfirmDeviceCredentialActivity: ComponentName? =
- info.componentNameForConfirmDeviceCredentialActivity
+ info.realCallerForConfirmDeviceCredentialActivity
val allowBackgroundAuthentication = info.isAllowBackgroundAuthentication
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java b/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
deleted file mode 100644
index b450896..0000000
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/BiometricPromptLayout.java
+++ /dev/null
@@ -1,188 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics.ui;
-
-import android.annotation.NonNull;
-import android.annotation.Nullable;
-import android.content.Context;
-import android.graphics.Insets;
-import android.util.AttributeSet;
-import android.util.Log;
-import android.view.View;
-import android.view.WindowInsets;
-import android.view.WindowManager;
-import android.widget.FrameLayout;
-import android.widget.LinearLayout;
-import android.widget.TextView;
-
-import com.android.systemui.biometrics.AuthController;
-import com.android.systemui.biometrics.AuthDialog;
-import com.android.systemui.biometrics.UdfpsDialogMeasureAdapter;
-import com.android.systemui.res.R;
-
-import kotlin.Pair;
-
-/**
- * Contains the Biometric views (title, subtitle, icon, buttons, etc.).
- *
- * TODO(b/251476085): get the udfps junk out of here, at a minimum. Likely can be replaced with a
- * normal LinearLayout.
- */
-public class BiometricPromptLayout extends LinearLayout {
-
- private static final String TAG = "BiometricPromptLayout";
-
- @NonNull
- private final WindowManager mWindowManager;
- @Nullable
- private AuthController.ScaleFactorProvider mScaleFactorProvider;
- @Nullable
- private UdfpsDialogMeasureAdapter mUdfpsAdapter;
-
- private final boolean mUseCustomBpSize;
- private final int mCustomBpWidth;
- private final int mCustomBpHeight;
-
- public BiometricPromptLayout(Context context) {
- this(context, null);
- }
-
- public BiometricPromptLayout(Context context, AttributeSet attrs) {
- super(context, attrs);
-
- mWindowManager = context.getSystemService(WindowManager.class);
-
- mUseCustomBpSize = getResources().getBoolean(R.bool.use_custom_bp_size);
- mCustomBpWidth = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_width);
- mCustomBpHeight = getResources().getDimensionPixelSize(R.dimen.biometric_dialog_height);
- }
-
- @Deprecated
- public void setUdfpsAdapter(@NonNull UdfpsDialogMeasureAdapter adapter,
- @NonNull AuthController.ScaleFactorProvider scaleProvider) {
- mUdfpsAdapter = adapter;
- mScaleFactorProvider = scaleProvider != null ? scaleProvider : () -> 1.0f;
- }
-
- @Deprecated
- public boolean isUdfps() {
- return mUdfpsAdapter != null;
- }
-
- @Deprecated
- public Pair<Integer, Integer> getUpdatedFingerprintAffordanceSize() {
- if (mUdfpsAdapter != null) {
- final int sensorDiameter = mUdfpsAdapter.getSensorDiameter(
- mScaleFactorProvider.provide());
- return new Pair(sensorDiameter, sensorDiameter);
- }
- return null;
- }
-
- @NonNull
- private AuthDialog.LayoutParams onMeasureInternal(int width, int height) {
- int totalHeight = 0;
- final int numChildren = getChildCount();
- for (int i = 0; i < numChildren; i++) {
- final View child = getChildAt(i);
-
- if (child.getId() == R.id.space_above_icon
- || child.getId() == R.id.space_above_content
- || child.getId() == R.id.space_below_icon
- || child.getId() == R.id.button_bar) {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
- MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.biometric_icon_frame) {
- final View iconView = findViewById(R.id.biometric_icon);
- child.measure(
- MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().width,
- MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(iconView.getLayoutParams().height,
- MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.logo) {
- child.measure(
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().width,
- MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(child.getLayoutParams().height,
- MeasureSpec.EXACTLY));
- } else if (child.getId() == R.id.biometric_icon) {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.AT_MOST),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
- } else {
- child.measure(
- MeasureSpec.makeMeasureSpec(width, MeasureSpec.EXACTLY),
- MeasureSpec.makeMeasureSpec(height, MeasureSpec.AT_MOST));
- }
-
- if (child.getVisibility() != View.GONE) {
- totalHeight += child.getMeasuredHeight();
- }
- }
-
- final AuthDialog.LayoutParams params = new AuthDialog.LayoutParams(width, totalHeight);
- if (mUdfpsAdapter != null) {
- return mUdfpsAdapter.onMeasureInternal(width, height, params,
- (mScaleFactorProvider != null) ? mScaleFactorProvider.provide() : 1.0f);
- } else {
- return params;
- }
- }
-
- @Override
- protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
- int width = MeasureSpec.getSize(widthMeasureSpec);
- int height = MeasureSpec.getSize(heightMeasureSpec);
-
- if (mUseCustomBpSize) {
- width = mCustomBpWidth;
- height = mCustomBpHeight;
- } else {
- width = Math.min(width, height);
- }
-
- // add nav bar insets since the parent AuthContainerView
- // uses LAYOUT_IN_DISPLAY_CUTOUT_MODE_ALWAYS
- final Insets insets = mWindowManager.getMaximumWindowMetrics().getWindowInsets()
- .getInsets(WindowInsets.Type.navigationBars());
- final AuthDialog.LayoutParams params = onMeasureInternal(width, height);
- setMeasuredDimension(params.mMediumWidth + insets.left + insets.right,
- params.mMediumHeight + insets.bottom);
- }
-
- @Override
- protected void onLayout(boolean changed, int left, int top, int right, int bottom) {
- super.onLayout(changed, left, top, right, bottom);
-
- if (mUdfpsAdapter != null) {
- // Move the UDFPS icon and indicator text if necessary. This probably only needs to
- // happen for devices where the UDFPS sensor is too low.
- // TODO(b/201510778): Update this logic to support cases where the sensor or text
- // overlap the button bar area.
- final float bottomSpacerHeight = mUdfpsAdapter.getBottomSpacerHeight();
- Log.w(TAG, "bottomSpacerHeight: " + bottomSpacerHeight);
- if (bottomSpacerHeight < 0) {
- final FrameLayout iconFrame = findViewById(R.id.biometric_icon_frame);
- iconFrame.setTranslationY(-bottomSpacerHeight);
- final TextView indicator = findViewById(R.id.indicator);
- indicator.setTranslationY(-bottomSpacerHeight);
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
index 7ccac03..0b474f8 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricCustomizedViewBinder.kt
@@ -38,14 +38,13 @@
import android.widget.Space
import android.widget.TextView
import com.android.settingslib.Utils
-import com.android.systemui.biometrics.ui.BiometricPromptLayout
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import kotlin.math.ceil
private const val TAG = "BiometricCustomizedViewBinder"
-/** Sub-binder for [BiometricPromptLayout.customized_view_container]. */
+/** Sub-binder for Biometric Prompt Customized View */
object BiometricCustomizedViewBinder {
fun bind(
customizedViewContainer: LinearLayout,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
index 43ba097..a20a17f 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewBinder.kt
@@ -45,13 +45,10 @@
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
import com.airbnb.lottie.LottieCompositionFactory
-import com.android.systemui.Flags.constraintBp
-import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.shared.model.BiometricModalities
import com.android.systemui.biometrics.shared.model.BiometricModality
import com.android.systemui.biometrics.shared.model.PromptKind
import com.android.systemui.biometrics.shared.model.asBiometricModality
-import com.android.systemui.biometrics.ui.BiometricPromptLayout
import com.android.systemui.biometrics.ui.viewmodel.FingerprintStartMode
import com.android.systemui.biometrics.ui.viewmodel.PromptMessage
import com.android.systemui.biometrics.ui.viewmodel.PromptSize
@@ -72,28 +69,18 @@
/** Top-most view binder for BiometricPrompt views. */
object BiometricViewBinder {
- /** Binds a [BiometricPromptLayout] to a [PromptViewModel]. */
+ /** Binds a Biometric Prompt View to a [PromptViewModel]. */
@SuppressLint("ClickableViewAccessibility")
@JvmStatic
fun bind(
view: View,
viewModel: PromptViewModel,
- panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
backgroundView: View,
legacyCallback: Spaghetti.Callback,
applicationScope: CoroutineScope,
vibratorHelper: VibratorHelper,
): Spaghetti {
- /**
- * View is only set visible in BiometricViewSizeBinder once PromptSize is determined that
- * accounts for iconView size, to prevent prompt resizing being visible to the user.
- *
- * TODO(b/288175072): May be able to remove this once constraint layout is implemented
- */
- if (!constraintBp()) {
- view.visibility = View.INVISIBLE
- }
val accessibilityManager = view.context.getSystemService(AccessibilityManager::class.java)!!
val textColorError =
@@ -104,9 +91,7 @@
R.style.TextAppearance_AuthCredential_Indicator,
intArrayOf(android.R.attr.textColor)
)
- val textColorHint =
- if (constraintBp()) attributes.getColor(0, 0)
- else view.resources.getColor(R.color.biometric_dialog_gray, view.context.theme)
+ val textColorHint = attributes.getColor(0, 0)
attributes.recycle()
val logoView = view.requireViewById<ImageView>(R.id.logo)
@@ -116,12 +101,7 @@
val descriptionView = view.requireViewById<TextView>(R.id.description)
val customizedViewContainer =
view.requireViewById<LinearLayout>(R.id.customized_view_container)
- val udfpsGuidanceView =
- if (constraintBp()) {
- view.requireViewById<View>(R.id.panel)
- } else {
- backgroundView
- }
+ val udfpsGuidanceView = view.requireViewById<View>(R.id.panel)
// set selected to enable marquee unless a screen reader is enabled
titleView.isSelected =
@@ -130,14 +110,6 @@
!accessibilityManager.isEnabled || !accessibilityManager.isTouchExplorationEnabled
val iconView = view.requireViewById<LottieAnimationView>(R.id.biometric_icon)
-
- val iconSizeOverride =
- if (constraintBp()) {
- null
- } else {
- (view as BiometricPromptLayout).updatedFingerprintAffordanceSize
- }
-
val indicatorMessageView = view.requireViewById<TextView>(R.id.indicator)
// Negative-side (left) buttons
@@ -213,7 +185,7 @@
subtitleView.text = viewModel.subtitle.first()
descriptionView.text = viewModel.description.first()
- if (Flags.customBiometricPrompt() && constraintBp()) {
+ if (Flags.customBiometricPrompt()) {
BiometricCustomizedViewBinder.bind(
customizedViewContainer,
viewModel.contentView.first(),
@@ -250,22 +222,6 @@
descriptionView,
customizedViewContainer,
),
- viewsToFadeInOnSizeChange =
- listOf(
- logoView,
- logoDescriptionView,
- titleView,
- subtitleView,
- descriptionView,
- customizedViewContainer,
- indicatorMessageView,
- negativeButton,
- cancelButton,
- retryButton,
- confirmationButton,
- credentialFallbackButton,
- ),
- panelViewController = panelViewController,
jankListener = jankListener,
)
}
@@ -275,7 +231,6 @@
if (!showWithoutIcon) {
PromptIconViewBinder.bind(
iconView,
- iconSizeOverride,
viewModel,
)
}
@@ -329,20 +284,6 @@
}
}
- // set padding
- launch {
- viewModel.promptPadding.collect { promptPadding ->
- if (!constraintBp()) {
- view.setPadding(
- promptPadding.left,
- promptPadding.top,
- promptPadding.right,
- promptPadding.bottom
- )
- }
- }
- }
-
// configure & hide/disable buttons
launch {
viewModel.credentialKind
@@ -546,24 +487,6 @@
fun onAuthenticatedAndConfirmed()
}
- @Deprecated("TODO(b/330788871): remove after replacing AuthContainerView")
- enum class BiometricState {
- /** Authentication hardware idle. */
- STATE_IDLE,
- /** UI animating in, authentication hardware active. */
- STATE_AUTHENTICATING_ANIMATING_IN,
- /** UI animated in, authentication hardware active. */
- STATE_AUTHENTICATING,
- /** UI animated in, authentication hardware active. */
- STATE_HELP,
- /** Hard error, e.g. ERROR_TIMEOUT. Authentication hardware idle. */
- STATE_ERROR,
- /** Authenticated, waiting for user confirmation. Authentication hardware idle. */
- STATE_PENDING_CONFIRMATION,
- /** Authenticated, dialog animating away soon. */
- STATE_AUTHENTICATED,
- }
-
private var lifecycleScope: CoroutineScope? = null
private var modalities: BiometricModalities = BiometricModalities()
private var legacyCallback: Callback? = null
@@ -699,15 +622,8 @@
}
fun startTransitionToCredentialUI(isError: Boolean) {
- if (!constraintBp()) {
- applicationScope.launch {
- viewModel.onSwitchToCredential()
- legacyCallback?.onUseDeviceCredential()
- }
- } else {
- viewModel.onSwitchToCredential()
- legacyCallback?.onUseDeviceCredential()
- }
+ viewModel.onSwitchToCredential()
+ legacyCallback?.onUseDeviceCredential()
}
fun cancelAnimation() {
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
index b9ec2de..85c3ae3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/BiometricViewSizeBinder.kt
@@ -38,10 +38,7 @@
import androidx.constraintlayout.widget.Guideline
import androidx.core.animation.addListener
import androidx.core.view.doOnLayout
-import androidx.core.view.isGone
import androidx.lifecycle.lifecycleScope
-import com.android.systemui.Flags.constraintBp
-import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.Utils
import com.android.systemui.biometrics.ui.viewmodel.PromptPosition
import com.android.systemui.biometrics.ui.viewmodel.PromptSize
@@ -49,7 +46,6 @@
import com.android.systemui.biometrics.ui.viewmodel.isLarge
import com.android.systemui.biometrics.ui.viewmodel.isLeft
import com.android.systemui.biometrics.ui.viewmodel.isMedium
-import com.android.systemui.biometrics.ui.viewmodel.isNullOrNotSmall
import com.android.systemui.biometrics.ui.viewmodel.isSmall
import com.android.systemui.biometrics.ui.viewmodel.isTop
import com.android.systemui.lifecycle.repeatWhenAttached
@@ -71,8 +67,6 @@
view: View,
viewModel: PromptViewModel,
viewsToHideWhenSmall: List<View>,
- viewsToFadeInOnSizeChange: List<View>,
- panelViewController: AuthPanelController?,
jankListener: BiometricJankListener,
) {
val windowManager = requireNotNull(view.context.getSystemService(WindowManager::class.java))
@@ -92,553 +86,366 @@
}
}
- if (constraintBp()) {
- val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
- val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline)
- val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
- val midGuideline = view.findViewById<Guideline>(R.id.midGuideline)
+ val leftGuideline = view.requireViewById<Guideline>(R.id.leftGuideline)
+ val topGuideline = view.requireViewById<Guideline>(R.id.topGuideline)
+ val rightGuideline = view.requireViewById<Guideline>(R.id.rightGuideline)
+ val midGuideline = view.findViewById<Guideline>(R.id.midGuideline)
- val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
- val panelView = view.requireViewById<View>(R.id.panel)
- val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
- val pxToDp =
- TypedValue.applyDimension(
- TypedValue.COMPLEX_UNIT_DIP,
- 1f,
- view.resources.displayMetrics
- )
- val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
+ val iconHolderView = view.requireViewById<View>(R.id.biometric_icon)
+ val panelView = view.requireViewById<View>(R.id.panel)
+ val cornerRadius = view.resources.getDimension(R.dimen.biometric_dialog_corner_size)
+ val pxToDp =
+ TypedValue.applyDimension(
+ TypedValue.COMPLEX_UNIT_DIP,
+ 1f,
+ view.resources.displayMetrics
+ )
+ val cornerRadiusPx = (pxToDp * cornerRadius).toInt()
- var currentSize: PromptSize? = null
- var currentPosition: PromptPosition = PromptPosition.Bottom
- panelView.outlineProvider =
- object : ViewOutlineProvider() {
- override fun getOutline(view: View, outline: Outline) {
- when (currentPosition) {
- PromptPosition.Right -> {
- outline.setRoundRect(
- 0,
- 0,
- view.width + cornerRadiusPx,
- view.height,
- cornerRadiusPx.toFloat()
- )
- }
- PromptPosition.Left -> {
- outline.setRoundRect(
- -cornerRadiusPx,
- 0,
- view.width,
- view.height,
- cornerRadiusPx.toFloat()
- )
- }
- PromptPosition.Bottom,
- PromptPosition.Top -> {
- outline.setRoundRect(
- 0,
- 0,
- view.width,
- view.height + cornerRadiusPx,
- cornerRadiusPx.toFloat()
- )
- }
- }
- }
- }
-
- // ConstraintSets for animating between prompt sizes
- val mediumConstraintSet = ConstraintSet()
- mediumConstraintSet.clone(view as ConstraintLayout)
-
- val smallConstraintSet = ConstraintSet()
- smallConstraintSet.clone(mediumConstraintSet)
-
- val largeConstraintSet = ConstraintSet()
- largeConstraintSet.clone(mediumConstraintSet)
- largeConstraintSet.constrainMaxWidth(R.id.panel, 0)
- largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0)
- largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0)
-
- // TODO: Investigate better way to handle 180 rotations
- val flipConstraintSet = ConstraintSet()
-
- view.doOnLayout {
- fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
- viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
- largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
- largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
-
- if (hideSensorIcon) {
- smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
- mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
- mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
- }
- }
-
- view.repeatWhenAttached {
- lifecycleScope.launch {
- viewModel.iconPosition.collect { position ->
- if (position != Rect()) {
- val iconParams =
- iconHolderView.layoutParams as ConstraintLayout.LayoutParams
-
- if (position.left != 0) {
- iconParams.endToEnd = ConstraintSet.UNSET
- iconParams.leftMargin = position.left
- mediumConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.RIGHT
- )
- mediumConstraintSet.connect(
- R.id.biometric_icon,
- ConstraintSet.LEFT,
- ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
- )
- mediumConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.LEFT,
- position.left
- )
- smallConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.RIGHT
- )
- smallConstraintSet.connect(
- R.id.biometric_icon,
- ConstraintSet.LEFT,
- ConstraintSet.PARENT_ID,
- ConstraintSet.LEFT
- )
- smallConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.LEFT,
- position.left
- )
- }
- if (position.top != 0) {
- iconParams.bottomToBottom = ConstraintSet.UNSET
- iconParams.topMargin = position.top
- mediumConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.BOTTOM
- )
- mediumConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.TOP,
- position.top
- )
- smallConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.BOTTOM
- )
- smallConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.TOP,
- position.top
- )
- }
- if (position.right != 0) {
- iconParams.startToStart = ConstraintSet.UNSET
- iconParams.rightMargin = position.right
- mediumConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.LEFT
- )
- mediumConstraintSet.connect(
- R.id.biometric_icon,
- ConstraintSet.RIGHT,
- ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
- )
- mediumConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.RIGHT,
- position.right
- )
- smallConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.LEFT
- )
- smallConstraintSet.connect(
- R.id.biometric_icon,
- ConstraintSet.RIGHT,
- ConstraintSet.PARENT_ID,
- ConstraintSet.RIGHT
- )
- smallConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.RIGHT,
- position.right
- )
- }
- if (position.bottom != 0) {
- iconParams.topToTop = ConstraintSet.UNSET
- iconParams.bottomMargin = position.bottom
- mediumConstraintSet.clear(
- R.id.biometric_icon,
- ConstraintSet.TOP
- )
- mediumConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.BOTTOM,
- position.bottom
- )
- smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
- smallConstraintSet.setMargin(
- R.id.biometric_icon,
- ConstraintSet.BOTTOM,
- position.bottom
- )
- }
- iconHolderView.layoutParams = iconParams
- }
- }
- }
-
- lifecycleScope.launch {
- viewModel.iconSize.collect { iconSize ->
- iconHolderView.layoutParams.width = iconSize.first
- iconHolderView.layoutParams.height = iconSize.second
- mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first)
- mediumConstraintSet.constrainHeight(
- R.id.biometric_icon,
- iconSize.second
+ var currentSize: PromptSize? = null
+ var currentPosition: PromptPosition = PromptPosition.Bottom
+ panelView.outlineProvider =
+ object : ViewOutlineProvider() {
+ override fun getOutline(view: View, outline: Outline) {
+ when (currentPosition) {
+ PromptPosition.Right -> {
+ outline.setRoundRect(
+ 0,
+ 0,
+ view.width + cornerRadiusPx,
+ view.height,
+ cornerRadiusPx.toFloat()
)
}
- }
-
- lifecycleScope.launch {
- viewModel.guidelineBounds.collect { bounds ->
- val bottomInset =
- windowManager.maximumWindowMetrics.windowInsets
- .getInsets(WindowInsets.Type.navigationBars())
- .bottom
- mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset)
-
- if (bounds.left >= 0) {
- mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
- smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
- } else if (bounds.left < 0) {
- mediumConstraintSet.setGuidelineEnd(
- leftGuideline.id,
- abs(bounds.left)
- )
- smallConstraintSet.setGuidelineEnd(
- leftGuideline.id,
- abs(bounds.left)
- )
- }
-
- if (bounds.right >= 0) {
- mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
- smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
- } else if (bounds.right < 0) {
- mediumConstraintSet.setGuidelineBegin(
- rightGuideline.id,
- abs(bounds.right)
- )
- smallConstraintSet.setGuidelineBegin(
- rightGuideline.id,
- abs(bounds.right)
- )
- }
-
- if (bounds.top >= 0) {
- mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
- smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
- } else if (bounds.top < 0) {
- mediumConstraintSet.setGuidelineEnd(
- topGuideline.id,
- abs(bounds.top)
- )
- smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
- }
-
- if (midGuideline != null) {
- val left =
- if (bounds.left >= 0) {
- abs(bounds.left)
- } else {
- view.width - abs(bounds.left)
- }
- val right =
- if (bounds.right >= 0) {
- view.width - abs(bounds.right)
- } else {
- abs(bounds.right)
- }
- val mid = (left + right) / 2
- mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid)
- }
+ PromptPosition.Left -> {
+ outline.setRoundRect(
+ -cornerRadiusPx,
+ 0,
+ view.width,
+ view.height,
+ cornerRadiusPx.toFloat()
+ )
}
- }
-
- lifecycleScope.launch {
- combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect {
- (hideSensorIcon, size) ->
- setVisibilities(hideSensorIcon, size)
- }
- }
-
- lifecycleScope.launch {
- combine(viewModel.position, viewModel.size, ::Pair).collect {
- (position, size) ->
- if (position.isLeft) {
- if (size.isSmall) {
- flipConstraintSet.clone(smallConstraintSet)
- } else {
- flipConstraintSet.clone(mediumConstraintSet)
- }
-
- // Move all content to other panel
- flipConstraintSet.connect(
- R.id.scrollView,
- ConstraintSet.LEFT,
- R.id.midGuideline,
- ConstraintSet.LEFT
- )
- flipConstraintSet.connect(
- R.id.scrollView,
- ConstraintSet.RIGHT,
- R.id.rightGuideline,
- ConstraintSet.RIGHT
- )
- } else if (position.isTop) {
- // Top position is only used for 180 rotation Udfps
- // Requires repositioning due to sensor location at top of screen
- mediumConstraintSet.connect(
- R.id.scrollView,
- ConstraintSet.TOP,
- R.id.indicator,
- ConstraintSet.BOTTOM
- )
- mediumConstraintSet.connect(
- R.id.scrollView,
- ConstraintSet.BOTTOM,
- R.id.button_bar,
- ConstraintSet.TOP
- )
- mediumConstraintSet.connect(
- R.id.panel,
- ConstraintSet.TOP,
- R.id.biometric_icon,
- ConstraintSet.TOP
- )
- mediumConstraintSet.setMargin(
- R.id.panel,
- ConstraintSet.TOP,
- (-24 * pxToDp).toInt()
- )
- mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
- }
-
- when {
- size.isSmall -> {
- if (position.isLeft) {
- flipConstraintSet.applyTo(view)
- } else {
- smallConstraintSet.applyTo(view)
- }
- }
- size.isMedium && currentSize.isSmall -> {
- val autoTransition = AutoTransition()
- autoTransition.setDuration(
- ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
- )
-
- TransitionManager.beginDelayedTransition(view, autoTransition)
-
- if (position.isLeft) {
- flipConstraintSet.applyTo(view)
- } else {
- mediumConstraintSet.applyTo(view)
- }
- }
- size.isMedium -> {
- if (position.isLeft) {
- flipConstraintSet.applyTo(view)
- } else {
- mediumConstraintSet.applyTo(view)
- }
- }
- size.isLarge && currentSize.isMedium -> {
- val autoTransition = AutoTransition()
- autoTransition.setDuration(
- ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
- )
-
- TransitionManager.beginDelayedTransition(view, autoTransition)
- largeConstraintSet.applyTo(view)
- }
- }
-
- currentSize = size
- currentPosition = position
- notifyAccessibilityChanged()
-
- panelView.invalidateOutline()
- view.invalidate()
- view.requestLayout()
+ PromptPosition.Bottom,
+ PromptPosition.Top -> {
+ outline.setRoundRect(
+ 0,
+ 0,
+ view.width,
+ view.height + cornerRadiusPx,
+ cornerRadiusPx.toFloat()
+ )
}
}
}
}
- } else if (panelViewController != null) {
- val iconHolderView = view.requireViewById<View>(R.id.biometric_icon_frame)
- val iconPadding = view.resources.getDimension(R.dimen.biometric_dialog_icon_padding)
- val fullSizeYOffset =
- view.resources.getDimension(
- R.dimen.biometric_dialog_medium_to_large_translation_offset
- )
- // cache the original position of the icon view (as done in legacy view)
- // this must happen before any size changes can be made
- view.doOnLayout {
- // TODO(b/251476085): this old way of positioning has proven itself unreliable
- // remove this and associated thing like (UdfpsDialogMeasureAdapter) and
- // pin to the physical sensor
- val iconHolderOriginalY = iconHolderView.y
+ // ConstraintSets for animating between prompt sizes
+ val mediumConstraintSet = ConstraintSet()
+ mediumConstraintSet.clone(view as ConstraintLayout)
- // bind to prompt
- // TODO(b/251476085): migrate the legacy panel controller and simplify this
- view.repeatWhenAttached {
- var currentSize: PromptSize? = null
- lifecycleScope.launch {
- /**
- * View is only set visible in BiometricViewSizeBinder once PromptSize is
- * determined that accounts for iconView size, to prevent prompt resizing
- * being visible to the user.
- *
- * TODO(b/288175072): May be able to remove isIconViewLoaded once constraint
- * layout is implemented
- */
- combine(viewModel.isIconViewLoaded, viewModel.size, ::Pair).collect {
- (isIconViewLoaded, size) ->
- if (!isIconViewLoaded) {
- return@collect
+ val smallConstraintSet = ConstraintSet()
+ smallConstraintSet.clone(mediumConstraintSet)
+
+ val largeConstraintSet = ConstraintSet()
+ largeConstraintSet.clone(mediumConstraintSet)
+ largeConstraintSet.constrainMaxWidth(R.id.panel, 0)
+ largeConstraintSet.setGuidelineBegin(R.id.leftGuideline, 0)
+ largeConstraintSet.setGuidelineEnd(R.id.rightGuideline, 0)
+
+ // TODO: Investigate better way to handle 180 rotations
+ val flipConstraintSet = ConstraintSet()
+
+ view.doOnLayout {
+ fun setVisibilities(hideSensorIcon: Boolean, size: PromptSize) {
+ viewsToHideWhenSmall.forEach { it.showContentOrHide(forceHide = size.isSmall) }
+ largeConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+ largeConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
+ largeConstraintSet.setVisibility(R.id.indicator, View.GONE)
+ largeConstraintSet.setVisibility(R.id.scrollView, View.GONE)
+
+ if (hideSensorIcon) {
+ smallConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+ smallConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
+ smallConstraintSet.setVisibility(R.id.indicator, View.GONE)
+ mediumConstraintSet.setVisibility(iconHolderView.id, View.GONE)
+ mediumConstraintSet.setVisibility(R.id.biometric_icon_overlay, View.GONE)
+ mediumConstraintSet.setVisibility(R.id.indicator, View.GONE)
+ }
+ }
+
+ view.repeatWhenAttached {
+ lifecycleScope.launch {
+ viewModel.iconPosition.collect { position ->
+ if (position != Rect()) {
+ val iconParams =
+ iconHolderView.layoutParams as ConstraintLayout.LayoutParams
+
+ if (position.left != 0) {
+ iconParams.endToEnd = ConstraintSet.UNSET
+ iconParams.leftMargin = position.left
+ mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
+ mediumConstraintSet.connect(
+ R.id.biometric_icon,
+ ConstraintSet.LEFT,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.LEFT
+ )
+ mediumConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.LEFT,
+ position.left
+ )
+ smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.RIGHT)
+ smallConstraintSet.connect(
+ R.id.biometric_icon,
+ ConstraintSet.LEFT,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.LEFT
+ )
+ smallConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.LEFT,
+ position.left
+ )
}
-
- // prepare for animated size transitions
- for (v in viewsToHideWhenSmall) {
- v.showContentOrHide(forceHide = size.isSmall)
+ if (position.top != 0) {
+ iconParams.bottomToBottom = ConstraintSet.UNSET
+ iconParams.topMargin = position.top
+ mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
+ mediumConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.TOP,
+ position.top
+ )
+ smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.BOTTOM)
+ smallConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.TOP,
+ position.top
+ )
}
-
- if (viewModel.hideSensorIcon.first()) {
- iconHolderView.visibility = View.GONE
+ if (position.right != 0) {
+ iconParams.startToStart = ConstraintSet.UNSET
+ iconParams.rightMargin = position.right
+ mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
+ mediumConstraintSet.connect(
+ R.id.biometric_icon,
+ ConstraintSet.RIGHT,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.RIGHT
+ )
+ mediumConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.RIGHT,
+ position.right
+ )
+ smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.LEFT)
+ smallConstraintSet.connect(
+ R.id.biometric_icon,
+ ConstraintSet.RIGHT,
+ ConstraintSet.PARENT_ID,
+ ConstraintSet.RIGHT
+ )
+ smallConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.RIGHT,
+ position.right
+ )
}
-
- if (currentSize == null && size.isSmall) {
- iconHolderView.alpha = 0f
+ if (position.bottom != 0) {
+ iconParams.topToTop = ConstraintSet.UNSET
+ iconParams.bottomMargin = position.bottom
+ mediumConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
+ mediumConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.BOTTOM,
+ position.bottom
+ )
+ smallConstraintSet.clear(R.id.biometric_icon, ConstraintSet.TOP)
+ smallConstraintSet.setMargin(
+ R.id.biometric_icon,
+ ConstraintSet.BOTTOM,
+ position.bottom
+ )
}
- if ((currentSize.isSmall && size.isMedium) || size.isSmall) {
- viewsToFadeInOnSizeChange.forEach { it.alpha = 0f }
- }
+ iconHolderView.layoutParams = iconParams
+ }
+ }
+ }
- // propagate size changes to legacy panel controller and animate
- // transitions
- view.doOnLayout {
- val width = view.measuredWidth
- val height = view.measuredHeight
+ lifecycleScope.launch {
+ viewModel.iconSize.collect { iconSize ->
+ iconHolderView.layoutParams.width = iconSize.first
+ iconHolderView.layoutParams.height = iconSize.second
+ mediumConstraintSet.constrainWidth(R.id.biometric_icon, iconSize.first)
+ mediumConstraintSet.constrainHeight(R.id.biometric_icon, iconSize.second)
+ }
+ }
- when {
- size.isSmall -> {
- iconHolderView.alpha = 1f
- val bottomInset =
- windowManager.maximumWindowMetrics.windowInsets
- .getInsets(WindowInsets.Type.navigationBars())
- .bottom
- iconHolderView.y =
- if (view.isLandscape()) {
- (view.height -
- iconHolderView.height -
- bottomInset) / 2f
- } else {
- view.height -
- iconHolderView.height -
- iconPadding -
- bottomInset
- }
- val newHeight =
- iconHolderView.height + (2 * iconPadding.toInt()) -
- iconHolderView.paddingTop -
- iconHolderView.paddingBottom
- panelViewController.updateForContentDimensions(
- width,
- newHeight + bottomInset,
- 0, /* animateDurationMs */
- )
- }
- size.isMedium && currentSize.isSmall -> {
- val duration = ANIMATE_SMALL_TO_MEDIUM_DURATION_MS
- panelViewController.updateForContentDimensions(
- width,
- height,
- duration,
- )
- startMonitoredAnimation(
- listOf(
- iconHolderView.asVerticalAnimator(
- duration = duration.toLong(),
- toY =
- iconHolderOriginalY -
- viewsToHideWhenSmall
- .filter { it.isGone }
- .sumOf { it.height },
- ),
- viewsToFadeInOnSizeChange.asFadeInAnimator(
- duration = duration.toLong(),
- delay = duration.toLong(),
- ),
- )
- )
- }
- size.isMedium && currentSize.isNullOrNotSmall -> {
- panelViewController.updateForContentDimensions(
- width,
- height,
- 0, /* animateDurationMs */
- )
- }
- size.isLarge -> {
- val duration = ANIMATE_MEDIUM_TO_LARGE_DURATION_MS
- panelViewController.setUseFullScreen(true)
- panelViewController.updateForContentDimensions(
- panelViewController.containerWidth,
- panelViewController.containerHeight,
- duration,
- )
+ lifecycleScope.launch {
+ viewModel.guidelineBounds.collect { bounds ->
+ val bottomInset =
+ windowManager.maximumWindowMetrics.windowInsets
+ .getInsets(WindowInsets.Type.navigationBars())
+ .bottom
+ mediumConstraintSet.setGuidelineEnd(R.id.bottomGuideline, bottomInset)
- startMonitoredAnimation(
- listOf(
- view.asVerticalAnimator(
- duration.toLong() * 2 / 3,
- toY = view.y - fullSizeYOffset
- ),
- listOf(view)
- .asFadeInAnimator(
- duration = duration.toLong() / 2,
- delay = duration.toLong(),
- ),
- )
- )
- // TODO(b/251476085): clean up (copied from legacy)
- if (view.isAttachedToWindow) {
- val parent = view.parent as? ViewGroup
- parent?.removeView(view)
- }
- }
+ if (bounds.left >= 0) {
+ mediumConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ smallConstraintSet.setGuidelineBegin(leftGuideline.id, bounds.left)
+ } else if (bounds.left < 0) {
+ mediumConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left))
+ smallConstraintSet.setGuidelineEnd(leftGuideline.id, abs(bounds.left))
+ }
+
+ if (bounds.right >= 0) {
+ mediumConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ smallConstraintSet.setGuidelineEnd(rightGuideline.id, bounds.right)
+ } else if (bounds.right < 0) {
+ mediumConstraintSet.setGuidelineBegin(
+ rightGuideline.id,
+ abs(bounds.right)
+ )
+ smallConstraintSet.setGuidelineBegin(
+ rightGuideline.id,
+ abs(bounds.right)
+ )
+ }
+
+ if (bounds.top >= 0) {
+ mediumConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
+ smallConstraintSet.setGuidelineBegin(topGuideline.id, bounds.top)
+ } else if (bounds.top < 0) {
+ mediumConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
+ smallConstraintSet.setGuidelineEnd(topGuideline.id, abs(bounds.top))
+ }
+
+ if (midGuideline != null) {
+ val left =
+ if (bounds.left >= 0) {
+ abs(bounds.left)
+ } else {
+ view.width - abs(bounds.left)
}
+ val right =
+ if (bounds.right >= 0) {
+ view.width - abs(bounds.right)
+ } else {
+ abs(bounds.right)
+ }
+ val mid = (left + right) / 2
+ mediumConstraintSet.setGuidelineBegin(midGuideline.id, mid)
+ }
+ }
+ }
- currentSize = size
- view.visibility = View.VISIBLE
- viewModel.setIsIconViewLoaded(false)
- notifyAccessibilityChanged()
+ lifecycleScope.launch {
+ combine(viewModel.hideSensorIcon, viewModel.size, ::Pair).collect {
+ (hideSensorIcon, size) ->
+ setVisibilities(hideSensorIcon, size)
+ }
+ }
+
+ lifecycleScope.launch {
+ combine(viewModel.position, viewModel.size, ::Pair).collect { (position, size)
+ ->
+ if (position.isLeft) {
+ if (size.isSmall) {
+ flipConstraintSet.clone(smallConstraintSet)
+ } else {
+ flipConstraintSet.clone(mediumConstraintSet)
+ }
+
+ // Move all content to other panel
+ flipConstraintSet.connect(
+ R.id.scrollView,
+ ConstraintSet.LEFT,
+ R.id.midGuideline,
+ ConstraintSet.LEFT
+ )
+ flipConstraintSet.connect(
+ R.id.scrollView,
+ ConstraintSet.RIGHT,
+ R.id.rightGuideline,
+ ConstraintSet.RIGHT
+ )
+ } else if (position.isTop) {
+ // Top position is only used for 180 rotation Udfps
+ // Requires repositioning due to sensor location at top of screen
+ mediumConstraintSet.connect(
+ R.id.scrollView,
+ ConstraintSet.TOP,
+ R.id.indicator,
+ ConstraintSet.BOTTOM
+ )
+ mediumConstraintSet.connect(
+ R.id.scrollView,
+ ConstraintSet.BOTTOM,
+ R.id.button_bar,
+ ConstraintSet.TOP
+ )
+ mediumConstraintSet.connect(
+ R.id.panel,
+ ConstraintSet.TOP,
+ R.id.biometric_icon,
+ ConstraintSet.TOP
+ )
+ mediumConstraintSet.setMargin(
+ R.id.panel,
+ ConstraintSet.TOP,
+ (-24 * pxToDp).toInt()
+ )
+ mediumConstraintSet.setVerticalBias(R.id.scrollView, 0f)
+ }
+
+ when {
+ size.isSmall -> {
+ if (position.isLeft) {
+ flipConstraintSet.applyTo(view)
+ } else {
+ smallConstraintSet.applyTo(view)
+ }
+ }
+ size.isMedium && currentSize.isSmall -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_SMALL_TO_MEDIUM_DURATION_MS.toLong()
+ )
+
+ TransitionManager.beginDelayedTransition(view, autoTransition)
+
+ if (position.isLeft) {
+ flipConstraintSet.applyTo(view)
+ } else {
+ mediumConstraintSet.applyTo(view)
+ }
+ }
+ size.isMedium -> {
+ if (position.isLeft) {
+ flipConstraintSet.applyTo(view)
+ } else {
+ mediumConstraintSet.applyTo(view)
+ }
+ }
+ size.isLarge && currentSize.isMedium -> {
+ val autoTransition = AutoTransition()
+ autoTransition.setDuration(
+ ANIMATE_MEDIUM_TO_LARGE_DURATION_MS.toLong()
+ )
+
+ TransitionManager.beginDelayedTransition(view, autoTransition)
+ largeConstraintSet.applyTo(view)
}
}
+
+ currentSize = size
+ currentPosition = position
+ notifyAccessibilityChanged()
+
+ panelView.invalidateOutline()
+ view.invalidate()
+ view.requestLayout()
}
}
}
@@ -646,17 +453,6 @@
}
}
-private fun View.isLandscape(): Boolean {
- val r = context.display.rotation
- return if (
- context.resources.getBoolean(com.android.internal.R.bool.config_reverseDefaultRotation)
- ) {
- r == Surface.ROTATION_0 || r == Surface.ROTATION_180
- } else {
- r == Surface.ROTATION_90 || r == Surface.ROTATION_270
- }
-}
-
private fun View.showContentOrHide(forceHide: Boolean = false) {
val isTextViewWithBlankText = this is TextView && this.text.isBlank()
val isImageViewWithoutImage = this is ImageView && this.drawable == null
@@ -667,26 +463,3 @@
View.VISIBLE
}
}
-
-private fun View.asVerticalAnimator(
- duration: Long,
- toY: Float,
- fromY: Float = this.y
-): ValueAnimator {
- val animator = ValueAnimator.ofFloat(fromY, toY)
- animator.duration = duration
- animator.addUpdateListener { y = it.animatedValue as Float }
- return animator
-}
-
-private fun List<View>.asFadeInAnimator(duration: Long, delay: Long): ValueAnimator {
- forEach { it.alpha = 0f }
- val animator = ValueAnimator.ofFloat(0f, 1f)
- animator.duration = duration
- animator.startDelay = delay
- animator.addUpdateListener {
- val alpha = it.animatedValue as Float
- forEach { view -> view.alpha = alpha }
- }
- return animator
-}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
index 18e2a56..49f4b05 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/CredentialViewBinder.kt
@@ -10,7 +10,6 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.android.app.animation.Interpolators
-import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.AuthPanelController
import com.android.systemui.biometrics.ui.CredentialPasswordView
import com.android.systemui.biometrics.ui.CredentialPatternView
@@ -82,7 +81,7 @@
subtitleView.textOrHide = header.subtitle
descriptionView.textOrHide = header.description
- if (Flags.customBiometricPrompt() && constraintBp()) {
+ if (Flags.customBiometricPrompt()) {
BiometricCustomizedViewBinder.bind(
customizedViewContainer,
header.contentView,
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
index 9e4aaaa..eab3b26 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/binder/PromptIconViewBinder.kt
@@ -22,9 +22,7 @@
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
import com.airbnb.lottie.LottieAnimationView
-import com.airbnb.lottie.LottieOnCompositionLoadedListener
import com.android.settingslib.widget.LottieColorUtils
-import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel
import com.android.systemui.biometrics.ui.viewmodel.PromptIconViewModel.AuthType
import com.android.systemui.biometrics.ui.viewmodel.PromptViewModel
@@ -44,55 +42,12 @@
@JvmStatic
fun bind(
iconView: LottieAnimationView,
- iconViewLayoutParamSizeOverride: Pair<Int, Int>?,
promptViewModel: PromptViewModel
) {
val viewModel = promptViewModel.iconViewModel
iconView.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
viewModel.onConfigurationChanged(iconView.context.resources.configuration)
- if (iconViewLayoutParamSizeOverride != null) {
- iconView.layoutParams.width = iconViewLayoutParamSizeOverride.first
- iconView.layoutParams.height = iconViewLayoutParamSizeOverride.second
- }
-
- if (!constraintBp()) {
- launch {
- var lottieOnCompositionLoadedListener: LottieOnCompositionLoadedListener? =
- null
-
- viewModel.iconSize.collect { iconSize ->
- /**
- * When we bind the BiometricPrompt View and ViewModel in
- * [BiometricViewBinder], the view is set invisible and
- * [isIconViewLoaded] is set to false. We configure the iconView with a
- * LottieOnCompositionLoadedListener that sets [isIconViewLoaded] to
- * true, in order to wait for the iconView to load before determining
- * the prompt size, and prevent any prompt resizing from being visible
- * to the user.
- *
- * TODO(b/288175072): May be able to remove this once constraint layout
- * is unflagged
- */
- if (lottieOnCompositionLoadedListener != null) {
- iconView.removeLottieOnCompositionLoadedListener(
- lottieOnCompositionLoadedListener!!
- )
- }
- lottieOnCompositionLoadedListener = LottieOnCompositionLoadedListener {
- promptViewModel.setIsIconViewLoaded(true)
- }
- iconView.addLottieOnCompositionLoadedListener(
- lottieOnCompositionLoadedListener!!
- )
-
- if (iconViewLayoutParamSizeOverride == null) {
- iconView.layoutParams.width = iconSize.first
- iconView.layoutParams.height = iconSize.second
- }
- }
- }
- }
launch {
viewModel.iconAsset
@@ -154,7 +109,7 @@
setAnimation(asset)
if (animatingFromSfpsAuthenticating(asset)) {
// Skipping to error / success / unlock segment of animation
- setMinFrame(151)
+ setMinFrame(158)
} else {
frame = 0
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
index 31af126..761c3da 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/CredentialViewModel.kt
@@ -6,7 +6,6 @@
import android.hardware.biometrics.PromptContentView
import android.text.InputType
import com.android.internal.widget.LockPatternView
-import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.Utils
import com.android.systemui.biometrics.domain.interactor.CredentialStatus
import com.android.systemui.biometrics.domain.interactor.PromptCredentialInteractor
@@ -39,7 +38,7 @@
credentialInteractor.prompt.filterIsInstance<BiometricPromptRequest.Credential>(),
credentialInteractor.showTitleOnly
) { request, showTitleOnly ->
- val flagEnabled = customBiometricPrompt() && constraintBp()
+ val flagEnabled = customBiometricPrompt()
val showTitleOnlyForCredential = showTitleOnly && flagEnabled
BiometricPromptHeaderViewModelImpl(
request,
@@ -82,8 +81,8 @@
val errorMessage: Flow<String> =
combine(credentialInteractor.verificationError, credentialInteractor.prompt) { error, p ->
when (error) {
- is CredentialStatus.Fail.Error -> error.error
- ?: applicationContext.asBadCredentialErrorMessage(p)
+ is CredentialStatus.Fail.Error ->
+ error.error ?: applicationContext.asBadCredentialErrorMessage(p)
is CredentialStatus.Fail.Throttled -> error.error
null -> ""
}
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
index 214420d..25d43d9 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModel.kt
@@ -36,7 +36,6 @@
import android.view.HapticFeedbackConstants
import android.view.MotionEvent
import com.android.launcher3.icons.IconProvider
-import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.UdfpsUtils
import com.android.systemui.biometrics.Utils
import com.android.systemui.biometrics.Utils.isSystem
@@ -470,7 +469,7 @@
promptSelectorInteractor.prompt
.map {
when {
- !(customBiometricPrompt() && constraintBp()) || it == null -> Pair(null, "")
+ !(customBiometricPrompt()) || it == null -> Pair(null, "")
else -> context.getUserBadgedLogoInfo(it, iconProvider, activityTaskManager)
}
}
@@ -487,7 +486,7 @@
/** Custom content view for the prompt. */
val contentView: Flow<PromptContentView?> =
promptSelectorInteractor.prompt
- .map { if (customBiometricPrompt() && constraintBp()) it?.contentView else null }
+ .map { if (customBiometricPrompt()) it?.contentView else null }
.distinctUntilChanged()
private val originalDescription =
@@ -1045,7 +1044,7 @@
val packageName =
when {
componentNameForLogo != null -> componentNameForLogo.packageName
- // TODO(b/339532378): We should check whether |allowBackgroundAuthentication| should be
+ // TODO(b/353597496): We should check whether |allowBackgroundAuthentication| should be
// removed.
// This is being consistent with the check in [AuthController.showDialog()].
allowBackgroundAuthentication || isSystem(context, opPackageName) -> opPackageName
diff --git a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
index 19ea007..c2a4ee3 100644
--- a/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModel.kt
@@ -28,7 +28,6 @@
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_NO_MOVE_ANIMATION
import android.view.WindowManager.LayoutParams.PRIVATE_FLAG_TRUSTED_OVERLAY
import com.airbnb.lottie.model.KeyPath
-import com.android.systemui.Flags.constraintBp
import com.android.systemui.biometrics.Utils
import com.android.systemui.biometrics.domain.interactor.DisplayStateInteractor
import com.android.systemui.biometrics.domain.interactor.SideFpsSensorInteractor
@@ -152,21 +151,6 @@
->
val topLeft = Point(sensorLocation.left, sensorLocation.top)
- if (!constraintBp()) {
- if (sensorLocation.isSensorVerticalInDefaultOrientation) {
- if (displayRotation == DisplayRotation.ROTATION_0) {
- topLeft.x -= bounds!!.width()
- } else if (displayRotation == DisplayRotation.ROTATION_270) {
- topLeft.y -= bounds!!.height()
- }
- } else {
- if (displayRotation == DisplayRotation.ROTATION_180) {
- topLeft.y -= bounds!!.height()
- } else if (displayRotation == DisplayRotation.ROTATION_270) {
- topLeft.x -= bounds!!.width()
- }
- }
- }
defaultOverlayViewParams.apply {
x = topLeft.x
y = topLeft.y
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
index b45ebd8..24ac542 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayAnimationsController.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.CrossFadeHelper
import javax.inject.Inject
import javax.inject.Named
+import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.launch
/** Controller for dream overlay animations. */
@@ -84,51 +85,62 @@
private var mCurrentBlurRadius: Float = 0f
+ private var mLifecycleFlowHandle: DisposableHandle? = null
+
fun init(view: View) {
this.view = view
- view.repeatWhenAttached {
- repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- dreamViewModel.dreamOverlayTranslationY.collect { px ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int -> setElementsTranslationYAtPosition(px, position) },
- POSITION_TOP or POSITION_BOTTOM
- )
+ mLifecycleFlowHandle =
+ view.repeatWhenAttached {
+ repeatOnLifecycle(Lifecycle.State.CREATED) {
+ launch {
+ dreamViewModel.dreamOverlayTranslationY.collect { px ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationYAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.dreamOverlayTranslationX.collect { px ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int -> setElementsTranslationXAtPosition(px, position) },
- POSITION_TOP or POSITION_BOTTOM
- )
+ launch {
+ dreamViewModel.dreamOverlayTranslationX.collect { px ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsTranslationXAtPosition(px, position)
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.dreamOverlayAlpha.collect { alpha ->
- ComplicationLayoutParams.iteratePositions(
- { position: Int ->
- setElementsAlphaAtPosition(
- alpha = alpha,
- position = position,
- fadingOut = true,
- )
- },
- POSITION_TOP or POSITION_BOTTOM
- )
+ launch {
+ dreamViewModel.dreamOverlayAlpha.collect { alpha ->
+ ComplicationLayoutParams.iteratePositions(
+ { position: Int ->
+ setElementsAlphaAtPosition(
+ alpha = alpha,
+ position = position,
+ fadingOut = true,
+ )
+ },
+ POSITION_TOP or POSITION_BOTTOM
+ )
+ }
}
- }
- launch {
- dreamViewModel.transitionEnded.collect { _ ->
- mOverlayStateController.setExitAnimationsRunning(false)
+ launch {
+ dreamViewModel.transitionEnded.collect { _ ->
+ mOverlayStateController.setExitAnimationsRunning(false)
+ }
}
}
}
- }
+ }
+
+ fun destroy() {
+ mLifecycleFlowHandle?.dispose()
}
/**
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
index 76c7d23..bf6d266 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayContainerViewController.java
@@ -59,6 +59,7 @@
import com.android.systemui.util.ViewController;
import kotlinx.coroutines.CoroutineDispatcher;
+import kotlinx.coroutines.DisposableHandle;
import kotlinx.coroutines.flow.FlowKt;
import java.util.Arrays;
@@ -185,6 +186,8 @@
}
};
+ private DisposableHandle mFlowHandle;
+
@Inject
public DreamOverlayContainerViewController(
DreamOverlayContainerView containerView,
@@ -252,6 +255,17 @@
}
@Override
+ public void destroy() {
+ mStateController.removeCallback(mDreamOverlayStateCallback);
+ mStatusBarViewController.destroy();
+ mComplicationHostViewController.destroy();
+ mDreamOverlayAnimationsController.destroy();
+ mLowLightTransitionCoordinator.setLowLightEnterListener(null);
+
+ super.destroy();
+ }
+
+ @Override
protected void onViewAttached() {
mWakingUpFromSwipe = false;
mJitterStartTimeMillis = System.currentTimeMillis();
@@ -263,7 +277,7 @@
emptyRegion.recycle();
if (dreamHandlesBeingObscured()) {
- collectFlow(
+ mFlowHandle = collectFlow(
mView,
FlowKt.distinctUntilChanged(combineFlows(
mKeyguardTransitionInteractor.isFinishedIn(
@@ -295,6 +309,10 @@
@Override
protected void onViewDetached() {
+ if (mFlowHandle != null) {
+ mFlowHandle.dispose();
+ mFlowHandle = null;
+ }
mHandler.removeCallbacksAndMessages(null);
mPrimaryBouncerCallbackInteractor.removeBouncerExpansionCallback(mBouncerExpansionCallback);
mBouncerlessScrimController.removeCallback(mBouncerlessExpansionCallback);
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
index 931066d..4b9e5a0 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/DreamOverlayService.java
@@ -70,8 +70,12 @@
import com.android.systemui.touch.TouchInsetManager;
import com.android.systemui.util.concurrency.DelayableExecutor;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashSet;
+import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -140,6 +144,8 @@
private ComponentName mCurrentBlockedGestureDreamActivityComponent;
+ private final ArrayList<Job> mFlows = new ArrayList<>();
+
/**
* This {@link LifecycleRegistry} controls when dream overlay functionality, like touch
* handling, should be active. It will automatically be paused when the dream overlay is hidden
@@ -309,12 +315,12 @@
mExecutor.execute(() -> setLifecycleStateLocked(Lifecycle.State.CREATED));
- collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
- mIsCommunalAvailableCallback);
- collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
- mCommunalVisibleConsumer);
- collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
- mBouncerShowingConsumer);
+ mFlows.add(collectFlow(getLifecycle(), mCommunalInteractor.isCommunalAvailable(),
+ mIsCommunalAvailableCallback));
+ mFlows.add(collectFlow(getLifecycle(), communalInteractor.isCommunalVisible(),
+ mCommunalVisibleConsumer));
+ mFlows.add(collectFlow(getLifecycle(), keyguardInteractor.primaryBouncerShowing,
+ mBouncerShowingConsumer));
}
@NonNull
@@ -339,6 +345,11 @@
public void onDestroy() {
mKeyguardUpdateMonitor.removeCallback(mKeyguardCallback);
+ for (Job job : mFlows) {
+ job.cancel(new CancellationException());
+ }
+ mFlows.clear();
+
mExecutor.execute(() -> {
setLifecycleStateLocked(Lifecycle.State.DESTROYED);
@@ -559,6 +570,7 @@
if (mStarted && mWindow != null) {
try {
+ mWindow.clearContentView();
mWindowManager.removeView(mWindow.getDecorView());
} catch (IllegalArgumentException e) {
Log.e(TAG, "Error removing decor view when resetting overlay", e);
@@ -569,7 +581,10 @@
mStateController.setLowLightActive(false);
mStateController.setEntryAnimationsFinished(false);
- mDreamOverlayContainerViewController = null;
+ if (mDreamOverlayContainerViewController != null) {
+ mDreamOverlayContainerViewController.destroy();
+ mDreamOverlayContainerViewController = null;
+ }
if (mTouchMonitor != null) {
mTouchMonitor.destroy();
diff --git a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
index ee7b6f5..5ba780f 100644
--- a/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
+++ b/packages/SystemUI/src/com/android/systemui/dreams/touch/CommunalTouchHandler.java
@@ -33,7 +33,11 @@
import com.android.systemui.dreams.touch.dagger.CommunalTouchModule;
import com.android.systemui.statusbar.phone.CentralSurfaces;
+import kotlinx.coroutines.Job;
+
+import java.util.ArrayList;
import java.util.Optional;
+import java.util.concurrent.CancellationException;
import java.util.function.Consumer;
import javax.inject.Inject;
@@ -49,6 +53,8 @@
private final ConfigurationInteractor mConfigurationInteractor;
private Boolean mIsEnabled = false;
+ private ArrayList<Job> mFlows = new ArrayList<>();
+
private int mLayoutDirection = LayoutDirection.LTR;
@VisibleForTesting
@@ -70,17 +76,17 @@
mCommunalInteractor = communalInteractor;
mConfigurationInteractor = configurationInteractor;
- collectFlow(
+ mFlows.add(collectFlow(
mLifecycle,
mCommunalInteractor.isCommunalAvailable(),
mIsCommunalAvailableCallback
- );
+ ));
- collectFlow(
+ mFlows.add(collectFlow(
mLifecycle,
mConfigurationInteractor.getLayoutDirection(),
mLayoutDirectionCallback
- );
+ ));
}
@Override
@@ -140,4 +146,13 @@
}
});
}
+
+ @Override
+ public void onDestroy() {
+ for (Job job : mFlows) {
+ job.cancel(new CancellationException());
+ }
+ mFlows.clear();
+ TouchHandler.super.onDestroy();
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
index 096556f..7e2c9f8 100644
--- a/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/education/dagger/ContextualEducationModule.kt
@@ -26,6 +26,7 @@
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractor
import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduStatsInteractorImpl
+import com.android.systemui.education.ui.view.ContextualEduUiCoordinator
import dagger.Binds
import dagger.Lazy
import dagger.Module
@@ -74,7 +75,7 @@
implLazy.get()
} else {
// No-op implementation when the flag is disabled.
- return NoOpContextualEducationInteractor
+ return NoOpCoreStartable
}
}
@@ -91,6 +92,8 @@
}
@Provides
+ @IntoMap
+ @ClassKey(KeyboardTouchpadEduInteractor::class)
fun provideKeyboardTouchpadEduInteractor(
implLazy: Lazy<KeyboardTouchpadEduInteractor>
): CoreStartable {
@@ -98,22 +101,32 @@
implLazy.get()
} else {
// No-op implementation when the flag is disabled.
- return NoOpKeyboardTouchpadEduInteractor
+ return NoOpCoreStartable
+ }
+ }
+
+ @Provides
+ @IntoMap
+ @ClassKey(ContextualEduUiCoordinator::class)
+ fun provideContextualEduUiCoordinator(
+ implLazy: Lazy<ContextualEduUiCoordinator>
+ ): CoreStartable {
+ return if (Flags.keyboardTouchpadContextualEducation()) {
+ implLazy.get()
+ } else {
+ // No-op implementation when the flag is disabled.
+ return NoOpCoreStartable
}
}
}
+}
- private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
- override fun incrementSignalCount(gestureType: GestureType) {}
+private object NoOpKeyboardTouchpadEduStatsInteractor : KeyboardTouchpadEduStatsInteractor {
+ override fun incrementSignalCount(gestureType: GestureType) {}
- override fun updateShortcutTriggerTime(gestureType: GestureType) {}
- }
+ override fun updateShortcutTriggerTime(gestureType: GestureType) {}
+}
- private object NoOpContextualEducationInteractor : CoreStartable {
- override fun start() {}
- }
-
- private object NoOpKeyboardTouchpadEduInteractor : CoreStartable {
- override fun start() {}
- }
+private object NoOpCoreStartable : CoreStartable {
+ override fun start() {}
}
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
new file mode 100644
index 0000000..b446ea2
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/view/ContextualEduUiCoordinator.kt
@@ -0,0 +1,68 @@
+/*
+ * Copyright 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.education.ui.view
+
+import android.content.Context
+import android.widget.Toast
+import com.android.systemui.CoreStartable
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.education.shared.model.EducationUiType
+import com.android.systemui.education.ui.viewmodel.ContextualEduContentViewModel
+import com.android.systemui.education.ui.viewmodel.ContextualEduViewModel
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
+
+/**
+ * A class to show contextual education on UI based on the edu produced from
+ * [ContextualEduViewModel]
+ */
+@SysUISingleton
+class ContextualEduUiCoordinator
+constructor(
+ @Application private val applicationScope: CoroutineScope,
+ private val viewModel: ContextualEduViewModel,
+ private val createToast: (String) -> Toast
+) : CoreStartable {
+
+ @Inject
+ constructor(
+ @Application applicationScope: CoroutineScope,
+ context: Context,
+ viewModel: ContextualEduViewModel,
+ ) : this(
+ applicationScope,
+ viewModel,
+ createToast = { message -> Toast.makeText(context, message, Toast.LENGTH_LONG) }
+ )
+
+ override fun start() {
+ applicationScope.launch {
+ viewModel.eduContent.collect { contentModel ->
+ if (contentModel.type == EducationUiType.Toast) {
+ showToast(contentModel)
+ }
+ }
+ }
+ }
+
+ private fun showToast(model: ContextualEduContentViewModel) {
+ val toast = createToast(model.message)
+ toast.show()
+ }
+}
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
similarity index 65%
copy from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
copy to packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
index 460de8c..3cba4c8 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduContentViewModel.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright 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.
@@ -14,18 +14,8 @@
* limitations under the License.
*/
-package android.telephony.satellite.stub;
+package com.android.systemui.education.ui.viewmodel
-/**
- * {@hide}
- */
-parcelable ProvisionSubscriberId {
- /** provision subscriberId */
- String subscriberId;
+import com.android.systemui.education.shared.model.EducationUiType
- /** carrier id */
- int mCarrierId;
-
- /** apn */
- String mNiddApn;
-}
+data class ContextualEduContentViewModel(val message: String, val type: EducationUiType)
diff --git a/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
new file mode 100644
index 0000000..58276e0
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/education/ui/viewmodel/ContextualEduViewModel.kt
@@ -0,0 +1,51 @@
+/*
+ * Copyright 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.education.ui.viewmodel
+
+import android.content.res.Resources
+import com.android.systemui.contextualeducation.GestureType
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.education.domain.interactor.KeyboardTouchpadEduInteractor
+import com.android.systemui.education.shared.model.EducationInfo
+import com.android.systemui.res.R
+import javax.inject.Inject
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.filterNotNull
+import kotlinx.coroutines.flow.map
+
+@SysUISingleton
+class ContextualEduViewModel
+@Inject
+constructor(@Main private val resources: Resources, interactor: KeyboardTouchpadEduInteractor) {
+ val eduContent: Flow<ContextualEduContentViewModel> =
+ interactor.educationTriggered.filterNotNull().map {
+ ContextualEduContentViewModel(getEduContent(it), it.educationUiType)
+ }
+
+ private fun getEduContent(educationInfo: EducationInfo): String {
+ // Todo: also check UiType in educationInfo to determine the string
+ val resourceId =
+ when (educationInfo.gestureType) {
+ GestureType.BACK -> R.string.back_edu_toast_content
+ GestureType.HOME -> R.string.home_edu_toast_content
+ GestureType.OVERVIEW -> R.string.overview_edu_toast_content
+ GestureType.ALL_APPS -> R.string.all_apps_edu_toast_content
+ }
+ return resources.getString(resourceId)
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
index 562ba36..cd0b3f9 100644
--- a/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
+++ b/packages/SystemUI/src/com/android/systemui/flags/FlagDependencies.kt
@@ -58,7 +58,7 @@
// Internal notification frontend dependencies
NotificationAvalancheSuppression.token dependsOn VisualInterruptionRefactor.token
PriorityPeopleSection.token dependsOn SortBySectionTimeFlag.token
- NotificationMinimalismPrototype.token dependsOn NotificationsHeadsUpRefactor.token
+ NotificationMinimalismPrototype.token dependsOn NotificationThrottleHun.token
NotificationsHeadsUpRefactor.token dependsOn NotificationThrottleHun.token
// SceneContainer dependencies
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt
index cfe64e2..9f46846 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/model/TutorialSchedulerInfo.kt
@@ -16,11 +16,6 @@
package com.android.systemui.inputdevice.tutorial.data.model
-data class TutorialSchedulerInfo(
- val keyboard: DeviceSchedulerInfo = DeviceSchedulerInfo(),
- val touchpad: DeviceSchedulerInfo = DeviceSchedulerInfo()
-)
-
data class DeviceSchedulerInfo(var isLaunched: Boolean = false, var connectTime: Long? = null) {
val wasEverConnected: Boolean
get() = connectTime != null
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
index 31ff018..b9b3895 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/data/repository/TutorialSchedulerRepository.kt
@@ -25,21 +25,32 @@
import androidx.datastore.preferences.preferencesDataStore
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo
-import com.android.systemui.inputdevice.tutorial.data.model.TutorialSchedulerInfo
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.first
import kotlinx.coroutines.flow.map
@SysUISingleton
class TutorialSchedulerRepository
@Inject
-constructor(@Application private val applicationContext: Context) {
+constructor(
+ @Application private val applicationContext: Context,
+ @Background private val backgroundScope: CoroutineScope
+) {
private val Context.dataStore: DataStore<Preferences> by
- preferencesDataStore(name = DATASTORE_NAME)
+ preferencesDataStore(name = DATASTORE_NAME, scope = backgroundScope)
- suspend fun loadData(): TutorialSchedulerInfo {
+ suspend fun isLaunched(deviceType: DeviceType): Boolean = loadData()[deviceType]!!.isLaunched
+
+ suspend fun wasEverConnected(deviceType: DeviceType): Boolean =
+ loadData()[deviceType]!!.wasEverConnected
+
+ suspend fun connectTime(deviceType: DeviceType): Long = loadData()[deviceType]!!.connectTime!!
+
+ private suspend fun loadData(): Map<DeviceType, DeviceSchedulerInfo> {
return applicationContext.dataStore.data.map { pref -> getSchedulerInfo(pref) }.first()
}
@@ -51,10 +62,10 @@
applicationContext.dataStore.edit { pref -> pref[getLaunchedKey(device)] = true }
}
- private fun getSchedulerInfo(pref: Preferences): TutorialSchedulerInfo {
- return TutorialSchedulerInfo(
- keyboard = getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD),
- touchpad = getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD)
+ private fun getSchedulerInfo(pref: Preferences): Map<DeviceType, DeviceSchedulerInfo> {
+ return mapOf(
+ DeviceType.KEYBOARD to getDeviceSchedulerInfo(pref, DeviceType.KEYBOARD),
+ DeviceType.TOUCHPAD to getDeviceSchedulerInfo(pref, DeviceType.TOUCHPAD)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
index 05e1044..b3b8f21 100644
--- a/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/inputdevice/tutorial/domain/interactor/TutorialSchedulerInteractor.kt
@@ -16,23 +16,25 @@
package com.android.systemui.inputdevice.tutorial.domain.interactor
-import android.content.Context
-import android.content.Intent
+import android.os.SystemProperties
+import android.util.Log
import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
-import com.android.systemui.inputdevice.tutorial.data.model.DeviceSchedulerInfo
+import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.KEYBOARD
+import com.android.systemui.inputdevice.tutorial.data.repository.DeviceType.TOUCHPAD
import com.android.systemui.inputdevice.tutorial.data.repository.TutorialSchedulerRepository
import com.android.systemui.keyboard.data.repository.KeyboardRepository
import com.android.systemui.touchpad.data.repository.TouchpadRepository
-import java.time.Duration
import java.time.Instant
import javax.inject.Inject
+import kotlin.time.Duration.Companion.hours
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.delay
-import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.first
+import kotlinx.coroutines.flow.flow
+import kotlinx.coroutines.flow.merge
import kotlinx.coroutines.launch
/**
@@ -43,62 +45,72 @@
class TutorialSchedulerInteractor
@Inject
constructor(
- @Application private val context: Context,
- @Application private val applicationScope: CoroutineScope,
- private val keyboardRepository: KeyboardRepository,
- private val touchpadRepository: TouchpadRepository,
- private val tutorialSchedulerRepository: TutorialSchedulerRepository
+ @Background private val backgroundScope: CoroutineScope,
+ keyboardRepository: KeyboardRepository,
+ touchpadRepository: TouchpadRepository,
+ private val repo: TutorialSchedulerRepository
) {
+ private val isAnyDeviceConnected =
+ mapOf(
+ KEYBOARD to keyboardRepository.isAnyKeyboardConnected,
+ TOUCHPAD to touchpadRepository.isAnyTouchpadConnected
+ )
+
fun start() {
- applicationScope.launch {
- val info = tutorialSchedulerRepository.loadData()
- if (!info.keyboard.isLaunched) {
- applicationScope.launch {
- schedule(
- keyboardRepository.isAnyKeyboardConnected,
- info.keyboard,
- DeviceType.KEYBOARD
- )
- }
- }
- if (!info.touchpad.isLaunched) {
- applicationScope.launch {
- schedule(
- touchpadRepository.isAnyTouchpadConnected,
- info.touchpad,
- DeviceType.TOUCHPAD
- )
- }
+ backgroundScope.launch {
+ // Merging two flows to ensure that launch tutorial is launched consecutively in order
+ // to avoid race condition
+ merge(touchpadScheduleFlow, keyboardScheduleFlow).collect {
+ val tutorialType = resolveTutorialType(it)
+ launchTutorial(tutorialType)
}
}
}
- private suspend fun schedule(
- isAnyDeviceConnected: Flow<Boolean>,
- info: DeviceSchedulerInfo,
- deviceType: DeviceType
- ) {
- if (!info.wasEverConnected) {
- waitForDeviceConnection(isAnyDeviceConnected)
- info.connectTime = Instant.now().toEpochMilli()
- tutorialSchedulerRepository.updateConnectTime(deviceType, info.connectTime!!)
+ private val touchpadScheduleFlow = flow {
+ if (!repo.isLaunched(TOUCHPAD)) {
+ schedule(TOUCHPAD)
+ emit(TOUCHPAD)
}
- delay(remainingTimeMillis(info.connectTime!!))
- waitForDeviceConnection(isAnyDeviceConnected)
- info.isLaunched = true
- tutorialSchedulerRepository.updateLaunch(deviceType)
- launchTutorial()
}
- private suspend fun waitForDeviceConnection(isAnyDeviceConnected: Flow<Boolean>): Boolean {
- return isAnyDeviceConnected.filter { it }.first()
+ private val keyboardScheduleFlow = flow {
+ if (!repo.isLaunched(KEYBOARD)) {
+ schedule(KEYBOARD)
+ emit(KEYBOARD)
+ }
}
- private fun launchTutorial() {
- val intent = Intent(TUTORIAL_ACTION)
- intent.addCategory(Intent.CATEGORY_DEFAULT)
- intent.setFlags(Intent.FLAG_ACTIVITY_NEW_TASK)
- context.startActivity(intent)
+ private suspend fun schedule(deviceType: DeviceType) {
+ if (!repo.wasEverConnected(deviceType)) {
+ waitForDeviceConnection(deviceType)
+ repo.updateConnectTime(deviceType, Instant.now().toEpochMilli())
+ }
+ delay(remainingTimeMillis(start = repo.connectTime(deviceType)))
+ waitForDeviceConnection(deviceType)
+ }
+
+ private suspend fun waitForDeviceConnection(deviceType: DeviceType) =
+ isAnyDeviceConnected[deviceType]!!.filter { it }.first()
+
+ private suspend fun launchTutorial(tutorialType: TutorialType) {
+ if (tutorialType == TutorialType.KEYBOARD || tutorialType == TutorialType.BOTH)
+ repo.updateLaunch(KEYBOARD)
+ if (tutorialType == TutorialType.TOUCHPAD || tutorialType == TutorialType.BOTH)
+ repo.updateLaunch(TOUCHPAD)
+ // TODO: launch tutorial
+ Log.d(TAG, "Launch tutorial for $tutorialType")
+ }
+
+ private suspend fun resolveTutorialType(deviceType: DeviceType): TutorialType {
+ // Resolve the type of tutorial depending on which device are connected when the tutorial is
+ // launched. E.g. when the keyboard is connected for [LAUNCH_DELAY], both keyboard and
+ // touchpad are connected, we launch the tutorial for both.
+ if (repo.isLaunched(deviceType)) return TutorialType.NONE
+ val otherDevice = if (deviceType == KEYBOARD) TOUCHPAD else KEYBOARD
+ val isOtherDeviceConnected = isAnyDeviceConnected[otherDevice]!!.first()
+ if (!repo.isLaunched(otherDevice) && isOtherDeviceConnected) return TutorialType.BOTH
+ return if (deviceType == KEYBOARD) TutorialType.KEYBOARD else TutorialType.TOUCHPAD
}
private fun remainingTimeMillis(start: Long): Long {
@@ -107,7 +119,20 @@
}
companion object {
- const val TUTORIAL_ACTION = "com.android.systemui.action.TOUCHPAD_TUTORIAL"
- private val LAUNCH_DELAY = Duration.ofHours(72).toMillis()
+ const val TAG = "TutorialSchedulerInteractor"
+ private val DEFAULT_LAUNCH_DELAY = 72.hours.inWholeMilliseconds
+ private val LAUNCH_DELAY: Long
+ get() =
+ SystemProperties.getLong(
+ "persist.peripheral_tutorial_delay_ms",
+ DEFAULT_LAUNCH_DELAY
+ )
+ }
+
+ enum class TutorialType {
+ KEYBOARD,
+ TOUCHPAD,
+ BOTH,
+ NONE
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
index 90867edd..da671e3 100644
--- a/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyboard/PhysicalKeyboardCoreStartable.kt
@@ -17,7 +17,6 @@
package com.android.systemui.keyboard
-import android.hardware.input.InputSettings
import com.android.systemui.CoreStartable
import com.android.systemui.Flags
import com.android.systemui.dagger.SysUISingleton
@@ -40,12 +39,10 @@
private val featureFlags: FeatureFlags,
) : CoreStartable {
override fun start() {
+ stickyKeysIndicatorCoordinator.get().startListening()
if (featureFlags.isEnabled(LegacyFlag.KEYBOARD_BACKLIGHT_INDICATOR)) {
keyboardBacklightDialogCoordinator.get().startListening()
}
- if (InputSettings.isAccessibilityStickyKeysFeatureEnabled()) {
- stickyKeysIndicatorCoordinator.get().startListening()
- }
if (Flags.keyboardDockingIndicator()) {
keyboardDockingIndicationViewBinder.get().startListening()
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
index 9f33113..871d046 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardService.java
@@ -645,6 +645,9 @@
public void showDismissibleKeyguard() {
trace("showDismissibleKeyguard");
checkPermission();
+ if (mFoldGracePeriodProvider.get().isEnabled()) {
+ mKeyguardInteractor.showDismissibleKeyguard();
+ }
mKeyguardViewMediator.showDismissibleKeyguard();
if (SceneContainerFlag.isEnabled() && mFoldGracePeriodProvider.get().isEnabled()) {
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
index 0b3d0f7..1f3df95 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/KeyguardViewMediator.java
@@ -2334,6 +2334,12 @@
Log.e(TAG, "doKeyguard: we're still showing, but going away. Re-show the "
+ "keyguard rather than short-circuiting and resetting.");
} else {
+ // We're removing "reset" in the refactor - "resetting" the views will happen
+ // as a reaction to the root cause of the "reset" signal.
+ if (KeyguardWmStateRefactor.isEnabled()) {
+ return;
+ }
+
// It's already showing, and we're not trying to show it while the screen is
// off. We can simply reset all of the views, but don't hide the bouncer in case
// the user is currently interacting with it.
@@ -2399,6 +2405,16 @@
*/
private void handleDismiss(IKeyguardDismissCallback callback, CharSequence message) {
if (mShowing) {
+ if (KeyguardWmStateRefactor.isEnabled()) {
+ Log.d(TAG, "Dismissing keyguard with keyguard_wm_refactor_enabled: "
+ + "cancelDoKeyguardLaterLocked");
+
+ // This won't get canceled in onKeyguardExitFinished() if the refactor is enabled,
+ // which can lead to the keyguard re-showing. Cancel here for now; this can be
+ // removed once we migrate the logic that posts doKeyguardLater in the first place.
+ cancelDoKeyguardLaterLocked();
+ }
+
if (callback != null) {
mDismissCallbackRegistry.addCallback(callback);
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
index edf17c1..81b0064 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardRepository.kt
@@ -232,6 +232,9 @@
/** Receive an event for doze time tick */
val dozeTimeTick: Flow<Long>
+ /** Receive an event lockscreen being shown in a dismissible state */
+ val showDismissibleKeyguard: MutableStateFlow<Long>
+
/** Observable for DismissAction */
val dismissAction: StateFlow<DismissAction>
@@ -305,6 +308,8 @@
fun dozeTimeTick()
+ fun showDismissibleKeyguard()
+
fun setDismissAction(dismissAction: DismissAction)
suspend fun setKeyguardDone(keyguardDoneType: KeyguardDone)
@@ -439,6 +444,12 @@
_dozeTimeTick.value = systemClock.uptimeMillis()
}
+ override val showDismissibleKeyguard = MutableStateFlow<Long>(0L)
+
+ override fun showDismissibleKeyguard() {
+ showDismissibleKeyguard.value = systemClock.uptimeMillis()
+ }
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
index de60c11..797a4ec 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepository.kt
@@ -285,7 +285,7 @@
state: TransitionState
) {
if (updateTransitionId != transitionId) {
- Log.wtf(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
+ Log.e(TAG, "Attempting to update with old/invalid transitionId: $transitionId")
return
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
index 3775d19..17c1e82 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromDreamingTransitionInteractor.kt
@@ -133,7 +133,12 @@
transitionInteractor.startedKeyguardState.replayCache.last() ==
KeyguardState.DREAMING
) {
- startTransitionTo(KeyguardState.LOCKSCREEN)
+ if (powerInteractor.detailedWakefulness.value.isAwake()) {
+ startTransitionTo(
+ KeyguardState.LOCKSCREEN,
+ ownerReason = "Dream has ended and device is awake"
+ )
+ }
}
}
}
@@ -144,7 +149,7 @@
scope.launch {
combine(
keyguardInteractor.isKeyguardOccluded,
- keyguardInteractor.isDreaming
+ keyguardInteractor.isAbleToDream
// Debounce the dreaming signal since there is a race condition between
// the occluded and dreaming signals. We therefore add a small delay
// to give enough time for occluded to flip to false when the dream
@@ -172,7 +177,7 @@
}
scope.launch {
- keyguardInteractor.isDreaming
+ keyguardInteractor.isAbleToDream
.filter { !it }
.sample(deviceEntryInteractor.isUnlocked, ::Pair)
.collect { (_, dismissable) ->
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
index 8f4110c..db5a63b 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromGoneTransitionInteractor.kt
@@ -74,6 +74,7 @@
listenForGoneToAodOrDozing()
listenForGoneToDreaming()
listenForGoneToLockscreenOrHub()
+ listenForGoneToOccluded()
listenForGoneToDreamingLockscreenHosted()
}
@@ -81,6 +82,27 @@
scope.launch("$TAG#showKeyguard") { startTransitionTo(KeyguardState.LOCKSCREEN) }
}
+ /**
+ * A special case supported on foldables, where folding the device may put the device on an
+ * unlocked lockscreen, but if an occluding app is already showing (like a active phone call),
+ * then go directly to OCCLUDED.
+ */
+ private fun listenForGoneToOccluded() {
+ scope.launch("$TAG#listenForGoneToOccluded") {
+ keyguardInteractor.showDismissibleKeyguard
+ .filterRelevantKeyguardState()
+ .sample(keyguardInteractor.isKeyguardOccluded, ::Pair)
+ .collect { (_, isKeyguardOccluded) ->
+ if (isKeyguardOccluded) {
+ startTransitionTo(
+ KeyguardState.OCCLUDED,
+ ownerReason = "Dismissible keyguard with occlusion"
+ )
+ }
+ }
+ }
+ }
+
// Primarily for when the user chooses to lock down the device
private fun listenForGoneToLockscreenOrHub() {
if (KeyguardWmStateRefactor.isEnabled) {
@@ -166,11 +188,12 @@
interpolator = Interpolators.LINEAR
duration =
when (toState) {
- KeyguardState.DREAMING -> TO_DREAMING_DURATION
KeyguardState.AOD -> TO_AOD_DURATION
KeyguardState.DOZING -> TO_DOZING_DURATION
+ KeyguardState.DREAMING -> TO_DREAMING_DURATION
KeyguardState.LOCKSCREEN -> TO_LOCKSCREEN_DURATION
KeyguardState.GLANCEABLE_HUB -> TO_GLANCEABLE_HUB_DURATION
+ KeyguardState.OCCLUDED -> TO_OCCLUDED_DURATION
else -> DEFAULT_DURATION
}.inWholeMilliseconds
}
@@ -179,10 +202,11 @@
companion object {
private const val TAG = "FromGoneTransitionInteractor"
private val DEFAULT_DURATION = 500.milliseconds
- val TO_DREAMING_DURATION = 933.milliseconds
val TO_AOD_DURATION = 1300.milliseconds
val TO_DOZING_DURATION = 933.milliseconds
+ val TO_DREAMING_DURATION = 933.milliseconds
val TO_LOCKSCREEN_DURATION = DEFAULT_DURATION
val TO_GLANCEABLE_HUB_DURATION = DEFAULT_DURATION
+ val TO_OCCLUDED_DURATION = 100.milliseconds
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
index 51d92f0..5dc020f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromLockscreenTransitionInteractor.kt
@@ -300,7 +300,9 @@
swipeToDismissInteractor.dismissFling
.filterNotNull()
.filterRelevantKeyguardState()
- .collect { _ -> startTransitionTo(KeyguardState.GONE) }
+ .collect { _ ->
+ startTransitionTo(KeyguardState.GONE, ownerReason = "dismissFling != null")
+ }
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
index 710b710a..aea57ce 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/FromOccludedTransitionInteractor.kt
@@ -157,6 +157,13 @@
}
}
+ /** Starts a transition to dismiss the keyguard from the OCCLUDED state. */
+ fun dismissFromOccluded() {
+ scope.launch {
+ startTransitionTo(KeyguardState.GONE, ownerReason = "Dismiss from occluded")
+ }
+ }
+
private fun listenForOccludedToGone() {
if (KeyguardWmStateRefactor.isEnabled) {
// We don't think OCCLUDED to GONE is possible. You should always have to go via a
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
index 42490c4..0df989e 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardInteractor.kt
@@ -1,20 +1,18 @@
/*
- * Copyright (C) 2022 The Android Open Source Project
+ * Copyright (C) 2022 The Android Open Source Project
*
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
+ * 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
+ * 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.
- *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
*/
-
@file:OptIn(ExperimentalCoroutinesApi::class)
package com.android.systemui.keyguard.domain.interactor
@@ -156,15 +154,24 @@
val isPulsing: Flow<Boolean> = dozeTransitionModel.map { it.to == DozeStateModel.DOZE_PULSING }
- /**
- * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
- * but not vice-versa.
- */
- val isDreaming: StateFlow<Boolean> = repository.isDreaming
-
/** Whether the system is dreaming with an overlay active */
val isDreamingWithOverlay: Flow<Boolean> = repository.isDreamingWithOverlay
+ /**
+ * Whether the system is dreaming. [isDreaming] will be always be true when [isDozing] is true,
+ * but not vice-versa. Also accounts for [isDreamingWithOverlay]
+ */
+ val isDreaming: StateFlow<Boolean> =
+ merge(
+ repository.isDreaming,
+ repository.isDreamingWithOverlay,
+ )
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.Eagerly,
+ initialValue = false,
+ )
+
/** Whether the system is dreaming and the active dream is hosted in lockscreen */
val isActiveDreamLockscreenHosted: StateFlow<Boolean> = repository.isActiveDreamLockscreenHosted
@@ -172,6 +179,9 @@
val onCameraLaunchDetected: Flow<CameraLaunchSourceModel> =
repository.onCameraLaunchDetected.filter { it.type != CameraLaunchType.IGNORE }
+ /** Event for when an unlocked keyguard has been requested, such as on device fold */
+ val showDismissibleKeyguard: Flow<Long> = repository.showDismissibleKeyguard.asStateFlow()
+
/**
* Dozing and dreaming have overlapping events. If the doze state remains in FINISH, it means
* that doze mode is not running and DREAMING is ok to commence.
@@ -179,12 +189,25 @@
* Allow a brief moment to prevent rapidly oscillating between true/false signals.
*/
val isAbleToDream: Flow<Boolean> =
- merge(isDreaming, isDreamingWithOverlay)
- .combine(dozeTransitionModel) { isDreaming, dozeTransitionModel ->
- isDreaming && isDozeOff(dozeTransitionModel.to)
+ dozeTransitionModel
+ .flatMapLatest { dozeTransitionModel ->
+ if (isDozeOff(dozeTransitionModel.to)) {
+ // When dozing stops, it is a very early signal that the device is exiting the
+ // dream state. DreamManagerService eventually notifies window manager, which
+ // invokes SystemUI through KeyguardService. Because of this substantial delay,
+ // do not immediately process any dreaming information when exiting AOD. It
+ // should actually be quite strange to leave AOD and then go straight to
+ // DREAMING so this should be fine.
+ delay(500L)
+ isDreaming
+ .sample(powerInteractor.isAwake) { isDreaming, isAwake ->
+ isDreaming && isAwake
+ }
+ .debounce(50L)
+ } else {
+ flowOf(false)
+ }
}
- .sample(powerInteractor.isAwake) { isAbleToDream, isAwake -> isAbleToDream && isAwake }
- .debounce(50L)
.stateIn(
scope = applicationScope,
started = SharingStarted.WhileSubscribed(),
@@ -469,6 +492,10 @@
CameraLaunchSourceModel(type = cameraLaunchSourceIntToType(source))
}
+ fun showDismissibleKeyguard() {
+ repository.showDismissibleKeyguard()
+ }
+
companion object {
private const val TAG = "KeyguardInteractor"
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
index e132eb7..b89eb27 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionAuditLogger.kt
@@ -98,6 +98,16 @@
}
scope.launch {
+ keyguardInteractor.isDreaming.collect { logger.log(TAG, VERBOSE, "isDreaming", it) }
+ }
+
+ scope.launch {
+ keyguardInteractor.isDreamingWithOverlay.collect {
+ logger.log(TAG, VERBOSE, "isDreamingWithOverlay", it)
+ }
+ }
+
+ scope.launch {
keyguardInteractor.isAbleToDream.collect {
logger.log(TAG, VERBOSE, "isAbleToDream", it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
index afbe357..efdae62 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractor.kt
@@ -75,6 +75,7 @@
private val fromAlternateBouncerTransitionInteractor:
dagger.Lazy<FromAlternateBouncerTransitionInteractor>,
private val fromDozingTransitionInteractor: dagger.Lazy<FromDozingTransitionInteractor>,
+ private val fromOccludedTransitionInteractor: dagger.Lazy<FromOccludedTransitionInteractor>,
private val sceneInteractor: SceneInteractor,
) {
private val transitionMap = mutableMapOf<Edge.StateToState, MutableSharedFlow<TransitionStep>>()
@@ -418,6 +419,7 @@
fromAlternateBouncerTransitionInteractor.get().dismissAlternateBouncer()
AOD -> fromAodTransitionInteractor.get().dismissAod()
DOZING -> fromDozingTransitionInteractor.get().dismissFromDozing()
+ KeyguardState.OCCLUDED -> fromOccludedTransitionInteractor.get().dismissFromOccluded()
KeyguardState.GONE ->
Log.i(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
index 86e4115..906d586 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/SwipeToDismissInteractor.kt
@@ -19,14 +19,15 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Background
import com.android.systemui.keyguard.shared.model.KeyguardState
+import com.android.systemui.keyguard.shared.model.StatusBarState
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.util.kotlin.Utils.Companion.sample
-import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.filter
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
+import javax.inject.Inject
/**
* Handles logic around the swipe to dismiss gesture, where the user swipes up on the dismissable
@@ -53,12 +54,14 @@
shadeRepository.currentFling
.sample(
transitionInteractor.startedKeyguardState,
- keyguardInteractor.isKeyguardDismissible
+ keyguardInteractor.isKeyguardDismissible,
+ keyguardInteractor.statusBarState,
)
- .filter { (flingInfo, startedState, keyguardDismissable) ->
+ .filter { (flingInfo, startedState, keyguardDismissable, statusBarState) ->
flingInfo != null &&
- !flingInfo.expand &&
- startedState == KeyguardState.LOCKSCREEN &&
+ !flingInfo.expand &&
+ statusBarState != StatusBarState.SHADE_LOCKED &&
+ startedState == KeyguardState.LOCKSCREEN &&
keyguardDismissable
}
.map { (flingInfo, _) -> flingInfo }
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
index e1b333d..25b2b7c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/domain/interactor/WindowManagerLockscreenVisibilityInteractor.kt
@@ -93,6 +93,14 @@
KeyguardState.ALTERNATE_BOUNCER -> {
fromAlternateBouncerInteractor.surfaceBehindVisibility
}
+ KeyguardState.OCCLUDED -> {
+ // OCCLUDED -> GONE occurs when an app is on top of the keyguard, and then
+ // requests manual dismissal of the keyguard in the background. The app will
+ // remain visible on top of the stack throughout this transition, so we
+ // should not trigger the keyguard going away animation by returning
+ // surfaceBehindVisibility = true.
+ flowOf(false)
+ }
else -> flowOf(null)
}
}
@@ -253,6 +261,18 @@
) {
// Dreams dismiss keyguard and return to GONE if they can.
false
+ } else if (
+ startedWithPrev.newValue.from == KeyguardState.OCCLUDED &&
+ startedWithPrev.newValue.to == KeyguardState.GONE
+ ) {
+ // OCCLUDED -> GONE directly, without transiting a *_BOUNCER state, occurs
+ // when an app uses intent flags to launch over an insecure keyguard without
+ // dismissing it, and then manually requests keyguard dismissal while
+ // OCCLUDED. This transition is not user-visible; the device unlocks in the
+ // background and the app remains on top, while we're now GONE. In this case
+ // we should simply tell WM that the lockscreen is no longer visible, and
+ // *not* play the going away animation or related animations.
+ false
} else {
// Otherwise, use the visibility of the current state.
KeyguardState.lockscreenVisibleInState(currentState)
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
index 9dc77d3..fb97191 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerUdfpsViewBinder.kt
@@ -26,6 +26,7 @@
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import kotlinx.coroutines.ExperimentalCoroutinesApi
@ExperimentalCoroutinesApi
@@ -52,7 +53,11 @@
}
}
- launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } }
+ if (SceneContainerFlag.isEnabled) {
+ view.alpha = 1f
+ } else {
+ launch("$TAG#viewModel.alpha") { viewModel.alpha.collect { view.alpha = it } }
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
index a250b22..91a7f7f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinder.kt
@@ -37,13 +37,13 @@
import com.android.systemui.deviceentry.ui.binder.UdfpsAccessibilityOverlayBinder
import com.android.systemui.deviceentry.ui.view.UdfpsAccessibilityOverlay
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
-import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerUdfpsIconViewModel
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerWindowViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scrim.ScrimView
import dagger.Lazy
import javax.inject.Inject
@@ -67,7 +67,6 @@
private val alternateBouncerDependencies: Lazy<AlternateBouncerDependencies>,
private val windowManager: Lazy<WindowManager>,
private val layoutInflater: Lazy<LayoutInflater>,
- private val dismissCallbackRegistry: DismissCallbackRegistry,
) : CoreStartable {
private val layoutParams: WindowManager.LayoutParams
get() =
@@ -95,9 +94,10 @@
private var alternateBouncerView: ConstraintLayout? = null
override fun start() {
- if (!DeviceEntryUdfpsRefactor.isEnabled) {
+ if (!DeviceEntryUdfpsRefactor.isEnabled || SceneContainerFlag.isEnabled) {
return
}
+
applicationScope.launch("$TAG#alternateBouncerWindowViewModel") {
alternateBouncerWindowViewModel.get().alternateBouncerWindowRequired.collect {
addAlternateBouncerWindowView ->
@@ -110,7 +110,7 @@
bind(alternateBouncerView!!, alternateBouncerDependencies.get())
} else {
removeViewFromWindowManager()
- alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
+ alternateBouncerDependencies.get().viewModel.onRemovedFromWindow()
}
}
}
@@ -144,7 +144,7 @@
private val onAttachAddBackGestureHandler =
object : View.OnAttachStateChangeListener {
private val onBackInvokedCallback: OnBackInvokedCallback = OnBackInvokedCallback {
- onBackRequested()
+ alternateBouncerDependencies.get().viewModel.onBackRequested()
}
override fun onViewAttachedToWindow(view: View) {
@@ -161,14 +161,12 @@
.findOnBackInvokedDispatcher()
?.unregisterOnBackInvokedCallback(onBackInvokedCallback)
}
-
- fun onBackRequested() {
- alternateBouncerDependencies.get().viewModel.hideAlternateBouncer()
- dismissCallbackRegistry.notifyDismissCancelled()
- }
}
private fun addViewToWindowManager() {
+ if (SceneContainerFlag.isEnabled) {
+ return
+ }
if (alternateBouncerView != null) {
return
}
@@ -190,6 +188,7 @@
if (DeviceEntryUdfpsRefactor.isUnexpectedlyInLegacyMode()) {
return
}
+
optionallyAddUdfpsViews(
view = view,
udfpsIconViewModel = alternateBouncerDependencies.udfpsIconViewModel,
@@ -202,12 +201,13 @@
viewModel = alternateBouncerDependencies.messageAreaViewModel,
)
- val scrim = view.requireViewById(R.id.alternate_bouncer_scrim) as ScrimView
+ val scrim: ScrimView = view.requireViewById(R.id.alternate_bouncer_scrim)
val viewModel = alternateBouncerDependencies.viewModel
val swipeUpAnywhereGestureHandler =
alternateBouncerDependencies.swipeUpAnywhereGestureHandler
val tapGestureDetector = alternateBouncerDependencies.tapGestureDetector
- view.repeatWhenAttached { alternateBouncerViewContainer ->
+
+ view.repeatWhenAttached {
repeatOnLifecycle(Lifecycle.State.STARTED) {
launch("$TAG#viewModel.registerForDismissGestures") {
viewModel.registerForDismissGestures.collect { registerForDismissGestures ->
@@ -216,11 +216,11 @@
swipeTag
) { _ ->
alternateBouncerDependencies.powerInteractor.onUserTouch()
- viewModel.showPrimaryBouncer()
+ viewModel.onTapped()
}
tapGestureDetector.addOnGestureDetectedCallback(tapTag) { _ ->
alternateBouncerDependencies.powerInteractor.onUserTouch()
- viewModel.showPrimaryBouncer()
+ viewModel.onTapped()
}
} else {
swipeUpAnywhereGestureHandler.removeOnGestureDetectedCallback(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
index 162a0d2..15e6b1d 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/binder/KeyguardQuickAffordanceViewBinder.kt
@@ -30,18 +30,23 @@
import androidx.core.view.updateLayoutParams
import androidx.lifecycle.Lifecycle
import androidx.lifecycle.repeatOnLifecycle
+import com.android.app.tracing.coroutines.launch
import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.settingslib.Utils
import com.android.systemui.animation.Expandable
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.common.shared.model.Icon
import com.android.systemui.common.ui.binder.IconViewBinder
+import com.android.systemui.dagger.SysUISingleton
+import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordanceViewModel
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.util.doOnEnd
+import javax.inject.Inject
+import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.combine
@@ -49,11 +54,20 @@
import kotlinx.coroutines.launch
/** This is only for a SINGLE Quick affordance */
-object KeyguardQuickAffordanceViewBinder {
+@SysUISingleton
+class KeyguardQuickAffordanceViewBinder
+@Inject
+constructor(
+ private val falsingManager: FalsingManager?,
+ private val vibratorHelper: VibratorHelper?,
+ private val logger: KeyguardQuickAffordancesLogger,
+ @Main private val mainImmediateDispatcher: CoroutineDispatcher,
+) {
- private const val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
- private const val SCALE_SELECTED_BUTTON = 1.23f
- private const val DIM_ALPHA = 0.3f
+ private val EXIT_DOZE_BUTTON_REVEAL_ANIMATION_DURATION_MS = 250L
+ private val SCALE_SELECTED_BUTTON = 1.23f
+ private val DIM_ALPHA = 0.3f
+ private val TAG = "KeyguardQuickAffordanceViewBinder"
/**
* Defines interface for an object that acts as the binding between the view and its view-model.
@@ -73,30 +87,24 @@
view: LaunchableImageView,
viewModel: Flow<KeyguardQuickAffordanceViewModel>,
alpha: Flow<Float>,
- falsingManager: FalsingManager?,
- vibratorHelper: VibratorHelper?,
- logger: KeyguardQuickAffordancesLogger,
messageDisplayer: (Int) -> Unit,
): Binding {
val button = view as ImageView
val configurationBasedDimensions = MutableStateFlow(loadFromResources(view))
val disposableHandle =
- view.repeatWhenAttached {
+ view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.STARTED) {
- launch {
+ launch("$TAG#viewModel") {
viewModel.collect { buttonModel ->
updateButton(
view = button,
viewModel = buttonModel,
- falsingManager = falsingManager,
messageDisplayer = messageDisplayer,
- vibratorHelper = vibratorHelper,
- logger = logger,
)
}
}
- launch {
+ launch("$TAG#updateButtonAlpha") {
updateButtonAlpha(
view = button,
viewModel = viewModel,
@@ -104,7 +112,7 @@
)
}
- launch {
+ launch("$TAG#configurationBasedDimensions") {
configurationBasedDimensions.collect { dimensions ->
button.updateLayoutParams<ViewGroup.LayoutParams> {
width = dimensions.buttonSizePx.width
@@ -131,10 +139,7 @@
private fun updateButton(
view: ImageView,
viewModel: KeyguardQuickAffordanceViewModel,
- falsingManager: FalsingManager?,
messageDisplayer: (Int) -> Unit,
- vibratorHelper: VibratorHelper?,
- logger: KeyguardQuickAffordancesLogger,
) {
if (!viewModel.isVisible) {
view.isInvisible = true
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
index 6faca1e..6031ef6 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/preview/KeyguardPreviewRenderer.kt
@@ -50,7 +50,6 @@
import com.android.internal.policy.SystemBarUtils
import com.android.keyguard.ClockEventController
import com.android.keyguard.KeyguardClockSwitch
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.biometrics.domain.interactor.UdfpsOverlayInteractor
import com.android.systemui.broadcast.BroadcastDispatcher
@@ -79,7 +78,6 @@
import com.android.systemui.keyguard.ui.viewmodel.OccludingAppDeviceEntryMessageViewModel
import com.android.systemui.monet.ColorScheme
import com.android.systemui.monet.Style
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.plugins.clocks.ClockController
import com.android.systemui.res.R
import com.android.systemui.shade.domain.interactor.ShadeInteractor
@@ -89,7 +87,6 @@
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import com.android.systemui.shared.quickaffordance.shared.model.KeyguardPreviewConstants
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import com.android.systemui.statusbar.lockscreen.LockscreenSmartspaceController
import com.android.systemui.statusbar.phone.KeyguardBottomAreaView
import com.android.systemui.statusbar.phone.ScreenOffAnimationController
@@ -133,8 +130,6 @@
private val broadcastDispatcher: BroadcastDispatcher,
private val lockscreenSmartspaceController: LockscreenSmartspaceController,
private val udfpsOverlayInteractor: UdfpsOverlayInteractor,
- private val falsingManager: FalsingManager,
- private val vibratorHelper: VibratorHelper,
private val indicationController: KeyguardIndicationController,
private val keyguardRootViewModel: KeyguardRootViewModel,
private val keyguardBlueprintViewModel: KeyguardBlueprintViewModel,
@@ -148,7 +143,7 @@
private val defaultShortcutsSection: DefaultShortcutsSection,
private val keyguardClockInteractor: KeyguardClockInteractor,
private val keyguardClockViewModel: KeyguardClockViewModel,
- private val quickAffordancesLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) {
val hostToken: IBinder? = bundle.getBinder(KEY_HOST_TOKEN)
private val width: Int = bundle.getInt(KEY_VIEW_WIDTH)
@@ -458,13 +453,10 @@
keyguardRootView.findViewById<LaunchableImageView?>(R.id.start_button)?.let { imageView ->
shortcutsBindings.add(
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
view = imageView,
viewModel = quickAffordancesCombinedViewModel.startButton,
alpha = flowOf(1f),
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
- logger = quickAffordancesLogger,
) { message ->
indicationController.showTransientIndication(message)
}
@@ -473,13 +465,10 @@
keyguardRootView.findViewById<LaunchableImageView?>(R.id.end_button)?.let { imageView ->
shortcutsBindings.add(
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
view = imageView,
viewModel = quickAffordancesCombinedViewModel.endButton,
alpha = flowOf(1f),
- falsingManager = falsingManager,
- vibratorHelper = vibratorHelper,
- logger = quickAffordancesLogger,
) { message ->
indicationController.showTransientIndication(message)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
index 2dc9301..bf6f2c4 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/KeyguardBlueprintModule.kt
@@ -42,12 +42,6 @@
): KeyguardBlueprint
@Binds
- @IntoSet
- abstract fun bindShortcutsBesideUdfpsLockscreenBlueprint(
- shortcutsBesideUdfpsLockscreenBlueprint: ShortcutsBesideUdfpsKeyguardBlueprint
- ): KeyguardBlueprint
-
- @Binds
@IntoMap
@ClassKey(KeyguardBlueprintInteractor::class)
abstract fun bindsKeyguardBlueprintInteractor(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
deleted file mode 100644
index b984a68..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/blueprints/ShortcutsBesideUdfpsKeyguardBlueprint.kt
+++ /dev/null
@@ -1,94 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.view.layout.blueprints
-
-import com.android.systemui.communal.ui.view.layout.sections.CommunalTutorialIndicatorSection
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.keyguard.shared.model.KeyguardBlueprint
-import com.android.systemui.keyguard.shared.model.KeyguardSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AccessibilityActionsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AlignShortcutsToUdfpsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AodBurnInSection
-import com.android.systemui.keyguard.ui.view.layout.sections.AodNotificationIconsSection
-import com.android.systemui.keyguard.ui.view.layout.sections.ClockSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultDeviceEntrySection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultIndicationAreaSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultNotificationStackScrollLayoutSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultSettingsPopupMenuSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusBarSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultStatusViewSection
-import com.android.systemui.keyguard.ui.view.layout.sections.DefaultUdfpsAccessibilityOverlaySection
-import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSectionsModule
-import com.android.systemui.keyguard.ui.view.layout.sections.KeyguardSliceViewSection
-import com.android.systemui.keyguard.ui.view.layout.sections.SmartspaceSection
-import com.android.systemui.util.kotlin.getOrNull
-import java.util.Optional
-import javax.inject.Inject
-import javax.inject.Named
-import kotlinx.coroutines.ExperimentalCoroutinesApi
-
-/** Vertically aligns the shortcuts with the udfps. */
-@ExperimentalCoroutinesApi
-@SysUISingleton
-class ShortcutsBesideUdfpsKeyguardBlueprint
-@Inject
-constructor(
- accessibilityActionsSection: AccessibilityActionsSection,
- alignShortcutsToUdfpsSection: AlignShortcutsToUdfpsSection,
- defaultIndicationAreaSection: DefaultIndicationAreaSection,
- defaultDeviceEntrySection: DefaultDeviceEntrySection,
- @Named(KeyguardSectionsModule.KEYGUARD_AMBIENT_INDICATION_AREA_SECTION)
- defaultAmbientIndicationAreaSection: Optional<KeyguardSection>,
- defaultSettingsPopupMenuSection: DefaultSettingsPopupMenuSection,
- defaultStatusViewSection: DefaultStatusViewSection,
- defaultStatusBarSection: DefaultStatusBarSection,
- defaultNotificationStackScrollLayoutSection: DefaultNotificationStackScrollLayoutSection,
- aodNotificationIconsSection: AodNotificationIconsSection,
- aodBurnInSection: AodBurnInSection,
- communalTutorialIndicatorSection: CommunalTutorialIndicatorSection,
- clockSection: ClockSection,
- smartspaceSection: SmartspaceSection,
- keyguardSliceViewSection: KeyguardSliceViewSection,
- udfpsAccessibilityOverlaySection: DefaultUdfpsAccessibilityOverlaySection,
-) : KeyguardBlueprint {
- override val id: String = SHORTCUTS_BESIDE_UDFPS
-
- override val sections =
- listOfNotNull(
- accessibilityActionsSection,
- defaultIndicationAreaSection,
- alignShortcutsToUdfpsSection,
- defaultAmbientIndicationAreaSection.getOrNull(),
- defaultSettingsPopupMenuSection,
- defaultStatusViewSection,
- defaultStatusBarSection,
- defaultNotificationStackScrollLayoutSection,
- aodNotificationIconsSection,
- smartspaceSection,
- aodBurnInSection,
- communalTutorialIndicatorSection,
- clockSection,
- keyguardSliceViewSection,
- defaultDeviceEntrySection,
- udfpsAccessibilityOverlaySection, // Add LAST: Intentionally has z-order above others
- )
-
- companion object {
- const val SHORTCUTS_BESIDE_UDFPS = "shortcuts-besides-udfps"
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
deleted file mode 100644
index 1ba830b..0000000
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/AlignShortcutsToUdfpsSection.kt
+++ /dev/null
@@ -1,114 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- *
- */
-
-package com.android.systemui.keyguard.ui.view.layout.sections
-
-import android.content.res.Resources
-import androidx.constraintlayout.widget.ConstraintLayout
-import androidx.constraintlayout.widget.ConstraintSet
-import androidx.constraintlayout.widget.ConstraintSet.BOTTOM
-import androidx.constraintlayout.widget.ConstraintSet.LEFT
-import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
-import androidx.constraintlayout.widget.ConstraintSet.RIGHT
-import androidx.constraintlayout.widget.ConstraintSet.TOP
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
-import com.android.systemui.dagger.qualifiers.Main
-import com.android.systemui.deviceentry.shared.DeviceEntryUdfpsRefactor
-import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
-import com.android.systemui.keyguard.ui.binder.KeyguardQuickAffordanceViewBinder
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
-import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.plugins.FalsingManager
-import com.android.systemui.res.R
-import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
-import javax.inject.Inject
-
-class AlignShortcutsToUdfpsSection
-@Inject
-constructor(
- @Main private val resources: Resources,
- private val keyguardQuickAffordancesCombinedViewModel:
- KeyguardQuickAffordancesCombinedViewModel,
- private val keyguardRootViewModel: KeyguardRootViewModel,
- private val falsingManager: FalsingManager,
- private val indicationController: KeyguardIndicationController,
- private val vibratorHelper: VibratorHelper,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
-) : BaseShortcutSection() {
- override fun addViews(constraintLayout: ConstraintLayout) {
- if (KeyguardBottomAreaRefactor.isEnabled) {
- addLeftShortcut(constraintLayout)
- addRightShortcut(constraintLayout)
- }
- }
-
- override fun bindData(constraintLayout: ConstraintLayout) {
- if (KeyguardBottomAreaRefactor.isEnabled) {
- leftShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
- constraintLayout.requireViewById(R.id.start_button),
- keyguardQuickAffordancesCombinedViewModel.startButton,
- keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
- ) {
- indicationController.showTransientIndication(it)
- }
- rightShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
- constraintLayout.requireViewById(R.id.end_button),
- keyguardQuickAffordancesCombinedViewModel.endButton,
- keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
- ) {
- indicationController.showTransientIndication(it)
- }
- }
- }
-
- override fun applyConstraints(constraintSet: ConstraintSet) {
- val width = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_width)
- val height = resources.getDimensionPixelSize(R.dimen.keyguard_affordance_fixed_height)
-
- val lockIconViewId =
- if (DeviceEntryUdfpsRefactor.isEnabled) {
- R.id.device_entry_icon_view
- } else {
- R.id.lock_icon_view
- }
-
- constraintSet.apply {
- constrainWidth(R.id.start_button, width)
- constrainHeight(R.id.start_button, height)
- connect(R.id.start_button, LEFT, PARENT_ID, LEFT)
- connect(R.id.start_button, RIGHT, lockIconViewId, LEFT)
- connect(R.id.start_button, TOP, lockIconViewId, TOP)
- connect(R.id.start_button, BOTTOM, lockIconViewId, BOTTOM)
-
- constrainWidth(R.id.end_button, width)
- constrainHeight(R.id.end_button, height)
- connect(R.id.end_button, RIGHT, PARENT_ID, RIGHT)
- connect(R.id.end_button, LEFT, lockIconViewId, RIGHT)
- connect(R.id.end_button, TOP, lockIconViewId, TOP)
- connect(R.id.end_button, BOTTOM, lockIconViewId, BOTTOM)
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
index 64c46db..e558033 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/view/layout/sections/DefaultShortcutsSection.kt
@@ -26,7 +26,6 @@
import androidx.constraintlayout.widget.ConstraintSet.PARENT_ID
import androidx.constraintlayout.widget.ConstraintSet.RIGHT
import androidx.constraintlayout.widget.ConstraintSet.VISIBILITY_MODE_IGNORE
-import com.android.keyguard.logging.KeyguardQuickAffordancesLogger
import com.android.systemui.animation.view.LaunchableImageView
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.keyguard.KeyguardBottomAreaRefactor
@@ -35,10 +34,8 @@
import com.android.systemui.keyguard.ui.view.layout.blueprints.transitions.IntraBlueprintTransition
import com.android.systemui.keyguard.ui.viewmodel.KeyguardQuickAffordancesCombinedViewModel
import com.android.systemui.keyguard.ui.viewmodel.KeyguardRootViewModel
-import com.android.systemui.plugins.FalsingManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.KeyguardIndicationController
-import com.android.systemui.statusbar.VibratorHelper
import dagger.Lazy
import javax.inject.Inject
@@ -49,11 +46,9 @@
private val keyguardQuickAffordancesCombinedViewModel:
KeyguardQuickAffordancesCombinedViewModel,
private val keyguardRootViewModel: KeyguardRootViewModel,
- private val falsingManager: FalsingManager,
private val indicationController: KeyguardIndicationController,
- private val vibratorHelper: VibratorHelper,
private val keyguardBlueprintInteractor: Lazy<KeyguardBlueprintInteractor>,
- private val shortcutsLogger: KeyguardQuickAffordancesLogger,
+ private val keyguardQuickAffordanceViewBinder: KeyguardQuickAffordanceViewBinder,
) : BaseShortcutSection() {
// Amount to increase the bottom margin by to avoid colliding with inset
@@ -82,24 +77,18 @@
override fun bindData(constraintLayout: ConstraintLayout) {
if (KeyguardBottomAreaRefactor.isEnabled) {
leftShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.start_button),
keyguardQuickAffordancesCombinedViewModel.startButton,
keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
rightShortcutHandle =
- KeyguardQuickAffordanceViewBinder.bind(
+ keyguardQuickAffordanceViewBinder.bind(
constraintLayout.requireViewById(R.id.end_button),
keyguardQuickAffordancesCombinedViewModel.endButton,
keyguardQuickAffordancesCombinedViewModel.transitionAlpha,
- falsingManager,
- vibratorHelper,
- shortcutsLogger,
) {
indicationController.showTransientIndication(it)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
index c590f07..992550c 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerToAodTransitionViewModel.kt
@@ -16,6 +16,7 @@
package com.android.systemui.keyguard.ui.viewmodel
+import android.util.MathUtils
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.FromAlternateBouncerTransitionInteractor
@@ -47,6 +48,15 @@
edge = Edge.create(from = ALTERNATE_BOUNCER, to = AOD),
)
+ fun lockscreenAlpha(viewState: ViewStateAccessor): Flow<Float> {
+ var startAlpha = 1f
+ return transitionAnimation.sharedFlow(
+ duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
+ onStart = { startAlpha = viewState.alpha() },
+ onStep = { MathUtils.lerp(startAlpha, 1f, it) },
+ )
+ }
+
val deviceEntryBackgroundViewAlpha: Flow<Float> =
transitionAnimation.sharedFlow(
duration = FromAlternateBouncerTransitionInteractor.TO_AOD_DURATION,
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
index df0b3dc..4908dbd 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerUdfpsIconViewModel.kt
@@ -23,6 +23,7 @@
import com.android.systemui.common.ui.domain.interactor.ConfigurationInteractor
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.ui.view.DeviceEntryIconView
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shared.recents.utilities.Utilities.clamp
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
@@ -50,6 +51,7 @@
private val isSupported: Flow<Boolean> = deviceEntryUdfpsInteractor.isUdfpsSupported
val alpha: Flow<Float> =
alternateBouncerViewModel.transitionToAlternateBouncerProgress.map {
+ SceneContainerFlag.assertInLegacyMode()
clamp(it * 2f, 0f, 1f)
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
index 5a559fc..7b0b23f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModel.kt
@@ -18,15 +18,20 @@
package com.android.systemui.keyguard.ui.viewmodel
import android.graphics.Color
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
+import com.android.systemui.keyguard.DismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
import com.android.systemui.keyguard.shared.model.KeyguardState.ALTERNATE_BOUNCER
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.statusbar.phone.StatusBarKeyguardViewManager
+import dagger.Lazy
import javax.inject.Inject
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.flow.map
+import kotlinx.coroutines.flow.onEach
@ExperimentalCoroutinesApi
class AlternateBouncerViewModel
@@ -34,16 +39,19 @@
constructor(
private val statusBarKeyguardViewManager: StatusBarKeyguardViewManager,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
+ private val dismissCallbackRegistry: DismissCallbackRegistry,
+ alternateBouncerInteractor: Lazy<AlternateBouncerInteractor>,
) {
// When we're fully transitioned to the AlternateBouncer, the alpha of the scrim should be:
private val alternateBouncerScrimAlpha = .66f
- /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */
- val transitionToAlternateBouncerProgress =
- keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER)
+ /** Reports the alternate bouncer visible state if the scene container flag is enabled. */
+ val isVisible: Flow<Boolean> =
+ alternateBouncerInteractor.get().isVisible.onEach { SceneContainerFlag.assertInNewMode() }
- val forcePluginOpen: Flow<Boolean> =
- transitionToAlternateBouncerProgress.map { it > 0f }.distinctUntilChanged()
+ /** Progress to a fully transitioned alternate bouncer. 1f represents fully transitioned. */
+ val transitionToAlternateBouncerProgress: Flow<Float> =
+ keyguardTransitionInteractor.transitionValue(ALTERNATE_BOUNCER)
/** An observable for the scrim alpha. */
val scrimAlpha = transitionToAlternateBouncerProgress.map { it * alternateBouncerScrimAlpha }
@@ -54,11 +62,16 @@
val registerForDismissGestures: Flow<Boolean> =
transitionToAlternateBouncerProgress.map { it == 1f }.distinctUntilChanged()
- fun showPrimaryBouncer() {
+ fun onTapped() {
statusBarKeyguardViewManager.showPrimaryBouncer(/* scrimmed */ true)
}
- fun hideAlternateBouncer() {
+ fun onRemovedFromWindow() {
statusBarKeyguardViewManager.hideAlternateBouncer(false)
}
+
+ fun onBackRequested() {
+ statusBarKeyguardViewManager.hideAlternateBouncer(false)
+ dismissCallbackRegistry.notifyDismissCancelled()
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
index 680f966..2426f97 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModel.kt
@@ -19,6 +19,7 @@
import androidx.annotation.VisibleForTesting
import com.android.app.tracing.FlowTracing.traceEmissionCount
+import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardQuickAffordanceInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardTransitionInteractor
@@ -29,19 +30,23 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shared.keyguard.shared.model.KeyguardQuickAffordanceSlots
import javax.inject.Inject
+import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.flatMapLatest
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.merge
+import kotlinx.coroutines.flow.stateIn
@OptIn(ExperimentalCoroutinesApi::class)
class KeyguardQuickAffordancesCombinedViewModel
@Inject
constructor(
+ @Application private val applicationScope: CoroutineScope,
private val quickAffordanceInteractor: KeyguardQuickAffordanceInteractor,
private val keyguardInteractor: KeyguardInteractor,
shadeInteractor: ShadeInteractor,
@@ -133,9 +138,14 @@
/** The source of truth of alpha for all of the quick affordances on lockscreen */
val transitionAlpha: Flow<Float> =
merge(
- fadeInAlpha,
- fadeOutAlpha,
- )
+ fadeInAlpha,
+ fadeOutAlpha,
+ )
+ .stateIn(
+ scope = applicationScope,
+ started = SharingStarted.WhileSubscribed(),
+ initialValue = 0f,
+ )
/**
* Whether quick affordances are "opaque enough" to be considered visible to and interactive by
@@ -199,38 +209,42 @@
private fun button(
position: KeyguardQuickAffordancePosition
): Flow<KeyguardQuickAffordanceViewModel> {
- return previewMode.flatMapLatest { previewMode ->
- combine(
- if (previewMode.isInPreviewMode) {
- quickAffordanceInteractor.quickAffordanceAlwaysVisible(position = position)
- } else {
- quickAffordanceInteractor.quickAffordance(position = position)
- },
- keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
- areQuickAffordancesFullyOpaque,
- selectedPreviewSlotId,
- quickAffordanceInteractor.useLongPress(),
- ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
- val slotId = position.toSlotId()
- val isSelected = selectedPreviewSlotId == slotId
- model.toViewModel(
- animateReveal = !previewMode.isInPreviewMode && animateReveal,
- isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
- isSelected =
- previewMode.isInPreviewMode &&
- previewMode.shouldHighlightSelectedAffordance &&
- isSelected,
- isDimmed =
- previewMode.isInPreviewMode &&
- previewMode.shouldHighlightSelectedAffordance &&
- !isSelected,
- forceInactive = previewMode.isInPreviewMode,
- slotId = slotId,
- useLongPress = useLongPress,
- )
- }
- .distinctUntilChanged()
- }.traceEmissionCount({"QuickAfforcances#button${position.toSlotId()}"})
+ return previewMode
+ .flatMapLatest { previewMode ->
+ combine(
+ if (previewMode.isInPreviewMode) {
+ quickAffordanceInteractor.quickAffordanceAlwaysVisible(
+ position = position
+ )
+ } else {
+ quickAffordanceInteractor.quickAffordance(position = position)
+ },
+ keyguardInteractor.animateDozingTransitions.distinctUntilChanged(),
+ areQuickAffordancesFullyOpaque,
+ selectedPreviewSlotId,
+ quickAffordanceInteractor.useLongPress(),
+ ) { model, animateReveal, isFullyOpaque, selectedPreviewSlotId, useLongPress ->
+ val slotId = position.toSlotId()
+ val isSelected = selectedPreviewSlotId == slotId
+ model.toViewModel(
+ animateReveal = !previewMode.isInPreviewMode && animateReveal,
+ isClickable = isFullyOpaque && !previewMode.isInPreviewMode,
+ isSelected =
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ isSelected,
+ isDimmed =
+ previewMode.isInPreviewMode &&
+ previewMode.shouldHighlightSelectedAffordance &&
+ !isSelected,
+ forceInactive = previewMode.isInPreviewMode,
+ slotId = slotId,
+ useLongPress = useLongPress,
+ )
+ }
+ .distinctUntilChanged()
+ }
+ .traceEmissionCount({ "QuickAfforcances#button${position.toSlotId()}" })
}
private fun KeyguardQuickAffordanceModel.toViewModel(
diff --git a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
index 38a2b1b..050ef6f 100644
--- a/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModel.kt
@@ -83,6 +83,7 @@
private val communalInteractor: CommunalInteractor,
private val keyguardTransitionInteractor: KeyguardTransitionInteractor,
private val notificationsKeyguardInteractor: NotificationsKeyguardInteractor,
+ private val alternateBouncerToAodTransitionViewModel: AlternateBouncerToAodTransitionViewModel,
private val alternateBouncerToGoneTransitionViewModel:
AlternateBouncerToGoneTransitionViewModel,
private val alternateBouncerToLockscreenTransitionViewModel:
@@ -239,6 +240,7 @@
merge(
alphaOnShadeExpansion,
keyguardInteractor.dismissAlpha,
+ alternateBouncerToAodTransitionViewModel.lockscreenAlpha(viewState),
alternateBouncerToGoneTransitionViewModel.lockscreenAlpha(viewState),
alternateBouncerToLockscreenTransitionViewModel.lockscreenAlpha(viewState),
aodToGoneTransitionViewModel.lockscreenAlpha(viewState),
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
index 661da6d..c2b5d98 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/RepeatWhenAttached.kt
@@ -227,13 +227,33 @@
}
/**
+ * Runs the given [block] in a new coroutine when `this` [View]'s Window's [WindowLifecycleState] is
+ * at least at [state] (or immediately after calling this function if the window is already at least
+ * at [state]), automatically canceling the work when the window is no longer at least at that
+ * state.
+ *
+ * [block] may be run multiple times, running once per every time this` [View]'s Window's
+ * [WindowLifecycleState] becomes at least at [state].
+ */
+suspend fun View.repeatOnWindowLifecycle(
+ state: WindowLifecycleState,
+ block: suspend CoroutineScope.() -> Unit,
+): Nothing {
+ when (state) {
+ WindowLifecycleState.ATTACHED -> repeatWhenAttachedToWindow(block)
+ WindowLifecycleState.VISIBLE -> repeatWhenWindowIsVisible(block)
+ WindowLifecycleState.FOCUSED -> repeatWhenWindowHasFocus(block)
+ }
+}
+
+/**
* Runs the given [block] every time the [View] becomes attached (or immediately after calling this
* function, if the view was already attached), automatically canceling the work when the view
* becomes detached.
*
* Only use from the main thread.
*
- * The [block] may be run multiple times, running once per every time the view is attached.
+ * [block] may be run multiple times, running once per every time the view is attached.
*/
@MainThread
suspend fun View.repeatWhenAttachedToWindow(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -249,7 +269,7 @@
*
* Only use from the main thread.
*
- * The [block] may be run multiple times, running once per every time the window becomes visible.
+ * [block] may be run multiple times, running once per every time the window becomes visible.
*/
@MainThread
suspend fun View.repeatWhenWindowIsVisible(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -265,7 +285,7 @@
*
* Only use from the main thread.
*
- * The [block] may be run multiple times, running once per every time the window is focused.
+ * [block] may be run multiple times, running once per every time the window is focused.
*/
@MainThread
suspend fun View.repeatWhenWindowHasFocus(block: suspend CoroutineScope.() -> Unit): Nothing {
@@ -274,6 +294,21 @@
awaitCancellation() // satisfies return type of Nothing
}
+/** Lifecycle states for a [View]'s interaction with a [android.view.Window]. */
+enum class WindowLifecycleState {
+ /** Indicates that the [View] is attached to a [android.view.Window]. */
+ ATTACHED,
+ /**
+ * Indicates that the [View] is attached to a [android.view.Window], and the window is visible.
+ */
+ VISIBLE,
+ /**
+ * Indicates that the [View] is attached to a [android.view.Window], and the window is visible
+ * and focused.
+ */
+ FOCUSED
+}
+
private val View.isAttached
get() = conflatedCallbackFlow {
val onAttachListener =
diff --git a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
index 0af5fea..7731481 100644
--- a/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/lifecycle/SysUiViewModel.kt
@@ -16,9 +16,10 @@
package com.android.systemui.lifecycle
+import android.view.View
import androidx.compose.runtime.Composable
-import androidx.compose.runtime.LaunchedEffect
-import androidx.compose.runtime.remember
+import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.launch
/** Base class for all System UI view-models. */
abstract class SysUiViewModel : SafeActivatable() {
@@ -37,8 +38,20 @@
fun <T : SysUiViewModel> rememberViewModel(
key: Any = Unit,
factory: () -> T,
-): T {
- val instance = remember(key) { factory() }
- LaunchedEffect(instance) { instance.activate() }
- return instance
-}
+): T = rememberActivated(key, factory)
+
+/**
+ * Invokes [block] in a new coroutine with a new [SysUiViewModel] that is automatically activated
+ * whenever `this` [View]'s Window's [WindowLifecycleState] is at least at
+ * [minWindowLifecycleState], and is automatically canceled once that is no longer the case.
+ */
+suspend fun <T : SysUiViewModel> View.viewModel(
+ minWindowLifecycleState: WindowLifecycleState,
+ factory: () -> T,
+ block: suspend CoroutineScope.(T) -> Unit,
+): Nothing =
+ repeatOnWindowLifecycle(minWindowLifecycleState) {
+ val instance = factory()
+ launch { instance.activate() }
+ block(instance)
+ }
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
index 46aa064..2ce7044 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewController.kt
@@ -27,7 +27,6 @@
import android.window.RemoteTransition
import androidx.recyclerview.widget.LinearLayoutManager
import androidx.recyclerview.widget.RecyclerView
-import com.android.systemui.Flags.pssAppSelectorAbruptExitFix
import com.android.systemui.Flags.pssAppSelectorRecentsSplitScreen
import com.android.systemui.display.naturalBounds
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -160,7 +159,7 @@
private fun createAnimation(task: RecentTask, view: View): ActivityOptions =
- if (pssAppSelectorAbruptExitFix() && task.isForegroundTask) {
+ if (task.isForegroundTask) {
// When the selected task is in the foreground, the scale up animation doesn't work.
// We fallback to the default close animation.
ActivityOptions.makeCustomTaskAnimation(
diff --git a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt
index 8bf2202..4b132d0 100644
--- a/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegate.kt
@@ -48,6 +48,7 @@
appName,
hostUid,
mediaProjectionMetricsLogger,
+ dialogIconDrawable = R.drawable.ic_present_to_all,
) {
override fun onCreate(dialog: AlertDialog, savedInstanceState: Bundle?) {
super.onCreate(dialog, savedInstanceState)
@@ -83,22 +84,28 @@
listOf(
ScreenShareOption(
mode = SINGLE_APP,
- spinnerText = R.string.screen_share_permission_dialog_option_single_app,
+ spinnerText =
+ R.string
+ .media_projection_entry_app_permission_dialog_option_text_single_app,
warningText =
R.string
.media_projection_entry_app_permission_dialog_warning_single_app,
startButtonText =
- R.string.media_projection_entry_app_permission_dialog_continue,
+ R.string
+ .media_projection_entry_generic_permission_dialog_continue_single_app,
spinnerDisabledText = singleAppDisabledText,
),
ScreenShareOption(
mode = ENTIRE_SCREEN,
- spinnerText = R.string.screen_share_permission_dialog_option_entire_screen,
+ spinnerText =
+ R.string
+ .media_projection_entry_app_permission_dialog_option_text_entire_screen,
warningText =
R.string
.media_projection_entry_app_permission_dialog_warning_entire_screen,
startButtonText =
- R.string.media_projection_entry_app_permission_dialog_continue,
+ R.string
+ .media_projection_entry_app_permission_dialog_continue_entire_screen,
)
)
return if (singleAppDisabledText != null) {
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
index e2ba761..a8b979e0 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/NavigationBarModule.java
@@ -16,11 +16,16 @@
package com.android.systemui.navigationbar;
+import static com.android.systemui.Flags.enableViewCaptureTracing;
+import static com.android.systemui.util.ConvenienceExtensionsKt.toKotlinLazy;
+
import android.content.Context;
import android.view.LayoutInflater;
import android.view.View;
import android.view.WindowManager;
+import com.android.app.viewcapture.ViewCapture;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.systemui.dagger.qualifiers.DisplayId;
import com.android.systemui.navigationbar.NavigationBarComponent.NavigationBarScope;
import com.android.systemui.navigationbar.gestural.EdgeBackGestureHandler;
@@ -28,6 +33,7 @@
import com.android.systemui.navigationbar.views.NavigationBarView;
import com.android.systemui.res.R;
+import dagger.Lazy;
import dagger.Module;
import dagger.Provides;
@@ -73,4 +79,15 @@
static WindowManager provideWindowManager(@DisplayId Context context) {
return context.getSystemService(WindowManager.class);
}
+
+ /** A ViewCaptureAwareWindowManager specific to the display's context. */
+ @Provides
+ @NavigationBarScope
+ @DisplayId
+ static ViewCaptureAwareWindowManager provideViewCaptureAwareWindowManager(
+ @DisplayId WindowManager windowManager, Lazy<ViewCapture> daggerLazyViewCapture) {
+ return new ViewCaptureAwareWindowManager(windowManager,
+ /* lazyViewCapture= */ toKotlinLazy(daggerLazyViewCapture),
+ /* isViewCaptureEnabled= */ enableViewCaptureTracing());
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
index 7b248eb..e895d83 100644
--- a/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
+++ b/packages/SystemUI/src/com/android/systemui/navigationbar/views/NavigationBar.java
@@ -102,6 +102,7 @@
import androidx.annotation.Nullable;
import androidx.annotation.VisibleForTesting;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.internal.accessibility.dialog.AccessibilityButtonChooserActivity;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEvent;
@@ -196,6 +197,7 @@
private final Context mContext;
private final Bundle mSavedState;
private final WindowManager mWindowManager;
+ private final ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
private final AccessibilityManager mAccessibilityManager;
private final DeviceProvisionedController mDeviceProvisionedController;
private final StatusBarStateController mStatusBarStateController;
@@ -556,6 +558,7 @@
@Nullable Bundle savedState,
@DisplayId Context context,
@DisplayId WindowManager windowManager,
+ @DisplayId ViewCaptureAwareWindowManager viewCaptureAwareWindowManager,
Lazy<AssistManager> assistManagerLazy,
AccessibilityManager accessibilityManager,
DeviceProvisionedController deviceProvisionedController,
@@ -601,6 +604,7 @@
mContext = context;
mSavedState = savedState;
mWindowManager = windowManager;
+ mViewCaptureAwareWindowManager = viewCaptureAwareWindowManager;
mAccessibilityManager = accessibilityManager;
mDeviceProvisionedController = deviceProvisionedController;
mStatusBarStateController = statusBarStateController;
@@ -721,7 +725,7 @@
if (DEBUG) Log.v(TAG, "addNavigationBar: about to add " + mView);
- mWindowManager.addView(mFrame,
+ mViewCaptureAwareWindowManager.addView(mFrame,
getBarLayoutParams(mContext.getResources().getConfiguration().windowConfiguration
.getRotation()));
mDisplayId = mContext.getDisplayId();
@@ -764,7 +768,7 @@
mCommandQueue.removeCallback(this);
Trace.beginSection("NavigationBar#removeViewImmediate");
try {
- mWindowManager.removeViewImmediate(mView.getRootView());
+ mViewCaptureAwareWindowManager.removeViewImmediate(mView.getRootView());
} finally {
Trace.endSection();
}
@@ -866,7 +870,7 @@
if (mOrientationHandle != null) {
resetSecondaryHandle();
getBarTransitions().removeDarkIntensityListener(mOrientationHandleIntensityListener);
- mWindowManager.removeView(mOrientationHandle);
+ mViewCaptureAwareWindowManager.removeView(mOrientationHandle);
mOrientationHandle.getViewTreeObserver().removeOnGlobalLayoutListener(
mOrientationHandleGlobalLayoutListener);
}
@@ -937,7 +941,7 @@
mOrientationParams.setTitle("SecondaryHomeHandle" + mContext.getDisplayId());
mOrientationParams.privateFlags |= PRIVATE_FLAG_NO_MOVE_ANIMATION
| WindowManager.LayoutParams.PRIVATE_FLAG_LAYOUT_SIZE_EXTENDED_BY_CUTOUT;
- mWindowManager.addView(mOrientationHandle, mOrientationParams);
+ mViewCaptureAwareWindowManager.addView(mOrientationHandle, mOrientationParams);
mOrientationHandle.setVisibility(View.GONE);
logNavbarOrientation("initSecondaryHomeHandleForRotation");
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt
similarity index 64%
rename from packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
rename to packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt
index fa8e13a..2d2b869 100644
--- a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneActionsViewModel.kt
@@ -20,23 +20,27 @@
import com.android.compose.animation.scene.Swipe
import com.android.compose.animation.scene.UserAction
import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.shade.shared.model.ShadeAlignment
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.flowOf
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
-/** Models UI state and handles user input for the Notifications Shade scene. */
-@SysUISingleton
-class NotificationsShadeSceneViewModel
-@Inject
+/**
+ * Models the UI state for the user actions that the user can perform to navigate to other scenes.
+ *
+ * Different from the [NotificationsShadeSceneContentViewModel] which models the _content_ of the
+ * scene.
+ */
+class NotificationsShadeSceneActionsViewModel
+@AssistedInject
constructor(
- shadeInteractor: ShadeInteractor,
-) {
- val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- flowOf(
+ private val shadeInteractor: ShadeInteractor,
+) : SceneActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ setActions(
mapOf(
if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
Swipe.Up
@@ -46,4 +50,10 @@
Back to SceneFamilies.Home,
)
)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): NotificationsShadeSceneActionsViewModel
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.kt
new file mode 100644
index 0000000..c1c7320
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/notifications/ui/viewmodel/NotificationsShadeSceneContentViewModel.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.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.notifications.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.shade.ui.viewmodel.BaseShadeSceneViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * Models UI state used to render the content of the notifications shade scene.
+ *
+ * Different from [NotificationsShadeSceneActionsViewModel], which only models user actions that can
+ * be performed to navigate to other scenes.
+ */
+class NotificationsShadeSceneContentViewModel
+@AssistedInject
+constructor(
+ deviceEntryInteractor: DeviceEntryInteractor,
+ sceneInteractor: SceneInteractor,
+) :
+ BaseShadeSceneViewModel(
+ deviceEntryInteractor,
+ sceneInteractor,
+ ) {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): NotificationsShadeSceneContentViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
index 1b34c33..89be17b 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
+++ b/packages/SystemUI/src/com/android/systemui/qs/UserSettingObserver.java
@@ -19,6 +19,7 @@
import android.database.ContentObserver;
import android.os.Handler;
+import com.android.systemui.Flags;
import com.android.systemui.statusbar.policy.Listenable;
import com.android.systemui.util.settings.SecureSettings;
import com.android.systemui.util.settings.SystemSettings;
@@ -76,10 +77,20 @@
mListening = listening;
if (listening) {
mObservedValue = getValueFromProvider();
- mSettingsProxy.registerContentObserverForUserSync(
- mSettingsProxy.getUriFor(mSettingName), false, this, mUserId);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.registerContentObserverForUserAsync(
+ mSettingsProxy.getUriFor(mSettingName), this, mUserId, () ->
+ mObservedValue = getValueFromProvider());
+ } else {
+ mSettingsProxy.registerContentObserverForUserSync(
+ mSettingsProxy.getUriFor(mSettingName), false, this, mUserId);
+ }
} else {
- mSettingsProxy.unregisterContentObserverSync(this);
+ if (Flags.qsRegisterSettingObserverOnBgThread()) {
+ mSettingsProxy.unregisterContentObserverAsync(this);
+ } else {
+ mSettingsProxy.unregisterContentObserverSync(this);
+ }
mObservedValue = mDefaultValue;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt
index 9b8dba1..9fb1d46 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/airplane/domain/AirplaneModeMapper.kt
@@ -37,19 +37,22 @@
override fun map(config: QSTileConfig, data: AirplaneModeTileModel): QSTileState =
QSTileState.build(resources, theme, config.uiConfig) {
- val icon =
+ iconRes =
+ if (data.isEnabled) {
+ R.drawable.qs_airplane_icon_on
+ } else {
+ R.drawable.qs_airplane_icon_off
+ }
+
+ icon = {
Icon.Loaded(
resources.getDrawable(
- if (data.isEnabled) {
- R.drawable.qs_airplane_icon_on
- } else {
- R.drawable.qs_airplane_icon_off
- },
+ iconRes!!,
theme,
),
contentDescription = null
)
- this.icon = { icon }
+ }
if (data.isEnabled) {
activationState = QSTileState.ActivationState.ACTIVE
secondaryLabel = resources.getStringArray(R.array.tile_states_airplane)[2]
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt
index 875079c..984228d 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/custom/domain/CustomTileMapper.kt
@@ -41,16 +41,25 @@
) : QSTileDataToStateMapper<CustomTileDataModel> {
override fun map(config: QSTileConfig, data: CustomTileDataModel): QSTileState {
- val userContext = context.createContextAsUser(UserHandle(data.user.identifier), 0)
+ val userContext =
+ try {
+ context.createContextAsUser(UserHandle(data.user.identifier), 0)
+ } catch (exception: IllegalStateException) {
+ null
+ }
val iconResult =
- getIconProvider(
- userContext = userContext,
- icon = data.tile.icon,
- callingAppUid = data.callingAppUid,
- packageName = data.componentName.packageName,
- defaultIcon = data.defaultTileIcon,
- )
+ if (userContext != null) {
+ getIconProvider(
+ userContext = userContext,
+ icon = data.tile.icon,
+ callingAppUid = data.callingAppUid,
+ packageName = data.componentName.packageName,
+ defaultIcon = data.defaultTileIcon,
+ )
+ } else {
+ IconResult({ null }, true)
+ }
return QSTileState.build(iconResult.iconProvider, data.tile.label) {
var tileState: Int = data.tile.state
@@ -61,7 +70,7 @@
icon = iconResult.iconProvider
activationState =
if (iconResult.failedToLoad) {
- QSTileState.ActivationState.INACTIVE
+ QSTileState.ActivationState.UNAVAILABLE
} else {
QSTileState.ActivationState.valueOf(tileState)
}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
index eec5d3d..204ead3 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/tiles/impl/internet/domain/interactor/InternetTileDataInteractor.kt
@@ -79,6 +79,7 @@
flowOf(
InternetTileModel.Active(
secondaryTitle = secondary,
+ iconId = wifiIcon.icon.res,
icon = Icon.Loaded(context.getDrawable(wifiIcon.icon.res)!!, null),
stateDescription = wifiIcon.contentDescription,
contentDescription = ContentDescription.Loaded("$internetLabel,$secondary"),
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
index b1cc55d..7258882 100644
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsSceneViewModel.kt
@@ -34,7 +34,6 @@
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
import com.android.systemui.shade.ui.viewmodel.ShadeHeaderViewModel
-import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationsPlaceholderViewModel
import java.util.concurrent.atomic.AtomicBoolean
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
@@ -48,10 +47,9 @@
class QuickSettingsSceneViewModel
@Inject
constructor(
- val brightnessMirrorViewModel: BrightnessMirrorViewModel,
- val shadeHeaderViewModel: ShadeHeaderViewModel,
+ val brightnessMirrorViewModelFactory: BrightnessMirrorViewModel.Factory,
+ val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
val qsSceneAdapter: QSSceneAdapter,
- val notifications: NotificationsPlaceholderViewModel,
private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
private val footerActionsController: FooterActionsController,
sceneBackInteractor: SceneBackInteractor,
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt
new file mode 100644
index 0000000..d2967b8
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModel.kt
@@ -0,0 +1,72 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.qs.ui.viewmodel
+
+import com.android.compose.animation.scene.Back
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeAlignment
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.map
+
+/**
+ * Models the UI state for the user actions that the user can perform to navigate to other scenes.
+ *
+ * Different from the [QuickSettingsShadeSceneContentViewModel] which models the _content_ of the
+ * scene.
+ */
+class QuickSettingsShadeSceneActionsViewModel
+@AssistedInject
+constructor(
+ private val shadeInteractor: ShadeInteractor,
+ val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
+) : SceneActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ quickSettingsContainerViewModel.editModeViewModel.isEditing
+ .map { editing ->
+ buildMap {
+ put(
+ if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
+ Swipe.Up
+ } else {
+ Swipe.Down
+ },
+ UserActionResult(SceneFamilies.Home)
+ )
+ if (!editing) {
+ put(Back, UserActionResult(SceneFamilies.Home))
+ }
+ }
+ }
+ .collectLatest { actions -> setActions(actions) }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): QuickSettingsShadeSceneActionsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt
new file mode 100644
index 0000000..abfca4b
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModel.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.qs.ui.viewmodel
+
+import com.android.systemui.lifecycle.SysUiViewModel
+import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+/**
+ * Models UI state used to render the content of the quick settings shade scene.
+ *
+ * Different from [QuickSettingsShadeSceneActionsViewModel], which only models user actions that can
+ * be performed to navigate to other scenes.
+ */
+class QuickSettingsShadeSceneContentViewModel
+@AssistedInject
+constructor(
+ val overlayShadeViewModelFactory: OverlayShadeViewModel.Factory,
+ val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
+) : SysUiViewModel() {
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): QuickSettingsShadeSceneContentViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
deleted file mode 100644
index e012f2c..0000000
--- a/packages/SystemUI/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModel.kt
+++ /dev/null
@@ -1,58 +0,0 @@
-/*
- * Copyright (C) 2024 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.qs.ui.viewmodel
-
-import com.android.compose.animation.scene.Back
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.model.ShadeAlignment
-import com.android.systemui.shade.ui.viewmodel.OverlayShadeViewModel
-import javax.inject.Inject
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.map
-
-/** Models UI state and handles user input for the Quick Settings Shade scene. */
-@SysUISingleton
-class QuickSettingsShadeSceneViewModel
-@Inject
-constructor(
- private val shadeInteractor: ShadeInteractor,
- val overlayShadeViewModel: OverlayShadeViewModel,
- val quickSettingsContainerViewModel: QuickSettingsContainerViewModel,
-) {
-
- val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- quickSettingsContainerViewModel.editModeViewModel.isEditing.map { editing ->
- buildMap {
- put(
- if (shadeInteractor.shadeAlignment == ShadeAlignment.Top) {
- Swipe.Up
- } else {
- Swipe.Down
- },
- UserActionResult(SceneFamilies.Home)
- )
- if (!editing) {
- put(Back, UserActionResult(SceneFamilies.Home))
- }
- }
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
index 98a61df..863a899 100644
--- a/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
+++ b/packages/SystemUI/src/com/android/systemui/recordissue/IssueRecordingService.kt
@@ -24,6 +24,7 @@
import android.net.Uri
import android.os.Handler
import android.os.UserHandle
+import android.util.Log
import com.android.internal.logging.UiEventLogger
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.dagger.qualifiers.LongRunning
@@ -71,6 +72,7 @@
override fun provideRecordingServiceStrings(): RecordingServiceStrings = IrsStrings(resources)
override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int {
+ Log.d(getTag(), "handling action: ${intent?.action}")
when (intent?.action) {
ACTION_START -> {
bgExecutor.execute {
@@ -95,7 +97,7 @@
bgExecutor.execute {
mNotificationManager.cancelAsUser(
null,
- mNotificationId,
+ intent.getIntExtra(EXTRA_NOTIFICATION_ID, mNotificationId),
UserHandle(mUserContextTracker.userContext.userId)
)
diff --git a/packages/SystemUI/src/com/android/systemui/scene/OWNERS b/packages/SystemUI/src/com/android/systemui/scene/OWNERS
new file mode 100644
index 0000000..2ffcad4
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/scene/OWNERS
@@ -0,0 +1,13 @@
+set noparent
+
+# Bug component: 1215786
+
+justinweir@google.com
+nijamkin@google.com
+
+# SysUI Dr No's.
+# Don't send reviews here.
+cinek@google.com
+dsandler@android.com
+juliacr@google.com
+pixel@google.com
diff --git a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
index 5b50133..3ec088c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/domain/startable/SceneContainerStartable.kt
@@ -25,6 +25,7 @@
import com.android.systemui.CoreStartable
import com.android.systemui.authentication.domain.interactor.AuthenticationInteractor
import com.android.systemui.authentication.shared.model.AuthenticationMethodModel
+import com.android.systemui.bouncer.domain.interactor.AlternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.BouncerInteractor
import com.android.systemui.bouncer.domain.interactor.SimBouncerInteractor
import com.android.systemui.bouncer.shared.logging.BouncerUiEvent
@@ -132,6 +133,7 @@
private val keyguardEnabledInteractor: KeyguardEnabledInteractor,
private val dismissCallbackRegistry: DismissCallbackRegistry,
private val statusBarStateController: SysuiStatusBarStateController,
+ private val alternateBouncerInteractor: AlternateBouncerInteractor,
) : CoreStartable {
private val centralSurfaces: CentralSurfaces?
get() = centralSurfacesOptLazy.get().getOrNull()
@@ -152,6 +154,7 @@
handleKeyguardEnabledness()
notifyKeyguardDismissCallbacks()
refreshLockscreenEnabled()
+ handleHideAlternateBouncerOnTransitionToGone()
} else {
sceneLogger.logFrameworkEnabled(
isEnabled = false,
@@ -228,13 +231,16 @@
},
headsUpInteractor.isHeadsUpOrAnimatingAway,
occlusionInteractor.invisibleDueToOcclusion,
+ alternateBouncerInteractor.isVisible,
) {
visibilityForTransitionState,
isHeadsUpOrAnimatingAway,
invisibleDueToOcclusion,
+ isAlternateBouncerVisible,
->
when {
isHeadsUpOrAnimatingAway -> true to "showing a HUN"
+ isAlternateBouncerVisible -> true to "showing alternate bouncer"
invisibleDueToOcclusion -> false to "invisible due to occlusion"
else -> visibilityForTransitionState
}
@@ -351,7 +357,9 @@
)
}
val isOnLockscreen = renderedScenes.contains(Scenes.Lockscreen)
- val isOnBouncer = renderedScenes.contains(Scenes.Bouncer)
+ val isOnBouncer =
+ renderedScenes.contains(Scenes.Bouncer) ||
+ alternateBouncerInteractor.isVisibleState()
if (!deviceUnlockStatus.isUnlocked) {
return@mapNotNull if (isOnLockscreen || isOnBouncer) {
// Already on lockscreen or bouncer, no need to change scenes.
@@ -379,12 +387,13 @@
!statusBarStateController.leaveOpenOnKeyguardHide()
) {
Scenes.Gone to
- "device was unlocked in Bouncer scene and shade" +
+ "device was unlocked with bouncer showing and shade" +
" didn't need to be left open"
} else {
val prevScene = previousScene.value
(prevScene ?: Scenes.Gone) to
- "device was unlocked in Bouncer scene, from sceneKey=$prevScene"
+ "device was unlocked with bouncer showing," +
+ " from sceneKey=$prevScene"
}
isOnLockscreen ->
// The lockscreen should be dismissed automatically in 2 scenarios:
@@ -776,4 +785,14 @@
.collectLatest { deviceEntryInteractor.refreshLockscreenEnabled() }
}
}
+
+ private fun handleHideAlternateBouncerOnTransitionToGone() {
+ applicationScope.launch {
+ sceneInteractor.transitionState
+ .map { it.isIdle(Scenes.Gone) }
+ .distinctUntilChanged()
+ .filter { it }
+ .collectLatest { alternateBouncerInteractor.hide() }
+ }
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
index bccbb11..f6924f2 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootView.kt
@@ -5,15 +5,18 @@
import android.view.MotionEvent
import android.view.View
import android.view.WindowInsets
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.scene.shared.model.Scene
import com.android.systemui.scene.shared.model.SceneContainerConfig
import com.android.systemui.scene.shared.model.SceneDataSourceDelegator
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.shade.TouchLogger
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.MutableStateFlow
/** A root view of the main SysUI window that supports scenes. */
+@ExperimentalCoroutinesApi
class SceneWindowRootView(
context: Context,
attrs: AttributeSet?,
@@ -35,6 +38,7 @@
scenes: Set<Scene>,
layoutInsetController: LayoutInsetsController,
sceneDataSourceDelegator: SceneDataSourceDelegator,
+ alternateBouncerDependencies: AlternateBouncerDependencies,
) {
this.viewModel = viewModel
setLayoutInsetsController(layoutInsetController)
@@ -49,6 +53,7 @@
super.setVisibility(if (isVisible) View.VISIBLE else View.INVISIBLE)
},
dataSourceDelegator = sceneDataSourceDelegator,
+ alternateBouncerDependencies = alternateBouncerDependencies,
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
index d31d6f4..73a8e4c 100644
--- a/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/scene/ui/view/SceneWindowRootViewBinder.kt
@@ -37,6 +37,8 @@
import com.android.systemui.common.ui.compose.windowinsets.CutoutLocation
import com.android.systemui.common.ui.compose.windowinsets.DisplayCutout
import com.android.systemui.common.ui.compose.windowinsets.ScreenDecorProvider
+import com.android.systemui.keyguard.ui.composable.AlternateBouncer
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.lifecycle.repeatWhenAttached
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -48,12 +50,14 @@
import com.android.systemui.scene.ui.viewmodel.SceneContainerViewModel
import com.android.systemui.statusbar.notification.stack.ui.view.SharedNotificationContainer
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
+@ExperimentalCoroutinesApi
object SceneWindowRootViewBinder {
/** Binds between the view and view-model pertaining to a specific scene container. */
@@ -66,6 +70,7 @@
scenes: Set<Scene>,
onVisibilityChangedInternal: (isVisible: Boolean) -> Unit,
dataSourceDelegator: SceneDataSourceDelegator,
+ alternateBouncerDependencies: AlternateBouncerDependencies,
) {
val unsortedSceneByKey: Map<SceneKey, Scene> = scenes.associateBy { scene -> scene.key }
val sortedSceneByKey: Map<SceneKey, Scene> = buildMap {
@@ -120,6 +125,14 @@
sharedNotificationContainer
)
view.addView(sharedNotificationContainer)
+
+ // TODO (b/358354906): use an overlay for the alternate bouncer
+ view.addView(
+ createAlternateBouncerView(
+ context = view.context,
+ alternateBouncerDependencies = alternateBouncerDependencies,
+ )
+ )
}
launch {
@@ -164,6 +177,19 @@
}
}
+ private fun createAlternateBouncerView(
+ context: Context,
+ alternateBouncerDependencies: AlternateBouncerDependencies,
+ ): ComposeView {
+ return ComposeView(context).apply {
+ setContent {
+ AlternateBouncer(
+ alternateBouncerDependencies = alternateBouncerDependencies,
+ )
+ }
+ }
+ }
+
// TODO(b/298525212): remove once Compose exposes window inset bounds.
private fun displayCutoutFromWindowInsets(
scope: CoroutineScope,
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
index 1170354..700253ba 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/RecordingService.java
@@ -63,7 +63,9 @@
protected static final int NOTIF_BASE_ID = 4273;
private static final String TAG = "RecordingService";
private static final String CHANNEL_ID = "screen_record";
- private static final String GROUP_KEY = "screen_record_saved";
+ @VisibleForTesting static final String GROUP_KEY_SAVED = "screen_record_saved";
+ private static final String GROUP_KEY_ERROR_STARTING = "screen_record_error_starting";
+ @VisibleForTesting static final String GROUP_KEY_ERROR_SAVING = "screen_record_error_saving";
private static final String EXTRA_RESULT_CODE = "extra_resultCode";
protected static final String EXTRA_PATH = "extra_path";
private static final String EXTRA_AUDIO_SOURCE = "extra_useAudio";
@@ -78,6 +80,7 @@
"com.android.systemui.screenrecord.STOP_FROM_NOTIF";
protected static final String ACTION_SHARE = "com.android.systemui.screenrecord.SHARE";
private static final String PERMISSION_SELF = "com.android.systemui.permission.SELF";
+ protected static final String EXTRA_NOTIFICATION_ID = "notification_id";
private final RecordingController mController;
protected final KeyguardDismissUtil mKeyguardDismissUtil;
@@ -181,7 +184,7 @@
mUiEventLogger.log(Events.ScreenRecordEvent.SCREEN_RECORD_START);
} else {
updateState(false);
- createErrorStartingNotification();
+ createErrorStartingNotification(currentUser);
stopForeground(STOP_FOREGROUND_DETACH);
stopSelf();
return Service.START_NOT_STICKY;
@@ -276,8 +279,8 @@
* errors.
*/
@VisibleForTesting
- protected void createErrorStartingNotification() {
- createErrorNotification(strings().getStartError());
+ protected void createErrorStartingNotification(UserHandle currentUser) {
+ createErrorNotification(currentUser, strings().getStartError(), GROUP_KEY_ERROR_STARTING);
}
/**
@@ -285,17 +288,22 @@
* errors.
*/
@VisibleForTesting
- protected void createErrorSavingNotification() {
- createErrorNotification(strings().getSaveError());
+ protected void createErrorSavingNotification(UserHandle currentUser) {
+ createErrorNotification(currentUser, strings().getSaveError(), GROUP_KEY_ERROR_SAVING);
}
- private void createErrorNotification(String notificationContentTitle) {
+ private void createErrorNotification(
+ UserHandle currentUser, String notificationContentTitle, String groupKey) {
+ // Make sure error notifications get their own group.
+ postGroupSummaryNotification(currentUser, notificationContentTitle, groupKey);
+
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME, strings().getTitle());
Notification.Builder builder = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
.setContentTitle(notificationContentTitle)
+ .setGroup(groupKey)
.addExtras(extras);
startForeground(mNotificationId, builder.build());
}
@@ -350,7 +358,7 @@
.setContentText(
strings().getBackgroundProcessingLabel())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setGroup(GROUP_KEY)
+ .setGroup(GROUP_KEY_SAVED)
.addExtras(extras);
return builder.build();
}
@@ -387,7 +395,7 @@
PendingIntent.FLAG_IMMUTABLE))
.addAction(shareAction)
.setAutoCancel(true)
- .setGroup(GROUP_KEY)
+ .setGroup(GROUP_KEY_SAVED)
.addExtras(extras);
// Add thumbnail if available
@@ -402,21 +410,28 @@
}
/**
- * Adds a group notification so that save notifications from multiple recordings are
- * grouped together, and the foreground service recording notification is not
+ * Adds a group summary notification for save notifications so that save notifications from
+ * multiple recordings are grouped together, and the foreground service recording notification
+ * is not.
*/
- private void postGroupNotification(UserHandle currentUser) {
+ private void postGroupSummaryNotificationForSaves(UserHandle currentUser) {
+ postGroupSummaryNotification(currentUser, strings().getSaveTitle(), GROUP_KEY_SAVED);
+ }
+
+ /** Posts a group summary notification for the given group. */
+ private void postGroupSummaryNotification(
+ UserHandle currentUser, String notificationContentTitle, String groupKey) {
Bundle extras = new Bundle();
extras.putString(Notification.EXTRA_SUBSTITUTE_APP_NAME,
strings().getTitle());
Notification groupNotif = new Notification.Builder(this, getChannelId())
.setSmallIcon(R.drawable.ic_screenrecord)
- .setContentTitle(strings().getSaveTitle())
- .setGroup(GROUP_KEY)
+ .setContentTitle(notificationContentTitle)
+ .setGroup(groupKey)
.setGroupSummary(true)
.setExtras(extras)
.build();
- mNotificationManager.notifyAsUser(getTag(), NOTIF_BASE_ID, groupNotif, currentUser);
+ mNotificationManager.notifyAsUser(getTag(), mNotificationId, groupNotif, currentUser);
}
private void stopService() {
@@ -427,6 +442,7 @@
if (userId == USER_ID_NOT_SPECIFIED) {
userId = mUserContextTracker.getUserContext().getUserId();
}
+ UserHandle currentUser = new UserHandle(userId);
Log.d(getTag(), "notifying for user " + userId);
setTapsVisible(mOriginalShowTaps);
try {
@@ -444,7 +460,7 @@
Log.e(getTag(), "stopRecording called, but there was an error when ending"
+ "recording");
exception.printStackTrace();
- createErrorSavingNotification();
+ createErrorSavingNotification(currentUser);
} catch (Throwable throwable) {
if (getRecorder() != null) {
// Something unexpected happen, SystemUI will crash but let's delete
@@ -468,7 +484,7 @@
Log.d(getTag(), "saving recording");
Notification notification = createSaveNotification(
getRecorder() != null ? getRecorder().save() : null);
- postGroupNotification(currentUser);
+ postGroupSummaryNotificationForSaves(currentUser);
mNotificationManager.notifyAsUser(null, mNotificationId, notification,
currentUser);
} catch (IOException | IllegalStateException e) {
@@ -527,7 +543,8 @@
private Intent getShareIntent(Context context, Uri path) {
return new Intent(context, this.getClass()).setAction(ACTION_SHARE)
- .putExtra(EXTRA_PATH, path);
+ .putExtra(EXTRA_PATH, path)
+ .putExtra(EXTRA_NOTIFICATION_ID, mNotificationId);
}
@Override
diff --git a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
index b54bf6c..46ac54f 100644
--- a/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
+++ b/packages/SystemUI/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegate.kt
@@ -270,15 +270,18 @@
return listOf(
ScreenShareOption(
SINGLE_APP,
- R.string.screen_share_permission_dialog_option_single_app,
+ R.string.screenrecord_permission_dialog_option_text_single_app,
R.string.screenrecord_permission_dialog_warning_single_app,
- startButtonText = R.string.screenrecord_permission_dialog_continue,
+ startButtonText =
+ R.string
+ .media_projection_entry_generic_permission_dialog_continue_single_app,
),
ScreenShareOption(
ENTIRE_SCREEN,
- R.string.screen_share_permission_dialog_option_entire_screen,
+ R.string.screenrecord_permission_dialog_option_text_entire_screen,
R.string.screenrecord_permission_dialog_warning_entire_screen,
- startButtonText = R.string.screenrecord_permission_dialog_continue,
+ startButtonText =
+ R.string.screenrecord_permission_dialog_continue_entire_screen,
)
)
}
diff --git a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
index 79e8b87..7f8c146 100644
--- a/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/settings/brightness/ui/viewModel/BrightnessMirrorViewModel.kt
@@ -19,25 +19,25 @@
import android.content.res.Resources
import android.util.Log
import android.view.View
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.res.R
import com.android.systemui.settings.brightness.BrightnessSliderController
import com.android.systemui.settings.brightness.MirrorController
import com.android.systemui.settings.brightness.ToggleSlider
import com.android.systemui.settings.brightness.domain.interactor.BrightnessMirrorShowingInteractor
-import javax.inject.Inject
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
-@SysUISingleton
class BrightnessMirrorViewModel
-@Inject
+@AssistedInject
constructor(
private val brightnessMirrorShowingInteractor: BrightnessMirrorShowingInteractor,
@Main private val resources: Resources,
val sliderControllerFactory: BrightnessSliderController.Factory,
-) : MirrorController {
+) : SysUiViewModel(), MirrorController {
private val tempPosition = IntArray(2)
@@ -99,6 +99,11 @@
override fun removeCallback(listener: MirrorController.BrightnessMirrorListener) {}
+ @AssistedFactory
+ interface Factory {
+ fun create(): BrightnessMirrorViewModel
+ }
+
companion object {
private const val TAG = "BrightnessMirrorViewModel"
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
index 05c50fe..15bbef0 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/GlanceableHubContainerController.kt
@@ -293,7 +293,7 @@
)
containerView.systemGestureExclusionRects =
- if (Flags.hubmodeFullscreenVerticalSwipe()) {
+ if (Flags.hubmodeFullscreenVerticalSwipeFix()) {
listOf(
// Disable back gestures on the left side of the screen, to avoid
// conflicting with scene transitions.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
index 0a092a0..16aef65 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/shade/QuickSettingsControllerImpl.java
@@ -2252,8 +2252,11 @@
// panel, mQs will not need to be null cause it will be tied to the same lifecycle.
if (fragment == mQs) {
// Clear it to remove bindings to mQs from the provider.
- mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null);
- mNotificationStackScrollLayoutController.setQsHeader(null);
+ if (QSComposeFragment.isEnabled()) {
+ mNotificationStackScrollLayoutController.setQsHeaderBoundsProvider(null);
+ } else {
+ mNotificationStackScrollLayoutController.setQsHeader(null);
+ }
mQs = null;
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
index bc23778..21bbaa5 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ShadeViewProviderModule.kt
@@ -31,6 +31,7 @@
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.flags.FeatureFlags
import com.android.systemui.keyguard.ui.view.KeyguardRootView
+import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.res.R
import com.android.systemui.scene.shared.flag.SceneContainerFlag
@@ -83,6 +84,7 @@
scenesProvider: Provider<Set<@JvmSuppressWildcards Scene>>,
layoutInsetController: NotificationInsetsController,
sceneDataSourceDelegator: Provider<SceneDataSourceDelegator>,
+ alternateBouncerDependencies: Provider<AlternateBouncerDependencies>,
): WindowRootView {
return if (SceneContainerFlag.isEnabled) {
checkNoSceneDuplicates(scenesProvider.get())
@@ -96,6 +98,7 @@
scenes = scenesProvider.get(),
layoutInsetController = layoutInsetController,
sceneDataSourceDelegator = sceneDataSourceDelegator.get(),
+ alternateBouncerDependencies = alternateBouncerDependencies.get(),
)
sceneWindowRootView
} else {
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
index 684a484..d64b21f 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorImpl.kt
@@ -55,7 +55,7 @@
keyguardRepository: KeyguardRepository,
keyguardTransitionInteractor: KeyguardTransitionInteractor,
powerInteractor: PowerInteractor,
- shadeRepository: ShadeRepository,
+ private val shadeRepository: ShadeRepository,
userSetupRepository: UserSetupRepository,
userSwitcherInteractor: UserSwitcherInteractor,
private val baseShadeInteractor: BaseShadeInteractor,
@@ -114,11 +114,13 @@
initialValue = determineShadeMode(isShadeLayoutWide.value)
)
- override val shadeAlignment: ShadeAlignment =
- if (shadeRepository.isDualShadeAlignedToBottom) {
- ShadeAlignment.Bottom
- } else {
- ShadeAlignment.Top
+ override val shadeAlignment: ShadeAlignment
+ get() {
+ return if (shadeRepository.isDualShadeAlignedToBottom) {
+ ShadeAlignment.Bottom
+ } else {
+ ShadeAlignment.Top
+ }
}
override val isExpandToQsEnabled: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
index 9e221d3..f48e31e 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorLegacyImpl.kt
@@ -21,6 +21,7 @@
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.keyguard.data.repository.KeyguardRepository
import com.android.systemui.keyguard.shared.model.StatusBarState
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
import javax.inject.Inject
@@ -46,6 +47,10 @@
sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
repository: ShadeRepository,
) : BaseShadeInteractor {
+ init {
+ SceneContainerFlag.assertInLegacyMode()
+ }
+
/**
* The amount [0-1] that the shade has been opened. Uses stateIn to avoid redundant calculations
* in downstream flows.
diff --git a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
index 9617b54..6a21531 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/domain/interactor/ShadeInteractorSceneContainerImpl.kt
@@ -22,8 +22,9 @@
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Application
import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.statusbar.notification.stack.domain.interactor.SharedNotificationContainerInteractor
+import com.android.systemui.shade.data.repository.ShadeRepository
import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
@@ -43,8 +44,12 @@
constructor(
@Application scope: CoroutineScope,
sceneInteractor: SceneInteractor,
- sharedNotificationContainerInteractor: SharedNotificationContainerInteractor,
+ shadeRepository: ShadeRepository,
) : BaseShadeInteractor {
+ init {
+ SceneContainerFlag.assertInNewMode()
+ }
+
override val shadeExpansion: StateFlow<Float> =
sceneBasedExpansion(sceneInteractor, SceneFamilies.NotifShade)
.traceAsCounter("panel_expansion") { (it * 100f).toInt() }
@@ -55,7 +60,7 @@
override val qsExpansion: StateFlow<Float> =
combine(
- sharedNotificationContainerInteractor.isSplitShadeEnabled,
+ shadeRepository.isShadeLayoutWide,
shadeExpansion,
sceneBasedQsExpansion,
) { isSplitShadeEnabled, shadeExpansion, qsExpansion ->
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt
new file mode 100644
index 0000000..068d6a7
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/BaseShadeSceneViewModel.kt
@@ -0,0 +1,56 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.lifecycle.SysUiViewModel
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.scene.shared.model.Scenes
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.MutableStateFlow
+import kotlinx.coroutines.flow.StateFlow
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
+
+/** Base class for classes that model UI state of the content of shade scenes. */
+abstract class BaseShadeSceneViewModel(
+ private val deviceEntryInteractor: DeviceEntryInteractor,
+ private val sceneInteractor: SceneInteractor,
+) : SysUiViewModel() {
+
+ private val _isEmptySpaceClickable =
+ MutableStateFlow(!deviceEntryInteractor.isDeviceEntered.value)
+ /** Whether clicking on the empty area of the shade does something */
+ val isEmptySpaceClickable: StateFlow<Boolean> = _isEmptySpaceClickable.asStateFlow()
+
+ override suspend fun onActivated() {
+ deviceEntryInteractor.isDeviceEntered.collectLatest { isDeviceEntered ->
+ _isEmptySpaceClickable.value = !isDeviceEntered
+ }
+ }
+
+ /** Notifies that the empty space in the shade has been clicked. */
+ fun onEmptySpaceClicked() {
+ if (!isEmptySpaceClickable.value) {
+ return
+ }
+
+ sceneInteractor.changeScene(Scenes.Lockscreen, "Shade empty space clicked.")
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
index 6551854..566bc16 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModel.kt
@@ -17,43 +17,39 @@
package com.android.systemui.shade.ui.viewmodel
import com.android.compose.animation.scene.SceneKey
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.model.SceneFamilies
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.SharingStarted
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.stateIn
+import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
/**
* Models UI state and handles user input for the overlay shade UI, which shows a shade as an
* overlay on top of another scene UI.
*/
-@SysUISingleton
class OverlayShadeViewModel
-@Inject
-constructor(
- @Application applicationScope: CoroutineScope,
- private val sceneInteractor: SceneInteractor,
- shadeInteractor: ShadeInteractor
-) {
+@AssistedInject
+constructor(private val sceneInteractor: SceneInteractor, shadeInteractor: ShadeInteractor) :
+ SysUiViewModel() {
+ private val _backgroundScene = MutableStateFlow(Scenes.Lockscreen)
/** The scene to show in the background when the overlay shade is open. */
- val backgroundScene: StateFlow<SceneKey> =
- sceneInteractor
- .resolveSceneFamily(SceneFamilies.Home)
- .stateIn(
- scope = applicationScope,
- started = SharingStarted.WhileSubscribed(),
- initialValue = Scenes.Lockscreen,
- )
+ val backgroundScene: StateFlow<SceneKey> = _backgroundScene.asStateFlow()
/** Dictates the alignment of the overlay shade panel on the screen. */
val panelAlignment = shadeInteractor.shadeAlignment
+ override suspend fun onActivated() {
+ sceneInteractor.resolveSceneFamily(SceneFamilies.Home).collectLatest { sceneKey ->
+ _backgroundScene.value = sceneKey
+ }
+ }
+
/** Notifies that the user has clicked the semi-transparent background scrim. */
fun onScrimClicked() {
sceneInteractor.changeScene(
@@ -61,4 +57,9 @@
loggingReason = "Shade scrim clicked",
)
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): OverlayShadeViewModel
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
index b2e0cd0..03fdfa9 100644
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModel.kt
@@ -24,8 +24,7 @@
import android.os.UserHandle
import android.provider.Settings
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.plugins.ActivityStarter
import com.android.systemui.privacy.OngoingPrivacyChip
import com.android.systemui.privacy.PrivacyItem
@@ -38,44 +37,40 @@
import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.pipeline.mobile.domain.interactor.MobileIconsInteractor
import com.android.systemui.statusbar.pipeline.mobile.ui.viewmodel.MobileIconsViewModel
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import java.util.Date
import java.util.Locale
-import javax.inject.Inject
-import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.asStateFlow
+import kotlinx.coroutines.flow.collectLatest
import kotlinx.coroutines.flow.launchIn
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
-import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
/** Models UI state for the shade header. */
-@SysUISingleton
class ShadeHeaderViewModel
-@Inject
+@AssistedInject
constructor(
- @Application private val applicationScope: CoroutineScope,
- context: Context,
+ private val context: Context,
private val activityStarter: ActivityStarter,
private val sceneInteractor: SceneInteractor,
- shadeInteractor: ShadeInteractor,
- mobileIconsInteractor: MobileIconsInteractor,
+ private val shadeInteractor: ShadeInteractor,
+ private val mobileIconsInteractor: MobileIconsInteractor,
val mobileIconsViewModel: MobileIconsViewModel,
private val privacyChipInteractor: PrivacyChipInteractor,
private val clockInteractor: ShadeHeaderClockInteractor,
- broadcastDispatcher: BroadcastDispatcher,
-) {
+ private val broadcastDispatcher: BroadcastDispatcher,
+) : SysUiViewModel() {
/** True if there is exactly one mobile connection. */
val isSingleCarrier: StateFlow<Boolean> = mobileIconsInteractor.isSingleCarrier
+ private val _mobileSubIds = MutableStateFlow(emptyList<Int>())
/** The list of subscription Ids for current mobile connections. */
- val mobileSubIds =
- mobileIconsInteractor.filteredSubscriptions
- .map { list -> list.map { it.subscriptionId } }
- .stateIn(applicationScope, SharingStarted.WhileSubscribed(), emptyList())
+ val mobileSubIds: StateFlow<List<Int>> = _mobileSubIds.asStateFlow()
/** The list of PrivacyItems to be displayed by the privacy chip. */
val privacyItems: StateFlow<List<PrivacyItem>> = privacyChipInteractor.privacyItems
@@ -94,11 +89,9 @@
/** Whether or not the privacy chip is enabled in the device privacy config. */
val isPrivacyChipEnabled: StateFlow<Boolean> = privacyChipInteractor.isChipEnabled
+ private val _isDisabled = MutableStateFlow(false)
/** Whether or not the Shade Header should be disabled based on disableFlags. */
- val isDisabled: StateFlow<Boolean> =
- shadeInteractor.isQsEnabled
- .map { !it }
- .stateIn(applicationScope, SharingStarted.WhileSubscribed(), false)
+ val isDisabled: StateFlow<Boolean> = _isDisabled.asStateFlow()
private val longerPattern = context.getString(R.string.abbrev_wday_month_day_no_year_alarm)
private val shorterPattern = context.getString(R.string.abbrev_month_day_no_year)
@@ -111,26 +104,40 @@
private val _longerDateText: MutableStateFlow<String> = MutableStateFlow("")
val longerDateText: StateFlow<String> = _longerDateText.asStateFlow()
- init {
- broadcastDispatcher
- .broadcastFlow(
- filter =
- IntentFilter().apply {
- addAction(Intent.ACTION_TIME_TICK)
- addAction(Intent.ACTION_TIME_CHANGED)
- addAction(Intent.ACTION_TIMEZONE_CHANGED)
- addAction(Intent.ACTION_LOCALE_CHANGED)
- },
- user = UserHandle.SYSTEM,
- map = { intent, _ ->
- intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
- intent.action == Intent.ACTION_LOCALE_CHANGED
- }
- )
- .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
- .launchIn(applicationScope)
+ override suspend fun onActivated() {
+ coroutineScope {
+ launch {
+ broadcastDispatcher
+ .broadcastFlow(
+ filter =
+ IntentFilter().apply {
+ addAction(Intent.ACTION_TIME_TICK)
+ addAction(Intent.ACTION_TIME_CHANGED)
+ addAction(Intent.ACTION_TIMEZONE_CHANGED)
+ addAction(Intent.ACTION_LOCALE_CHANGED)
+ },
+ user = UserHandle.SYSTEM,
+ map = { intent, _ ->
+ intent.action == Intent.ACTION_TIMEZONE_CHANGED ||
+ intent.action == Intent.ACTION_LOCALE_CHANGED
+ }
+ )
+ .onEach { invalidateFormats -> updateDateTexts(invalidateFormats) }
+ .launchIn(this)
+ }
- applicationScope.launch { updateDateTexts(false) }
+ launch { updateDateTexts(false) }
+
+ launch {
+ mobileIconsInteractor.filteredSubscriptions
+ .map { list -> list.map { it.subscriptionId } }
+ .collectLatest { _mobileSubIds.value = it }
+ }
+
+ launch {
+ shadeInteractor.isQsEnabled.map { !it }.collectLatest { _isDisabled.value = it }
+ }
+ }
}
/** Notifies that the privacy chip was clicked. */
@@ -182,4 +189,9 @@
format.setContext(DisplayContext.CAPITALIZATION_FOR_BEGINNING_OF_SENTENCE)
return format
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): ShadeHeaderViewModel
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt
new file mode 100644
index 0000000..bdc0fdb
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModel.kt
@@ -0,0 +1,77 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.compose.animation.scene.Swipe
+import com.android.compose.animation.scene.SwipeDirection
+import com.android.compose.animation.scene.UserAction
+import com.android.compose.animation.scene.UserActionResult
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.scene.shared.model.SceneFamilies
+import com.android.systemui.scene.shared.model.Scenes
+import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
+import com.android.systemui.scene.ui.viewmodel.SceneActionsViewModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import kotlinx.coroutines.flow.collectLatest
+import kotlinx.coroutines.flow.combine
+
+/**
+ * Models the UI state for the user actions that the user can perform to navigate to other scenes.
+ *
+ * Different from the [ShadeSceneContentViewModel] which models the _content_ of the scene.
+ */
+class ShadeSceneActionsViewModel
+@AssistedInject
+constructor(
+ private val qsSceneAdapter: QSSceneAdapter,
+ private val shadeInteractor: ShadeInteractor,
+) : SceneActionsViewModel() {
+
+ override suspend fun hydrateActions(setActions: (Map<UserAction, UserActionResult>) -> Unit) {
+ combine(
+ shadeInteractor.shadeMode,
+ qsSceneAdapter.isCustomizerShowing,
+ ) { shadeMode, isCustomizerShowing ->
+ buildMap<UserAction, UserActionResult> {
+ if (!isCustomizerShowing) {
+ set(
+ Swipe(SwipeDirection.Up),
+ UserActionResult(
+ SceneFamilies.Home,
+ ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
+ )
+ )
+ }
+
+ // TODO(b/330200163) Add an else to be able to collapse the shade while
+ // customizing
+ if (shadeMode is ShadeMode.Single) {
+ set(Swipe(SwipeDirection.Down), UserActionResult(Scenes.QuickSettings))
+ }
+ }
+ }
+ .collectLatest { actions -> setActions(actions) }
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): ShadeSceneActionsViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt
new file mode 100644
index 0000000..3cdff96
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModel.kt
@@ -0,0 +1,89 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.shade.ui.viewmodel
+
+import androidx.lifecycle.LifecycleOwner
+import com.android.systemui.deviceentry.domain.interactor.DeviceEntryInteractor
+import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
+import com.android.systemui.qs.FooterActionsController
+import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
+import com.android.systemui.qs.ui.adapter.QSSceneAdapter
+import com.android.systemui.scene.domain.interactor.SceneInteractor
+import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
+import com.android.systemui.shade.shared.model.ShadeMode
+import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
+import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.flow.StateFlow
+
+/**
+ * Models UI state used to render the content of the shade scene.
+ *
+ * Different from [ShadeSceneActionsViewModel], which only models user actions that can be performed
+ * to navigate to other scenes.
+ */
+class ShadeSceneContentViewModel
+@AssistedInject
+constructor(
+ val qsSceneAdapter: QSSceneAdapter,
+ val shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory,
+ val brightnessMirrorViewModelFactory: BrightnessMirrorViewModel.Factory,
+ val mediaCarouselInteractor: MediaCarouselInteractor,
+ shadeInteractor: ShadeInteractor,
+ private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
+ private val footerActionsController: FooterActionsController,
+ private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
+ deviceEntryInteractor: DeviceEntryInteractor,
+ sceneInteractor: SceneInteractor,
+) :
+ BaseShadeSceneViewModel(
+ deviceEntryInteractor,
+ sceneInteractor,
+ ) {
+
+ val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
+
+ val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation
+
+ private val footerActionsControllerInitialized = AtomicBoolean(false)
+
+ /**
+ * Amount of X-axis translation to apply to various elements as the unfolded foldable is folded
+ * slightly, in pixels.
+ */
+ fun unfoldTranslationX(isOnStartSide: Boolean): Flow<Float> {
+ return unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide)
+ }
+
+ fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
+ if (footerActionsControllerInitialized.compareAndSet(false, true)) {
+ footerActionsController.init()
+ }
+ return footerActionsViewModelFactory.create(lifecycleOwner)
+ }
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): ShadeSceneContentViewModel
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt b/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
deleted file mode 100644
index 06298ef..0000000
--- a/packages/SystemUI/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModel.kt
+++ /dev/null
@@ -1,152 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.ui.viewmodel
-
-import androidx.lifecycle.LifecycleOwner
-import com.android.compose.animation.scene.SceneKey
-import com.android.compose.animation.scene.Swipe
-import com.android.compose.animation.scene.SwipeDirection
-import com.android.compose.animation.scene.UserAction
-import com.android.compose.animation.scene.UserActionResult
-import com.android.systemui.dagger.SysUISingleton
-import com.android.systemui.lifecycle.Activatable
-import com.android.systemui.media.controls.domain.pipeline.interactor.MediaCarouselInteractor
-import com.android.systemui.qs.FooterActionsController
-import com.android.systemui.qs.footer.ui.viewmodel.FooterActionsViewModel
-import com.android.systemui.qs.ui.adapter.QSSceneAdapter
-import com.android.systemui.scene.domain.interactor.SceneInteractor
-import com.android.systemui.scene.shared.model.SceneFamilies
-import com.android.systemui.scene.shared.model.Scenes
-import com.android.systemui.scene.shared.model.TransitionKeys.ToSplitShade
-import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
-import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.shared.model.ShadeMode
-import com.android.systemui.unfold.domain.interactor.UnfoldTransitionInteractor
-import com.android.systemui.utils.coroutines.flow.flatMapLatestConflated
-import java.util.concurrent.atomic.AtomicBoolean
-import javax.inject.Inject
-import kotlinx.coroutines.coroutineScope
-import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.MutableStateFlow
-import kotlinx.coroutines.flow.StateFlow
-import kotlinx.coroutines.flow.asStateFlow
-import kotlinx.coroutines.flow.collectLatest
-import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.flowOf
-import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.launch
-
-/** Models UI state and handles user input for the shade scene. */
-@SysUISingleton
-class ShadeSceneViewModel
-@Inject
-constructor(
- val qsSceneAdapter: QSSceneAdapter,
- val shadeHeaderViewModel: ShadeHeaderViewModel,
- val brightnessMirrorViewModel: BrightnessMirrorViewModel,
- val mediaCarouselInteractor: MediaCarouselInteractor,
- shadeInteractor: ShadeInteractor,
- private val footerActionsViewModelFactory: FooterActionsViewModel.Factory,
- private val footerActionsController: FooterActionsController,
- private val sceneInteractor: SceneInteractor,
- private val unfoldTransitionInteractor: UnfoldTransitionInteractor,
-) : Activatable {
- val destinationScenes: Flow<Map<UserAction, UserActionResult>> =
- combine(
- shadeInteractor.shadeMode,
- qsSceneAdapter.isCustomizerShowing,
- ) { shadeMode, isCustomizerShowing ->
- buildMap {
- if (!isCustomizerShowing) {
- set(
- Swipe(SwipeDirection.Up),
- UserActionResult(
- SceneFamilies.Home,
- ToSplitShade.takeIf { shadeMode is ShadeMode.Split }
- )
- )
- }
-
- // TODO(b/330200163) Add an else to be able to collapse the shade while customizing
- if (shadeMode is ShadeMode.Single) {
- set(Swipe(SwipeDirection.Down), UserActionResult(Scenes.QuickSettings))
- }
- }
- }
-
- private val upDestinationSceneKey: Flow<SceneKey?> =
- destinationScenes.map { it[Swipe(SwipeDirection.Up)]?.toScene }
-
- private val _isClickable = MutableStateFlow(false)
- /** Whether or not the shade container should be clickable. */
- val isClickable: StateFlow<Boolean> = _isClickable.asStateFlow()
-
- /**
- * Activates the view-model.
- *
- * Serves as an entrypoint to kick off coroutine work that the view-model requires in order to
- * keep its state fresh and/or perform side-effects.
- *
- * Suspends the caller forever as it will keep doing work until canceled.
- *
- * **Must be invoked** when the scene becomes the current scene or when it becomes visible
- * during a transition (the choice is the responsibility of the parent). Similarly, the work
- * must be canceled when the scene stops being visible or the current scene.
- */
- override suspend fun activate() {
- coroutineScope {
- launch {
- upDestinationSceneKey
- .flatMapLatestConflated { key ->
- key?.let { sceneInteractor.resolveSceneFamily(key) } ?: flowOf(null)
- }
- .map { it == Scenes.Lockscreen }
- .collectLatest { _isClickable.value = it }
- }
- }
- }
-
- val shadeMode: StateFlow<ShadeMode> = shadeInteractor.shadeMode
-
- val isMediaVisible: StateFlow<Boolean> = mediaCarouselInteractor.hasActiveMediaOrRecommendation
-
- /**
- * Amount of X-axis translation to apply to various elements as the unfolded foldable is folded
- * slightly, in pixels.
- */
- fun unfoldTranslationX(isOnStartSide: Boolean): Flow<Float> {
- return unfoldTransitionInteractor.unfoldTranslationX(isOnStartSide)
- }
-
- /** Notifies that some content in the shade was clicked. */
- fun onContentClicked() {
- if (!isClickable.value) {
- return
- }
-
- sceneInteractor.changeScene(Scenes.Lockscreen, "Shade empty content clicked")
- }
-
- private val footerActionsControllerInitialized = AtomicBoolean(false)
-
- fun getFooterActionsViewModel(lifecycleOwner: LifecycleOwner): FooterActionsViewModel {
- if (footerActionsControllerInitialized.compareAndSet(false, true)) {
- footerActionsController.init()
- }
- return footerActionsViewModelFactory.create(lifecycleOwner)
- }
-}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
index c1eb8bc..5eef8ea 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/KeyguardIndicationController.java
@@ -785,7 +785,8 @@
private void updateLockScreenAdaptiveAuthMsg(int userId) {
final boolean deviceLocked = mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(userId);
- if (deviceLocked) {
+ final boolean canSkipBouncer = mKeyguardUpdateMonitor.getUserCanSkipBouncer(userId);
+ if (deviceLocked && !canSkipBouncer) {
mRotateTextViewController.updateIndication(
INDICATION_TYPE_ADAPTIVE_AUTH,
new KeyguardIndication.Builder()
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
index 1cb59f1..9b382e6 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/domain/interactor/ActiveNotificationsInteractor.kt
@@ -81,11 +81,16 @@
* [CallType.Ongoing].
*/
val ongoingCallNotification: Flow<ActiveNotificationModel?> =
- allRepresentativeNotifications.map { notifMap ->
- // Once a call has started, its `whenTime` should stay the same, so we can use it as a
- // stable sort value.
- notifMap.values.filter { it.callType == CallType.Ongoing }.minByOrNull { it.whenTime }
- }
+ allRepresentativeNotifications
+ .map { notifMap ->
+ // Once a call has started, its `whenTime` should stay the same, so we can use it as
+ // a stable sort value.
+ notifMap.values
+ .filter { it.callType == CallType.Ongoing }
+ .minByOrNull { it.whenTime }
+ }
+ .distinctUntilChanged()
+ .flowOn(backgroundDispatcher)
/** Are any notifications being actively presented in the notification stack? */
val areAnyNotificationsPresent: Flow<Boolean> =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
index 2537aff..5d37476 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractor.kt
@@ -23,7 +23,9 @@
import com.android.systemui.deviceentry.domain.interactor.DeviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
import com.android.systemui.res.R
+import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.LargeScreenHeaderHelper
+import com.android.systemui.shade.domain.interactor.ShadeInteractor
import com.android.systemui.statusbar.policy.SplitShadeStateController
import dagger.Lazy
import javax.inject.Inject
@@ -43,7 +45,8 @@
constructor(
configurationRepository: ConfigurationRepository,
private val context: Context,
- private val splitShadeStateController: SplitShadeStateController,
+ private val splitShadeStateController: Lazy<SplitShadeStateController>,
+ private val shadeInteractor: Lazy<ShadeInteractor>,
keyguardInteractor: KeyguardInteractor,
deviceEntryUdfpsInteractor: DeviceEntryUdfpsInteractor,
largeScreenHeaderHelperLazy: Lazy<LargeScreenHeaderHelper>,
@@ -56,16 +59,33 @@
/** An internal modification was made to notifications */
val notificationStackChanged = _notificationStackChanged.debounce(20L)
+ private val configurationChangeEvents =
+ configurationRepository.onAnyConfigurationChange.onStart { emit(Unit) }
+
+ /* Warning: Even though the value it emits only contains the split shade status, this flow must
+ * emit a value whenever the configuration *or* the split shade status changes. Adding a
+ * distinctUntilChanged() to this would cause configurationBasedDimensions to miss configuration
+ * updates that affect other resources, like margins or the large screen header flag.
+ */
+ private val dimensionsUpdateEventsWithShouldUseSplitShade: Flow<Boolean> =
+ if (SceneContainerFlag.isEnabled) {
+ combine(configurationChangeEvents, shadeInteractor.get().isShadeLayoutWide) {
+ _,
+ isShadeLayoutWide ->
+ isShadeLayoutWide
+ }
+ } else {
+ configurationChangeEvents.map {
+ splitShadeStateController.get().shouldUseSplitNotificationShade(context.resources)
+ }
+ }
+
val configurationBasedDimensions: Flow<ConfigurationBasedDimensions> =
- configurationRepository.onAnyConfigurationChange
- .onStart { emit(Unit) }
- .map { _ ->
+ dimensionsUpdateEventsWithShouldUseSplitShade
+ .map { shouldUseSplitShade ->
with(context.resources) {
ConfigurationBasedDimensions(
- useSplitShade =
- splitShadeStateController.shouldUseSplitNotificationShade(
- context.resources
- ),
+ useSplitShade = shouldUseSplitShade,
useLargeScreenHeader =
getBoolean(R.bool.config_use_large_screen_shade_header),
marginHorizontal =
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
index fd08e89..a30b877 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/NotificationScrollViewBinder.kt
@@ -17,14 +17,14 @@
package com.android.systemui.statusbar.notification.stack.ui.viewbinder
import android.util.Log
-import androidx.lifecycle.Lifecycle
-import androidx.lifecycle.repeatOnLifecycle
import com.android.systemui.common.ui.ConfigurationState
import com.android.systemui.common.ui.view.onLayoutChanged
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dagger.qualifiers.Main
import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.WindowLifecycleState
import com.android.systemui.lifecycle.repeatWhenAttached
+import com.android.systemui.lifecycle.viewModel
import com.android.systemui.res.R
import com.android.systemui.statusbar.notification.stack.ui.view.NotificationScrollView
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationScrollViewModel
@@ -33,7 +33,6 @@
import javax.inject.Inject
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.DisposableHandle
-import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.launch
@@ -46,7 +45,7 @@
dumpManager: DumpManager,
@Main private val mainImmediateDispatcher: CoroutineDispatcher,
private val view: NotificationScrollView,
- private val viewModel: NotificationScrollViewModel,
+ private val viewModelFactory: NotificationScrollViewModel.Factory,
private val configuration: ConfigurationState,
) : FlowDumperImpl(dumpManager) {
@@ -61,38 +60,42 @@
}
fun bindWhileAttached(): DisposableHandle {
- return view.asView().repeatWhenAttached(mainImmediateDispatcher) {
- repeatOnLifecycle(Lifecycle.State.CREATED) { bind() }
- }
+ return view.asView().repeatWhenAttached(mainImmediateDispatcher) { bind() }
}
- suspend fun bind() = coroutineScope {
- launchAndDispose {
- updateViewPosition()
- view.asView().onLayoutChanged { updateViewPosition() }
- }
+ suspend fun bind(): Nothing =
+ view.asView().viewModel(
+ minWindowLifecycleState = WindowLifecycleState.ATTACHED,
+ factory = viewModelFactory::create,
+ ) { viewModel ->
+ launchAndDispose {
+ updateViewPosition()
+ view.asView().onLayoutChanged { updateViewPosition() }
+ }
- launch {
- viewModel
- .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset)
- .collect { view.setScrimClippingShape(it) }
- }
+ launch {
+ viewModel
+ .shadeScrimShape(cornerRadius = scrimRadius, viewLeftOffset = viewLeftOffset)
+ .collect { view.setScrimClippingShape(it) }
+ }
- launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } }
- launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
- launch { viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) } }
- launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
- launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
+ launch { viewModel.maxAlpha.collect { view.setMaxAlpha(it) } }
+ launch { viewModel.scrolledToTop.collect { view.setScrolledToTop(it) } }
+ launch {
+ viewModel.expandFraction.collect { view.setExpandFraction(it.coerceIn(0f, 1f)) }
+ }
+ launch { viewModel.isScrollable.collect { view.setScrollingEnabled(it) } }
+ launch { viewModel.isDozing.collect { isDozing -> view.setDozing(isDozing) } }
- launchAndDispose {
- view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
- view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
- DisposableHandle {
- view.setSyntheticScrollConsumer(null)
- view.setCurrentGestureOverscrollConsumer(null)
+ launchAndDispose {
+ view.setSyntheticScrollConsumer(viewModel.syntheticScrollConsumer)
+ view.setCurrentGestureOverscrollConsumer(viewModel.currentGestureOverscrollConsumer)
+ DisposableHandle {
+ view.setSyntheticScrollConsumer(null)
+ view.setCurrentGestureOverscrollConsumer(null)
+ }
}
}
- }
/** flow of the scrim clipping radius */
private val scrimRadius: Flow<Int>
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
index 0541550..aa1911e 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewbinder/SharedNotificationContainerBinder.kt
@@ -99,19 +99,20 @@
disposables +=
view.repeatWhenAttached(mainImmediateDispatcher) {
repeatOnLifecycle(Lifecycle.State.CREATED) {
- launch {
- // Only temporarily needed, until flexi notifs go live
- viewModel.shadeCollapseFadeIn.collect { fadeIn ->
- if (fadeIn) {
- android.animation.ValueAnimator.ofFloat(0f, 1f).apply {
- duration = 250
- addUpdateListener { animation ->
- controller.setMaxAlphaForKeyguard(
- animation.animatedFraction,
- "SharedNotificationContainerVB (collapseFadeIn)"
- )
+ if (!SceneContainerFlag.isEnabled) {
+ launch {
+ viewModel.shadeCollapseFadeIn.collect { fadeIn ->
+ if (fadeIn) {
+ android.animation.ValueAnimator.ofFloat(0f, 1f).apply {
+ duration = 250
+ addUpdateListener { animation ->
+ controller.setMaxAlphaForKeyguard(
+ animation.animatedFraction,
+ "SharedNotificationContainerVB (collapseFadeIn)"
+ )
+ }
+ start()
}
- start()
}
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
index 2ba79a8..bfb624a 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationScrollViewModel.kt
@@ -19,9 +19,9 @@
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
-import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
import com.android.systemui.keyguard.domain.interactor.KeyguardInteractor
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.SceneFamilies
@@ -33,9 +33,11 @@
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimShape
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_DELAYED_STACK_FADE_IN
import com.android.systemui.statusbar.notification.stack.ui.viewmodel.NotificationTransitionThresholds.EXPANSION_FOR_MAX_SCRIM_ALPHA
-import com.android.systemui.util.kotlin.FlowDumperImpl
+import com.android.systemui.util.kotlin.ActivatableFlowDumper
+import com.android.systemui.util.kotlin.ActivatableFlowDumperImpl
import dagger.Lazy
-import javax.inject.Inject
+import dagger.assisted.AssistedFactory
+import dagger.assisted.AssistedInject
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
@@ -43,9 +45,8 @@
import kotlinx.coroutines.flow.map
/** ViewModel which represents the state of the NSSL/Controller in the world of flexiglass */
-@SysUISingleton
class NotificationScrollViewModel
-@Inject
+@AssistedInject
constructor(
dumpManager: DumpManager,
stackAppearanceInteractor: NotificationStackAppearanceInteractor,
@@ -54,7 +55,14 @@
// TODO(b/336364825) Remove Lazy when SceneContainerFlag is released -
// while the flag is off, creating this object too early results in a crash
keyguardInteractor: Lazy<KeyguardInteractor>,
-) : FlowDumperImpl(dumpManager) {
+) :
+ ActivatableFlowDumper by ActivatableFlowDumperImpl(dumpManager, "NotificationScrollViewModel"),
+ SysUiViewModel() {
+
+ override suspend fun onActivated() {
+ activateFlowDumper()
+ }
+
/**
* The expansion fraction of the notification stack. It should go from 0 to 1 when transitioning
* from Gone to Shade scenes, and remain at 1 when in Lockscreen or Shade scenes and while
@@ -186,4 +194,9 @@
keyguardInteractor.get().isDozing.dumpWhileCollecting("isDozing")
}
}
+
+ @AssistedFactory
+ interface Factory {
+ fun create(): NotificationScrollViewModel
+ }
}
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
index db91eed..d179888 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModel.kt
@@ -22,7 +22,6 @@
import com.android.systemui.flags.Flags
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.shade.domain.interactor.ShadeInteractor
-import com.android.systemui.shade.ui.viewmodel.ShadeSceneViewModel
import com.android.systemui.statusbar.notification.domain.interactor.HeadsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.NotificationStackAppearanceInteractor
import com.android.systemui.statusbar.notification.stack.shared.model.ShadeScrimBounds
@@ -30,7 +29,6 @@
import com.android.systemui.util.kotlin.FlowDumperImpl
import javax.inject.Inject
import kotlinx.coroutines.flow.Flow
-import kotlinx.coroutines.flow.StateFlow
/**
* ViewModel used by the Notification placeholders inside the scene container to update the
@@ -43,7 +41,6 @@
dumpManager: DumpManager,
private val interactor: NotificationStackAppearanceInteractor,
shadeInteractor: ShadeInteractor,
- private val shadeSceneViewModel: ShadeSceneViewModel,
private val headsUpNotificationInteractor: HeadsUpNotificationInteractor,
featureFlags: FeatureFlagsClassic,
) : FlowDumperImpl(dumpManager) {
@@ -63,20 +60,11 @@
interactor.setConstrainedAvailableSpace(height)
}
- /** Notifies that empty space on the notification scrim has been clicked. */
- fun onEmptySpaceClicked() {
- shadeSceneViewModel.onContentClicked()
- }
-
/** Sets the content alpha for the current state of the brightness mirror */
fun setAlphaForBrightnessMirror(alpha: Float) {
interactor.setAlphaForBrightnessMirror(alpha)
}
- /** Whether or not the notification scrim should be clickable. */
- val isClickable: StateFlow<Boolean>
- get() = shadeSceneViewModel.isClickable
-
/** True when a HUN is pinned or animating away. */
val isHeadsUpOrAnimatingAway: Flow<Boolean> =
headsUpNotificationInteractor.isHeadsUpOrAnimatingAway
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
index b6d58d6..ada3c52 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/CentralSurfacesImpl.java
@@ -2353,7 +2353,11 @@
&& !mStatusBarKeyguardViewManager.primaryBouncerIsOrWillBeShowing()
&& mStatusBarKeyguardViewManager.isSecure()) {
Log.d(TAG, "showBouncerOrLockScreenIfKeyguard, showingBouncer");
- mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ if (SceneContainerFlag.isEnabled()) {
+ mStatusBarKeyguardViewManager.showPrimaryBouncer(true /* scrimmed */);
+ } else {
+ mStatusBarKeyguardViewManager.showBouncer(true /* scrimmed */);
+ }
}
}
}
@@ -2985,7 +2989,7 @@
@Override
public void onFalse() {
// Hides quick settings, bouncer, and quick-quick settings.
- mStatusBarKeyguardViewManager.reset(true);
+ mStatusBarKeyguardViewManager.reset(true, /* isFalsingReset= */true);
}
};
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
index 2d775b7..5486abb 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManager.java
@@ -708,7 +708,7 @@
* Shows the notification keyguard or the bouncer depending on
* {@link #needsFullscreenBouncer()}.
*/
- protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing) {
+ protected void showBouncerOrKeyguard(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
boolean isDozing = mDozing;
if (Flags.simPinRaceConditionOnRestart()) {
KeyguardState toState = mKeyguardTransitionInteractor.getTransitionState().getValue()
@@ -734,8 +734,11 @@
mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
}
}
- } else {
- Log.e(TAG, "Attempted to show the sim bouncer when it is already showing.");
+ } else if (!isFalsingReset) {
+ // Falsing resets can cause this to flicker, so don't reset in this case
+ Log.i(TAG, "Sim bouncer is already showing, issuing a refresh");
+ mPrimaryBouncerInteractor.show(/* isScrimmed= */ true);
+
}
} else {
mCentralSurfaces.showKeyguard();
@@ -957,6 +960,10 @@
@Override
public void reset(boolean hideBouncerWhenShowing) {
+ reset(hideBouncerWhenShowing, /* isFalsingReset= */false);
+ }
+
+ public void reset(boolean hideBouncerWhenShowing, boolean isFalsingReset) {
if (mKeyguardStateController.isShowing() && !bouncerIsAnimatingAway()) {
final boolean isOccluded = mKeyguardStateController.isOccluded();
// Hide quick settings.
@@ -965,10 +972,14 @@
if (isOccluded && !mDozing) {
mCentralSurfaces.hideKeyguard();
if (hideBouncerWhenShowing || needsFullscreenBouncer()) {
- hideBouncer(false /* destroyView */);
+ // We're removing "reset" in the refactor - bouncer will be hidden by the root
+ // cause of the "reset" calls.
+ if (!KeyguardWmStateRefactor.isEnabled()) {
+ hideBouncer(false /* destroyView */);
+ }
}
} else {
- showBouncerOrKeyguard(hideBouncerWhenShowing);
+ showBouncerOrKeyguard(hideBouncerWhenShowing, isFalsingReset);
}
if (hideBouncerWhenShowing) {
hideAlternateBouncer(true);
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
index 4368239..bd6a1c0 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/phone/ongoingcall/OngoingCallController.kt
@@ -227,6 +227,15 @@
callNotificationInfo
// This shouldn't happen, but protect against it in case
?: return OngoingCallModel.NoCall
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ bool1 = Flags.statusBarCallChipNotificationIcon()
+ bool2 = currentInfo.notificationIconView != null
+ },
+ { "Creating OngoingCallModel.InCall. notifIconFlag=$bool1 hasIcon=$bool2" }
+ )
val icon =
if (Flags.statusBarCallChipNotificationIcon()) {
currentInfo.notificationIconView
@@ -257,6 +266,7 @@
private fun updateInfoFromNotifModel(notifModel: ActiveNotificationModel?) {
if (notifModel == null) {
+ logger.log(TAG, LogLevel.DEBUG, {}, { "NotifInteractorCallModel: null" })
removeChip()
} else if (notifModel.callType != CallType.Ongoing) {
logger.log(
@@ -267,6 +277,18 @@
)
removeChip()
} else {
+ logger.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = notifModel.key
+ long1 = notifModel.whenTime
+ str1 = notifModel.callType.name
+ bool1 = notifModel.statusBarChipIconView != null
+ },
+ { "NotifInteractorCallModel: key=$str1 when=$long1 callType=$str2 hasIcon=$bool1" }
+ )
+
val newOngoingCallInfo =
CallNotificationInfo(
notifModel.key,
diff --git a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
index b7531b0..44b692f 100644
--- a/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/statusbar/policy/ui/dialog/viewmodel/ModesDialogViewModel.kt
@@ -131,7 +131,7 @@
val on = context.resources.getString(R.string.zen_mode_on)
val off = context.resources.getString(R.string.zen_mode_off)
- return mode.rule.triggerDescription ?: if (mode.isActive) on else off
+ return mode.getDynamicDescription(context) ?: if (mode.isActive) on else off
}
private fun makeZenModeDialog(): Dialog {
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt
new file mode 100644
index 0000000..f7f2631
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionKeyTutorialScreen.kt
@@ -0,0 +1,105 @@
+/*
+ * 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.composable
+
+import androidx.activity.compose.BackHandler
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.runtime.mutableStateOf
+import androidx.compose.runtime.remember
+import androidx.compose.runtime.setValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.input.key.Key
+import androidx.compose.ui.input.key.KeyEvent
+import androidx.compose.ui.input.key.KeyEventType
+import androidx.compose.ui.input.key.key
+import androidx.compose.ui.input.key.onKeyEvent
+import androidx.compose.ui.input.key.type
+import com.airbnb.lottie.compose.rememberLottieDynamicProperties
+import com.android.compose.theme.LocalAndroidColorScheme
+import com.android.systemui.res.R
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED
+
+@Composable
+fun ActionKeyTutorialScreen(
+ onDoneButtonClicked: () -> Unit,
+ onBack: () -> Unit,
+) {
+ BackHandler(onBack = onBack)
+ val screenConfig = buildScreenConfig()
+ var actionState by remember { mutableStateOf(NOT_STARTED) }
+ Box(
+ modifier =
+ Modifier.fillMaxSize().onKeyEvent { keyEvent: KeyEvent ->
+ // temporary before we can access Action/Meta key
+ if (keyEvent.key == Key.AltLeft && keyEvent.type == KeyEventType.KeyUp) {
+ actionState = FINISHED
+ }
+ true
+ }
+ ) {
+ ActionTutorialContent(actionState, onDoneButtonClicked, screenConfig)
+ }
+}
+
+@Composable
+private fun buildScreenConfig() =
+ TutorialScreenConfig(
+ colors = rememberScreenColors(),
+ strings =
+ TutorialScreenConfig.Strings(
+ titleResId = R.string.tutorial_action_key_title,
+ bodyResId = R.string.tutorial_action_key_guidance,
+ titleSuccessResId = R.string.tutorial_action_key_success_title,
+ bodySuccessResId = R.string.tutorial_action_key_success_body
+ ),
+ animations =
+ TutorialScreenConfig.Animations(
+ educationResId = R.raw.action_key_edu,
+ successResId = R.raw.action_key_success
+ )
+ )
+
+@Composable
+private fun rememberScreenColors(): TutorialScreenConfig.Colors {
+ val primaryFixedDim = LocalAndroidColorScheme.current.primaryFixedDim
+ val secondaryFixedDim = LocalAndroidColorScheme.current.secondaryFixedDim
+ val onSecondaryFixed = LocalAndroidColorScheme.current.onSecondaryFixed
+ val onSecondaryFixedVariant = LocalAndroidColorScheme.current.onSecondaryFixedVariant
+ val surfaceContainer = MaterialTheme.colorScheme.surfaceContainer
+ val dynamicProperties =
+ rememberLottieDynamicProperties(
+ rememberColorFilterProperty(".primaryFixedDim", primaryFixedDim),
+ rememberColorFilterProperty(".secondaryFixedDim", secondaryFixedDim),
+ rememberColorFilterProperty(".onSecondaryFixed", onSecondaryFixed),
+ rememberColorFilterProperty(".onSecondaryFixedVariant", onSecondaryFixedVariant)
+ )
+ val screenColors =
+ remember(surfaceContainer, dynamicProperties) {
+ TutorialScreenConfig.Colors(
+ background = onSecondaryFixed,
+ successBackground = surfaceContainer,
+ title = primaryFixedDim,
+ animationColors = dynamicProperties,
+ )
+ }
+ return screenColors
+}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt
new file mode 100644
index 0000000..2b7f674
--- /dev/null
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/ActionTutorialContent.kt
@@ -0,0 +1,237 @@
+/*
+ * 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.composable
+
+import android.graphics.ColorFilter
+import android.graphics.PorterDuff
+import android.graphics.PorterDuffColorFilter
+import androidx.annotation.RawRes
+import androidx.annotation.StringRes
+import androidx.compose.animation.AnimatedContent
+import androidx.compose.animation.EnterTransition
+import androidx.compose.animation.ExitTransition
+import androidx.compose.animation.animateColorAsState
+import androidx.compose.animation.core.LinearEasing
+import androidx.compose.animation.core.snap
+import androidx.compose.animation.core.tween
+import androidx.compose.animation.fadeIn
+import androidx.compose.animation.fadeOut
+import androidx.compose.animation.togetherWith
+import androidx.compose.foundation.layout.Arrangement
+import androidx.compose.foundation.layout.Box
+import androidx.compose.foundation.layout.Column
+import androidx.compose.foundation.layout.Row
+import androidx.compose.foundation.layout.Spacer
+import androidx.compose.foundation.layout.fillMaxSize
+import androidx.compose.foundation.layout.fillMaxWidth
+import androidx.compose.foundation.layout.height
+import androidx.compose.foundation.layout.padding
+import androidx.compose.foundation.layout.width
+import androidx.compose.material3.MaterialTheme
+import androidx.compose.material3.Text
+import androidx.compose.runtime.Composable
+import androidx.compose.runtime.getValue
+import androidx.compose.ui.Modifier
+import androidx.compose.ui.draw.drawBehind
+import androidx.compose.ui.graphics.Color
+import androidx.compose.ui.graphics.toArgb
+import androidx.compose.ui.res.stringResource
+import androidx.compose.ui.unit.dp
+import com.airbnb.lottie.LottieProperty
+import com.airbnb.lottie.compose.LottieAnimation
+import com.airbnb.lottie.compose.LottieCompositionSpec
+import com.airbnb.lottie.compose.LottieConstants
+import com.airbnb.lottie.compose.LottieDynamicProperties
+import com.airbnb.lottie.compose.LottieDynamicProperty
+import com.airbnb.lottie.compose.animateLottieCompositionAsState
+import com.airbnb.lottie.compose.rememberLottieComposition
+import com.airbnb.lottie.compose.rememberLottieDynamicProperty
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.FINISHED
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.IN_PROGRESS
+import com.android.systemui.touchpad.tutorial.ui.composable.TutorialActionState.NOT_STARTED
+
+enum class TutorialActionState {
+ NOT_STARTED,
+ IN_PROGRESS,
+ FINISHED
+}
+
+@Composable
+fun ActionTutorialContent(
+ actionState: TutorialActionState,
+ onDoneButtonClicked: () -> Unit,
+ config: TutorialScreenConfig
+) {
+ val animatedColor by
+ animateColorAsState(
+ targetValue =
+ if (actionState == FINISHED) config.colors.successBackground
+ else config.colors.background,
+ animationSpec = tween(durationMillis = 150, easing = LinearEasing),
+ label = "backgroundColor"
+ )
+ Column(
+ verticalArrangement = Arrangement.Center,
+ modifier =
+ Modifier.fillMaxSize()
+ .drawBehind { drawRect(animatedColor) }
+ .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
+ ) {
+ Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
+ TutorialDescription(
+ titleTextId =
+ if (actionState == FINISHED) config.strings.titleSuccessResId
+ else config.strings.titleResId,
+ titleColor = config.colors.title,
+ bodyTextId =
+ if (actionState == FINISHED) config.strings.bodySuccessResId
+ else config.strings.bodyResId,
+ modifier = Modifier.weight(1f)
+ )
+ Spacer(modifier = Modifier.width(76.dp))
+ TutorialAnimation(
+ actionState,
+ config,
+ modifier = Modifier.weight(1f).padding(top = 8.dp)
+ )
+ }
+ DoneButton(onDoneButtonClicked = onDoneButtonClicked)
+ }
+}
+
+@Composable
+fun TutorialDescription(
+ @StringRes titleTextId: Int,
+ titleColor: Color,
+ @StringRes bodyTextId: Int,
+ modifier: Modifier = Modifier
+) {
+ Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
+ Text(
+ text = stringResource(id = titleTextId),
+ style = MaterialTheme.typography.displayLarge,
+ color = titleColor
+ )
+ Spacer(modifier = Modifier.height(16.dp))
+ Text(
+ text = stringResource(id = bodyTextId),
+ style = MaterialTheme.typography.bodyLarge,
+ color = Color.White
+ )
+ }
+}
+
+@Composable
+fun TutorialAnimation(
+ actionState: TutorialActionState,
+ config: TutorialScreenConfig,
+ modifier: Modifier = Modifier
+) {
+ Box(modifier = modifier.fillMaxWidth()) {
+ AnimatedContent(
+ targetState = actionState,
+ transitionSpec = {
+ if (initialState == NOT_STARTED) {
+ val transitionDurationMillis = 150
+ fadeIn(animationSpec = tween(transitionDurationMillis, easing = LinearEasing))
+ .togetherWith(
+ fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
+ )
+ // we explicitly don't want size transform because when targetState
+ // animation is loaded for the first time, AnimatedContent thinks target
+ // size is smaller and tries to shrink initial state animation
+ .using(sizeTransform = null)
+ } else {
+ // empty transition works because all remaining transitions are from IN_PROGRESS
+ // state which shares initial animation frame with both FINISHED and NOT_STARTED
+ EnterTransition.None togetherWith ExitTransition.None
+ }
+ }
+ ) { state ->
+ when (state) {
+ NOT_STARTED ->
+ EducationAnimation(
+ config.animations.educationResId,
+ config.colors.animationColors
+ )
+ IN_PROGRESS ->
+ FrozenSuccessAnimation(
+ config.animations.successResId,
+ config.colors.animationColors
+ )
+ FINISHED ->
+ SuccessAnimation(config.animations.successResId, config.colors.animationColors)
+ }
+ }
+ }
+}
+
+@Composable
+private fun FrozenSuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ LottieAnimation(
+ composition = composition,
+ progress = { 0f }, // animation should freeze on 1st frame
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun EducationAnimation(
+ @RawRes educationAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
+ val progress by
+ animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+private fun SuccessAnimation(
+ @RawRes successAnimationId: Int,
+ animationProperties: LottieDynamicProperties
+) {
+ val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
+ val progress by animateLottieCompositionAsState(composition, iterations = 1)
+ LottieAnimation(
+ composition = composition,
+ progress = { progress },
+ dynamicProperties = animationProperties,
+ )
+}
+
+@Composable
+fun rememberColorFilterProperty(
+ layerName: String,
+ color: Color
+): LottieDynamicProperty<ColorFilter> {
+ return rememberLottieDynamicProperty(
+ LottieProperty.COLOR_FILTER,
+ value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
+ // "**" below means match zero or more layers, so ** layerName ** means find layer with that
+ // name at any depth
+ keyPath = arrayOf("**", layerName, "**")
+ )
+}
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 1b00ae2..5980e1d 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
@@ -38,8 +38,8 @@
TutorialScreenConfig.Strings(
titleResId = R.string.touchpad_back_gesture_action_title,
bodyResId = R.string.touchpad_back_gesture_guidance,
- titleSuccessResId = R.string.touchpad_tutorial_gesture_done,
- bodySuccessResId = R.string.touchpad_back_gesture_finished
+ titleSuccessResId = R.string.touchpad_back_gesture_success_title,
+ bodySuccessResId = R.string.touchpad_back_gesture_success_body
),
animations =
TutorialScreenConfig.Animations(
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
index 416c562..dfe9c0d 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/composable/GestureTutorialScreen.kt
@@ -16,57 +16,18 @@
package com.android.systemui.touchpad.tutorial.ui.composable
-import android.graphics.ColorFilter
-import android.graphics.PorterDuff
-import android.graphics.PorterDuffColorFilter
import androidx.activity.compose.BackHandler
-import androidx.annotation.RawRes
-import androidx.annotation.StringRes
-import androidx.compose.animation.AnimatedContent
-import androidx.compose.animation.EnterTransition
-import androidx.compose.animation.ExitTransition
-import androidx.compose.animation.animateColorAsState
-import androidx.compose.animation.core.LinearEasing
-import androidx.compose.animation.core.snap
-import androidx.compose.animation.core.tween
-import androidx.compose.animation.fadeIn
-import androidx.compose.animation.fadeOut
-import androidx.compose.animation.togetherWith
-import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.BoxScope
-import androidx.compose.foundation.layout.Column
-import androidx.compose.foundation.layout.Row
-import androidx.compose.foundation.layout.Spacer
import androidx.compose.foundation.layout.fillMaxSize
-import androidx.compose.foundation.layout.fillMaxWidth
-import androidx.compose.foundation.layout.height
-import androidx.compose.foundation.layout.padding
-import androidx.compose.foundation.layout.width
-import androidx.compose.material3.MaterialTheme
-import androidx.compose.material3.Text
import androidx.compose.runtime.Composable
import androidx.compose.runtime.getValue
import androidx.compose.runtime.mutableStateOf
import androidx.compose.runtime.remember
import androidx.compose.runtime.setValue
import androidx.compose.ui.Modifier
-import androidx.compose.ui.draw.drawBehind
-import androidx.compose.ui.graphics.Color
-import androidx.compose.ui.graphics.toArgb
import androidx.compose.ui.input.pointer.pointerInteropFilter
import androidx.compose.ui.platform.LocalContext
-import androidx.compose.ui.res.stringResource
-import androidx.compose.ui.unit.dp
-import com.airbnb.lottie.LottieProperty
-import com.airbnb.lottie.compose.LottieAnimation
-import com.airbnb.lottie.compose.LottieCompositionSpec
-import com.airbnb.lottie.compose.LottieConstants
-import com.airbnb.lottie.compose.LottieDynamicProperties
-import com.airbnb.lottie.compose.LottieDynamicProperty
-import com.airbnb.lottie.compose.animateLottieCompositionAsState
-import com.airbnb.lottie.compose.rememberLottieComposition
-import com.airbnb.lottie.compose.rememberLottieDynamicProperty
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.FINISHED
import com.android.systemui.touchpad.tutorial.ui.gesture.GestureState.IN_PROGRESS
@@ -81,6 +42,14 @@
): TouchpadGestureMonitor
}
+fun GestureState.toTutorialActionState(): TutorialActionState {
+ return when (this) {
+ NOT_STARTED -> TutorialActionState.NOT_STARTED
+ IN_PROGRESS -> TutorialActionState.IN_PROGRESS
+ FINISHED -> TutorialActionState.FINISHED
+ }
+}
+
@Composable
fun GestureTutorialScreen(
screenConfig: TutorialScreenConfig,
@@ -104,7 +73,11 @@
)
}
TouchpadGesturesHandlingBox(gestureHandler, gestureState) {
- GestureTutorialContent(gestureState, onDoneButtonClicked, screenConfig)
+ ActionTutorialContent(
+ gestureState.toTutorialActionState(),
+ onDoneButtonClicked,
+ screenConfig
+ )
}
}
@@ -135,165 +108,3 @@
content()
}
}
-
-@Composable
-private fun GestureTutorialContent(
- gestureState: GestureState,
- onDoneButtonClicked: () -> Unit,
- config: TutorialScreenConfig
-) {
- val animatedColor by
- animateColorAsState(
- targetValue =
- if (gestureState == FINISHED) config.colors.successBackground
- else config.colors.background,
- animationSpec = tween(durationMillis = 150, easing = LinearEasing),
- label = "backgroundColor"
- )
- Column(
- verticalArrangement = Arrangement.Center,
- modifier =
- Modifier.fillMaxSize()
- .drawBehind { drawRect(animatedColor) }
- .padding(start = 48.dp, top = 124.dp, end = 48.dp, bottom = 48.dp)
- ) {
- Row(modifier = Modifier.fillMaxWidth().weight(1f)) {
- TutorialDescription(
- titleTextId =
- if (gestureState == FINISHED) config.strings.titleSuccessResId
- else config.strings.titleResId,
- titleColor = config.colors.title,
- bodyTextId =
- if (gestureState == FINISHED) config.strings.bodySuccessResId
- else config.strings.bodyResId,
- modifier = Modifier.weight(1f)
- )
- Spacer(modifier = Modifier.width(76.dp))
- TutorialAnimation(
- gestureState,
- config,
- modifier = Modifier.weight(1f).padding(top = 8.dp)
- )
- }
- DoneButton(onDoneButtonClicked = onDoneButtonClicked)
- }
-}
-
-@Composable
-fun TutorialDescription(
- @StringRes titleTextId: Int,
- titleColor: Color,
- @StringRes bodyTextId: Int,
- modifier: Modifier = Modifier
-) {
- Column(verticalArrangement = Arrangement.Top, modifier = modifier) {
- Text(
- text = stringResource(id = titleTextId),
- style = MaterialTheme.typography.displayLarge,
- color = titleColor
- )
- Spacer(modifier = Modifier.height(16.dp))
- Text(
- text = stringResource(id = bodyTextId),
- style = MaterialTheme.typography.bodyLarge,
- color = Color.White
- )
- }
-}
-
-@Composable
-fun TutorialAnimation(
- gestureState: GestureState,
- config: TutorialScreenConfig,
- modifier: Modifier = Modifier
-) {
- Box(modifier = modifier.fillMaxWidth()) {
- AnimatedContent(
- targetState = gestureState,
- transitionSpec = {
- if (initialState == NOT_STARTED && targetState == IN_PROGRESS) {
- val transitionDurationMillis = 150
- fadeIn(
- animationSpec = tween(transitionDurationMillis, easing = LinearEasing)
- ) togetherWith
- fadeOut(animationSpec = snap(delayMillis = transitionDurationMillis))
- } else {
- // empty transition works because all remaining transitions are from IN_PROGRESS
- // state which shares initial animation frame with both FINISHED and NOT_STARTED
- EnterTransition.None togetherWith ExitTransition.None
- }
- }
- ) { gestureState ->
- when (gestureState) {
- NOT_STARTED ->
- EducationAnimation(
- config.animations.educationResId,
- config.colors.animationColors
- )
- IN_PROGRESS ->
- FrozenSuccessAnimation(
- config.animations.successResId,
- config.colors.animationColors
- )
- FINISHED ->
- SuccessAnimation(config.animations.successResId, config.colors.animationColors)
- }
- }
- }
-}
-
-@Composable
-private fun FrozenSuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- LottieAnimation(
- composition = composition,
- progress = { 0f }, // animation should freeze on 1st frame
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun EducationAnimation(
- @RawRes educationAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(educationAnimationId))
- val progress by
- animateLottieCompositionAsState(composition, iterations = LottieConstants.IterateForever)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-private fun SuccessAnimation(
- @RawRes successAnimationId: Int,
- animationProperties: LottieDynamicProperties
-) {
- val composition by rememberLottieComposition(LottieCompositionSpec.RawRes(successAnimationId))
- val progress by animateLottieCompositionAsState(composition, iterations = 1)
- LottieAnimation(
- composition = composition,
- progress = { progress },
- dynamicProperties = animationProperties,
- )
-}
-
-@Composable
-fun rememberColorFilterProperty(
- layerName: String,
- color: Color
-): LottieDynamicProperty<ColorFilter> {
- return rememberLottieDynamicProperty(
- LottieProperty.COLOR_FILTER,
- value = PorterDuffColorFilter(color.toArgb(), PorterDuff.Mode.SRC_ATOP),
- // "**" below means match zero or more layers, so ** layerName ** means find layer with that
- // name at any depth
- keyPath = arrayOf("**", layerName, "**")
- )
-}
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 51b14c2..ed3110c 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
@@ -38,8 +38,8 @@
TutorialScreenConfig.Strings(
titleResId = R.string.touchpad_home_gesture_action_title,
bodyResId = R.string.touchpad_home_gesture_guidance,
- titleSuccessResId = R.string.touchpad_home_gesture_done,
- bodySuccessResId = R.string.touchpad_home_gesture_finished
+ titleSuccessResId = R.string.touchpad_home_gesture_success_title,
+ bodySuccessResId = R.string.touchpad_home_gesture_success_body
),
animations =
TutorialScreenConfig.Animations(
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
index ad8ab30..48e6397 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/view/TouchpadTutorialActivity.kt
@@ -27,9 +27,11 @@
import androidx.lifecycle.Lifecycle.State.STARTED
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import com.android.compose.theme.PlatformTheme
+import com.android.systemui.touchpad.tutorial.ui.composable.ActionKeyTutorialScreen
import com.android.systemui.touchpad.tutorial.ui.composable.BackGestureTutorialScreen
import com.android.systemui.touchpad.tutorial.ui.composable.HomeGestureTutorialScreen
import com.android.systemui.touchpad.tutorial.ui.composable.TutorialSelectionScreen
+import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.ACTION_KEY
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.BACK_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.HOME_GESTURE
import com.android.systemui.touchpad.tutorial.ui.viewmodel.Screen.TUTORIAL_SELECTION
@@ -71,7 +73,7 @@
TutorialSelectionScreen(
onBackTutorialClicked = { vm.goTo(BACK_GESTURE) },
onHomeTutorialClicked = { vm.goTo(HOME_GESTURE) },
- onActionKeyTutorialClicked = {},
+ onActionKeyTutorialClicked = { vm.goTo(ACTION_KEY) },
onDoneButtonClicked = closeTutorial
)
BACK_GESTURE ->
@@ -84,5 +86,10 @@
onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
onBack = { vm.goTo(TUTORIAL_SELECTION) },
)
+ ACTION_KEY -> // TODO(b/358105049) move action key tutorial to OOBE flow
+ ActionKeyTutorialScreen(
+ onDoneButtonClicked = { vm.goTo(TUTORIAL_SELECTION) },
+ onBack = { vm.goTo(TUTORIAL_SELECTION) },
+ )
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
index 11984af..d3aeaa7 100644
--- a/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/touchpad/tutorial/ui/viewmodel/TouchpadTutorialViewModel.kt
@@ -55,4 +55,5 @@
TUTORIAL_SELECTION,
BACK_GESTURE,
HOME_GESTURE,
+ ACTION_KEY,
}
diff --git a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
index 56b46624..32f2ca6 100644
--- a/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
+++ b/packages/SystemUI/src/com/android/systemui/user/CreateUserActivity.java
@@ -116,7 +116,7 @@
return mCreateUserDialogController.createDialog(
this,
this::startActivity,
- (mUserCreator.isMultipleAdminEnabled() && mUserCreator.isUserAdmin()
+ (mUserCreator.canCreateAdminUser() && mUserCreator.isUserAdmin()
&& !isKeyguardShowing),
this::addUserNow,
this::finish
diff --git a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
index 9304a46..a426da9 100644
--- a/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
+++ b/packages/SystemUI/src/com/android/systemui/user/UserCreator.kt
@@ -19,6 +19,7 @@
import android.content.Context
import android.content.pm.UserInfo
import android.graphics.drawable.Drawable
+import android.multiuser.Flags
import android.os.UserManager
import com.android.internal.util.UserIcons
import com.android.settingslib.users.UserCreatingDialog
@@ -91,7 +92,17 @@
return userManager.isAdminUser
}
- fun isMultipleAdminEnabled(): Boolean {
- return UserManager.isMultipleAdminEnabled()
+ /**
+ * Checks if the creation of a new admin user is allowed.
+ *
+ * @return `true` if creating a new admin is allowed, `false` otherwise.
+ */
+ fun canCreateAdminUser(): Boolean {
+ return if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ UserManager.isMultipleAdminEnabled() &&
+ !userManager.hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN)
+ } else {
+ UserManager.isMultipleAdminEnabled()
+ }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
index e17274c..ade6c3d 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/FlowDumper.kt
@@ -19,11 +19,14 @@
import android.util.IndentingPrintWriter
import com.android.systemui.Dumpable
import com.android.systemui.dump.DumpManager
+import com.android.systemui.lifecycle.SafeActivatable
+import com.android.systemui.lifecycle.SysUiViewModel
import com.android.systemui.util.asIndenting
import com.android.systemui.util.printCollection
import java.io.PrintWriter
import java.util.concurrent.ConcurrentHashMap
import java.util.concurrent.atomic.AtomicBoolean
+import kotlinx.coroutines.awaitCancellation
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharedFlow
import kotlinx.coroutines.flow.StateFlow
@@ -64,19 +67,20 @@
}
/**
- * An implementation of [FlowDumper]. This be extended directly, or can be used to implement
- * [FlowDumper] by delegation.
- *
- * @param dumpManager if provided, this will be used by the [FlowDumperImpl] to register and
- * unregister itself when there is something to dump.
- * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this
- * class's name will be used. If you're implementing by delegation, you probably want to provide
- * this tag to get a meaningful dumpable name.
+ * The minimal implementation of FlowDumper. The owner must either register this with the
+ * DumpManager, or else call [dumpFlows] from its own [Dumpable.dump] method.
*/
-open class FlowDumperImpl(private val dumpManager: DumpManager?, tag: String? = null) : FlowDumper {
+open class SimpleFlowDumper : FlowDumper {
+
private val stateFlowMap = ConcurrentHashMap<String, StateFlow<*>>()
private val sharedFlowMap = ConcurrentHashMap<String, SharedFlow<*>>()
private val flowCollectionMap = ConcurrentHashMap<Pair<String, String>, Any>()
+
+ protected fun isNotEmpty(): Boolean =
+ stateFlowMap.isNotEmpty() || sharedFlowMap.isNotEmpty() || flowCollectionMap.isNotEmpty()
+
+ protected open fun onMapKeysChanged(added: Boolean) {}
+
override fun dumpFlows(pw: IndentingPrintWriter) {
pw.printCollection("StateFlow (value)", stateFlowMap.toSortedMap().entries) { (key, flow) ->
append(key).append('=').println(flow.value)
@@ -92,43 +96,62 @@
}
}
- private val Any.idString: String
- get() = Integer.toHexString(System.identityHashCode(this))
-
override fun <T> Flow<T>.dumpWhileCollecting(dumpName: String): Flow<T> = flow {
val mapKey = dumpName to idString
try {
collect {
flowCollectionMap[mapKey] = it ?: "null"
- updateRegistration(required = true)
+ onMapKeysChanged(added = true)
emit(it)
}
} finally {
flowCollectionMap.remove(mapKey)
- updateRegistration(required = false)
+ onMapKeysChanged(added = false)
}
}
override fun <T, F : StateFlow<T>> F.dumpValue(dumpName: String): F {
stateFlowMap[dumpName] = this
+ onMapKeysChanged(added = true)
return this
}
override fun <T, F : SharedFlow<T>> F.dumpReplayCache(dumpName: String): F {
sharedFlowMap[dumpName] = this
+ onMapKeysChanged(added = true)
return this
}
- private val dumpManagerName = tag ?: "[$idString] ${javaClass.simpleName}"
+ protected val Any.idString: String
+ get() = Integer.toHexString(System.identityHashCode(this))
+}
+
+/**
+ * An implementation of [FlowDumper] that registers itself whenever there is something to dump. This
+ * class is meant to be extended.
+ *
+ * @param dumpManager this will be used by the [FlowDumperImpl] to register and unregister itself
+ * when there is something to dump.
+ * @param tag a static name by which this [FlowDumperImpl] is registered. If not provided, this
+ * class's name will be used.
+ */
+abstract class FlowDumperImpl(
+ private val dumpManager: DumpManager,
+ private val tag: String? = null,
+) : SimpleFlowDumper() {
+
+ override fun onMapKeysChanged(added: Boolean) {
+ updateRegistration(required = added)
+ }
+
+ private val dumpManagerName = "[$idString] ${tag ?: javaClass.simpleName}"
+
private var registered = AtomicBoolean(false)
+
private fun updateRegistration(required: Boolean) {
- if (dumpManager == null) return
if (required && registered.get()) return
synchronized(registered) {
- val shouldRegister =
- stateFlowMap.isNotEmpty() ||
- sharedFlowMap.isNotEmpty() ||
- flowCollectionMap.isNotEmpty()
+ val shouldRegister = isNotEmpty()
val wasRegistered = registered.getAndSet(shouldRegister)
if (wasRegistered != shouldRegister) {
if (shouldRegister) {
@@ -140,3 +163,49 @@
}
}
}
+
+/**
+ * A [FlowDumper] that also has an [activateFlowDumper] suspend function that allows the dumper to
+ * be registered with the [DumpManager] only when activated, just like
+ * [Activatable.activate()][com.android.systemui.lifecycle.Activatable.activate].
+ */
+interface ActivatableFlowDumper : FlowDumper {
+ suspend fun activateFlowDumper()
+}
+
+/**
+ * Implementation of [ActivatableFlowDumper] that only registers when activated.
+ *
+ * This is generally used to implement [ActivatableFlowDumper] by delegation, especially for
+ * [SysUiViewModel] implementations.
+ *
+ * @param dumpManager used to automatically register and unregister this instance when activated and
+ * there is something to dump.
+ * @param tag the name with which this is dumper registered.
+ */
+class ActivatableFlowDumperImpl(
+ private val dumpManager: DumpManager,
+ tag: String,
+) : SimpleFlowDumper(), ActivatableFlowDumper {
+
+ private val registration =
+ object : SafeActivatable() {
+ override suspend fun onActivated() {
+ try {
+ dumpManager.registerCriticalDumpable(
+ dumpManagerName,
+ this@ActivatableFlowDumperImpl
+ )
+ awaitCancellation()
+ } finally {
+ dumpManager.unregisterDumpable(dumpManagerName)
+ }
+ }
+ }
+
+ private val dumpManagerName = "[$idString] $tag"
+
+ override suspend fun activateFlowDumper() {
+ registration.activate()
+ }
+}
diff --git a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
index 28ac2c0..055671c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/kotlin/JavaAdapter.kt
@@ -28,6 +28,7 @@
import kotlin.coroutines.CoroutineContext
import kotlin.coroutines.EmptyCoroutineContext
import kotlinx.coroutines.CoroutineScope
+import kotlinx.coroutines.DisposableHandle
import kotlinx.coroutines.Job
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.combine
@@ -62,7 +63,9 @@
/**
* Collect information for the given [flow], calling [consumer] for each emitted event. Defaults to
* [LifeCycle.State.CREATED] to better align with legacy ViewController usage of attaching listeners
- * during onViewAttached() and removing during onViewRemoved()
+ * during onViewAttached() and removing during onViewRemoved().
+ *
+ * @return a disposable handle in order to cancel the flow in the future.
*/
@JvmOverloads
fun <T> collectFlow(
@@ -71,8 +74,8 @@
consumer: Consumer<T>,
coroutineContext: CoroutineContext = EmptyCoroutineContext,
state: Lifecycle.State = Lifecycle.State.CREATED,
-) {
- view.repeatWhenAttached(coroutineContext) {
+): DisposableHandle {
+ return view.repeatWhenAttached(coroutineContext) {
repeatOnLifecycle(state) { flow.collect { consumer.accept(it) } }
}
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
index 816f55d..7fcabe4 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/GlobalSettingsImpl.java
@@ -42,28 +42,31 @@
mBgDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.Global.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgDispatcher;
}
@Override
- public String getString(String name) {
+ public String getString(@NonNull String name) {
return Settings.Global.getString(mContentResolver, name);
}
@Override
- public boolean putString(String name, String value) {
+ public boolean putString(@NonNull String name, String value) {
return Settings.Global.putString(mContentResolver, name, value);
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
index f1da27f..c296481 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SecureSettingsImpl.java
@@ -16,12 +16,11 @@
package com.android.systemui.util.settings;
+import android.annotation.NonNull;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.Settings;
-import androidx.annotation.NonNull;
-
import com.android.systemui.util.kotlin.SettingsSingleThreadBackground;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -43,46 +42,50 @@
mBgDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
public CurrentUserIdProvider getCurrentUserProvider() {
return mCurrentUserProvider;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.Secure.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgDispatcher;
}
@Override
- public String getStringForUser(String name, int userHandle) {
+ public String getStringForUser(@NonNull String name, int userHandle) {
return Settings.Secure.getStringForUser(mContentResolver, name,
getRealUserHandle(userHandle));
}
@Override
- public boolean putString(String name, String value, boolean overrideableByRestore) {
+ public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) {
return Settings.Secure.putString(mContentResolver, name, value, overrideableByRestore);
}
@Override
- public boolean putStringForUser(String name, String value, int userHandle) {
+ public boolean putStringForUser(@NonNull String name, String value, int userHandle) {
return Settings.Secure.putStringForUser(mContentResolver, name, value,
getRealUserHandle(userHandle));
}
@Override
- public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
- int userHandle, boolean overrideableByRestore) {
+ public boolean putStringForUser(@NonNull String name, String value, String tag,
+ boolean makeDefault, int userHandle, boolean overrideableByRestore) {
return Settings.Secure.putStringForUser(
mContentResolver, name, value, tag, makeDefault, getRealUserHandle(userHandle),
overrideableByRestore);
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
index 0ee997e..82f41a7 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SettingsProxy.kt
@@ -346,7 +346,7 @@
* @param value to associate with the name
* @return true if the value was set, false on database errors
*/
- fun putString(name: String, value: String): Boolean
+ fun putString(name: String, value: String?): Boolean
/**
* Store a name/value pair into the database.
@@ -377,7 +377,7 @@
* @return true if the value was set, false on database errors.
* @see .resetToDefaults
*/
- fun putString(name: String, value: String, tag: String, makeDefault: Boolean): Boolean
+ fun putString(name: String, value: String?, tag: String?, makeDefault: Boolean): Boolean
/**
* Convenience function for retrieving a single secure settings value as an integer. Note that
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
index 1e80357..e670b2c 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/SystemSettingsImpl.java
@@ -16,12 +16,11 @@
package com.android.systemui.util.settings;
+import android.annotation.NonNull;
import android.content.ContentResolver;
import android.net.Uri;
import android.provider.Settings;
-import androidx.annotation.NonNull;
-
import com.android.systemui.util.kotlin.SettingsSingleThreadBackground;
import kotlinx.coroutines.CoroutineDispatcher;
@@ -42,46 +41,50 @@
mBgCoroutineDispatcher = bgDispatcher;
}
+ @NonNull
@Override
public ContentResolver getContentResolver() {
return mContentResolver;
}
+ @NonNull
@Override
public CurrentUserIdProvider getCurrentUserProvider() {
return mCurrentUserProvider;
}
+ @NonNull
@Override
- public Uri getUriFor(String name) {
+ public Uri getUriFor(@NonNull String name) {
return Settings.System.getUriFor(name);
}
+ @NonNull
@Override
public CoroutineDispatcher getBackgroundDispatcher() {
return mBgCoroutineDispatcher;
}
@Override
- public String getStringForUser(String name, int userHandle) {
+ public String getStringForUser(@NonNull String name, int userHandle) {
return Settings.System.getStringForUser(mContentResolver, name,
getRealUserHandle(userHandle));
}
@Override
- public boolean putString(String name, String value, boolean overrideableByRestore) {
+ public boolean putString(@NonNull String name, String value, boolean overrideableByRestore) {
return Settings.System.putString(mContentResolver, name, value, overrideableByRestore);
}
@Override
- public boolean putStringForUser(String name, String value, int userHandle) {
+ public boolean putStringForUser(@NonNull String name, String value, int userHandle) {
return Settings.System.putStringForUser(mContentResolver, name, value,
getRealUserHandle(userHandle));
}
@Override
- public boolean putStringForUser(String name, String value, String tag, boolean makeDefault,
- int userHandle, boolean overrideableByRestore) {
+ public boolean putStringForUser(@NonNull String name, String value, String tag,
+ boolean makeDefault, int userHandle, boolean overrideableByRestore) {
throw new UnsupportedOperationException(
"This method only exists publicly for Settings.Secure and Settings.Global");
}
diff --git a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
index 9ae8f03..8e3b813 100644
--- a/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
+++ b/packages/SystemUI/src/com/android/systemui/util/settings/UserSettingsProxy.kt
@@ -368,19 +368,19 @@
* @param value to associate with the name
* @return true if the value was set, false on database errors
*/
- fun putString(name: String, value: String, overrideableByRestore: Boolean): Boolean
+ fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean
- override fun putString(name: String, value: String): Boolean {
+ override fun putString(name: String, value: String?): Boolean {
return putStringForUser(name, value, userId)
}
/** Similar implementation to [putString] for the specified [userHandle]. */
- fun putStringForUser(name: String, value: String, userHandle: Int): Boolean
+ fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean
/** Similar implementation to [putString] for the specified [userHandle]. */
fun putStringForUser(
name: String,
- value: String,
+ value: String?,
tag: String?,
makeDefault: Boolean,
@UserIdInt userHandle: Int,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt
index e46ce26..24fb001 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepository.kt
@@ -19,6 +19,7 @@
import com.android.systemui.Dumpable
import com.android.systemui.dagger.SysUISingleton
import com.android.systemui.dump.DumpManager
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState
import java.io.PrintWriter
import javax.inject.Inject
@@ -27,10 +28,15 @@
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.update
-private const val TAG = "VolumePanelGlobalState"
+private const val TAG = "VolumePanelGlobalStateRepository"
@SysUISingleton
-class VolumePanelGlobalStateRepository @Inject constructor(dumpManager: DumpManager) : Dumpable {
+class VolumePanelGlobalStateRepository
+@Inject
+constructor(
+ dumpManager: DumpManager,
+ private val logger: VolumePanelLogger,
+) : Dumpable {
private val mutableGlobalState =
MutableStateFlow(
@@ -48,6 +54,7 @@
update: (currentState: VolumePanelGlobalState) -> VolumePanelGlobalState
) {
mutableGlobalState.update(update)
+ logger.onVolumePanelGlobalStateChanged(mutableGlobalState.value)
}
override fun dump(pw: PrintWriter, args: Array<out String>) {
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
index 5301b00..9de862a 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractor.kt
@@ -19,6 +19,7 @@
import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
import com.android.systemui.volume.panel.domain.ComponentAvailabilityCriteria
import com.android.systemui.volume.panel.domain.model.ComponentModel
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import javax.inject.Inject
import javax.inject.Provider
@@ -26,8 +27,12 @@
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.combine
+import kotlinx.coroutines.flow.conflate
+import kotlinx.coroutines.flow.distinctUntilChanged
+import kotlinx.coroutines.flow.filterNotNull
import kotlinx.coroutines.flow.map
-import kotlinx.coroutines.flow.shareIn
+import kotlinx.coroutines.flow.onEach
+import kotlinx.coroutines.flow.stateIn
interface ComponentsInteractor {
@@ -45,6 +50,7 @@
enabledComponents: Collection<VolumePanelComponentKey>,
defaultCriteria: Provider<ComponentAvailabilityCriteria>,
@VolumePanelScope coroutineScope: CoroutineScope,
+ private val logger: VolumePanelLogger,
private val criteriaByKey:
Map<
VolumePanelComponentKey,
@@ -57,12 +63,18 @@
combine(
enabledComponents.map { componentKey ->
val componentCriteria = (criteriaByKey[componentKey] ?: defaultCriteria).get()
- componentCriteria.isAvailable().map { isAvailable ->
- ComponentModel(componentKey, isAvailable = isAvailable)
- }
+ componentCriteria
+ .isAvailable()
+ .distinctUntilChanged()
+ .conflate()
+ .onEach { logger.onComponentAvailabilityChanged(componentKey, it) }
+ .map { isAvailable ->
+ ComponentModel(componentKey, isAvailable = isAvailable)
+ }
}
) {
it.asList()
}
- .shareIn(coroutineScope, SharingStarted.Eagerly, replay = 1)
+ .stateIn(coroutineScope, SharingStarted.Eagerly, null)
+ .filterNotNull()
}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
index cc513b5..276326c 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/shared/VolumePanelLogger.kt
@@ -20,15 +20,41 @@
import com.android.systemui.log.LogBuffer
import com.android.systemui.log.core.LogLevel
import com.android.systemui.log.dagger.VolumeLog
-import com.android.systemui.volume.panel.dagger.scope.VolumePanelScope
+import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
+import com.android.systemui.volume.panel.shared.model.VolumePanelGlobalState
+import com.android.systemui.volume.panel.ui.viewmodel.VolumePanelState
import javax.inject.Inject
private const val TAG = "SysUI_VolumePanel"
/** Logs events related to the Volume Panel. */
-@VolumePanelScope
class VolumePanelLogger @Inject constructor(@VolumeLog private val logBuffer: LogBuffer) {
+ fun onVolumePanelStateChanged(state: VolumePanelState) {
+ logBuffer.log(TAG, LogLevel.DEBUG, { str1 = state.toString() }, { "State changed: $str1" })
+ }
+
+ fun onComponentAvailabilityChanged(key: VolumePanelComponentKey, isAvailable: Boolean) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ {
+ str1 = key
+ bool1 = isAvailable
+ },
+ { "$str1 isAvailable=$bool1" }
+ )
+ }
+
+ fun onVolumePanelGlobalStateChanged(globalState: VolumePanelGlobalState) {
+ logBuffer.log(
+ TAG,
+ LogLevel.DEBUG,
+ { bool1 = globalState.isVisible },
+ { "Global state changed: isVisible=$bool1" }
+ )
+ }
+
fun onSetVolumeRequested(audioStream: AudioStream, volume: Int) {
logBuffer.log(
TAG,
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
index 1c51236..a06d3e3 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/layout/ComponentsLayout.kt
@@ -17,6 +17,7 @@
package com.android.systemui.volume.panel.ui.layout
import com.android.systemui.volume.panel.ui.viewmodel.ComponentState
+import com.android.systemui.volume.panel.ui.viewmodel.toLogString
/** Represents components grouping into the layout. */
data class ComponentsLayout(
@@ -29,3 +30,12 @@
/** This is a separated entity that is always visible on the bottom of the Volume Panel. */
val bottomBarComponent: ComponentState,
)
+
+fun ComponentsLayout.toLogString(): String {
+ return "(" +
+ " headerComponents=${headerComponents.joinToString { it.toLogString() }}" +
+ " contentComponents=${contentComponents.joinToString { it.toLogString() }}" +
+ " footerComponents=${footerComponents.joinToString { it.toLogString() }}" +
+ " bottomBarComponent=${bottomBarComponent.toLogString()}" +
+ " )"
+}
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
index 5f4dbfb..41c80fa 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/ComponentState.kt
@@ -32,3 +32,5 @@
val component: VolumePanelUiComponent,
val isVisible: Boolean,
)
+
+fun ComponentState.toLogString(): String = "$key:visible=$isVisible"
diff --git a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
index f495a02f..2f60c4b 100644
--- a/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
+++ b/packages/SystemUI/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModel.kt
@@ -19,23 +19,30 @@
import android.content.Context
import android.content.IntentFilter
import android.content.res.Resources
+import com.android.systemui.Dumpable
import com.android.systemui.broadcast.BroadcastDispatcher
import com.android.systemui.dagger.qualifiers.Application
+import com.android.systemui.dump.DumpManager
import com.android.systemui.res.R
import com.android.systemui.statusbar.policy.ConfigurationController
import com.android.systemui.statusbar.policy.onConfigChanged
+import com.android.systemui.util.kotlin.launchAndDispose
import com.android.systemui.volume.VolumePanelDialogReceiver
import com.android.systemui.volume.panel.dagger.VolumePanelComponent
import com.android.systemui.volume.panel.dagger.factory.VolumePanelComponentFactory
import com.android.systemui.volume.panel.domain.VolumePanelStartable
import com.android.systemui.volume.panel.domain.interactor.ComponentsInteractor
import com.android.systemui.volume.panel.domain.interactor.VolumePanelGlobalStateInteractor
+import com.android.systemui.volume.panel.shared.VolumePanelLogger
import com.android.systemui.volume.panel.ui.composable.ComponentsFactory
import com.android.systemui.volume.panel.ui.layout.ComponentsLayout
import com.android.systemui.volume.panel.ui.layout.ComponentsLayoutManager
+import com.android.systemui.volume.panel.ui.layout.toLogString
+import java.io.PrintWriter
import javax.inject.Inject
import kotlinx.coroutines.CoroutineScope
-import kotlinx.coroutines.flow.Flow
+import kotlinx.coroutines.DisposableHandle
+import kotlinx.coroutines.ExperimentalCoroutinesApi
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
@@ -43,19 +50,23 @@
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.onEach
import kotlinx.coroutines.flow.onStart
-import kotlinx.coroutines.flow.shareIn
import kotlinx.coroutines.flow.stateIn
+private const val TAG = "VolumePanelViewModel"
+
// Can't inject a constructor here because VolumePanelComponent provides this view model for its
// components.
+@OptIn(ExperimentalCoroutinesApi::class)
class VolumePanelViewModel(
resources: Resources,
coroutineScope: CoroutineScope,
daggerComponentFactory: VolumePanelComponentFactory,
configurationController: ConfigurationController,
broadcastDispatcher: BroadcastDispatcher,
+ private val dumpManager: DumpManager,
+ private val logger: VolumePanelLogger,
private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor,
-) {
+) : Dumpable {
private val volumePanelComponent: VolumePanelComponent =
daggerComponentFactory.create(this, coroutineScope)
@@ -77,9 +88,10 @@
.onStart { emit(resources.configuration) }
.map { configuration ->
VolumePanelState(
- orientation = configuration.orientation,
- isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen),
- )
+ orientation = configuration.orientation,
+ isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen),
+ )
+ .also { logger.onVolumePanelStateChanged(it) }
}
.stateIn(
scope,
@@ -89,7 +101,7 @@
isLargeScreen = resources.getBoolean(R.bool.volume_panel_is_large_screen)
),
)
- val componentsLayout: Flow<ComponentsLayout> =
+ val componentsLayout: StateFlow<ComponentsLayout?> =
combine(
componentsInteractor.components,
volumePanelState,
@@ -104,13 +116,18 @@
}
componentsLayoutManager.layout(scope, componentStates)
}
- .shareIn(
+ .stateIn(
scope,
SharingStarted.Eagerly,
- replay = 1,
+ null,
)
init {
+ scope.launchAndDispose {
+ dumpManager.registerNormalDumpable(TAG, this)
+ DisposableHandle { dumpManager.unregisterDumpable(TAG) }
+ }
+
volumePanelComponent.volumePanelStartables().onEach(VolumePanelStartable::start)
broadcastDispatcher
.broadcastFlow(IntentFilter(VolumePanelDialogReceiver.DISMISS_ACTION))
@@ -122,6 +139,13 @@
volumePanelGlobalStateInteractor.setVisible(false)
}
+ override fun dump(pw: PrintWriter, args: Array<out String>) {
+ with(pw) {
+ println("volumePanelState=${volumePanelState.value}")
+ println("componentsLayout=${componentsLayout.value?.toLogString()}")
+ }
+ }
+
class Factory
@Inject
constructor(
@@ -129,6 +153,8 @@
private val daggerComponentFactory: VolumePanelComponentFactory,
private val configurationController: ConfigurationController,
private val broadcastDispatcher: BroadcastDispatcher,
+ private val dumpManager: DumpManager,
+ private val logger: VolumePanelLogger,
private val volumePanelGlobalStateInteractor: VolumePanelGlobalStateInteractor,
) {
@@ -139,6 +165,8 @@
daggerComponentFactory,
configurationController,
broadcastDispatcher,
+ dumpManager,
+ logger,
volumePanelGlobalStateInteractor,
)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
new file mode 100644
index 0000000..2ac5d10
--- /dev/null
+++ b/packages/SystemUI/tests/src/com/android/systemui/accessibility/hearingaid/HearingDevicesPresetsControllerTest.java
@@ -0,0 +1,285 @@
+/*
+ * 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.accessibility.hearingaid;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyList;
+import static org.mockito.ArgumentMatchers.eq;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import static java.util.Collections.emptyList;
+
+import android.bluetooth.BluetoothCsipSetCoordinator;
+import android.bluetooth.BluetoothDevice;
+import android.bluetooth.BluetoothHapClient;
+import android.bluetooth.BluetoothHapPresetInfo;
+import android.testing.TestableLooper;
+
+import androidx.test.ext.junit.runners.AndroidJUnit4;
+import androidx.test.filters.SmallTest;
+
+import com.android.settingslib.bluetooth.CachedBluetoothDevice;
+import com.android.settingslib.bluetooth.HapClientProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfile;
+import com.android.settingslib.bluetooth.LocalBluetoothProfileManager;
+import com.android.systemui.SysuiTestCase;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.Mockito;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+import java.util.List;
+import java.util.concurrent.Executor;
+
+/** Tests for {@link HearingDevicesPresetsController}. */
+@RunWith(AndroidJUnit4.class)
+@TestableLooper.RunWithLooper(setAsMainLooper = true)
+@SmallTest
+public class HearingDevicesPresetsControllerTest extends SysuiTestCase {
+
+ private static final int TEST_PRESET_INDEX = 1;
+ private static final String TEST_PRESET_NAME = "test_preset";
+ private static final int TEST_HAP_GROUP_ID = 1;
+ private static final int TEST_REASON = 1024;
+
+ @Rule
+ public MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock
+ private LocalBluetoothProfileManager mProfileManager;
+ @Mock
+ private HapClientProfile mHapClientProfile;
+ @Mock
+ private CachedBluetoothDevice mCachedBluetoothDevice;
+ @Mock
+ private CachedBluetoothDevice mSubCachedBluetoothDevice;
+ @Mock
+ private BluetoothDevice mBluetoothDevice;
+ @Mock
+ private BluetoothDevice mSubBluetoothDevice;
+
+ @Mock
+ private HearingDevicesPresetsController.PresetCallback mCallback;
+
+ private HearingDevicesPresetsController mController;
+
+ @Before
+ public void setUp() {
+ when(mProfileManager.getHapClientProfile()).thenReturn(mHapClientProfile);
+ when(mHapClientProfile.isProfileReady()).thenReturn(true);
+ when(mCachedBluetoothDevice.getDevice()).thenReturn(mBluetoothDevice);
+ when(mCachedBluetoothDevice.getSubDevice()).thenReturn(mSubCachedBluetoothDevice);
+ when(mSubCachedBluetoothDevice.getDevice()).thenReturn(mSubBluetoothDevice);
+
+ mController = new HearingDevicesPresetsController(mProfileManager, mCallback);
+ }
+
+ @Test
+ public void onServiceConnected_callExpectedCallback() {
+ mController.onServiceConnected();
+
+ verify(mHapClientProfile).registerCallback(any(Executor.class),
+ any(BluetoothHapClient.Callback.class));
+ verify(mCallback).onPresetInfoUpdated(anyList(), anyInt());
+ }
+
+ @Test
+ public void getAllPresetInfo_setInvalidHearingDevice_getEmpty() {
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(emptyList());
+ mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).isEmpty();
+ }
+
+ @Test
+ public void getAllPresetInfo_containsNotAvailablePresetInfo_getEmpty() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(false);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).isEmpty();
+ }
+
+ @Test
+ public void getAllPresetInfo_containsOnePresetInfo_getOnePresetInfo() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+
+ assertThat(mController.getAllPresetInfo()).contains(hapPresetInfo);
+ }
+
+ @Test
+ public void getActivePresetIndex_getExpectedIndex() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ assertThat(mController.getActivePresetIndex()).isEqualTo(TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onPresetSelected_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ mController.onPresetSelected(mBluetoothDevice, TEST_PRESET_INDEX, TEST_REASON);
+
+ verify(mCallback).onPresetInfoUpdated(eq(List.of(hapPresetInfo)), eq(TEST_PRESET_INDEX));
+ }
+
+ @Test
+ public void onPresetInfoChanged_presetIndex_callOnPresetInfoUpdatedWithExpectedPresetIndex() {
+ setValidHearingDeviceSupportHap();
+ BluetoothHapPresetInfo hapPresetInfo = getHapPresetInfo(true);
+ when(mHapClientProfile.getAllPresetInfo(mBluetoothDevice)).thenReturn(
+ List.of(hapPresetInfo));
+ when(mHapClientProfile.getActivePresetIndex(mBluetoothDevice)).thenReturn(
+ TEST_PRESET_INDEX);
+
+ mController.onPresetInfoChanged(mBluetoothDevice, List.of(hapPresetInfo), TEST_REASON);
+
+ verify(mCallback).onPresetInfoUpdated(List.of(hapPresetInfo), TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onPresetSelectionFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onPresetSelectionFailed(mBluetoothDevice, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void onSetPresetNameFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onSetPresetNameFailed(mBluetoothDevice, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void onPresetSelectionForGroupFailed_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ mController.selectPreset(TEST_PRESET_INDEX);
+ Mockito.reset(mHapClientProfile);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.onPresetSelectionForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void onSetPresetNameForGroupFailed_callOnPresetCommandFailed() {
+ setValidHearingDeviceSupportHap();
+
+ mController.onSetPresetNameForGroupFailed(TEST_HAP_GROUP_ID, TEST_REASON);
+
+ verify(mCallback).onPresetCommandFailed(TEST_REASON);
+ }
+
+ @Test
+ public void registerHapCallback_callHapRegisterCallback() {
+ mController.registerHapCallback();
+
+ verify(mHapClientProfile).registerCallback(any(Executor.class),
+ any(BluetoothHapClient.Callback.class));
+ }
+
+ @Test
+ public void unregisterHapCallback_callHapUnregisterCallback() {
+ mController.unregisterHapCallback();
+
+ verify(mHapClientProfile).unregisterCallback(any(BluetoothHapClient.Callback.class));
+ }
+
+ @Test
+ public void selectPreset_supportSynchronized_validGroupId_callSelectPresetForGroup() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPresetForGroup(TEST_HAP_GROUP_ID, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void selectPreset_supportSynchronized_invalidGroupId_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(true);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(
+ BluetoothCsipSetCoordinator.GROUP_ID_INVALID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ @Test
+ public void selectPreset_notSupportSynchronized_validGroupId_callSelectPresetIndividual() {
+ setValidHearingDeviceSupportHap();
+ when(mHapClientProfile.supportsSynchronizedPresets(mBluetoothDevice)).thenReturn(false);
+ when(mHapClientProfile.getHapGroup(mBluetoothDevice)).thenReturn(TEST_HAP_GROUP_ID);
+
+ mController.selectPreset(TEST_PRESET_INDEX);
+
+ verify(mHapClientProfile).selectPreset(mBluetoothDevice, TEST_PRESET_INDEX);
+ verify(mHapClientProfile).selectPreset(mSubBluetoothDevice, TEST_PRESET_INDEX);
+ }
+
+ private BluetoothHapPresetInfo getHapPresetInfo(boolean available) {
+ BluetoothHapPresetInfo info = mock(BluetoothHapPresetInfo.class);
+ when(info.getName()).thenReturn(TEST_PRESET_NAME);
+ when(info.getIndex()).thenReturn(TEST_PRESET_INDEX);
+ when(info.isAvailable()).thenReturn(available);
+ return info;
+ }
+
+ private void setValidHearingDeviceSupportHap() {
+ LocalBluetoothProfile hapClientProfile = mock(HapClientProfile.class);
+ List<LocalBluetoothProfile> profiles = List.of(hapClientProfile);
+ when(mCachedBluetoothDevice.getProfiles()).thenReturn(profiles);
+
+ mController.setHearingDeviceIfSupportHap(mCachedBluetoothDevice);
+ }
+}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
index 5600b87..a18d272 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/ambient/touch/TouchMonitorTest.java
@@ -711,6 +711,16 @@
}
@Test
+ public void testDestroy_cleansUpHandler() {
+ final TouchHandler touchHandler = createTouchHandler();
+
+ final Environment environment = new Environment(Stream.of(touchHandler)
+ .collect(Collectors.toCollection(HashSet::new)), mKosmos);
+ environment.destroyMonitor();
+ verify(touchHandler).onDestroy();
+ }
+
+ @Test
public void testLastSessionPop_createsNewInputSession() {
final TouchHandler touchHandler = createTouchHandler();
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
index dc69cda..f94a6f2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthContainerViewTest.kt
@@ -29,7 +29,6 @@
import android.hardware.face.FaceSensorPropertiesInternal
import android.hardware.fingerprint.FingerprintManager
import android.hardware.fingerprint.FingerprintSensorPropertiesInternal
-import android.os.Handler
import android.os.IBinder
import android.os.UserManager
import android.testing.TestableLooper
@@ -45,7 +44,6 @@
import com.android.internal.jank.InteractionJankMonitor
import com.android.internal.widget.LockPatternUtils
import com.android.launcher3.icons.IconProvider
-import com.android.systemui.Flags.FLAG_CONSTRAINT_BP
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.data.repository.FakeBiometricStatusRepository
import com.android.systemui.biometrics.data.repository.FakeDisplayStateRepository
@@ -105,7 +103,6 @@
@Mock lateinit var fingerprintManager: FingerprintManager
@Mock lateinit var lockPatternUtils: LockPatternUtils
@Mock lateinit var wakefulnessLifecycle: WakefulnessLifecycle
- @Mock lateinit var panelInteractionDetector: AuthDialogPanelInteractionDetector
@Mock lateinit var windowToken: IBinder
@Mock lateinit var interactionJankMonitor: InteractionJankMonitor
@Mock lateinit var vibrator: VibratorHelper
@@ -265,14 +262,6 @@
}
@Test
- fun testActionCancel_panelInteractionDetectorDisable() {
- val container = initializeFingerprintContainer()
- container.mBiometricCallback.onUserCanceled()
- waitForIdleSync()
- verify(panelInteractionDetector).disable()
- }
-
- @Test
fun testActionAuthenticated_sendsDismissedAuthenticated() {
val container = initializeFingerprintContainer()
container.mBiometricCallback.onAuthenticated()
@@ -416,19 +405,7 @@
}
@Test
- fun testShowBiometricUI() {
- mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP)
- val container = initializeFingerprintContainer()
-
- waitForIdleSync()
-
- assertThat(container.hasCredentialView()).isFalse()
- assertThat(container.hasBiometricPrompt()).isTrue()
- }
-
- @Test
fun testShowBiometricUI_ContentViewWithMoreOptionsButton() {
- mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP)
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
var isButtonClicked = false
val contentView =
@@ -466,7 +443,6 @@
@Test
fun testShowCredentialUI_withVerticalListContentView() {
- mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP)
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val container =
initializeFingerprintContainer(
@@ -488,7 +464,6 @@
@Test
fun testShowCredentialUI_withContentViewWithMoreOptionsButton() {
- mSetFlagsRule.enableFlags(FLAG_CONSTRAINT_BP)
mSetFlagsRule.enableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
val contentView =
PromptContentViewWithMoreOptionsButton.Builder()
@@ -674,7 +649,6 @@
fingerprintProps,
faceProps,
wakefulnessLifecycle,
- panelInteractionDetector,
userManager,
lockPatternUtils,
interactionJankMonitor,
@@ -690,7 +664,6 @@
activityTaskManager
),
{ credentialViewModel },
- Handler(TestableLooper.get(this).looper),
fakeExecutor,
vibrator
) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
deleted file mode 100644
index 0231486..0000000
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/AuthDialogPanelInteractionDetectorTest.kt
+++ /dev/null
@@ -1,156 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.biometrics
-
-import android.platform.test.flag.junit.FlagsParameterization
-import androidx.test.filters.SmallTest
-import com.android.systemui.SysuiTestCase
-import com.android.systemui.flags.andSceneContainer
-import com.android.systemui.kosmos.applicationCoroutineScope
-import com.android.systemui.kosmos.testScope
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.shadeTestUtil
-import com.android.systemui.testKosmos
-import kotlinx.coroutines.test.runCurrent
-import kotlinx.coroutines.test.runTest
-import org.junit.Before
-import org.junit.Test
-import org.junit.runner.RunWith
-import org.mockito.Mock
-import org.mockito.Mockito.verify
-import org.mockito.MockitoAnnotations
-import org.mockito.kotlin.verifyZeroInteractions
-import platform.test.runner.parameterized.ParameterizedAndroidJunit4
-import platform.test.runner.parameterized.Parameters
-
-@SmallTest
-@RunWith(ParameterizedAndroidJunit4::class)
-class AuthDialogPanelInteractionDetectorTest(flags: FlagsParameterization?) : SysuiTestCase() {
-
- companion object {
- @JvmStatic
- @Parameters(name = "{0}")
- fun getParams(): List<FlagsParameterization> {
- return FlagsParameterization.allCombinationsOf().andSceneContainer()
- }
- }
-
- init {
- mSetFlagsRule.setFlagsParameterization(flags!!)
- }
-
- private val kosmos = testKosmos()
- private val testScope = kosmos.testScope
-
- private val shadeTestUtil by lazy { kosmos.shadeTestUtil }
-
- @Mock private lateinit var action: Runnable
-
- lateinit var detector: AuthDialogPanelInteractionDetector
-
- @Before
- fun setUp() {
- MockitoAnnotations.initMocks(this)
- detector =
- AuthDialogPanelInteractionDetector(
- kosmos.applicationCoroutineScope,
- { kosmos.shadeInteractor },
- )
- }
-
- @Test
- fun enableDetector_expand_shouldRunAction() =
- testScope.runTest {
- // GIVEN shade is closed and detector is enabled
- shadeTestUtil.setShadeExpansion(0f)
- detector.enable(action)
- runCurrent()
-
- // WHEN shade expands
- shadeTestUtil.setTracking(true)
- shadeTestUtil.setShadeExpansion(.5f)
- runCurrent()
-
- // THEN action was run
- verify(action).run()
- }
-
- @Test
- fun enableDetector_isUserInteractingTrue_shouldNotPostRunnable() =
- testScope.runTest {
- // GIVEN isInteracting starts true
- shadeTestUtil.setTracking(true)
- runCurrent()
- detector.enable(action)
-
- // THEN action was not run
- verifyZeroInteractions(action)
- }
-
- @Test
- fun enableDetector_shadeExpandImmediate_shouldNotPostRunnable() =
- testScope.runTest {
- // GIVEN shade is closed and detector is enabled
- shadeTestUtil.setShadeExpansion(0f)
- detector.enable(action)
- runCurrent()
-
- // WHEN shade expands fully instantly
- shadeTestUtil.setShadeExpansion(1f)
- runCurrent()
-
- // THEN action not run
- verifyZeroInteractions(action)
- detector.disable()
- }
-
- @Test
- fun disableDetector_shouldNotPostRunnable() =
- testScope.runTest {
- // GIVEN shade is closed and detector is enabled
- shadeTestUtil.setShadeExpansion(0f)
- detector.enable(action)
- runCurrent()
-
- // WHEN detector is disabled and shade opens
- detector.disable()
- runCurrent()
- shadeTestUtil.setTracking(true)
- shadeTestUtil.setShadeExpansion(.5f)
- runCurrent()
-
- // THEN action not run
- verifyZeroInteractions(action)
- }
-
- @Test
- fun enableDetector_beginCollapse_shouldNotPostRunnable() =
- testScope.runTest {
- // GIVEN shade is open and detector is enabled
- shadeTestUtil.setShadeExpansion(1f)
- detector.enable(action)
- runCurrent()
-
- // WHEN shade begins to collapse
- shadeTestUtil.programmaticCollapseShade()
- runCurrent()
-
- // THEN action not run
- verifyZeroInteractions(action)
- detector.disable()
- }
-}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
index 720f207..dc499cd 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/domain/interactor/PromptSelectorInteractorImplTest.kt
@@ -155,7 +155,7 @@
Authenticators.BIOMETRIC_STRONG
}
isDeviceCredentialAllowed = allowCredentialFallback
- componentNameForConfirmDeviceCredentialActivity =
+ realCallerForConfirmDeviceCredentialActivity =
if (setComponentNameForConfirmDeviceCredentialActivity)
componentNameOverriddenForConfirmDeviceCredentialActivity
else null
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
index 534f25c..6047e7d 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/PromptViewModelTest.kt
@@ -43,7 +43,6 @@
import android.view.Surface
import androidx.test.filters.SmallTest
import com.android.app.activityTaskManager
-import com.android.systemui.Flags.FLAG_CONSTRAINT_BP
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.AuthController
import com.android.systemui.biometrics.Utils.toBitmap
@@ -1385,7 +1384,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun descriptionOverriddenByVerticalListContentView() =
runGenericTest(description = "test description", contentView = promptContentView) {
val contentView by collectLastValue(kosmos.promptViewModel.contentView)
@@ -1396,7 +1395,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun descriptionOverriddenByContentViewWithMoreOptionsButton() =
runGenericTest(
description = "test description",
@@ -1410,7 +1409,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun descriptionWithoutContentView() =
runGenericTest(description = "test description") {
val contentView by collectLastValue(kosmos.promptViewModel.contentView)
@@ -1421,7 +1420,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_nullIfPkgNameNotFound() =
runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1430,7 +1429,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_defaultFromActivityInfo() =
runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1445,7 +1444,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_defaultIsNull() =
runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1454,7 +1453,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_default() = runGenericTest {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
assertThat(logoInfo).isNotNull()
@@ -1462,7 +1461,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_resSetByApp() =
runGenericTest(logoRes = logoResFromApp) {
val expectedBitmap = context.getDrawable(logoResFromApp).toBitmap()
@@ -1472,7 +1471,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logo_bitmapSetByApp() =
runGenericTest(logoBitmap = logoBitmapFromApp) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1480,7 +1479,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logoDescription_emptyIfPkgNameNotFound() =
runGenericTest(packageName = OP_PACKAGE_NAME_CAN_NOT_BE_FOUND) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1488,7 +1487,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logoDescription_defaultFromActivityInfo() =
runGenericTest(packageName = OP_PACKAGE_NAME_WITH_ACTIVITY_LOGO) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1500,7 +1499,7 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logoDescription_defaultIsEmpty() =
runGenericTest(packageName = OP_PACKAGE_NAME_NO_ICON) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1508,14 +1507,14 @@
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logoDescription_default() = runGenericTest {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
assertThat(logoInfo!!.second).isEqualTo(defaultLogoDescriptionFromAppInfo)
}
@Test
- @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT, FLAG_CONSTRAINT_BP)
+ @EnableFlags(FLAG_CUSTOM_BIOMETRIC_PROMPT)
fun logoDescription_setByApp() =
runGenericTest(logoDescription = logoDescriptionFromApp) {
val logoInfo by collectLastValue(kosmos.promptViewModel.logoInfo)
@@ -1523,7 +1522,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_bottom_rotation0() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
val position by collectLastValue(kosmos.promptViewModel.position)
@@ -1531,7 +1529,6 @@
} // TODO(b/335278136): Add test for no sensor landscape
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_bottom_forceLarge() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
kosmos.promptViewModel.onSwitchToCredential()
@@ -1540,7 +1537,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_bottom_largeScreen() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
kosmos.displayStateRepository.setIsLargeScreen(true)
@@ -1549,7 +1545,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_right_rotation90() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
val position by collectLastValue(kosmos.promptViewModel.position)
@@ -1557,7 +1552,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_left_rotation270() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
val position by collectLastValue(kosmos.promptViewModel.position)
@@ -1565,7 +1559,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun position_top_rotation180() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
val position by collectLastValue(kosmos.promptViewModel.position)
@@ -1577,7 +1570,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_bottom() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_0)
val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
@@ -1585,7 +1577,6 @@
} // TODO(b/335278136): Add test for no sensor landscape
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_right() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
@@ -1602,7 +1593,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_right_onlyShortTitle() =
runGenericTest(subtitle = "") {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
@@ -1617,7 +1607,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_left() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
@@ -1634,7 +1623,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_left_onlyShortTitle() =
runGenericTest(subtitle = "") {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
@@ -1649,7 +1637,6 @@
}
@Test
- @EnableFlags(FLAG_CONSTRAINT_BP)
fun guideline_top() = runGenericTest {
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_180)
val guidelineBounds by collectLastValue(kosmos.promptViewModel.guidelineBounds)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
index 3b2cf61..0db7b62 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/biometrics/ui/viewmodel/SideFpsOverlayViewModelTest.kt
@@ -32,7 +32,6 @@
import com.airbnb.lottie.model.KeyPath
import com.android.keyguard.keyguardUpdateMonitor
import com.android.settingslib.Utils
-import com.android.systemui.Flags.FLAG_CONSTRAINT_BP
import com.android.systemui.SysuiTestCase
import com.android.systemui.biometrics.FingerprintInteractiveToAuthProvider
import com.android.systemui.biometrics.data.repository.biometricStatusRepository
@@ -199,7 +198,6 @@
@Test
fun updatesOverlayViewParams_onDisplayRotationChange_xAlignedSensor() {
kosmos.testScope.runTest {
- mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP)
setupTestConfiguration(
DeviceConfig.X_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
@@ -230,11 +228,11 @@
.isEqualTo(
displayWidth - sensorLocation.sensorLocationX - sensorLocation.sensorRadius * 2
)
- assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight)
+ assertThat(overlayViewParams!!.y).isEqualTo(displayHeight)
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_270)
assertThat(overlayViewParams).isNotNull()
- assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParams!!.x).isEqualTo(displayWidth)
assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationX)
}
}
@@ -242,7 +240,6 @@
@Test
fun updatesOverlayViewParams_onDisplayRotationChange_yAlignedSensor() {
kosmos.testScope.runTest {
- mSetFlagsRule.disableFlags(FLAG_CONSTRAINT_BP)
setupTestConfiguration(
DeviceConfig.Y_ALIGNED,
rotation = DisplayRotation.ROTATION_0,
@@ -256,7 +253,7 @@
runCurrent()
assertThat(overlayViewParams).isNotNull()
- assertThat(overlayViewParams!!.x).isEqualTo(displayWidth - boundsWidth)
+ assertThat(overlayViewParams!!.x).isEqualTo(displayWidth)
assertThat(overlayViewParams!!.y).isEqualTo(sensorLocation.sensorLocationY)
kosmos.displayStateRepository.setCurrentRotation(DisplayRotation.ROTATION_90)
@@ -278,7 +275,7 @@
.isEqualTo(
displayWidth - sensorLocation.sensorLocationY - sensorLocation.sensorRadius * 2
)
- assertThat(overlayViewParams!!.y).isEqualTo(displayHeight - boundsHeight)
+ assertThat(overlayViewParams!!.y).isEqualTo(displayHeight)
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
index 2af4d87..bfe89de 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/data/repository/KeyguardTransitionRepositoryTest.kt
@@ -17,9 +17,6 @@
package com.android.systemui.keyguard.data.repository
import android.animation.ValueAnimator
-import android.util.Log
-import android.util.Log.TerribleFailure
-import android.util.Log.TerribleFailureHandler
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.FlakyTest
import androidx.test.filters.SmallTest
@@ -53,7 +50,6 @@
import kotlinx.coroutines.test.advanceTimeBy
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
@@ -67,23 +63,14 @@
private val testScope = kosmos.testScope
private lateinit var underTest: KeyguardTransitionRepository
- private lateinit var oldWtfHandler: TerribleFailureHandler
- private lateinit var wtfHandler: WtfHandler
private lateinit var runner: KeyguardTransitionRunner
@Before
fun setUp() {
underTest = KeyguardTransitionRepositoryImpl(Dispatchers.Main)
- wtfHandler = WtfHandler()
- oldWtfHandler = Log.setWtfHandler(wtfHandler)
runner = KeyguardTransitionRunner(underTest)
}
- @After
- fun tearDown() {
- oldWtfHandler?.let { Log.setWtfHandler(it) }
- }
-
@Test
fun startTransitionRunsAnimatorToCompletion() =
testScope.runTest {
@@ -333,15 +320,17 @@
}
@Test
- fun attemptTomanuallyUpdateTransitionWithInvalidUUIDthrowsException() =
+ fun attemptTomanuallyUpdateTransitionWithInvalidUUIDEmitsNothing() =
testScope.runTest {
+ val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
underTest.updateTransition(UUID.randomUUID(), 0f, TransitionState.RUNNING)
- assertThat(wtfHandler.failed).isTrue()
+ assertThat(steps.size).isEqualTo(0)
}
@Test
- fun attemptToManuallyUpdateTransitionAfterFINISHEDstateThrowsException() =
+ fun attemptToManuallyUpdateTransitionAfterFINISHEDstateEmitsNothing() =
testScope.runTest {
+ val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
val uuid =
underTest.startTransition(
TransitionInfo(
@@ -356,12 +345,19 @@
underTest.updateTransition(it, 1f, TransitionState.FINISHED)
underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
}
- assertThat(wtfHandler.failed).isTrue()
+ assertThat(steps.size).isEqualTo(2)
+ assertThat(steps[0])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
+ assertThat(steps[1])
+ .isEqualTo(
+ TransitionStep(AOD, LOCKSCREEN, 1f, TransitionState.FINISHED, OWNER_NAME)
+ )
}
@Test
- fun attemptToManuallyUpdateTransitionAfterCANCELEDstateThrowsException() =
+ fun attemptToManuallyUpdateTransitionAfterCANCELEDstateEmitsNothing() =
testScope.runTest {
+ val steps by collectValues(underTest.transitions.dropWhile { step -> step.from == OFF })
val uuid =
underTest.startTransition(
TransitionInfo(
@@ -376,7 +372,13 @@
underTest.updateTransition(it, 0.2f, TransitionState.CANCELED)
underTest.updateTransition(it, 0.5f, TransitionState.RUNNING)
}
- assertThat(wtfHandler.failed).isTrue()
+ assertThat(steps.size).isEqualTo(2)
+ assertThat(steps[0])
+ .isEqualTo(TransitionStep(AOD, LOCKSCREEN, 0f, TransitionState.STARTED, OWNER_NAME))
+ assertThat(steps[1])
+ .isEqualTo(
+ TransitionStep(AOD, LOCKSCREEN, 0.2f, TransitionState.CANCELED, OWNER_NAME)
+ )
}
@Test
@@ -530,8 +532,6 @@
}
assertThat(steps[steps.size - 1])
.isEqualTo(TransitionStep(from, to, lastValue, status, OWNER_NAME))
-
- assertThat(wtfHandler.failed).isFalse()
}
private fun getAnimator(): ValueAnimator {
@@ -541,14 +541,6 @@
}
}
- private class WtfHandler : TerribleFailureHandler {
- var failed = false
-
- override fun onTerribleFailure(tag: String, what: TerribleFailure, system: Boolean) {
- failed = true
- }
- }
-
companion object {
private const val OWNER_NAME = "KeyguardTransitionRunner"
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
index e44bc7b..d8e96bc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelTest.kt
@@ -51,14 +51,14 @@
@Test
fun showPrimaryBouncer() =
testScope.runTest {
- underTest.showPrimaryBouncer()
+ underTest.onTapped()
verify(statusBarKeyguardViewManager).showPrimaryBouncer(any())
}
@Test
fun hideAlternateBouncer() =
testScope.runTest {
- underTest.hideAlternateBouncer()
+ underTest.onRemovedFromWindow()
verify(statusBarKeyguardViewManager).hideAlternateBouncer(any())
}
@@ -102,34 +102,6 @@
}
@Test
- fun forcePluginOpen() =
- testScope.runTest {
- val forcePluginOpen by collectLastValue(underTest.forcePluginOpen)
-
- transitionRepository.sendTransitionSteps(
- listOf(
- stepToAlternateBouncer(0f, TransitionState.STARTED),
- stepToAlternateBouncer(.4f),
- stepToAlternateBouncer(.6f),
- stepToAlternateBouncer(1f),
- ),
- testScope,
- )
- assertThat(forcePluginOpen).isTrue()
-
- transitionRepository.sendTransitionSteps(
- listOf(
- stepFromAlternateBouncer(0f, TransitionState.STARTED),
- stepFromAlternateBouncer(.3f),
- stepFromAlternateBouncer(.6f),
- stepFromAlternateBouncer(1f),
- ),
- testScope,
- )
- assertThat(forcePluginOpen).isFalse()
- }
-
- @Test
fun registerForDismissGestures() =
testScope.runTest {
val registerForDismissGestures by collectLastValue(underTest.registerForDismissGestures)
diff --git a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
index 77977f3..24bea2c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardQuickAffordancesCombinedViewModelTest.kt
@@ -195,9 +195,7 @@
mSetFlagsRule.enableFlags(AConfigFlags.FLAG_KEYGUARD_BOTTOM_AREA_REFACTOR)
val featureFlags =
- FakeFeatureFlags().apply {
- set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false)
- }
+ FakeFeatureFlags().apply { set(Flags.LOCK_SCREEN_LONG_PRESS_ENABLED, false) }
val withDeps = KeyguardInteractorFactory.create(featureFlags = featureFlags)
keyguardInteractor = withDeps.keyguardInteractor
@@ -289,6 +287,7 @@
underTest =
KeyguardQuickAffordancesCombinedViewModel(
+ applicationScope = testScope.backgroundScope,
quickAffordanceInteractor =
KeyguardQuickAffordanceInteractor(
keyguardInteractor = keyguardInteractor,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
index b337ccf..8fbd3c8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/appselector/view/MediaProjectionRecentsViewControllerTest.kt
@@ -24,7 +24,6 @@
import android.view.ViewGroup
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
-import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX
import com.android.systemui.Flags.FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN
import com.android.systemui.SysuiTestCase
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorResultHandler
@@ -141,27 +140,13 @@
}
@Test
- fun onRecentAppClicked_fullScreenTaskInForeground_flagOff_usesScaleUpAnimation() {
- mSetFlagsRule.disableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX)
-
- controller.onRecentAppClicked(fullScreenTask, taskView)
-
- assertThat(getStartedTaskActivityOptions(fullScreenTask.taskId).animationType)
- .isEqualTo(ActivityOptions.ANIM_SCALE_UP)
- }
-
- @Test
- fun onRecentAppClicked_fullScreenTaskInForeground_flagOn_usesDefaultAnimation() {
- mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX)
+ fun onRecentAppClicked_fullScreenTaskInForeground_usesDefaultAnimation() {
assertForegroundTaskUsesDefaultCloseAnimation(fullScreenTask)
}
@Test
fun onRecentAppClicked_splitScreenTaskInForeground_flagOn_usesDefaultAnimation() {
- mSetFlagsRule.enableFlags(
- FLAG_PSS_APP_SELECTOR_ABRUPT_EXIT_FIX,
- FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN
- )
+ mSetFlagsRule.enableFlags(FLAG_PSS_APP_SELECTOR_RECENTS_SPLIT_SCREEN)
assertForegroundTaskUsesDefaultCloseAnimation(splitScreenTask)
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
index c57aa36..04ef1be 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/mediaprojection/permission/ShareToAppPermissionDialogDelegateTest.kt
@@ -29,6 +29,7 @@
import com.android.systemui.res.R
import com.android.systemui.statusbar.phone.AlertDialogWithDelegate
import com.android.systemui.statusbar.phone.SystemUIDialog
+import com.google.common.truth.Truth.assertThat
import kotlin.test.assertEquals
import org.junit.After
import org.junit.Test
@@ -44,8 +45,10 @@
private val appName = "Test App"
- private val resIdSingleApp = R.string.screen_share_permission_dialog_option_single_app
- private val resIdFullScreen = R.string.screen_share_permission_dialog_option_entire_screen
+ private val resIdSingleApp =
+ R.string.media_projection_entry_app_permission_dialog_option_text_single_app
+ private val resIdFullScreen =
+ R.string.media_projection_entry_app_permission_dialog_option_text_entire_screen
private val resIdSingleAppDisabled =
R.string.media_projection_entry_app_permission_dialog_single_app_disabled
@@ -115,6 +118,36 @@
assertEquals(context.getString(resIdFullScreen), secondOptionText)
}
+ @Test
+ fun startButtonText_entireScreenSelected() {
+ setUpAndShowDialog()
+ onSpinnerItemSelected(ENTIRE_SCREEN)
+
+ val startButtonText = dialog.requireViewById<TextView>(android.R.id.button1).text
+
+ assertThat(startButtonText)
+ .isEqualTo(
+ context.getString(
+ R.string.media_projection_entry_app_permission_dialog_continue_entire_screen
+ )
+ )
+ }
+
+ @Test
+ fun startButtonText_singleAppSelected() {
+ setUpAndShowDialog()
+ onSpinnerItemSelected(SINGLE_APP)
+
+ val startButtonText = dialog.requireViewById<TextView>(android.R.id.button1).text
+
+ assertThat(startButtonText)
+ .isEqualTo(
+ context.getString(
+ R.string.media_projection_entry_generic_permission_dialog_continue_single_app
+ )
+ )
+ }
+
private fun setUpAndShowDialog(
mediaProjectionConfig: MediaProjectionConfig? = null,
overrideDisableSingleAppOption: Boolean = false,
@@ -142,4 +175,10 @@
delegate.onCreate(dialog, savedInstanceState = null)
dialog.show()
}
+
+ private fun onSpinnerItemSelected(position: Int) {
+ val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
+ checkNotNull(spinner.onItemSelectedListener)
+ .onItemSelected(spinner, mock(), position, /* id= */ 0)
+ }
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
index a52ab0c..04d140c 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/navigationbar/views/NavigationBarTest.java
@@ -78,6 +78,7 @@
import androidx.test.ext.junit.runners.AndroidJUnit4;
import androidx.test.filters.SmallTest;
+import com.android.app.viewcapture.ViewCaptureAwareWindowManager;
import com.android.internal.logging.MetricsLogger;
import com.android.internal.logging.UiEventLogger;
import com.android.systemui.SysuiTestCase;
@@ -214,6 +215,8 @@
@Mock
private WindowManager mWindowManager;
@Mock
+ private ViewCaptureAwareWindowManager mViewCaptureAwareWindowManager;
+ @Mock
private TelecomManager mTelecomManager;
@Mock
private InputMethodManager mInputMethodManager;
@@ -619,6 +622,7 @@
null,
context,
mWindowManager,
+ mViewCaptureAwareWindowManager,
() -> mAssistManager,
mock(AccessibilityManager.class),
deviceProvisionedController,
diff --git a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
index 90e0dd8..0c2b59f 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/qs/UserSettingObserverTest.kt
@@ -17,25 +17,34 @@
package com.android.systemui.qs
import android.os.Handler
+import android.platform.test.flag.junit.FlagsParameterization
import android.testing.TestableLooper
-import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
+import com.android.systemui.Flags
import com.android.systemui.SysuiTestCase
-import com.android.systemui.util.settings.FakeSettings
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.testScope
import com.android.systemui.util.settings.SecureSettings
+import com.android.systemui.util.settings.fakeSettings
import com.google.common.truth.Truth.assertThat
import junit.framework.Assert.fail
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+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 platform.test.runner.parameterized.ParameterizedAndroidJunit4
+import platform.test.runner.parameterized.Parameters
private typealias Callback = (Int, Boolean) -> Unit
+@OptIn(ExperimentalCoroutinesApi::class)
@SmallTest
-@RunWith(AndroidJUnit4::class)
+@RunWith(ParameterizedAndroidJunit4::class)
@TestableLooper.RunWithLooper
-class UserSettingObserverTest : SysuiTestCase() {
+class UserSettingObserverTest(flags: FlagsParameterization) : SysuiTestCase() {
companion object {
private const val TEST_SETTING = "setting"
@@ -43,8 +52,23 @@
private const val OTHER_USER = 1
private const val DEFAULT_VALUE = 1
private val FAIL_CALLBACK: Callback = { _, _ -> fail("Callback should not be called") }
+
+ @JvmStatic
+ @Parameters(name = "{0}")
+ fun getParams(): List<FlagsParameterization> {
+ return FlagsParameterization.allCombinationsOf(
+ Flags.FLAG_QS_REGISTER_SETTING_OBSERVER_ON_BG_THREAD
+ )
+ }
}
+ init {
+ mSetFlagsRule.setFlagsParameterization(flags)
+ }
+
+ private val kosmos = Kosmos()
+ private val testScope = kosmos.testScope
+
private lateinit var testableLooper: TestableLooper
private lateinit var setting: UserSettingObserver
private lateinit var secureSettings: SecureSettings
@@ -54,7 +78,7 @@
@Before
fun setUp() {
testableLooper = TestableLooper.get(this)
- secureSettings = FakeSettings()
+ secureSettings = kosmos.fakeSettings
setting =
object :
@@ -76,92 +100,107 @@
@After
fun tearDown() {
- setting.isListening = false
+ setListening(false)
}
@Test
- fun testNotListeningByDefault() {
- callback = FAIL_CALLBACK
+ fun testNotListeningByDefault() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
- assertThat(setting.isListening).isFalse()
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- testableLooper.processAllMessages()
- }
+ assertThat(setting.isListening).isFalse()
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testChangedWhenListeningCallsCallback() {
- var changed = false
- callback = { _, _ -> changed = true }
+ fun testChangedWhenListeningCallsCallback() =
+ testScope.runTest {
+ var changed = false
+ callback = { _, _ -> changed = true }
- setting.isListening = true
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- testableLooper.processAllMessages()
+ setListening(true)
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ testableLooper.processAllMessages()
- assertThat(changed).isTrue()
- }
+ assertThat(changed).isTrue()
+ }
@Test
- fun testListensToCorrectSetting() {
- callback = FAIL_CALLBACK
+ fun testListensToCorrectSetting() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
- setting.isListening = true
- secureSettings.putIntForUser("other", 2, USER)
- testableLooper.processAllMessages()
- }
+ setListening(true)
+ secureSettings.putIntForUser("other", 2, USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testGetCorrectValue() {
- secureSettings.putIntForUser(TEST_SETTING, 2, USER)
- assertThat(setting.value).isEqualTo(2)
+ fun testGetCorrectValue() =
+ testScope.runTest {
+ secureSettings.putIntForUser(TEST_SETTING, 2, USER)
+ assertThat(setting.value).isEqualTo(2)
- secureSettings.putIntForUser(TEST_SETTING, 4, USER)
- assertThat(setting.value).isEqualTo(4)
- }
+ secureSettings.putIntForUser(TEST_SETTING, 4, USER)
+ assertThat(setting.value).isEqualTo(4)
+ }
@Test
- fun testSetValue() {
- setting.value = 5
- assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5)
- }
+ fun testSetValue() =
+ testScope.runTest {
+ setting.value = 5
+ assertThat(secureSettings.getIntForUser(TEST_SETTING, USER)).isEqualTo(5)
+ }
@Test
- fun testChangeUser() {
- setting.isListening = true
- setting.setUserId(OTHER_USER)
+ fun testChangeUser() =
+ testScope.runTest {
+ setListening(true)
+ setting.setUserId(OTHER_USER)
- setting.isListening = true
- assertThat(setting.currentUser).isEqualTo(OTHER_USER)
- }
+ setListening(true)
+ assertThat(setting.currentUser).isEqualTo(OTHER_USER)
+ }
@Test
- fun testDoesntListenInOtherUsers() {
- callback = FAIL_CALLBACK
- setting.isListening = true
+ fun testDoesntListenInOtherUsers() =
+ testScope.runTest {
+ callback = FAIL_CALLBACK
+ setListening(true)
- secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER)
- testableLooper.processAllMessages()
- }
+ secureSettings.putIntForUser(TEST_SETTING, 3, OTHER_USER)
+ testableLooper.processAllMessages()
+ }
@Test
- fun testListensToCorrectUserAfterChange() {
- var changed = false
- callback = { _, _ -> changed = true }
+ fun testListensToCorrectUserAfterChange() =
+ testScope.runTest {
+ var changed = false
+ callback = { _, _ -> changed = true }
- setting.isListening = true
- setting.setUserId(OTHER_USER)
- secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER)
- testableLooper.processAllMessages()
+ setListening(true)
+ setting.setUserId(OTHER_USER)
+ testScope.runCurrent()
+ secureSettings.putIntForUser(TEST_SETTING, 2, OTHER_USER)
+ testableLooper.processAllMessages()
- assertThat(changed).isTrue()
- }
+ assertThat(changed).isTrue()
+ }
@Test
- fun testDefaultValue() {
- // Check default value before listening
- assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ fun testDefaultValue() =
+ testScope.runTest {
+ // Check default value before listening
+ assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
- // Check default value if setting is not set
- setting.isListening = true
- assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ // Check default value if setting is not set
+ setListening(true)
+ assertThat(setting.value).isEqualTo(DEFAULT_VALUE)
+ }
+
+ fun setListening(listening: Boolean) {
+ setting.isListening = listening
+ testScope.runCurrent()
}
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
index 79c206c..3f550ca 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/RecordingServiceTest.java
@@ -16,8 +16,13 @@
package com.android.systemui.screenrecord;
+import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_ERROR_SAVING;
+import static com.android.systemui.screenrecord.RecordingService.GROUP_KEY_SAVED;
+
import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertFalse;
import static org.junit.Assert.assertThrows;
+import static org.junit.Assert.assertTrue;
import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.eq;
@@ -25,6 +30,7 @@
import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.doThrow;
import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.reset;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
@@ -73,8 +79,6 @@
@Mock
private ScreenMediaRecorder mScreenMediaRecorder;
@Mock
- private Notification mNotification;
- @Mock
private Executor mExecutor;
@Mock
private Handler mHandler;
@@ -124,10 +128,6 @@
// Mock notifications
doNothing().when(mRecordingService).createRecordingNotification();
- doReturn(mNotification).when(mRecordingService).createProcessingNotification();
- doReturn(mNotification).when(mRecordingService).createSaveNotification(any());
- doNothing().when(mRecordingService).createErrorStartingNotification();
- doNothing().when(mRecordingService).createErrorSavingNotification();
doNothing().when(mRecordingService).showErrorToast(anyInt());
doNothing().when(mRecordingService).stopForeground(anyInt());
@@ -228,6 +228,33 @@
}
@Test
+ public void testOnSystemRequestedStop_whenRecordingInProgress_showsNotifications() {
+ doReturn(true).when(mController).isRecording();
+
+ mRecordingService.onStopped();
+
+ // Processing notification
+ ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
+ verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ assertEquals(GROUP_KEY_SAVED, notifCaptor.getValue().getGroup());
+
+ reset(mNotificationManager);
+ verify(mExecutor).execute(mRunnableCaptor.capture());
+ mRunnableCaptor.getValue().run();
+
+ verify(mNotificationManager, times(2))
+ .notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ // Saved notification
+ Notification saveNotification = notifCaptor.getAllValues().get(0);
+ assertFalse(saveNotification.isGroupSummary());
+ assertEquals(GROUP_KEY_SAVED, saveNotification.getGroup());
+ // Group summary notification
+ Notification groupSummaryNotification = notifCaptor.getAllValues().get(1);
+ assertTrue(groupSummaryNotification.isGroupSummary());
+ assertEquals(GROUP_KEY_SAVED, groupSummaryNotification.getGroup());
+ }
+
+ @Test
public void testOnSystemRequestedStop_recorderEndThrowsRuntimeException_showsErrorNotification()
throws IOException {
doReturn(true).when(mController).isRecording();
@@ -235,7 +262,11 @@
mRecordingService.onStopped();
- verify(mRecordingService).createErrorSavingNotification();
+ verify(mRecordingService).createErrorSavingNotification(any());
+ ArgumentCaptor<Notification> notifCaptor = ArgumentCaptor.forClass(Notification.class);
+ verify(mNotificationManager).notifyAsUser(any(), anyInt(), notifCaptor.capture(), any());
+ assertTrue(notifCaptor.getValue().isGroupSummary());
+ assertEquals(GROUP_KEY_ERROR_SAVING, notifCaptor.getValue().getGroup());
}
@Test
diff --git a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
index 11b0bdf..7dae5cc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/screenrecord/ScreenRecordPermissionDialogDelegateTest.kt
@@ -21,13 +21,13 @@
import android.testing.TestableLooper
import android.view.View
import android.widget.Spinner
+import android.widget.TextView
import androidx.test.ext.junit.runners.AndroidJUnit4
import androidx.test.filters.SmallTest
import com.android.systemui.Dependency
import com.android.systemui.SysuiTestCase
import com.android.systemui.animation.DialogTransitionAnimator
import com.android.systemui.broadcast.BroadcastDispatcher
-import com.android.systemui.flags.FeatureFlags
import com.android.systemui.mediaprojection.MediaProjectionMetricsLogger
import com.android.systemui.mediaprojection.appselector.MediaProjectionAppSelectorActivity
import com.android.systemui.mediaprojection.permission.ENTIRE_SCREEN
@@ -60,7 +60,6 @@
@Mock private lateinit var starter: ActivityStarter
@Mock private lateinit var controller: RecordingController
@Mock private lateinit var userContextProvider: UserContextProvider
- @Mock private lateinit var flags: FeatureFlags
@Mock private lateinit var onStartRecordingClicked: Runnable
@Mock private lateinit var mediaProjectionMetricsLogger: MediaProjectionMetricsLogger
@@ -128,6 +127,32 @@
}
@Test
+ fun startButtonText_entireScreenSelected() {
+ showDialog()
+
+ onSpinnerItemSelected(ENTIRE_SCREEN)
+
+ assertThat(getStartButton().text)
+ .isEqualTo(
+ context.getString(R.string.screenrecord_permission_dialog_continue_entire_screen)
+ )
+ }
+
+ @Test
+ fun startButtonText_singleAppSelected() {
+ showDialog()
+
+ onSpinnerItemSelected(SINGLE_APP)
+
+ assertThat(getStartButton().text)
+ .isEqualTo(
+ context.getString(
+ R.string.media_projection_entry_generic_permission_dialog_continue_single_app
+ )
+ )
+ }
+
+ @Test
fun startClicked_singleAppSelected_passesHostUidToAppSelector() {
showDialog()
onSpinnerItemSelected(SINGLE_APP)
@@ -152,7 +177,8 @@
showDialog()
val spinner = dialog.requireViewById<Spinner>(R.id.screen_share_mode_options)
- val singleApp = context.getString(R.string.screen_share_permission_dialog_option_single_app)
+ val singleApp =
+ context.getString(R.string.screenrecord_permission_dialog_option_text_single_app)
assertEquals(spinner.adapter.getItem(0), singleApp)
}
@@ -208,8 +234,10 @@
dialog.requireViewById<View>(android.R.id.button2).performClick()
}
+ private fun getStartButton() = dialog.requireViewById<TextView>(android.R.id.button1)
+
private fun clickOnStart() {
- dialog.requireViewById<View>(android.R.id.button1).performClick()
+ getStartButton().performClick()
}
private fun onSpinnerItemSelected(position: Int) {
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
index 2803035..8125ef5 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/NotificationPanelViewControllerBaseTest.java
@@ -192,6 +192,7 @@
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherController;
import com.android.systemui.statusbar.policy.KeyguardUserSwitcherView;
import com.android.systemui.statusbar.policy.ResourcesSplitShadeStateController;
+import com.android.systemui.statusbar.policy.SplitShadeStateController;
import com.android.systemui.statusbar.policy.data.repository.FakeUserSetupRepository;
import com.android.systemui.statusbar.window.StatusBarWindowStateController;
import com.android.systemui.unfold.SysUIUnfoldComponent;
@@ -428,6 +429,9 @@
mock(DeviceEntryUdfpsInteractor.class);
when(deviceEntryUdfpsInteractor.isUdfpsSupported()).thenReturn(MutableStateFlow(false));
+ final SplitShadeStateController splitShadeStateController =
+ new ResourcesSplitShadeStateController();
+
mShadeInteractor = new ShadeInteractorImpl(
mTestScope.getBackgroundScope(),
mKosmos.getDeviceProvisioningInteractor(),
@@ -445,7 +449,8 @@
new SharedNotificationContainerInteractor(
new FakeConfigurationRepository(),
mContext,
- new ResourcesSplitShadeStateController(),
+ () -> splitShadeStateController,
+ () -> mShadeInteractor,
mKeyguardInteractor,
deviceEntryUdfpsInteractor,
() -> mLargeScreenHeaderHelper
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
index e57382d..505f799 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplBaseTest.java
@@ -225,7 +225,8 @@
new SharedNotificationContainerInteractor(
configurationRepository,
mContext,
- splitShadeStateController,
+ () -> splitShadeStateController,
+ () -> mShadeInteractor,
keyguardInteractor,
deviceEntryUdfpsInteractor,
() -> mLargeScreenHeaderHelper),
@@ -266,6 +267,7 @@
when(mPanelView.getParent()).thenReturn(mPanelViewParent);
when(mQs.getHeader()).thenReturn(mQsHeader);
+ when(mQSFragment.getHeader()).thenReturn(mQsHeader);
doAnswer(invocation -> {
mLockscreenShadeTransitionCallback = invocation.getArgument(0);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
index e7db469..2e9d6e8 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/shade/QuickSettingsControllerImplTest.java
@@ -24,34 +24,41 @@
import static android.view.MotionEvent.BUTTON_SECONDARY;
import static android.view.MotionEvent.BUTTON_STYLUS_PRIMARY;
-import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR;
import static com.android.systemui.Flags.FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT;
import static com.android.systemui.statusbar.StatusBarState.KEYGUARD;
import static com.android.systemui.statusbar.StatusBarState.SHADE;
import static com.google.common.truth.Truth.assertThat;
+import static org.mockito.ArgumentMatchers.any;
import static org.mockito.ArgumentMatchers.anyFloat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.atLeastOnce;
+import static org.mockito.Mockito.inOrder;
+import static org.mockito.Mockito.never;
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
+import android.graphics.Rect;
+import android.platform.test.annotations.DisableFlags;
import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.FlagsParameterization;
import android.testing.TestableLooper;
import android.view.MotionEvent;
+import android.view.ViewGroup;
import androidx.test.filters.SmallTest;
import com.android.systemui.plugins.qs.QS;
+import com.android.systemui.qs.flags.QSComposeFragment;
import com.android.systemui.res.R;
import com.android.systemui.statusbar.notification.footer.shared.FooterViewRefactor;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.ArgumentCaptor;
+import org.mockito.InOrder;
import java.util.List;
@@ -65,7 +72,7 @@
@Parameters(name = "{0}")
public static List<FlagsParameterization> getParams() {
- return progressionOf(FLAG_QS_UI_REFACTOR, FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT);
+ return progressionOf(FLAG_QS_UI_REFACTOR_COMPOSE_FRAGMENT);
}
public QuickSettingsControllerImplTest(FlagsParameterization flags) {
@@ -244,6 +251,61 @@
}
@Test
+ @DisableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQsFragmentAttached_qsComposeFragmentDisabled_setHeaderInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController)
+ .setQsHeader((ViewGroup) mQSFragment.getHeader());
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(any());
+ }
+
+ @Test
+ @EnableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQsFragmentAttached_qsComposeFragmentEnabled_setQsHeaderBoundsProviderInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController, never())
+ .setQsHeader((ViewGroup) mQSFragment.getHeader());
+ ArgumentCaptor<QSHeaderBoundsProvider> argumentCaptor =
+ ArgumentCaptor.forClass(QSHeaderBoundsProvider.class);
+
+ verify(mNotificationStackScrollLayoutController)
+ .setQsHeaderBoundsProvider(argumentCaptor.capture());
+
+ argumentCaptor.getValue().getLeftProvider().invoke();
+ argumentCaptor.getValue().getHeightProvider().invoke();
+ argumentCaptor.getValue().getBoundsOnScreenProvider().invoke(new Rect());
+ InOrder inOrderVerifier = inOrder(mQSFragment);
+
+ inOrderVerifier.verify(mQSFragment).getHeaderLeft();
+ inOrderVerifier.verify(mQSFragment).getHeaderHeight();
+ inOrderVerifier.verify(mQSFragment).getHeaderBoundsOnScreen(new Rect());
+ }
+
+ @Test
+ @DisableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQSFragmentDetached_qsComposeFragmentFlagDisabled_setViewToNullInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController).setQsHeader(null);
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeaderBoundsProvider(null);
+ }
+
+ @Test
+ @EnableFlags(QSComposeFragment.FLAG_NAME)
+ public void onQSFragmentDetached_qsComposeFragmentFlagEnabled_setBoundsProviderToNullInNSSL() {
+ mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
+
+ mFragmentListener.onFragmentViewDestroyed(QS.TAG, mQSFragment);
+
+ verify(mNotificationStackScrollLayoutController, never()).setQsHeader(null);
+ verify(mNotificationStackScrollLayoutController).setQsHeaderBoundsProvider(null);
+ }
+
+ @Test
public void onQsFragmentAttached_notFullWidth_setsFullWidthFalseOnQS() {
setIsFullWidth(false);
mFragmentListener.onFragmentViewCreated(QS.TAG, mQSFragment);
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
index 80011dc..a75d7b2 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/KeyguardIndicationControllerTest.java
@@ -25,6 +25,7 @@
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_AVAILABLE;
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FACE_NOT_RECOGNIZED;
import static com.android.keyguard.KeyguardUpdateMonitor.BIOMETRIC_HELP_FINGERPRINT_NOT_RECOGNIZED;
+import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ADAPTIVE_AUTH;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_ALIGNMENT;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BATTERY;
import static com.android.systemui.keyguard.KeyguardIndicationRotateTextViewController.INDICATION_TYPE_BIOMETRIC_MESSAGE;
@@ -1535,6 +1536,48 @@
trustGrantedMsg);
}
+ @Test
+ public void updateAdaptiveAuthMessage_whenNotLockedByAdaptiveAuth_doesNotShowMsg() {
+ // When the device is not locked by adaptive auth
+ when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser()))
+ .thenReturn(false);
+ createController();
+ mController.setVisible(true);
+
+ // Verify that the adaptive auth message does not show
+ verifyNoMessage(INDICATION_TYPE_ADAPTIVE_AUTH);
+ }
+
+ @Test
+ public void updateAdaptiveAuthMessage_whenLockedByAdaptiveAuth_cannotSkipBouncer_showsMsg() {
+ // When the device is locked by adaptive auth, and the user cannot skip bouncer
+ when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser()))
+ .thenReturn(true);
+ when(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser())).thenReturn(false);
+ createController();
+ mController.setVisible(true);
+
+ // Verify that the adaptive auth message shows
+ String message = mContext.getString(R.string.keyguard_indication_after_adaptive_auth_lock);
+ verifyIndicationMessage(INDICATION_TYPE_ADAPTIVE_AUTH, message);
+ }
+
+ @Test
+ public void updateAdaptiveAuthMessage_whenLockedByAdaptiveAuth_canSkipBouncer_doesNotShowMsg() {
+ createController();
+ mController.setVisible(true);
+
+ // When the device is locked by adaptive auth, but the device unlocked state changes and the
+ // user can skip bouncer
+ when(mKeyguardUpdateMonitor.isDeviceLockedByAdaptiveAuth(getCurrentUser()))
+ .thenReturn(true);
+ when(mKeyguardUpdateMonitor.getUserCanSkipBouncer(getCurrentUser())).thenReturn(true);
+ mKeyguardStateControllerCallback.onUnlockedChanged();
+
+ // Verify that the adaptive auth message does not show
+ verifyNoMessage(INDICATION_TYPE_ADAPTIVE_AUTH);
+ }
+
private void screenIsTurningOn() {
when(mScreenLifecycle.getScreenState()).thenReturn(SCREEN_TURNING_ON);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
index 3f28164..491919a 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/notification/row/NotificationRowContentBinderImplTest.kt
@@ -44,6 +44,7 @@
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationCallback
import com.android.systemui.statusbar.notification.row.NotificationRowContentBinder.InflationFlag
import com.android.systemui.statusbar.notification.row.shared.HeadsUpStatusBarModel
+import com.android.systemui.statusbar.notification.row.shared.LockscreenOtpRedaction
import com.android.systemui.statusbar.notification.row.shared.NewRemoteViews
import com.android.systemui.statusbar.notification.row.shared.NotificationContentModel
import com.android.systemui.statusbar.notification.row.shared.NotificationRowContentBinderRefactor
@@ -78,7 +79,7 @@
@SmallTest
@RunWith(AndroidJUnit4::class)
@RunWithLooper
-@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME)
+@EnableFlags(NotificationRowContentBinderRefactor.FLAG_NAME, LockscreenOtpRedaction.FLAG_NAME)
class NotificationRowContentBinderImplTest : SysuiTestCase() {
private lateinit var notificationInflater: NotificationRowContentBinderImpl
private lateinit var builder: Notification.Builder
diff --git a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
index af5e60e..9b61105 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
+++ b/packages/SystemUI/tests/src/com/android/systemui/statusbar/phone/StatusBarKeyguardViewManagerTest.java
@@ -1068,7 +1068,7 @@
public void testShowBouncerOrKeyguard_needsFullScreen() {
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mCentralSurfaces).hideKeyguard();
verify(mPrimaryBouncerInteractor).show(true);
}
@@ -1084,7 +1084,7 @@
.thenReturn(KeyguardState.LOCKSCREEN);
reset(mCentralSurfaces);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, false);
verify(mPrimaryBouncerInteractor).show(true);
verify(mCentralSurfaces).showKeyguard();
}
@@ -1092,11 +1092,26 @@
@Test
@DisableSceneContainer
public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing() {
+ boolean isFalsingReset = false;
when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
KeyguardSecurityModel.SecurityMode.SimPin);
when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
- mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
verify(mCentralSurfaces, never()).hideKeyguard();
+ verify(mPrimaryBouncerInteractor).show(true);
+ }
+
+ @Test
+ @DisableSceneContainer
+ public void testShowBouncerOrKeyguard_needsFullScreen_bouncerAlreadyShowing_onFalsing() {
+ boolean isFalsingReset = true;
+ when(mKeyguardSecurityModel.getSecurityMode(anyInt())).thenReturn(
+ KeyguardSecurityModel.SecurityMode.SimPin);
+ when(mPrimaryBouncerInteractor.isFullyShowing()).thenReturn(true);
+ mStatusBarKeyguardViewManager.showBouncerOrKeyguard(false, isFalsingReset);
+ verify(mCentralSurfaces, never()).hideKeyguard();
+
+ // Do not refresh the full screen bouncer if the call is from falsing
verify(mPrimaryBouncerInteractor, never()).show(true);
}
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
index b0acd03..2e6d0fc 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/SettingsProxyTest.kt
@@ -385,7 +385,7 @@
private class FakeSettingsProxy(val testDispatcher: CoroutineDispatcher) : SettingsProxy {
private val mContentResolver = mock(ContentResolver::class.java)
- private val settingToValueMap: MutableMap<String, String> = mutableMapOf()
+ private val settingToValueMap: MutableMap<String, String?> = mutableMapOf()
override fun getContentResolver() = mContentResolver
@@ -399,15 +399,15 @@
return settingToValueMap[name] ?: ""
}
- override fun putString(name: String, value: String): Boolean {
+ override fun putString(name: String, value: String?): Boolean {
settingToValueMap[name] = value
return true
}
override fun putString(
name: String,
- value: String,
- tag: String,
+ value: String?,
+ tag: String?,
makeDefault: Boolean
): Boolean {
settingToValueMap[name] = value
diff --git a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
index eaeece9..00b8cd0 100644
--- a/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
+++ b/packages/SystemUI/tests/src/com/android/systemui/util/settings/UserSettingsProxyTest.kt
@@ -561,7 +561,7 @@
) : UserSettingsProxy {
private val mContentResolver = mock(ContentResolver::class.java)
- private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String>> =
+ private val userIdToSettingsValueMap: MutableMap<Int, MutableMap<String, String?>> =
mutableMapOf()
override fun getContentResolver() = mContentResolver
@@ -577,7 +577,7 @@
override fun putString(
name: String,
- value: String,
+ value: String?,
overrideableByRestore: Boolean
): Boolean {
userIdToSettingsValueMap[DEFAULT_USER_ID]?.put(name, value)
@@ -586,22 +586,22 @@
override fun putString(
name: String,
- value: String,
- tag: String,
+ value: String?,
+ tag: String?,
makeDefault: Boolean
): Boolean {
putStringForUser(name, value, DEFAULT_USER_ID)
return true
}
- override fun putStringForUser(name: String, value: String, userHandle: Int): Boolean {
+ override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean {
userIdToSettingsValueMap[userHandle] = mutableMapOf(Pair(name, value))
return true
}
override fun putStringForUser(
name: String,
- value: String,
+ value: String?,
tag: String?,
makeDefault: Boolean,
userHandle: Int,
diff --git a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
index 185deea..a61233a 100644
--- a/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/android/content/ContextKosmos.kt
@@ -16,10 +16,12 @@
package android.content
+import com.android.systemui.SysuiTestableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testCase
import com.android.systemui.util.mockito.mock
-var Kosmos.applicationContext: Context by
+var Kosmos.testableContext: SysuiTestableContext by
Kosmos.Fixture { testCase.context.apply { ensureTestableResources() } }
+var Kosmos.applicationContext: Context by Kosmos.Fixture { testableContext }
val Kosmos.mockedContext: Context by Kosmos.Fixture { mock<Context>() }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
index 727de9e..4571c19 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/data/repository/FakeKeyguardRepository.kt
@@ -74,6 +74,8 @@
private val _dozeTimeTick = MutableStateFlow<Long>(0L)
override val dozeTimeTick = _dozeTimeTick
+ override val showDismissibleKeyguard = MutableStateFlow<Long>(0L)
+
private val _lastDozeTapToWakePosition = MutableStateFlow<Point?>(null)
override val lastDozeTapToWakePosition = _lastDozeTapToWakePosition.asStateFlow()
@@ -206,6 +208,10 @@
_dozeTimeTick.value = millis
}
+ override fun showDismissibleKeyguard() {
+ showDismissibleKeyguard.value = showDismissibleKeyguard.value + 1
+ }
+
override fun setLastDozeTapToWakePosition(position: Point) {
_lastDozeTapToWakePosition.value = position
}
@@ -216,6 +222,9 @@
override fun setDreaming(isDreaming: Boolean) {
_isDreaming.value = isDreaming
+ // Intentionally set both for testing, to avoid races with merge() in the interactor that
+ // would make testing difficult
+ _isDreamingWithOverlay.value = isDreaming
}
fun setDreamingWithOverlay(isDreaming: Boolean) {
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
index c5da10e..b68d6a0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/domain/interactor/KeyguardTransitionInteractorKosmos.kt
@@ -33,6 +33,7 @@
fromAodTransitionInteractor = { fromAodTransitionInteractor },
fromAlternateBouncerTransitionInteractor = { fromAlternateBouncerTransitionInteractor },
fromDozingTransitionInteractor = { fromDozingTransitionInteractor },
+ fromOccludedTransitionInteractor = { fromOccludedTransitionInteractor },
sceneInteractor = sceneInteractor
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
index 2919d3f..1e95fc1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/binder/AlternateBouncerViewBinderKosmos.kt
@@ -17,7 +17,6 @@
package com.android.systemui.keyguard.ui.binder
import android.content.applicationContext
-import android.view.layoutInflater
import android.view.mockedLayoutInflater
import android.view.windowManager
import com.android.systemui.biometrics.domain.interactor.fingerprintPropertyInteractor
@@ -25,7 +24,6 @@
import com.android.systemui.common.ui.domain.interactor.configurationInteractor
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.deviceentry.ui.viewmodel.AlternateBouncerUdfpsAccessibilityOverlayViewModel
-import com.android.systemui.keyguard.dismissCallbackRegistry
import com.android.systemui.keyguard.ui.SwipeUpAnywhereGestureHandler
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerDependencies
import com.android.systemui.keyguard.ui.viewmodel.AlternateBouncerMessageAreaViewModel
@@ -50,10 +48,10 @@
alternateBouncerDependencies = { alternateBouncerDependencies },
windowManager = { windowManager },
layoutInflater = { mockedLayoutInflater },
- dismissCallbackRegistry = dismissCallbackRegistry,
)
}
+@ExperimentalCoroutinesApi
private val Kosmos.alternateBouncerDependencies by
Kosmos.Fixture {
AlternateBouncerDependencies(
@@ -69,6 +67,7 @@
)
}
+@ExperimentalCoroutinesApi
private val Kosmos.alternateBouncerUdfpsIconViewModel by
Kosmos.Fixture {
AlternateBouncerUdfpsIconViewModel(
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
index bdd4afa..2958315 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/AlternateBouncerViewModelKosmos.kt
@@ -18,6 +18,8 @@
package com.android.systemui.keyguard.ui.viewmodel
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
+import com.android.systemui.keyguard.dismissCallbackRegistry
import com.android.systemui.keyguard.domain.interactor.keyguardTransitionInteractor
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
@@ -28,5 +30,7 @@
AlternateBouncerViewModel(
statusBarKeyguardViewManager = statusBarKeyguardViewManager,
keyguardTransitionInteractor = keyguardTransitionInteractor,
+ dismissCallbackRegistry = dismissCallbackRegistry,
+ alternateBouncerInteractor = { alternateBouncerInteractor },
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
index 82860fc..b9443bc 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/keyguard/ui/viewmodel/KeyguardRootViewModelKosmos.kt
@@ -39,6 +39,7 @@
communalInteractor = communalInteractor,
keyguardTransitionInteractor = keyguardTransitionInteractor,
notificationsKeyguardInteractor = notificationsKeyguardInteractor,
+ alternateBouncerToAodTransitionViewModel = alternateBouncerToAodTransitionViewModel,
alternateBouncerToGoneTransitionViewModel = alternateBouncerToGoneTransitionViewModel,
alternateBouncerToLockscreenTransitionViewModel =
alternateBouncerToLockscreenTransitionViewModel,
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
similarity index 64%
copy from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
index 460de8c..73b1859 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/tiles/impl/airplane/AirplaneModeTileKosmos.kt
@@ -14,18 +14,11 @@
* limitations under the License.
*/
-package android.telephony.satellite.stub;
+package com.android.systemui.qs.tiles.impl.airplane
-/**
- * {@hide}
- */
-parcelable ProvisionSubscriberId {
- /** provision subscriberId */
- String subscriberId;
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.qs.qsEventLogger
+import com.android.systemui.statusbar.connectivity.ConnectivityModule
- /** carrier id */
- int mCarrierId;
-
- /** apn */
- String mNiddApn;
-}
+val Kosmos.qsAirplaneModeTileConfig by
+ Kosmos.Fixture { ConnectivityModule.provideAirplaneModeTileConfig(qsEventLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt
similarity index 78%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt
index 299b22e..5d70ed6 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneActionsViewModelKosmos.kt
@@ -18,13 +18,11 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModel
-val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by
+val Kosmos.quickSettingsShadeSceneActionsViewModel: QuickSettingsShadeSceneActionsViewModel by
Kosmos.Fixture {
- QuickSettingsShadeSceneViewModel(
+ QuickSettingsShadeSceneActionsViewModel(
shadeInteractor = shadeInteractor,
- overlayShadeViewModel = overlayShadeViewModel,
quickSettingsContainerViewModel = quickSettingsContainerViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt
similarity index 73%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt
index 299b22e..5ad5cb2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/qs/ui/viewmodel/QuickSettingsShadeSceneContentViewModelKosmos.kt
@@ -14,17 +14,18 @@
* limitations under the License.
*/
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
package com.android.systemui.qs.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModel
+import com.android.systemui.shade.ui.viewmodel.overlayShadeViewModelFactory
+import kotlinx.coroutines.ExperimentalCoroutinesApi
-val Kosmos.quickSettingsShadeSceneViewModel: QuickSettingsShadeSceneViewModel by
+val Kosmos.quickSettingsShadeSceneContentViewModel: QuickSettingsShadeSceneContentViewModel by
Kosmos.Fixture {
- QuickSettingsShadeSceneViewModel(
- shadeInteractor = shadeInteractor,
- overlayShadeViewModel = overlayShadeViewModel,
+ QuickSettingsShadeSceneContentViewModel(
+ overlayShadeViewModelFactory = overlayShadeViewModelFactory,
quickSettingsContainerViewModel = quickSettingsContainerViewModel,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
index 8e76a0b..53b6a2e 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/scene/domain/startable/SceneContainerStartableKosmos.kt
@@ -18,6 +18,7 @@
import com.android.internal.logging.uiEventLogger
import com.android.systemui.authentication.domain.interactor.authenticationInteractor
+import com.android.systemui.bouncer.domain.interactor.alternateBouncerInteractor
import com.android.systemui.bouncer.domain.interactor.bouncerInteractor
import com.android.systemui.bouncer.domain.interactor.simBouncerInteractor
import com.android.systemui.classifier.falsingCollector
@@ -80,5 +81,6 @@
keyguardEnabledInteractor = keyguardEnabledInteractor,
dismissCallbackRegistry = dismissCallbackRegistry,
statusBarStateController = sysuiStatusBarStateController,
+ alternateBouncerInteractor = alternateBouncerInteractor,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt
index 8fb370c..32a5614 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/settings/brightness/ui/viewmodel/BrightnessMirrorViewModelKosmos.kt
@@ -18,15 +18,23 @@
import android.content.res.mainResources
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.settings.brightness.domain.interactor.brightnessMirrorShowingInteractor
import com.android.systemui.settings.brightness.ui.viewModel.BrightnessMirrorViewModel
import com.android.systemui.settings.brightnessSliderControllerFactory
-val Kosmos.brightnessMirrorViewModel by
- Kosmos.Fixture {
- BrightnessMirrorViewModel(
- brightnessMirrorShowingInteractor,
- mainResources,
- brightnessSliderControllerFactory,
- )
+val Kosmos.brightnessMirrorViewModel by Fixture {
+ BrightnessMirrorViewModel(
+ brightnessMirrorShowingInteractor,
+ mainResources,
+ brightnessSliderControllerFactory,
+ )
+}
+
+val Kosmos.brightnessMirrorViewModelFactory by Fixture {
+ object : BrightnessMirrorViewModel.Factory {
+ override fun create(): BrightnessMirrorViewModel {
+ return brightnessMirrorViewModel
+ }
}
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
index ea02d0c..6d488d2 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtil.kt
@@ -18,10 +18,13 @@
import com.android.compose.animation.scene.ObservableTransitionState
import com.android.compose.animation.scene.SceneKey
+import com.android.systemui.SysuiTestableContext
+import com.android.systemui.res.R
import com.android.systemui.scene.domain.interactor.SceneInteractor
import com.android.systemui.scene.shared.flag.SceneContainerFlag
import com.android.systemui.scene.shared.model.Scenes
import com.android.systemui.shade.data.repository.FakeShadeRepository
+import com.android.systemui.shade.data.repository.ShadeRepository
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.flowOf
import kotlinx.coroutines.test.TestScope
@@ -86,6 +89,11 @@
delegate.assertFlagValid()
delegate.setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer)
}
+
+ fun setSplitShade(splitShade: Boolean) {
+ delegate.assertFlagValid()
+ delegate.setSplitShade(splitShade)
+ }
}
/** Sets up shade state for tests for a specific value of the scene container flag. */
@@ -117,11 +125,16 @@
fun setQsFullscreen(qsFullscreen: Boolean)
fun setLegacyExpandedOrAwaitingInputTransfer(legacyExpandedOrAwaitingInputTransfer: Boolean)
+
+ fun setSplitShade(splitShade: Boolean)
}
/** Sets up shade state for tests when the scene container flag is disabled. */
-class ShadeTestUtilLegacyImpl(val testScope: TestScope, val shadeRepository: FakeShadeRepository) :
- ShadeTestUtilDelegate {
+class ShadeTestUtilLegacyImpl(
+ val testScope: TestScope,
+ val shadeRepository: FakeShadeRepository,
+ val context: SysuiTestableContext
+) : ShadeTestUtilDelegate {
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
shadeRepository.setLegacyShadeExpansion(shadeExpansion)
shadeRepository.setQsExpansion(qsExpansion)
@@ -168,11 +181,22 @@
override fun setLegacyExpandedOrAwaitingInputTransfer(expanded: Boolean) {
shadeRepository.setLegacyExpandedOrAwaitingInputTransfer(expanded)
}
+
+ override fun setSplitShade(splitShade: Boolean) {
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_split_notification_shade, splitShade)
+ testScope.runCurrent()
+ }
}
/** Sets up shade state for tests when the scene container flag is enabled. */
-class ShadeTestUtilSceneImpl(val testScope: TestScope, val sceneInteractor: SceneInteractor) :
- ShadeTestUtilDelegate {
+class ShadeTestUtilSceneImpl(
+ val testScope: TestScope,
+ val sceneInteractor: SceneInteractor,
+ val shadeRepository: ShadeRepository,
+ val context: SysuiTestableContext,
+) : ShadeTestUtilDelegate {
val isUserInputOngoing = MutableStateFlow(true)
override fun setShadeAndQsExpansion(shadeExpansion: Float, qsExpansion: Float) {
@@ -263,6 +287,14 @@
testScope.runCurrent()
}
+ override fun setSplitShade(splitShade: Boolean) {
+ context
+ .getOrCreateTestableResources()
+ .addOverride(R.bool.config_use_split_notification_shade, splitShade)
+ shadeRepository.setShadeLayoutWide(splitShade)
+ testScope.runCurrent()
+ }
+
override fun assertFlagValid() {
Assert.assertTrue(SceneContainerFlag.isEnabled)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
index 9eeb345..a1551e0 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ShadeTestUtilKosmos.kt
@@ -16,6 +16,7 @@
package com.android.systemui.shade
+import android.content.testableContext
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.testScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
@@ -26,9 +27,14 @@
Kosmos.Fixture {
ShadeTestUtil(
if (SceneContainerFlag.isEnabled) {
- ShadeTestUtilSceneImpl(testScope, sceneInteractor)
+ ShadeTestUtilSceneImpl(
+ testScope,
+ sceneInteractor,
+ fakeShadeRepository,
+ testableContext
+ )
} else {
- ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository)
+ ShadeTestUtilLegacyImpl(testScope, fakeShadeRepository, testableContext)
}
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
index bfd6614..54208b9 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/domain/interactor/ShadeInteractorKosmos.kt
@@ -44,7 +44,7 @@
ShadeInteractorSceneContainerImpl(
scope = applicationCoroutineScope,
sceneInteractor = sceneInteractor,
- sharedNotificationContainerInteractor = sharedNotificationContainerInteractor,
+ shadeRepository = shadeRepository,
)
}
val Kosmos.shadeInteractorLegacyImpl by
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt
similarity index 74%
rename from packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
rename to packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt
index 72a80d4..9bf4756 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneActionsViewModelKosmos.kt
@@ -17,8 +17,13 @@
package com.android.systemui.shade.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneActionsViewModel
import com.android.systemui.shade.domain.interactor.shadeInteractor
-val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
- Kosmos.Fixture { NotificationsShadeSceneViewModel(shadeInteractor) }
+val Kosmos.notificationsShadeSceneActionsViewModel:
+ NotificationsShadeSceneActionsViewModel by Fixture {
+ NotificationsShadeSceneActionsViewModel(
+ shadeInteractor = shadeInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt
new file mode 100644
index 0000000..9240102
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneContentViewModelKosmos.kt
@@ -0,0 +1,34 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+@file:OptIn(ExperimentalCoroutinesApi::class)
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneContentViewModel
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import kotlinx.coroutines.ExperimentalCoroutinesApi
+
+val Kosmos.notificationsShadeSceneContentViewModel:
+ NotificationsShadeSceneContentViewModel by Fixture {
+ NotificationsShadeSceneContentViewModel(
+ deviceEntryInteractor = deviceEntryInteractor,
+ sceneInteractor = sceneInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt
index 8d4d547..00f1526 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/OverlayShadeViewModelKosmos.kt
@@ -17,15 +17,22 @@
package com.android.systemui.shade.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.shadeInteractor
val Kosmos.overlayShadeViewModel: OverlayShadeViewModel by
Kosmos.Fixture {
OverlayShadeViewModel(
- applicationScope = applicationCoroutineScope,
sceneInteractor = sceneInteractor,
shadeInteractor = shadeInteractor,
)
}
+
+val Kosmos.overlayShadeViewModelFactory: OverlayShadeViewModel.Factory by
+ Kosmos.Fixture {
+ object : OverlayShadeViewModel.Factory {
+ override fun create(): OverlayShadeViewModel {
+ return overlayShadeViewModel
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
index 0e21698..7eb9f34 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeHeaderViewModelKosmos.kt
@@ -19,7 +19,6 @@
import android.content.applicationContext
import com.android.systemui.broadcast.broadcastDispatcher
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.plugins.activityStarter
import com.android.systemui.scene.domain.interactor.sceneInteractor
import com.android.systemui.shade.domain.interactor.privacyChipInteractor
@@ -31,7 +30,6 @@
val Kosmos.shadeHeaderViewModel: ShadeHeaderViewModel by
Kosmos.Fixture {
ShadeHeaderViewModel(
- applicationScope = applicationCoroutineScope,
context = applicationContext,
activityStarter = activityStarter,
sceneInteractor = sceneInteractor,
@@ -43,3 +41,12 @@
broadcastDispatcher = broadcastDispatcher,
)
}
+
+val Kosmos.shadeHeaderViewModelFactory: ShadeHeaderViewModel.Factory by
+ Kosmos.Fixture {
+ object : ShadeHeaderViewModel.Factory {
+ override fun create(): ShadeHeaderViewModel {
+ return shadeHeaderViewModel
+ }
+ }
+ }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt
similarity index 66%
copy from packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
copy to packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt
index 72a80d4..2387aa8 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/NotificationsShadeSceneViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneActionsViewModelKosmos.kt
@@ -1,5 +1,5 @@
/*
- * Copyright (C) 2024 The Android Open Source Project
+ * Copyright (C) 2023 The Android Open Source Project
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
@@ -17,8 +17,13 @@
package com.android.systemui.shade.ui.viewmodel
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.notifications.ui.viewmodel.NotificationsShadeSceneViewModel
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.qs.ui.adapter.qsSceneAdapter
import com.android.systemui.shade.domain.interactor.shadeInteractor
-val Kosmos.notificationsShadeSceneViewModel: NotificationsShadeSceneViewModel by
- Kosmos.Fixture { NotificationsShadeSceneViewModel(shadeInteractor) }
+val Kosmos.shadeSceneActionsViewModel: ShadeSceneActionsViewModel by Fixture {
+ ShadeSceneActionsViewModel(
+ qsSceneAdapter = qsSceneAdapter,
+ shadeInteractor = shadeInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelKosmos.kt
new file mode 100644
index 0000000..7097d31
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneContentViewModelKosmos.kt
@@ -0,0 +1,44 @@
+/*
+ * Copyright (C) 2023 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.systemui.shade.ui.viewmodel
+
+import com.android.systemui.deviceentry.domain.interactor.deviceEntryInteractor
+import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.kosmos.Kosmos.Fixture
+import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
+import com.android.systemui.qs.footerActionsController
+import com.android.systemui.qs.footerActionsViewModelFactory
+import com.android.systemui.qs.ui.adapter.qsSceneAdapter
+import com.android.systemui.scene.domain.interactor.sceneInteractor
+import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModelFactory
+import com.android.systemui.shade.domain.interactor.shadeInteractor
+import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
+
+val Kosmos.shadeSceneContentViewModel: ShadeSceneContentViewModel by Fixture {
+ ShadeSceneContentViewModel(
+ shadeHeaderViewModelFactory = shadeHeaderViewModelFactory,
+ qsSceneAdapter = qsSceneAdapter,
+ brightnessMirrorViewModelFactory = brightnessMirrorViewModelFactory,
+ mediaCarouselInteractor = mediaCarouselInteractor,
+ shadeInteractor = shadeInteractor,
+ footerActionsViewModelFactory = footerActionsViewModelFactory,
+ footerActionsController = footerActionsController,
+ unfoldTransitionInteractor = unfoldTransitionInteractor,
+ deviceEntryInteractor = deviceEntryInteractor,
+ sceneInteractor = sceneInteractor,
+ )
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt
deleted file mode 100644
index 2c5a0f4..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/shade/ui/viewmodel/ShadeSceneViewModelKosmos.kt
+++ /dev/null
@@ -1,42 +0,0 @@
-/*
- * Copyright (C) 2023 The Android Open Source Project
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
-
-package com.android.systemui.shade.ui.viewmodel
-
-import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.media.controls.domain.pipeline.interactor.mediaCarouselInteractor
-import com.android.systemui.qs.footerActionsController
-import com.android.systemui.qs.footerActionsViewModelFactory
-import com.android.systemui.qs.ui.adapter.qsSceneAdapter
-import com.android.systemui.scene.domain.interactor.sceneInteractor
-import com.android.systemui.settings.brightness.ui.viewmodel.brightnessMirrorViewModel
-import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.unfold.domain.interactor.unfoldTransitionInteractor
-
-val Kosmos.shadeSceneViewModel: ShadeSceneViewModel by
- Kosmos.Fixture {
- ShadeSceneViewModel(
- shadeHeaderViewModel = shadeHeaderViewModel,
- qsSceneAdapter = qsSceneAdapter,
- brightnessMirrorViewModel = brightnessMirrorViewModel,
- mediaCarouselInteractor = mediaCarouselInteractor,
- shadeInteractor = shadeInteractor,
- footerActionsViewModelFactory = footerActionsViewModelFactory,
- footerActionsController = footerActionsController,
- sceneInteractor = sceneInteractor,
- unfoldTransitionInteractor = unfoldTransitionInteractor,
- )
- }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
index 8909d75..3234e66 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/domain/interactor/SharedNotificationContainerInteractorKosmos.kt
@@ -21,6 +21,7 @@
import com.android.systemui.deviceentry.domain.interactor.deviceEntryUdfpsInteractor
import com.android.systemui.keyguard.domain.interactor.keyguardInteractor
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.shade.domain.interactor.shadeInteractor
import com.android.systemui.shade.largeScreenHeaderHelper
import com.android.systemui.statusbar.policy.splitShadeStateController
@@ -29,7 +30,8 @@
SharedNotificationContainerInteractor(
configurationRepository = configurationRepository,
context = applicationContext,
- splitShadeStateController = splitShadeStateController,
+ splitShadeStateController = { splitShadeStateController },
+ shadeInteractor = { shadeInteractor },
keyguardInteractor = keyguardInteractor,
deviceEntryUdfpsInteractor = deviceEntryUdfpsInteractor,
largeScreenHeaderHelperLazy = { largeScreenHeaderHelper }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
index afb8acb..20dc668 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/statusbar/notification/stack/ui/viewmodel/NotificationsPlaceholderViewModelKosmos.kt
@@ -21,7 +21,6 @@
import com.android.systemui.kosmos.Kosmos
import com.android.systemui.kosmos.Kosmos.Fixture
import com.android.systemui.shade.domain.interactor.shadeInteractor
-import com.android.systemui.shade.ui.viewmodel.shadeSceneViewModel
import com.android.systemui.statusbar.notification.stack.domain.interactor.headsUpNotificationInteractor
import com.android.systemui.statusbar.notification.stack.domain.interactor.notificationStackAppearanceInteractor
@@ -30,7 +29,6 @@
dumpManager = dumpManager,
interactor = notificationStackAppearanceInteractor,
shadeInteractor = shadeInteractor,
- shadeSceneViewModel = shadeSceneViewModel,
headsUpNotificationInteractor = headsUpNotificationInteractor,
featureFlags = featureFlagsClassic,
)
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
deleted file mode 100644
index d117466..0000000
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.java
+++ /dev/null
@@ -1,198 +0,0 @@
-/*
- * 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.
- */
-
-package com.android.systemui.util.settings;
-
-import static kotlinx.coroutines.test.TestCoroutineDispatchersKt.StandardTestDispatcher;
-
-import android.annotation.UserIdInt;
-import android.content.ContentResolver;
-import android.database.ContentObserver;
-import android.net.Uri;
-import android.os.UserHandle;
-import android.util.Pair;
-
-import androidx.annotation.NonNull;
-import androidx.annotation.VisibleForTesting;
-
-import kotlinx.coroutines.CoroutineDispatcher;
-
-import java.util.ArrayList;
-import java.util.HashMap;
-import java.util.List;
-import java.util.Map;
-
-public class FakeSettings implements SecureSettings, SystemSettings {
- private final Map<SettingsKey, String> mValues = new HashMap<>();
- private final Map<SettingsKey, List<ContentObserver>> mContentObservers =
- new HashMap<>();
- private final Map<String, List<ContentObserver>> mContentObserversAllUsers = new HashMap<>();
- private final CoroutineDispatcher mDispatcher;
-
- public static final Uri CONTENT_URI = Uri.parse("content://settings/fake");
- @UserIdInt
- private int mUserId = UserHandle.USER_CURRENT;
-
- private final CurrentUserIdProvider mCurrentUserProvider;
-
- /**
- * @deprecated Please use FakeSettings(testDispatcher) to provide the same dispatcher used
- * by main test scope.
- */
- @Deprecated
- public FakeSettings() {
- mDispatcher = StandardTestDispatcher(/* scheduler = */ null, /* name = */ null);
- mCurrentUserProvider = () -> mUserId;
- }
-
- public FakeSettings(CoroutineDispatcher dispatcher) {
- mDispatcher = dispatcher;
- mCurrentUserProvider = () -> mUserId;
- }
-
- public FakeSettings(CoroutineDispatcher dispatcher, CurrentUserIdProvider currentUserProvider) {
- mDispatcher = dispatcher;
- mCurrentUserProvider = currentUserProvider;
- }
-
- @VisibleForTesting
- FakeSettings(String initialKey, String initialValue) {
- this();
- putString(initialKey, initialValue);
- }
-
- @VisibleForTesting
- FakeSettings(Map<String, String> initialValues) {
- this();
- for (Map.Entry<String, String> kv : initialValues.entrySet()) {
- putString(kv.getKey(), kv.getValue());
- }
- }
-
- @Override
- @NonNull
- public ContentResolver getContentResolver() {
- throw new UnsupportedOperationException(
- "FakeSettings.getContentResolver is not implemented");
- }
-
- @NonNull
- @Override
- public CurrentUserIdProvider getCurrentUserProvider() {
- return mCurrentUserProvider;
- }
-
- @NonNull
- @Override
- public CoroutineDispatcher getBackgroundDispatcher() {
- return mDispatcher;
- }
-
- @Override
- public void registerContentObserverForUserSync(@NonNull Uri uri, boolean notifyDescendants,
- @NonNull ContentObserver settingsObserver, int userHandle) {
- List<ContentObserver> observers;
- if (userHandle == UserHandle.USER_ALL) {
- mContentObserversAllUsers.putIfAbsent(uri.toString(), new ArrayList<>());
- observers = mContentObserversAllUsers.get(uri.toString());
- } else {
- SettingsKey key = new SettingsKey(userHandle, uri.toString());
- mContentObservers.putIfAbsent(key, new ArrayList<>());
- observers = mContentObservers.get(key);
- }
- observers.add(settingsObserver);
- }
-
- @Override
- public void unregisterContentObserverSync(@NonNull ContentObserver settingsObserver) {
- for (List<ContentObserver> observers : mContentObservers.values()) {
- observers.remove(settingsObserver);
- }
- for (List<ContentObserver> observers : mContentObserversAllUsers.values()) {
- observers.remove(settingsObserver);
- }
- }
-
- @NonNull
- @Override
- public Uri getUriFor(@NonNull String name) {
- return Uri.withAppendedPath(CONTENT_URI, name);
- }
-
- public void setUserId(@UserIdInt int userId) {
- mUserId = userId;
- }
-
- @Override
- public int getUserId() {
- return mUserId;
- }
-
- @Override
- public String getString(@NonNull String name) {
- return getStringForUser(name, getUserId());
- }
-
- @Override
- public String getStringForUser(@NonNull String name, int userHandle) {
- return mValues.get(new SettingsKey(userHandle, getUriFor(name).toString()));
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value,
- boolean overrideableByRestore) {
- return putStringForUser(name, value, null, false, getUserId(), overrideableByRestore);
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value) {
- return putString(name, value, false);
- }
-
- @Override
- public boolean putStringForUser(@NonNull String name, @NonNull String value, int userHandle) {
- return putStringForUser(name, value, null, false, userHandle, false);
- }
-
- @Override
- public boolean putStringForUser(@NonNull String name, @NonNull String value, String tag,
- boolean makeDefault, int userHandle, boolean overrideableByRestore) {
- SettingsKey key = new SettingsKey(userHandle, getUriFor(name).toString());
- mValues.put(key, value);
-
- Uri uri = getUriFor(name);
- for (ContentObserver observer : mContentObservers.getOrDefault(key, new ArrayList<>())) {
- observer.dispatchChange(false, List.of(uri), 0, userHandle);
- }
- for (ContentObserver observer :
- mContentObserversAllUsers.getOrDefault(uri.toString(), new ArrayList<>())) {
- observer.dispatchChange(false, List.of(uri), 0, userHandle);
- }
- return true;
- }
-
- @Override
- public boolean putString(@NonNull String name, @NonNull String value, @NonNull String tag,
- boolean makeDefault) {
- return putString(name, value);
- }
-
- private static class SettingsKey extends Pair<Integer, String> {
- SettingsKey(Integer first, String second) {
- super(first, second);
- }
- }
-}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt
new file mode 100644
index 0000000..e5d113b
--- /dev/null
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/util/settings/FakeSettings.kt
@@ -0,0 +1,307 @@
+/*
+ * 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.
+ */
+package com.android.systemui.util.settings
+
+import android.annotation.UserIdInt
+import android.content.ContentResolver
+import android.database.ContentObserver
+import android.net.Uri
+import android.os.UserHandle
+import android.util.Pair
+import androidx.annotation.VisibleForTesting
+import com.android.systemui.util.settings.SettingsProxy.CurrentUserIdProvider
+import kotlinx.coroutines.CoroutineDispatcher
+import kotlinx.coroutines.Job
+import kotlinx.coroutines.test.StandardTestDispatcher
+import kotlinx.coroutines.test.TestDispatcher
+
+class FakeSettings : SecureSettings, SystemSettings, UserSettingsProxy {
+ private val values = mutableMapOf<SettingsKey, String?>()
+ private val contentObservers = mutableMapOf<SettingsKey, MutableList<ContentObserver>>()
+ private val contentObserversAllUsers = mutableMapOf<String, MutableList<ContentObserver>>()
+
+ override val backgroundDispatcher: CoroutineDispatcher
+
+ @UserIdInt override var userId = UserHandle.USER_CURRENT
+ override val currentUserProvider: CurrentUserIdProvider
+
+ @Deprecated(
+ """Please use FakeSettings(testDispatcher) to provide the same dispatcher used
+ by main test scope."""
+ )
+ constructor() {
+ backgroundDispatcher = StandardTestDispatcher(scheduler = null, name = null)
+ currentUserProvider = CurrentUserIdProvider { userId }
+ }
+
+ constructor(dispatcher: CoroutineDispatcher) {
+ backgroundDispatcher = dispatcher
+ currentUserProvider = CurrentUserIdProvider { userId }
+ }
+
+ constructor(dispatcher: CoroutineDispatcher, currentUserProvider: CurrentUserIdProvider) {
+ backgroundDispatcher = dispatcher
+ this.currentUserProvider = currentUserProvider
+ }
+
+ @VisibleForTesting
+ internal constructor(initialKey: String, initialValue: String) : this() {
+ putString(initialKey, initialValue)
+ }
+
+ @VisibleForTesting
+ internal constructor(initialValues: Map<String, String>) : this() {
+ for ((key, value) in initialValues) {
+ putString(key, value)
+ }
+ }
+
+ override fun getContentResolver(): ContentResolver {
+ throw UnsupportedOperationException("FakeSettings.getContentResolver is not implemented")
+ }
+
+ override fun registerContentObserverForUserSync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) {
+ if (userHandle == UserHandle.USER_ALL) {
+ contentObserversAllUsers
+ .getOrPut(uri.toString()) { mutableListOf() }
+ .add(settingsObserver)
+ } else {
+ val key = SettingsKey(userHandle, uri.toString())
+ contentObservers.getOrPut(key) { mutableListOf() }.add(settingsObserver)
+ }
+ }
+
+ override fun unregisterContentObserverSync(settingsObserver: ContentObserver) {
+ contentObservers.values.onEach { it.remove(settingsObserver) }
+ contentObserversAllUsers.values.onEach { it.remove(settingsObserver) }
+ }
+
+ override suspend fun registerContentObserver(uri: Uri, settingsObserver: ContentObserver) =
+ suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserver(uri, settingsObserver)
+ }
+
+ override fun registerContentObserverAsync(uri: Uri, settingsObserver: ContentObserver): Job =
+ advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverAsync(uri, settingsObserver)
+ }
+
+ override suspend fun registerContentObserver(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserver(
+ uri,
+ notifyForDescendants,
+ settingsObserver
+ )
+ }
+
+ override fun registerContentObserverAsync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverAsync(
+ uri,
+ notifyForDescendants,
+ settingsObserver
+ )
+ }
+
+ override suspend fun registerContentObserverForUser(
+ name: String,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(name, settingsObserver, userHandle)
+ }
+
+ override fun registerContentObserverForUserAsync(
+ name: String,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ name,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun unregisterContentObserverAsync(settingsObserver: ContentObserver): Job =
+ advanceDispatcher {
+ super<UserSettingsProxy>.unregisterContentObserverAsync(settingsObserver)
+ }
+
+ override suspend fun registerContentObserverForUser(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(uri, settingsObserver, userHandle)
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ settingsObserver: ContentObserver,
+ userHandle: Int,
+ registered: Runnable
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ settingsObserver,
+ userHandle,
+ registered
+ )
+ }
+
+ override suspend fun registerContentObserverForUser(
+ name: String,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = suspendAdvanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUser(
+ name,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ name: String,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ) = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ name,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun registerContentObserverForUserAsync(
+ uri: Uri,
+ notifyForDescendants: Boolean,
+ settingsObserver: ContentObserver,
+ userHandle: Int
+ ): Job = advanceDispatcher {
+ super<UserSettingsProxy>.registerContentObserverForUserAsync(
+ uri,
+ notifyForDescendants,
+ settingsObserver,
+ userHandle
+ )
+ }
+
+ override fun getUriFor(name: String): Uri {
+ return Uri.withAppendedPath(CONTENT_URI, name)
+ }
+
+ override fun getString(name: String): String? {
+ return getStringForUser(name, userId)
+ }
+
+ override fun getStringForUser(name: String, userHandle: Int): String? {
+ return values[SettingsKey(userHandle, getUriFor(name).toString())]
+ }
+
+ override fun putString(name: String, value: String?, overrideableByRestore: Boolean): Boolean {
+ return putStringForUser(name, value, null, false, userId, overrideableByRestore)
+ }
+
+ override fun putString(name: String, value: String?): Boolean {
+ return putString(name, value, false)
+ }
+
+ override fun putStringForUser(name: String, value: String?, userHandle: Int): Boolean {
+ return putStringForUser(name, value, null, false, userHandle, false)
+ }
+
+ override fun putStringForUser(
+ name: String,
+ value: String?,
+ tag: String?,
+ makeDefault: Boolean,
+ userHandle: Int,
+ overrideableByRestore: Boolean
+ ): Boolean {
+ val key = SettingsKey(userHandle, getUriFor(name).toString())
+ values[key] = value
+ val uri = getUriFor(name)
+ contentObservers[key]?.onEach { it.dispatchChange(false, listOf(uri), 0, userHandle) }
+ contentObserversAllUsers[uri.toString()]?.onEach {
+ it.dispatchChange(false, listOf(uri), 0, userHandle)
+ }
+ return true
+ }
+
+ override fun putString(
+ name: String,
+ value: String?,
+ tag: String?,
+ makeDefault: Boolean
+ ): Boolean {
+ return putString(name, value)
+ }
+
+ /** Runs current jobs on dispatcher after calling the method. */
+ private fun <T> advanceDispatcher(f: () -> T): T {
+ val result = f()
+ testDispatcherRunCurrent()
+ return result
+ }
+
+ private suspend fun <T> suspendAdvanceDispatcher(f: suspend () -> T): T {
+ val result = f()
+ testDispatcherRunCurrent()
+ return result
+ }
+
+ private fun testDispatcherRunCurrent() {
+ val testDispatcher = backgroundDispatcher as? TestDispatcher
+ testDispatcher?.scheduler?.runCurrent()
+ }
+
+ private data class SettingsKey(val first: Int, val second: String) :
+ Pair<Int, String>(first, second)
+
+ companion object {
+ val CONTENT_URI = Uri.parse("content://settings/fake")
+ }
+}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt
index 2ba1211..0b438d1 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/data/repository/VolumePanelGlobalStateRepositoryKosmos.kt
@@ -18,6 +18,7 @@
import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
+import com.android.systemui.volume.shared.volumePanelLogger
val Kosmos.volumePanelGlobalStateRepository by
- Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager) }
+ Kosmos.Fixture { VolumePanelGlobalStateRepository(dumpManager, volumePanelLogger) }
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt
index a18f498..3804a9f 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/domain/interactor/ComponentsInteractorKosmos.kt
@@ -28,6 +28,7 @@
import com.android.systemui.volume.panel.domain.defaultCriteria
import com.android.systemui.volume.panel.shared.model.VolumePanelComponentKey
import com.android.systemui.volume.panel.ui.composable.enabledComponents
+import com.android.systemui.volume.shared.volumePanelLogger
import javax.inject.Provider
var Kosmos.criteriaByKey: Map<VolumePanelComponentKey, Provider<ComponentAvailabilityCriteria>> by
@@ -50,6 +51,7 @@
enabledComponents,
{ defaultCriteria },
testScope.backgroundScope,
+ volumePanelLogger,
criteriaByKey,
)
}
diff --git a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
index 34a008f..c4fb9e4 100644
--- a/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
+++ b/packages/SystemUI/tests/utils/src/com/android/systemui/volume/panel/ui/viewmodel/VolumePanelViewModelKosmos.kt
@@ -18,17 +18,19 @@
import android.content.applicationContext
import com.android.systemui.broadcast.broadcastDispatcher
+import com.android.systemui.dump.dumpManager
import com.android.systemui.kosmos.Kosmos
-import com.android.systemui.kosmos.testScope
+import com.android.systemui.kosmos.applicationCoroutineScope
import com.android.systemui.statusbar.policy.configurationController
import com.android.systemui.volume.panel.dagger.factory.volumePanelComponentFactory
import com.android.systemui.volume.panel.domain.VolumePanelStartable
import com.android.systemui.volume.panel.domain.interactor.volumePanelGlobalStateInteractor
+import com.android.systemui.volume.shared.volumePanelLogger
var Kosmos.volumePanelStartables: Set<VolumePanelStartable> by Kosmos.Fixture { emptySet() }
var Kosmos.volumePanelViewModel: VolumePanelViewModel by
- Kosmos.Fixture { volumePanelViewModelFactory.create(testScope.backgroundScope) }
+ Kosmos.Fixture { volumePanelViewModelFactory.create(applicationCoroutineScope) }
val Kosmos.volumePanelViewModelFactory: VolumePanelViewModel.Factory by
Kosmos.Fixture {
@@ -37,6 +39,8 @@
volumePanelComponentFactory,
configurationController,
broadcastDispatcher,
+ dumpManager,
+ volumePanelLogger,
volumePanelGlobalStateInteractor,
)
}
diff --git a/ravenwood/Android.bp b/ravenwood/Android.bp
index 4faf03c..6150343 100644
--- a/ravenwood/Android.bp
+++ b/ravenwood/Android.bp
@@ -280,10 +280,10 @@
src: "scripts/ravenwood-stats-checker.sh",
test_suites: ["general-tests"],
data: [
- ":hoststubgen_framework-minus-apex_stats.csv",
- ":hoststubgen_framework-minus-apex_apis.csv",
- ":hoststubgen_framework-minus-apex_keep_all.txt",
- ":hoststubgen_framework-minus-apex_dump.txt",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_stats.csv}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_apis.csv}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_keep_all.txt}",
+ ":framework-minus-apex.ravenwood-base_all{hoststubgen_framework-minus-apex_dump.txt}",
":services.core.ravenwood-base{hoststubgen_services.core_stats.csv}",
":services.core.ravenwood-base{hoststubgen_services.core_apis.csv}",
":services.core.ravenwood-base{hoststubgen_services.core_keep_all.txt}",
diff --git a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
index 68f185e..cc9b70e 100644
--- a/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
+++ b/ravenwood/texts/ravenwood-annotation-allowed-classes.txt
@@ -281,6 +281,12 @@
com.android.server.LocalServices
+com.android.internal.graphics.cam.Cam
+com.android.internal.graphics.cam.CamUtils
+com.android.internal.graphics.cam.Frame
+com.android.internal.graphics.cam.HctSolver
+com.android.internal.graphics.ColorUtils
+
com.android.internal.util.BitUtils
com.android.internal.util.BitwiseInputStream
com.android.internal.util.BitwiseOutputStream
diff --git a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
index 56da231..2ce5c2b 100644
--- a/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
+++ b/services/accessibility/java/com/android/server/accessibility/MouseKeysInterceptor.java
@@ -78,6 +78,9 @@
private final AccessibilityManagerService mAms;
private final Handler mHandler;
+ /** Thread to wait for virtual mouse creation to complete */
+ private final Thread mCreateVirtualMouseThread;
+
VirtualDeviceManager.VirtualDevice mVirtualDevice = null;
private VirtualMouse mVirtualMouse = null;
@@ -154,34 +157,47 @@
mHandler = new Handler(looper, this);
// Create the virtual mouse on a separate thread since virtual device creation
// should happen on an auxiliary thread, and not from the handler's thread.
- // This is because virtual device creation is a blocking operation and can cause a
- // deadlock if it is called from the handler's thread.
- new Thread(() -> {
+ // This is because the handler thread is the same as the main thread,
+ // and the main thread will be blocked waiting for the virtual device to be created.
+ mCreateVirtualMouseThread = new Thread(() -> {
mVirtualMouse = createVirtualMouse(displayId);
- }).start();
+ });
+ mCreateVirtualMouseThread.start();
+ }
+ /**
+ * Wait for {@code mVirtualMouse} to be created.
+ * This will ensure that {@code mVirtualMouse} is always created before
+ * trying to send mouse events.
+ **/
+ private void waitForVirtualMouseCreation() {
+ try {
+ // Block the current thread until the virtual mouse creation thread completes.
+ mCreateVirtualMouseThread.join();
+ } catch (InterruptedException e) {
+ Thread.currentThread().interrupt();
+ throw new RuntimeException(e);
+ }
}
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void sendVirtualMouseRelativeEvent(float x, float y) {
- if (mVirtualMouse != null) {
- mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
- .setRelativeX(x)
- .setRelativeY(y)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendRelativeEvent(new VirtualMouseRelativeEvent.Builder()
+ .setRelativeX(x)
+ .setRelativeY(y)
+ .build()
+ );
}
@RequiresPermission(android.Manifest.permission.CREATE_VIRTUAL_DEVICE)
private void sendVirtualMouseButtonEvent(int buttonCode, int actionCode) {
- if (mVirtualMouse != null) {
- mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
- .setAction(actionCode)
- .setButtonCode(buttonCode)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendButtonEvent(new VirtualMouseButtonEvent.Builder()
+ .setAction(actionCode)
+ .setButtonCode(buttonCode)
+ .build()
+ );
}
/**
@@ -205,12 +221,11 @@
case DOWN_MOVE_OR_SCROLL -> -1.0f;
default -> 0.0f;
};
- if (mVirtualMouse != null) {
- mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
- .setYAxisMovement(y)
- .build()
- );
- }
+ waitForVirtualMouseCreation();
+ mVirtualMouse.sendScrollEvent(new VirtualMouseScrollEvent.Builder()
+ .setYAxisMovement(y)
+ .build()
+ );
if (DEBUG) {
Slog.d(LOG_TAG, "Performed mouse key event: " + mouseKeyEvent.name()
+ " for scroll action with axis movement (y=" + y + ")");
diff --git a/services/appfunctions/OWNERS b/services/appfunctions/OWNERS
new file mode 100644
index 0000000..b310894
--- /dev/null
+++ b/services/appfunctions/OWNERS
@@ -0,0 +1 @@
+include /core/java/android/app/appfunctions/OWNERS
diff --git a/services/core/java/com/android/server/am/ActiveServices.java b/services/core/java/com/android/server/am/ActiveServices.java
index 2e1416b..d4f729c 100644
--- a/services/core/java/com/android/server/am/ActiveServices.java
+++ b/services/core/java/com/android/server/am/ActiveServices.java
@@ -6962,7 +6962,8 @@
}
private boolean collectPackageServicesLocked(String packageName, Set<String> filterByClasses,
- boolean evenPersistent, boolean doit, ArrayMap<ComponentName, ServiceRecord> services) {
+ boolean evenPersistent, boolean doit, int minOomAdj,
+ ArrayMap<ComponentName, ServiceRecord> services) {
boolean didSomething = false;
for (int i = services.size() - 1; i >= 0; i--) {
ServiceRecord service = services.valueAt(i);
@@ -6970,6 +6971,11 @@
|| (service.packageName.equals(packageName)
&& (filterByClasses == null
|| filterByClasses.contains(service.name.getClassName())));
+ if (service.app != null && service.app.mState.getCurAdj() < minOomAdj) {
+ Slog.i(TAG, "Skip force stopping service " + service
+ + ": below minimum oom adj level");
+ continue;
+ }
if (sameComponent
&& (service.app == null || evenPersistent || !service.app.isPersistent())) {
if (!doit) {
@@ -6993,6 +6999,12 @@
boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses,
int userId, boolean evenPersistent, boolean fullStop, boolean doit) {
+ return bringDownDisabledPackageServicesLocked(packageName, filterByClasses, userId,
+ evenPersistent, fullStop, doit, ProcessList.INVALID_ADJ);
+ }
+
+ boolean bringDownDisabledPackageServicesLocked(String packageName, Set<String> filterByClasses,
+ int userId, boolean evenPersistent, boolean fullStop, boolean doit, int minOomAdj) {
boolean didSomething = false;
if (mTmpCollectionResults != null) {
@@ -7002,7 +7014,8 @@
if (userId == UserHandle.USER_ALL) {
for (int i = mServiceMap.size() - 1; i >= 0; i--) {
didSomething |= collectPackageServicesLocked(packageName, filterByClasses,
- evenPersistent, doit, mServiceMap.valueAt(i).mServicesByInstanceName);
+ evenPersistent, doit, minOomAdj,
+ mServiceMap.valueAt(i).mServicesByInstanceName);
if (!doit && didSomething) {
return true;
}
@@ -7015,7 +7028,7 @@
if (smap != null) {
ArrayMap<ComponentName, ServiceRecord> items = smap.mServicesByInstanceName;
didSomething = collectPackageServicesLocked(packageName, filterByClasses,
- evenPersistent, doit, items);
+ evenPersistent, doit, minOomAdj, items);
}
if (doit && filterByClasses == null) {
forceStopPackageLocked(packageName, userId);
diff --git a/services/core/java/com/android/server/am/ActivityManagerService.java b/services/core/java/com/android/server/am/ActivityManagerService.java
index 9d8f337..4a18cb1 100644
--- a/services/core/java/com/android/server/am/ActivityManagerService.java
+++ b/services/core/java/com/android/server/am/ActivityManagerService.java
@@ -4377,6 +4377,16 @@
}
@GuardedBy("this")
+ final boolean forceStopUserPackagesLocked(int userId, String reasonString,
+ boolean evenImportantServices) {
+ int minOomAdj = evenImportantServices ? ProcessList.INVALID_ADJ
+ : ProcessList.FOREGROUND_APP_ADJ;
+ return forceStopPackageInternalLocked(null, -1, false, false,
+ true, false, false, false, userId, reasonString,
+ ApplicationExitInfo.REASON_USER_STOPPED, minOomAdj);
+ }
+
+ @GuardedBy("this")
final boolean forceStopPackageLocked(String packageName, int appId,
boolean callerWillRestart, boolean purgeCache, boolean doit,
boolean evenPersistent, boolean uninstalling, boolean packageStateStopped,
@@ -4385,7 +4395,6 @@
: ApplicationExitInfo.REASON_USER_REQUESTED;
return forceStopPackageLocked(packageName, appId, callerWillRestart, purgeCache, doit,
evenPersistent, uninstalling, packageStateStopped, userId, reasonString, reason);
-
}
@GuardedBy("this")
@@ -4393,6 +4402,16 @@
boolean callerWillRestart, boolean purgeCache, boolean doit,
boolean evenPersistent, boolean uninstalling, boolean packageStateStopped,
int userId, String reasonString, int reason) {
+ return forceStopPackageInternalLocked(packageName, appId, callerWillRestart, purgeCache,
+ doit, evenPersistent, uninstalling, packageStateStopped, userId, reasonString,
+ reason, ProcessList.INVALID_ADJ);
+ }
+
+ @GuardedBy("this")
+ private boolean forceStopPackageInternalLocked(String packageName, int appId,
+ boolean callerWillRestart, boolean purgeCache, boolean doit,
+ boolean evenPersistent, boolean uninstalling, boolean packageStateStopped,
+ int userId, String reasonString, int reason, int minOomAdj) {
int i;
if (userId == UserHandle.USER_ALL && packageName == null) {
@@ -4431,7 +4450,7 @@
}
didSomething |= mProcessList.killPackageProcessesLSP(packageName, appId, userId,
- ProcessList.INVALID_ADJ, callerWillRestart, false /* allowRestart */, doit,
+ minOomAdj, callerWillRestart, false /* allowRestart */, doit,
evenPersistent, true /* setRemoved */, uninstalling,
reason,
subReason,
@@ -4440,7 +4459,8 @@
}
if (mServices.bringDownDisabledPackageServicesLocked(
- packageName, null /* filterByClasses */, userId, evenPersistent, true, doit)) {
+ packageName, null /* filterByClasses */, userId, evenPersistent,
+ true, doit, minOomAdj)) {
if (!doit) {
return true;
}
@@ -19872,6 +19892,11 @@
}
@Override
+ public boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) {
+ return mUserController.isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId);
+ }
+
+ @Override
public void setStopUserOnSwitch(int value) {
ActivityManagerService.this.setStopUserOnSwitch(value);
}
diff --git a/services/core/java/com/android/server/am/UserController.java b/services/core/java/com/android/server/am/UserController.java
index 30efa3e..e57fe13 100644
--- a/services/core/java/com/android/server/am/UserController.java
+++ b/services/core/java/com/android/server/am/UserController.java
@@ -439,6 +439,15 @@
@GuardedBy("mLock")
private final List<PendingUserStart> mPendingUserStarts = new ArrayList<>();
+ /**
+ * Contains users which cannot abort the shutdown process.
+ *
+ * <p> For example, we don't abort shutdown for users whose processes have already been stopped
+ * due to {@link #isEarlyPackageKillEnabledForUserSwitch(int, int)}.
+ */
+ @GuardedBy("mLock")
+ private final ArraySet<Integer> mDoNotAbortShutdownUserIds = new ArraySet<>();
+
private final UserLifecycleListener mUserLifecycleListener = new UserLifecycleListener() {
@Override
public void onUserCreated(UserInfo user, Object token) {
@@ -509,11 +518,11 @@
}
}
- private boolean shouldStopUserOnSwitch() {
+ private boolean isStopUserOnSwitchEnabled() {
synchronized (mLock) {
if (mStopUserOnSwitch != STOP_USER_ON_SWITCH_DEFAULT) {
final boolean value = mStopUserOnSwitch == STOP_USER_ON_SWITCH_TRUE;
- Slogf.i(TAG, "shouldStopUserOnSwitch(): returning overridden value (%b)", value);
+ Slogf.i(TAG, "isStopUserOnSwitchEnabled(): returning overridden value (%b)", value);
return value;
}
}
@@ -521,6 +530,26 @@
return property == -1 ? mDelayUserDataLocking : property == 1;
}
+ /**
+ * Get whether or not the previous user's packages will be killed before the user is
+ * stopped during a user switch.
+ *
+ * <p> The primary use case of this method is for {@link com.android.server.SystemService}
+ * classes to call this API in their
+ * {@link com.android.server.SystemService#onUserSwitching} method implementation to prevent
+ * restarting any of the previous user's processes that will be killed during the user switch.
+ */
+ boolean isEarlyPackageKillEnabledForUserSwitch(int fromUserId, int toUserId) {
+ // NOTE: The logic in this method could be extended to cover other cases where
+ // the previous user is also stopped like: guest users, ephemeral users,
+ // and users with DISALLOW_RUN_IN_BACKGROUND. Currently, this is not done
+ // because early killing is not enabled for these cases by default.
+ if (fromUserId == UserHandle.USER_SYSTEM) {
+ return false;
+ }
+ return isStopUserOnSwitchEnabled();
+ }
+
void finishUserSwitch(UserState uss) {
// This call holds the AM lock so we post to the handler.
mHandler.post(() -> {
@@ -1247,6 +1276,7 @@
return;
}
uss.setState(UserState.STATE_SHUTDOWN);
+ mDoNotAbortShutdownUserIds.remove(userId);
}
TimingsTraceAndSlog t = new TimingsTraceAndSlog();
t.traceBegin("setUserState-STATE_SHUTDOWN-" + userId + "-[stopUser]");
@@ -1555,7 +1585,8 @@
private void stopPackagesOfStoppedUser(@UserIdInt int userId, String reason) {
if (DEBUG_MU) Slogf.i(TAG, "stopPackagesOfStoppedUser(%d): %s", userId, reason);
- mInjector.activityManagerForceStopPackage(userId, reason);
+ mInjector.activityManagerForceStopUserPackages(userId, reason,
+ /* evenImportantServices= */ true);
if (mInjector.getUserManager().isPreCreated(userId)) {
// Don't fire intent for precreated.
return;
@@ -1608,6 +1639,21 @@
}
}
+ private void stopPreviousUserPackagesIfEnabled(int fromUserId, int toUserId) {
+ if (!android.multiuser.Flags.stopPreviousUserApps()
+ || !isEarlyPackageKillEnabledForUserSwitch(fromUserId, toUserId)) {
+ return;
+ }
+ // Stop the previous user's packages early to reduce resource usage
+ // during user switching. Only do this when the previous user will
+ // be stopped regardless.
+ synchronized (mLock) {
+ mDoNotAbortShutdownUserIds.add(fromUserId);
+ }
+ mInjector.activityManagerForceStopUserPackages(fromUserId,
+ "early stop user packages", /* evenImportantServices= */ false);
+ }
+
void scheduleStartProfiles() {
// Parent user transition to RUNNING_UNLOCKING happens on FgThread, so it is busy, there is
// a chance the profile will reach RUNNING_LOCKED while parent is still locked, so no
@@ -1889,7 +1935,8 @@
updateStartedUserArrayLU();
needStart = true;
updateUmState = true;
- } else if (uss.state == UserState.STATE_SHUTDOWN) {
+ } else if (uss.state == UserState.STATE_SHUTDOWN
+ || mDoNotAbortShutdownUserIds.contains(userId)) {
Slogf.i(TAG, "User #" + userId
+ " is shutting down - will start after full shutdown");
mPendingUserStarts.add(new PendingUserStart(userId, userStartMode,
@@ -2293,7 +2340,7 @@
hasUserRestriction(UserManager.DISALLOW_RUN_IN_BACKGROUND, oldUserId);
synchronized (mLock) {
// If running in background is disabled or mStopUserOnSwitch mode, stop the user.
- if (hasRestriction || shouldStopUserOnSwitch()) {
+ if (hasRestriction || isStopUserOnSwitchEnabled()) {
Slogf.i(TAG, "Stopping user %d and its profiles on user switch", oldUserId);
stopUsersLU(oldUserId, /* allowDelayedLocking= */ false, null, null);
return;
@@ -3425,7 +3472,7 @@
pw.println(" mLastActiveUsersForDelayedLocking:" + mLastActiveUsersForDelayedLocking);
pw.println(" mDelayUserDataLocking:" + mDelayUserDataLocking);
pw.println(" mAllowUserUnlocking:" + mAllowUserUnlocking);
- pw.println(" shouldStopUserOnSwitch():" + shouldStopUserOnSwitch());
+ pw.println(" isStopUserOnSwitchEnabled():" + isStopUserOnSwitchEnabled());
pw.println(" mStopUserOnSwitch:" + mStopUserOnSwitch);
pw.println(" mMaxRunningUsers:" + mMaxRunningUsers);
pw.println(" mBackgroundUserScheduledStopTimeSecs:"
@@ -3522,6 +3569,7 @@
Integer.toString(msg.arg1), msg.arg1);
mInjector.getSystemServiceManager().onUserSwitching(msg.arg2, msg.arg1);
+ stopPreviousUserPackagesIfEnabled(msg.arg2, msg.arg1);
scheduleOnUserCompletedEvent(msg.arg1,
UserCompletedEventType.EVENT_TYPE_USER_SWITCHING,
USER_COMPLETED_EVENT_DELAY_MS);
@@ -3896,10 +3944,10 @@
}.sendNext();
}
- void activityManagerForceStopPackage(@UserIdInt int userId, String reason) {
+ void activityManagerForceStopUserPackages(@UserIdInt int userId, String reason,
+ boolean evenImportantServices) {
synchronized (mService) {
- mService.forceStopPackageLocked(null, -1, false, false, true, false, false, false,
- userId, reason);
+ mService.forceStopUserPackagesLocked(userId, reason, evenImportantServices);
}
};
diff --git a/services/core/java/com/android/server/appop/DiscreteRegistry.java b/services/core/java/com/android/server/appop/DiscreteRegistry.java
index 539dbca..2ce4623 100644
--- a/services/core/java/com/android/server/appop/DiscreteRegistry.java
+++ b/services/core/java/com/android/server/appop/DiscreteRegistry.java
@@ -1413,11 +1413,11 @@
pw.print("-");
pw.print(flagsToString(mOpFlag));
pw.print("] at ");
- date.setTime(discretizeTimeStamp(mNoteTime));
+ date.setTime(mNoteTime);
pw.print(sdf.format(date));
if (mNoteDuration != -1) {
pw.print(" for ");
- pw.print(discretizeDuration(mNoteDuration));
+ pw.print(mNoteDuration);
pw.print(" milliseconds ");
}
if (mAttributionFlags != AppOpsManager.ATTRIBUTION_FLAGS_NONE) {
diff --git a/services/core/java/com/android/server/biometrics/Utils.java b/services/core/java/com/android/server/biometrics/Utils.java
index df29ca4..fb8a81b 100644
--- a/services/core/java/com/android/server/biometrics/Utils.java
+++ b/services/core/java/com/android/server/biometrics/Utils.java
@@ -586,6 +586,8 @@
}
}
+ // LINT.IfChange
+
/**
* Checks if a client package is running in the background.
*
@@ -618,4 +620,6 @@
return true;
}
+ // LINT.ThenChange(frameworks/base/packages/SystemUI/shared/biometrics/src/com/android
+ // /systemui/biometrics/Utils.kt)
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
index 0fdd57d..dca1491 100644
--- a/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/face/aidl/BiometricTestSessionImpl.java
@@ -264,4 +264,11 @@
}
});
}
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
+ @Override
+ public int getSensorId() {
+ super.getSensorId_enforcePermission();
+ return mSensorId;
+ }
}
diff --git a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
index 8dc560b..caa2c1c 100644
--- a/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
+++ b/services/core/java/com/android/server/biometrics/sensors/fingerprint/aidl/BiometricTestSessionImpl.java
@@ -293,4 +293,11 @@
}
});
}
+
+ @android.annotation.EnforcePermission(android.Manifest.permission.TEST_BIOMETRIC)
+ @Override
+ public int getSensorId() {
+ super.getSensorId_enforcePermission();
+ return mSensorId;
+ }
}
\ No newline at end of file
diff --git a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
index 7746276..3161b77 100644
--- a/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
+++ b/services/core/java/com/android/server/hdmi/HdmiCecMessageValidator.java
@@ -523,8 +523,7 @@
if ((value & 0x80) != 0x00) {
return false;
}
- // Validate than not more than one bit is set
- return (Integer.bitCount(value) <= 1);
+ return true;
}
/**
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
index 3863cf0..8afbd56 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodManagerService.java
@@ -330,7 +330,7 @@
@UserIdInt
@BinderThread
private int resolveImeUserIdLocked(@UserIdInt int callingProcessUserId) {
- return mConcurrentMultiUserModeEnabled ? callingProcessUserId : mCurrentUserId;
+ return mConcurrentMultiUserModeEnabled ? callingProcessUserId : mCurrentImeUserId;
}
/**
@@ -343,7 +343,7 @@
@UserIdInt
private int resolveImeUserIdFromDisplayIdLocked(int displayId) {
return mConcurrentMultiUserModeEnabled
- ? mUserManagerInternal.getUserAssignedToDisplay(displayId) : mCurrentUserId;
+ ? mUserManagerInternal.getUserAssignedToDisplay(displayId) : mCurrentImeUserId;
}
/**
@@ -359,7 +359,7 @@
final int displayId = mWindowManagerInternal.getDisplayIdForWindow(windowToken);
return mUserManagerInternal.getUserAssignedToDisplay(displayId);
}
- return mCurrentUserId;
+ return mCurrentImeUserId;
}
final Context mContext;
@@ -370,10 +370,23 @@
@NonNull
private final Handler mIoHandler;
+ /**
+ * The user ID whose IME should be used if {@link #mConcurrentMultiUserModeEnabled} is
+ * {@code false}, otherwise remains to be the initial value, which is obtained by
+ * {@link ActivityManagerInternal#getCurrentUserId()} while the device is booting up.
+ *
+ * <p>Never get confused with {@link ActivityManagerInternal#getCurrentUserId()}, which is
+ * in general useless when designing and implementing interactions between apps and IMEs.</p>
+ *
+ * <p>You can also not assume that the IME client process belongs to {@link #mCurrentImeUserId}.
+ * A most important outlier is System UI process, which always runs under
+ * {@link UserHandle#USER_SYSTEM} in all the known configurations including Headless System User
+ * Mode (HSUM).</p>
+ */
@MultiUserUnawareField
@UserIdInt
@GuardedBy("ImfLock.class")
- private int mCurrentUserId;
+ private int mCurrentImeUserId;
/** Holds all user related data */
@SharedByAllUsersField
@@ -540,7 +553,7 @@
@GuardedBy("ImfLock.class")
@Nullable
IInputMethodInvoker getCurMethodLocked() {
- return getInputMethodBindingController(mCurrentUserId).getCurMethod();
+ return getInputMethodBindingController(mCurrentImeUserId).getCurMethod();
}
/**
@@ -587,7 +600,7 @@
switch (key) {
case Settings.Secure.SHOW_IME_WITH_HARD_KEYBOARD: {
if (!Flags.imeSwitcherRevamp()) {
- if (userId == mCurrentUserId) {
+ if (userId == mCurrentImeUserId) {
mMenuController.updateKeyboardFromSettingsLocked(userId);
}
}
@@ -649,11 +662,11 @@
// sender userId can be a real user ID or USER_ALL.
final int senderUserId = pendingResult.getSendingUserId();
synchronized (ImfLock.class) {
- if (senderUserId != UserHandle.USER_ALL && senderUserId != mCurrentUserId) {
+ if (senderUserId != UserHandle.USER_ALL && senderUserId != mCurrentImeUserId) {
// A background user is trying to hide the dialog. Ignore.
return;
}
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
if (Flags.imeSwitcherRevamp()) {
final var bindingController = getInputMethodBindingController(userId);
mMenuControllerNew.hide(bindingController.getCurTokenDisplayId(), userId);
@@ -1187,7 +1200,7 @@
mShowOngoingImeSwitcherForPhones = false;
- mCurrentUserId = mActivityManagerInternal.getCurrentUserId();
+ mCurrentImeUserId = mActivityManagerInternal.getCurrentUserId();
final IntFunction<InputMethodBindingController>
bindingControllerFactory = userId -> new InputMethodBindingController(userId,
InputMethodManagerService.this);
@@ -1282,7 +1295,7 @@
@GuardedBy("ImfLock.class")
private void switchUserOnHandlerLocked(@UserIdInt int newUserId,
IInputMethodClientInvoker clientToBeReset) {
- final int prevUserId = mCurrentUserId;
+ final int prevUserId = mCurrentImeUserId;
if (DEBUG) {
Slog.d(TAG, "Switching user stage 1/3. newUserId=" + newUserId
+ " prevUserId=" + prevUserId);
@@ -1307,7 +1320,7 @@
// TODO(b/342027196): Double check if we need to always reset upon user switching.
newUserData.mLastEnabledInputMethodsStr = "";
- mCurrentUserId = newUserId;
+ mCurrentImeUserId = newUserId;
final String defaultImiId = SecureSettingsWrapper.getString(
Settings.Secure.DEFAULT_INPUT_METHOD, null, newUserId);
@@ -1407,13 +1420,13 @@
}
if (!mSystemReady) {
mSystemReady = true;
- final int currentUserId = mCurrentUserId;
+ final int currentImeUserId = mCurrentImeUserId;
mStatusBarManagerInternal =
LocalServices.getService(StatusBarManagerInternal.class);
- hideStatusBarIconLocked(currentUserId);
- final var bindingController = getInputMethodBindingController(currentUserId);
+ hideStatusBarIconLocked(currentImeUserId);
+ final var bindingController = getInputMethodBindingController(currentImeUserId);
updateSystemUiLocked(bindingController.getImeWindowVis(),
- bindingController.getBackDisposition(), currentUserId);
+ bindingController.getBackDisposition(), currentImeUserId);
mShowOngoingImeSwitcherForPhones = mRes.getBoolean(
com.android.internal.R.bool.show_ongoing_ime_switcher);
if (mShowOngoingImeSwitcherForPhones) {
@@ -1593,7 +1606,7 @@
}
// Check if selected IME of current user supports handwriting.
- if (userId == mCurrentUserId) {
+ if (userId == mCurrentImeUserId) {
final var bindingController = getInputMethodBindingController(userId);
return bindingController.supportsStylusHandwriting()
&& (!connectionless
@@ -2545,7 +2558,7 @@
final int userId = userData.mUserId;
// To minimize app compat risk, ignore background users' request for single-user mode.
// TODO(b/357178609): generalize the logic and remove this special rule.
- if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) {
+ if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) {
return;
}
if (iconId == 0) {
@@ -2577,7 +2590,7 @@
private void hideStatusBarIconLocked(@UserIdInt int userId) {
// To minimize app compat risk, ignore background users' request for single-user mode.
// TODO(b/357178609): generalize the logic and remove this special rule.
- if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) {
+ if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) {
return;
}
if (mStatusBarManagerInternal != null) {
@@ -2777,7 +2790,7 @@
private void updateSystemUiLocked(int vis, int backDisposition, @UserIdInt int userId) {
// To minimize app compat risk, ignore background users' request for single-user mode.
// TODO(b/357178609): generalize the logic and remove this special rule.
- if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) {
+ if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) {
return;
}
final var userData = getUserData(userId);
@@ -2933,7 +2946,7 @@
// TODO(b/357663774): Figure out how to better handle this scenario.
userData.mSubtypeForKeyboardLayoutMapping =
Pair.create(newSubtypeHandle, normalizedSubtype);
- if (userId != mCurrentUserId) {
+ if (userId != mCurrentImeUserId) {
return;
}
mInputManagerInternal.onInputMethodSubtypeChangedForKeyboardLayoutMapping(
@@ -3703,7 +3716,7 @@
return InputBindResult.USER_SWITCHING;
}
final int[] profileIdsWithDisabled = mUserManagerInternal.getProfileIds(
- mCurrentUserId, false /* enabledOnly */);
+ mCurrentImeUserId, false /* enabledOnly */);
for (int profileId : profileIdsWithDisabled) {
if (profileId == userId) {
scheduleSwitchUserTaskLocked(userId, cs.mClient);
@@ -3750,9 +3763,9 @@
}
// Verify if caller is a background user.
- if (!mConcurrentMultiUserModeEnabled && userId != mCurrentUserId) {
+ if (!mConcurrentMultiUserModeEnabled && userId != mCurrentImeUserId) {
if (ArrayUtils.contains(
- mUserManagerInternal.getProfileIds(mCurrentUserId, false),
+ mUserManagerInternal.getProfileIds(mCurrentImeUserId, false),
userId)) {
// cross-profile access is always allowed here to allow
// profile-switching.
@@ -4170,29 +4183,27 @@
// and the framework couldn't find the last ime, we will make the last ime be
// the most applicable enabled keyboard subtype of the system imes.
final List<InputMethodInfo> enabled = settings.getEnabledInputMethodList();
- if (enabled != null) {
- final int enabledCount = enabled.size();
- final String locale;
- if (currentSubtype != null
- && !TextUtils.isEmpty(currentSubtype.getLocale())) {
- locale = currentSubtype.getLocale();
- } else {
- locale = SystemLocaleWrapper.get(userId).get(0).toString();
- }
- for (int i = 0; i < enabledCount; ++i) {
- final InputMethodInfo imi = enabled.get(i);
- if (imi.getSubtypeCount() > 0 && imi.isSystem()) {
- InputMethodSubtype keyboardSubtype =
- SubtypeUtils.findLastResortApplicableSubtype(
- SubtypeUtils.getSubtypes(imi),
- SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
- if (keyboardSubtype != null) {
- targetLastImiId = imi.getId();
- subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi,
- keyboardSubtype.hashCode());
- if (keyboardSubtype.getLocale().equals(locale)) {
- break;
- }
+ final int enabledCount = enabled.size();
+ final String locale;
+ if (currentSubtype != null
+ && !TextUtils.isEmpty(currentSubtype.getLocale())) {
+ locale = currentSubtype.getLocale();
+ } else {
+ locale = SystemLocaleWrapper.get(userId).get(0).toString();
+ }
+ for (int i = 0; i < enabledCount; ++i) {
+ final InputMethodInfo imi = enabled.get(i);
+ if (imi.getSubtypeCount() > 0 && imi.isSystem()) {
+ InputMethodSubtype keyboardSubtype =
+ SubtypeUtils.findLastResortApplicableSubtype(
+ SubtypeUtils.getSubtypes(imi),
+ SubtypeUtils.SUBTYPE_MODE_KEYBOARD, locale, true);
+ if (keyboardSubtype != null) {
+ targetLastImiId = imi.getId();
+ subtypeIndex = SubtypeUtils.getSubtypeIndexFromHashCode(imi,
+ keyboardSubtype.hashCode());
+ if (keyboardSubtype.getLocale().equals(locale)) {
+ break;
}
}
}
@@ -4442,7 +4453,7 @@
mStylusIds.add(deviceId);
// a new Stylus is detected. If IME supports handwriting, and we don't have
// handwriting initialized, lets do it now.
- final var bindingController = getInputMethodBindingController(mCurrentUserId);
+ final var bindingController = getInputMethodBindingController(mCurrentImeUserId);
if (!mHwController.getCurrentRequestId().isPresent()
&& bindingController.supportsStylusHandwriting()) {
scheduleResetStylusHandwriting();
@@ -4631,7 +4642,7 @@
private void dumpDebug(ProtoOutputStream proto, long fieldId) {
synchronized (ImfLock.class) {
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
final var userData = getUserData(userId);
final var bindingController = userData.mBindingController;
final var visibilityStateComputer = userData.mVisibilityStateComputer;
@@ -4950,7 +4961,7 @@
case MSG_REMOVE_IME_SURFACE: {
synchronized (ImfLock.class) {
// TODO(b/305849394): Needs to figure out what to do where for background users.
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
final var userData = getUserData(userId);
try {
if (userData.mEnabledSession != null
@@ -5016,7 +5027,8 @@
case MSG_RESET_HANDWRITING: {
synchronized (ImfLock.class) {
- final var bindingController = getInputMethodBindingController(mCurrentUserId);
+ final var bindingController =
+ getInputMethodBindingController(mCurrentImeUserId);
if (bindingController.supportsStylusHandwriting()
&& bindingController.getCurMethod() != null
&& hasSupportedStylusLocked()) {
@@ -5100,7 +5112,7 @@
private void handleSetInteractive(final boolean interactive) {
synchronized (ImfLock.class) {
// TODO(b/305849394): Support multiple IMEs.
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
final var userData = getUserData(userId);
final var bindingController = userData.mBindingController;
mIsInteractive = interactive;
@@ -6059,7 +6071,7 @@
final Printer p = new PrintWriterPrinter(pw);
synchronized (ImfLock.class) {
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
final InputMethodSettings settings = InputMethodSettingsRepository.get(userId);
final var userData = getUserData(userId);
p.println("Current Input Method Manager state:");
@@ -6091,7 +6103,7 @@
};
mClientController.forAllClients(clientControllerDump);
final var bindingController = userData.mBindingController;
- p.println(" mCurrentUserId=" + userData.mUserId);
+ p.println(" mCurrentImeUserId=" + userData.mUserId);
p.println(" mCurMethodId=" + bindingController.getSelectedMethodId());
client = userData.mCurClient;
p.println(" mCurClient=" + client + " mCurSeq="
@@ -6184,7 +6196,7 @@
p.println("No input method client.");
}
synchronized (ImfLock.class) {
- final int userId = mCurrentUserId;
+ final int userId = mCurrentImeUserId;
final var userData = getUserData(userId);
if (userData.mImeBindingState.mFocusedWindowClient != null
&& client != userData.mImeBindingState.mFocusedWindowClient) {
@@ -6415,7 +6427,7 @@
}
final int[] userIds;
synchronized (ImfLock.class) {
- userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, mCurrentUserId,
+ userIds = InputMethodUtils.resolveUserId(userIdToBeResolved, mCurrentImeUserId,
shellCommand.getErrPrintWriter());
}
try (PrintWriter pr = shellCommand.getOutPrintWriter()) {
@@ -6461,7 +6473,7 @@
PrintWriter error = shellCommand.getErrPrintWriter()) {
synchronized (ImfLock.class) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mCurrentUserId, shellCommand.getErrPrintWriter());
+ mCurrentImeUserId, shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
@@ -6556,7 +6568,7 @@
PrintWriter error = shellCommand.getErrPrintWriter()) {
synchronized (ImfLock.class) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mCurrentUserId, shellCommand.getErrPrintWriter());
+ mCurrentImeUserId, shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
@@ -6576,6 +6588,28 @@
out.print(imeId);
out.print(" selected for user #");
out.println(userId);
+
+ // Workaround for b/354782333.
+ final InputMethodSettings settings =
+ InputMethodSettingsRepository.get(userId);
+ final var bindingController = getInputMethodBindingController(userId);
+ final int deviceId = bindingController.getDeviceIdToShowIme();
+ final String settingsValue;
+ if (deviceId == DEVICE_ID_DEFAULT) {
+ settingsValue = settings.getSelectedInputMethod();
+ } else {
+ settingsValue = settings.getSelectedDefaultDeviceInputMethod();
+ }
+ if (!TextUtils.equals(settingsValue, imeId)) {
+ Slog.w(TAG, "DEFAULT_INPUT_METHOD=" + settingsValue
+ + " is not updated. Fixing it up to " + imeId
+ + " See b/354782333.");
+ if (deviceId == DEVICE_ID_DEFAULT) {
+ settings.putSelectedInputMethod(imeId);
+ } else {
+ settings.putSelectedDefaultDeviceInputMethod(imeId);
+ }
+ }
}
hasFailed |= failedToSelectUnknownIme;
}
@@ -6597,7 +6631,7 @@
synchronized (ImfLock.class) {
try (PrintWriter out = shellCommand.getOutPrintWriter()) {
final int[] userIds = InputMethodUtils.resolveUserId(userIdToBeResolved,
- mCurrentUserId, shellCommand.getErrPrintWriter());
+ mCurrentImeUserId, shellCommand.getErrPrintWriter());
for (int userId : userIds) {
if (!userHasDebugPriv(userId, shellCommand)) {
continue;
diff --git a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
index 4dbbfa2..c77b768 100644
--- a/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
+++ b/services/core/java/com/android/server/inputmethod/InputMethodSubtypeSwitchingController.java
@@ -512,6 +512,9 @@
/**
* Gets the next input method and subtype from the given ones.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
@@ -523,17 +526,20 @@
public ImeSubtypeListItem next(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype, boolean onlyCurrentIme,
boolean useRecency, boolean forward) {
- final int size = mItems.size();
- if (size <= 1) {
+ if (mItems.isEmpty()) {
return null;
}
final int index = getIndex(imi, subtype, useRecency);
if (index < 0) {
- return null;
+ Slog.w(TAG, "Trying to switch away from input method: " + imi
+ + " and subtype " + subtype + " which are not in the list,"
+ + " falling back to most recent item in list.");
+ return mItems.get(mRecencyMap[0]);
}
final int incrementSign = (forward ? 1 : -1);
+ final int size = mItems.size();
for (int i = 1; i < size; i++) {
final int nextIndex = (index + i * incrementSign + size) % size;
final int mappedIndex = useRecency ? mRecencyMap[nextIndex] : nextIndex;
@@ -554,7 +560,7 @@
*/
public boolean setMostRecent(@NonNull InputMethodInfo imi,
@Nullable InputMethodSubtype subtype) {
- if (mItems.size() <= 1) {
+ if (mItems.isEmpty()) {
return false;
}
@@ -849,6 +855,9 @@
/**
* Gets the next input method and subtype, starting from the given ones, in the given direction.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
@@ -867,6 +876,9 @@
* Gets the next input method and subtype suitable for hardware keyboards, starting from the
* given ones, in the given direction.
*
+ * <p>If the given input method and subtype are not found, this returns the most recent
+ * input method and subtype.</p>
+ *
* @param onlyCurrentIme whether to consider only subtypes of the current input method.
* @param imi the input method to find the next value from.
* @param subtype the input method subtype to find the next value from, if any.
diff --git a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
index 17f6561..a6f4c0e 100644
--- a/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
+++ b/services/core/java/com/android/server/net/NetworkPolicyManagerService.java
@@ -530,6 +530,12 @@
*/
private boolean mUseDifferentDelaysForBackgroundChain;
+ /**
+ * Core uids and apps without the internet permission will not have any firewall rules applied
+ * to them.
+ */
+ private boolean mNeverApplyRulesToCoreUids;
+
// See main javadoc for instructions on how to use these locks.
final Object mUidRulesFirstLock = new Object();
final Object mNetworkPoliciesSecondLock = new Object();
@@ -760,7 +766,8 @@
/** List of apps indexed by uid and whether they have the internet permission */
@GuardedBy("mUidRulesFirstLock")
- private final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray();
+ @VisibleForTesting
+ final SparseBooleanArray mInternetPermissionMap = new SparseBooleanArray();
/**
* Map of uid -> UidStateCallbackInfo objects holding the data received from
@@ -1038,6 +1045,7 @@
mUseMeteredFirewallChains = Flags.useMeteredFirewallChains();
mUseDifferentDelaysForBackgroundChain = Flags.useDifferentDelaysForBackgroundChain();
+ mNeverApplyRulesToCoreUids = Flags.neverApplyRulesToCoreUids();
synchronized (mUidRulesFirstLock) {
synchronized (mNetworkPoliciesSecondLock) {
@@ -4088,6 +4096,8 @@
+ mUseMeteredFirewallChains);
fout.println(Flags.FLAG_USE_DIFFERENT_DELAYS_FOR_BACKGROUND_CHAIN + ": "
+ mUseDifferentDelaysForBackgroundChain);
+ fout.println(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS + ": "
+ + mNeverApplyRulesToCoreUids);
fout.println();
fout.println("mRestrictBackgroundLowPowerMode: " + mRestrictBackgroundLowPowerMode);
@@ -4878,6 +4888,12 @@
int[] idleUids = mUsageStats.getIdleUidsForUser(user.id);
for (int uid : idleUids) {
if (!mPowerSaveTempWhitelistAppIds.get(UserHandle.getAppId(uid), false)) {
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ // This check is needed to keep mUidFirewallStandbyRules free of any
+ // such uids. Doing this keeps it in sync with the actual rules applied
+ // in the underlying connectivity stack.
+ continue;
+ }
// quick check: if this uid doesn't have INTERNET permission, it
// doesn't have network access anyway, so it is a waste to mess
// with it here.
@@ -5180,6 +5196,11 @@
@GuardedBy("mUidRulesFirstLock")
private boolean isUidValidForAllowlistRulesUL(int uid) {
+ return isUidValidForRulesUL(uid);
+ }
+
+ @GuardedBy("mUidRulesFirstLock")
+ private boolean isUidValidForRulesUL(int uid) {
return UserHandle.isApp(uid) && hasInternetPermissionUL(uid);
}
@@ -6194,41 +6215,33 @@
}
}
- private void addSdkSandboxUidsIfNeeded(SparseIntArray uidRules) {
- final int size = uidRules.size();
- final SparseIntArray sdkSandboxUids = new SparseIntArray();
- for (int index = 0; index < size; index++) {
- final int uid = uidRules.keyAt(index);
- final int rule = uidRules.valueAt(index);
- if (Process.isApplicationUid(uid)) {
- sdkSandboxUids.put(Process.toSdkSandboxUid(uid), rule);
- }
- }
-
- for (int index = 0; index < sdkSandboxUids.size(); index++) {
- final int uid = sdkSandboxUids.keyAt(index);
- final int rule = sdkSandboxUids.valueAt(index);
- uidRules.put(uid, rule);
- }
- }
-
/**
* Set uid rules on a particular firewall chain. This is going to synchronize the rules given
* here to netd. It will clean up dead rules and make sure the target chain only contains rules
* specified here.
*/
+ @GuardedBy("mUidRulesFirstLock")
private void setUidFirewallRulesUL(int chain, SparseIntArray uidRules) {
- addSdkSandboxUidsIfNeeded(uidRules);
try {
int size = uidRules.size();
- int[] uids = new int[size];
- int[] rules = new int[size];
+ final IntArray uids = new IntArray(size);
+ final IntArray rules = new IntArray(size);
for(int index = size - 1; index >= 0; --index) {
- uids[index] = uidRules.keyAt(index);
- rules[index] = uidRules.valueAt(index);
+ final int uid = uidRules.keyAt(index);
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ continue;
+ }
+ uids.add(uid);
+ rules.add(uidRules.valueAt(index));
+ if (Process.isApplicationUid(uid)) {
+ uids.add(Process.toSdkSandboxUid(uid));
+ rules.add(uidRules.valueAt(index));
+ }
}
- mNetworkManager.setFirewallUidRules(chain, uids, rules);
- mLogger.firewallRulesChanged(chain, uids, rules);
+ final int[] uidArray = uids.toArray();
+ final int[] ruleArray = rules.toArray();
+ mNetworkManager.setFirewallUidRules(chain, uidArray, ruleArray);
+ mLogger.firewallRulesChanged(chain, uidArray, ruleArray);
} catch (IllegalStateException e) {
Log.wtf(TAG, "problem setting firewall uid rules", e);
} catch (RemoteException e) {
@@ -6241,6 +6254,9 @@
*/
@GuardedBy("mUidRulesFirstLock")
private void setUidFirewallRuleUL(int chain, int uid, int rule) {
+ if (mNeverApplyRulesToCoreUids && !isUidValidForRulesUL(uid)) {
+ return;
+ }
if (Trace.isTagEnabled(Trace.TRACE_TAG_NETWORK)) {
Trace.traceBegin(Trace.TRACE_TAG_NETWORK,
"setUidFirewallRuleUL: " + chain + "/" + uid + "/" + rule);
@@ -6249,8 +6265,6 @@
if (chain == FIREWALL_CHAIN_STANDBY) {
mUidFirewallStandbyRules.put(uid, rule);
}
- // Note that we do not need keep a separate cache of uid rules for chains that we do
- // not call #setUidFirewallRulesUL for.
try {
mNetworkManager.setFirewallUidRule(chain, uid, rule);
@@ -6295,6 +6309,8 @@
* Resets all firewall rules associated with an UID.
*/
private void resetUidFirewallRules(int uid) {
+ // Resetting rules for uids with isUidValidForRulesUL = false should be OK as no rules
+ // should be previously set and the downstream code will skip no-op changes.
try {
mNetworkManager.setFirewallUidRule(FIREWALL_CHAIN_DOZABLE, uid,
FIREWALL_RULE_DEFAULT);
diff --git a/services/core/java/com/android/server/net/flags.aconfig b/services/core/java/com/android/server/net/flags.aconfig
index 586baf0..7f04e66 100644
--- a/services/core/java/com/android/server/net/flags.aconfig
+++ b/services/core/java/com/android/server/net/flags.aconfig
@@ -27,3 +27,13 @@
purpose: PURPOSE_BUGFIX
}
}
+
+flag {
+ name: "never_apply_rules_to_core_uids"
+ namespace: "backstage_power"
+ description: "Removes all rule bookkeeping and evaluation logic for core uids and uids without the internet permission"
+ bug: "356956588"
+ metadata {
+ purpose: PURPOSE_BUGFIX
+ }
+}
diff --git a/services/core/java/com/android/server/notification/ConditionProviders.java b/services/core/java/com/android/server/notification/ConditionProviders.java
index a44e553..66e61c0 100644
--- a/services/core/java/com/android/server/notification/ConditionProviders.java
+++ b/services/core/java/com/android/server/notification/ConditionProviders.java
@@ -16,9 +16,6 @@
package com.android.server.notification;
-import static android.service.notification.Condition.STATE_TRUE;
-import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
-
import android.app.INotificationManager;
import android.app.NotificationManager;
import android.content.ComponentName;
@@ -322,20 +319,7 @@
final Condition c = conditions[i];
final ConditionRecord r = getRecordLocked(c.id, info.component, true /*create*/);
r.info = info;
- if (android.app.Flags.modesUi()) {
- // if user turned on the mode, ignore the update unless the app also wants the
- // mode on. this will update the origin of the mode and let the owner turn it
- // off when the context ends
- if (r.condition != null && r.condition.source == ORIGIN_USER_IN_SYSTEMUI) {
- if (r.condition.state == STATE_TRUE && c.state == STATE_TRUE) {
- r.condition = c;
- }
- } else {
- r.condition = c;
- }
- } else {
- r.condition = c;
- }
+ r.condition = c;
}
}
final int N = conditions.length;
diff --git a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
index b4459cb..9818916 100644
--- a/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
+++ b/services/core/java/com/android/server/notification/NotificationAttentionHelper.java
@@ -1519,7 +1519,14 @@
@Override
public void setLastNotificationUpdateTimeMs(NotificationRecord record,
long timestampMillis) {
- super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ if (Flags.politeNotificationsAttnUpdate()) {
+ // Set last update per package/channel only for exempt notifications
+ if (isAvalancheExempted(record)) {
+ super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ }
+ } else {
+ super.setLastNotificationUpdateTimeMs(record, timestampMillis);
+ }
mLastNotificationTimestamp = timestampMillis;
mAppStrategy.setLastNotificationUpdateTimeMs(record, timestampMillis);
}
diff --git a/services/core/java/com/android/server/notification/NotificationManagerService.java b/services/core/java/com/android/server/notification/NotificationManagerService.java
index 45b8da5..c7c984b 100644
--- a/services/core/java/com/android/server/notification/NotificationManagerService.java
+++ b/services/core/java/com/android/server/notification/NotificationManagerService.java
@@ -2963,8 +2963,9 @@
};
cancelGroupChildrenLocked(userId, pkg, Binder.getCallingUid(),
Binder.getCallingPid(), null,
- false, childrenFlagChecker, groupKey,
- REASON_APP_CANCEL, SystemClock.elapsedRealtime());
+ false, childrenFlagChecker,
+ NotificationManagerService::wasChildOfForceRegroupedGroupChecker,
+ groupKey, REASON_APP_CANCEL, SystemClock.elapsedRealtime());
}
}
});
@@ -8667,8 +8668,8 @@
if (r.getNotification().isGroupSummary()) {
cancelGroupChildrenLocked(mUserId, mPkg, mCallingUid, mCallingPid,
listenerName, mSendDelete, childrenFlagChecker,
- r.getNotification().getGroup(), mReason,
- mCancellationElapsedTimeMs);
+ NotificationManagerService::isChildOfCurrentGroupChecker,
+ r.getGroupKey(), mReason, mCancellationElapsedTimeMs);
}
mAttentionHelper.updateLightsLocked();
if (mShortcutHelper != null) {
@@ -9390,8 +9391,8 @@
if (oldIsSummary && (!isSummary || !oldGroup.equals(group))) {
cancelGroupChildrenLocked(old.getUserId(), old.getSbn().getPackageName(), callingUid,
callingPid, null, false /* sendDelete */, childrenFlagChecker,
- old.getNotification().getGroup(), REASON_APP_CANCEL,
- SystemClock.elapsedRealtime());
+ NotificationManagerService::isChildOfCurrentGroupChecker, old.getGroupKey(),
+ REASON_APP_CANCEL, SystemClock.elapsedRealtime());
}
}
@@ -10372,13 +10373,45 @@
public boolean apply(int flags);
}
- private static boolean isChildOfGroup(final NotificationRecord childRecord, int userId,
+ @FunctionalInterface
+ private interface GroupChildChecker {
+ // Returns true if the childRecord is a child of the group defined
+ // by the rest of the parameters
+ boolean apply(NotificationRecord childRecord, int userId, String pkg, String groupKey);
+ }
+
+ /**
+ * Checks that the notification is currently a child of the group
+ * @param childRecord the notification to check
+ * @param userId userId of the group
+ * @param pkg package name of the group
+ * @param groupKey group key for a current group
+ * @return true if the childRecord is currently a child of the group
+ */
+ private static boolean isChildOfCurrentGroupChecker(NotificationRecord childRecord, int userId,
String pkg, String groupKey) {
return (childRecord.getUser().getIdentifier() == userId
&& childRecord.getSbn().getPackageName().equals(pkg)
&& childRecord.getSbn().isGroup()
&& !childRecord.getNotification().isGroupSummary()
- && TextUtils.equals(groupKey, childRecord.getNotification().getGroup()));
+ && TextUtils.equals(groupKey, childRecord.getGroupKey()));
+ }
+
+ /**
+ * Checks that the notification was originally a child of the group
+ * @param childRecord the notification to check
+ * @param userId userId of the group
+ * @param pkg package name of the group
+ * @param groupKey original/initial group key for a group that was force grouped
+ * @return true if the childRecord was originally a child of the group
+ */
+ private static boolean wasChildOfForceRegroupedGroupChecker(NotificationRecord childRecord,
+ int userId, String pkg, String groupKey) {
+ return (childRecord.getUser().getIdentifier() == userId
+ && childRecord.getSbn().getPackageName().equals(pkg)
+ && childRecord.getSbn().isGroup()
+ && !childRecord.getNotification().isGroupSummary()
+ && TextUtils.equals(groupKey, childRecord.getOriginalGroupKey()));
}
@GuardedBy("mNotificationLock")
@@ -10539,18 +10572,19 @@
// Warning: The caller is responsible for invoking updateLightsLocked().
@GuardedBy("mNotificationLock")
private void cancelGroupChildrenLocked(int userId, String pkg, int callingUid, int callingPid,
- String listenerName, boolean sendDelete, FlagChecker flagChecker, String groupKey,
- int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
+ String listenerName, boolean sendDelete, FlagChecker flagChecker,
+ GroupChildChecker groupChildChecker, String groupKey, int reason,
+ @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
if (pkg == null) {
if (DBG) Slog.e(TAG, "No package for group summary");
return;
}
cancelGroupChildrenByListLocked(mNotificationList, userId, pkg, callingUid, callingPid,
- listenerName, sendDelete, true, flagChecker, groupKey,
+ listenerName, sendDelete, true, flagChecker, groupChildChecker, groupKey,
reason, cancellationElapsedTimeMs);
cancelGroupChildrenByListLocked(mEnqueuedNotifications, userId, pkg, callingUid, callingPid,
- listenerName, sendDelete, false, flagChecker, groupKey,
+ listenerName, sendDelete, false, flagChecker, groupChildChecker, groupKey,
reason, cancellationElapsedTimeMs);
}
@@ -10558,12 +10592,13 @@
private void cancelGroupChildrenByListLocked(ArrayList<NotificationRecord> notificationList,
int userId, String pkg, int callingUid, int callingPid,
String listenerName, boolean sendDelete, boolean wasPosted, FlagChecker flagChecker,
- String groupKey, int reason, @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
+ GroupChildChecker grouChildChecker, String groupKey, int reason,
+ @ElapsedRealtimeLong long cancellationElapsedTimeMs) {
final int childReason = REASON_GROUP_SUMMARY_CANCELED;
for (int i = notificationList.size() - 1; i >= 0; i--) {
final NotificationRecord childR = notificationList.get(i);
final StatusBarNotification childSbn = childR.getSbn();
- if (isChildOfGroup(childR, userId, pkg, groupKey)
+ if (grouChildChecker.apply(childR, userId, pkg, groupKey)
&& (flagChecker == null || flagChecker.apply(childR.getFlags()))
&& (!childR.getChannel().isImportantConversation() || reason != REASON_CANCEL)) {
EventLogTags.writeNotificationCancel(callingUid, callingPid, pkg, childSbn.getId(),
diff --git a/services/core/java/com/android/server/notification/NotificationRecord.java b/services/core/java/com/android/server/notification/NotificationRecord.java
index bd00901..1392003 100644
--- a/services/core/java/com/android/server/notification/NotificationRecord.java
+++ b/services/core/java/com/android/server/notification/NotificationRecord.java
@@ -1163,6 +1163,21 @@
getSbn().setOverrideGroupKey(overrideGroupKey);
}
+ /**
+ * Get the original group key that was set via {@link Notification.Builder#setGroup}
+ *
+ * This value is different than the value returned by {@link #getGroupKey()} as it does
+ * not contain any userId or package name.
+ *
+ * This value is different than the value returned
+ * by {@link StatusBarNotification#getGroup()} if the notification group
+ * was overridden: by NotificationAssistantService or by autogrouping.
+ */
+ @Nullable
+ public String getOriginalGroupKey() {
+ return getSbn().getNotification().getGroup();
+ }
+
public NotificationChannel getChannel() {
return mChannel;
}
diff --git a/services/core/java/com/android/server/notification/ZenModeConditions.java b/services/core/java/com/android/server/notification/ZenModeConditions.java
index 268d835..d495ef5 100644
--- a/services/core/java/com/android/server/notification/ZenModeConditions.java
+++ b/services/core/java/com/android/server/notification/ZenModeConditions.java
@@ -82,7 +82,7 @@
for (ZenRule automaticRule : config.automaticRules.values()) {
if (automaticRule.component != null) {
evaluateRule(automaticRule, current, trigger, processSubscriptions, false);
- updateSnoozing(automaticRule);
+ automaticRule.reconsiderConditionOverride();
}
}
@@ -187,13 +187,4 @@
+ rule.conditionId);
}
}
-
- private boolean updateSnoozing(ZenRule rule) {
- if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) {
- rule.snoozing = false;
- if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId);
- return true;
- }
- return false;
- }
}
diff --git a/services/core/java/com/android/server/notification/ZenModeHelper.java b/services/core/java/com/android/server/notification/ZenModeHelper.java
index 8c280ed..db48835 100644
--- a/services/core/java/com/android/server/notification/ZenModeHelper.java
+++ b/services/core/java/com/android/server/notification/ZenModeHelper.java
@@ -37,6 +37,8 @@
import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN;
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP;
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
import static com.android.internal.util.FrameworkStatsLog.DND_MODE_RULE;
import static com.android.internal.util.Preconditions.checkArgument;
@@ -643,10 +645,10 @@
if ((rule.userModifiedFields & AutomaticZenRule.FIELD_INTERRUPTION_FILTER) == 0) {
rule.zenMode = zenMode;
}
- rule.snoozing = false;
rule.condition = new Condition(rule.conditionId,
mContext.getString(R.string.zen_mode_implicit_activated),
STATE_TRUE);
+ rule.resetConditionOverride();
setConfigLocked(newConfig, /* triggeringComponent= */ null, ORIGIN_APP,
"applyGlobalZenModeAsImplicitZenRule", callingUid);
@@ -867,8 +869,8 @@
ZenRule deletedRule = ruleToRemove.copy();
deletedRule.deletionInstant = Instant.now(mClock);
// If the rule is restored it shouldn't be active (or snoozed).
- deletedRule.snoozing = false;
deletedRule.condition = null;
+ deletedRule.resetConditionOverride();
// Overwrites a previously-deleted rule with the same conditionId, but that's okay.
config.deletedRules.put(deletedKey, deletedRule);
}
@@ -885,7 +887,12 @@
if (rule == null || !canManageAutomaticZenRule(rule)) {
return Condition.STATE_UNKNOWN;
}
- return rule.condition != null ? rule.condition.state : STATE_FALSE;
+ if (Flags.modesApi() && Flags.modesUi()) {
+ return rule.isAutomaticActive() ? STATE_TRUE : STATE_FALSE;
+ } else {
+ // Buggy, does not consider snoozing!
+ return rule.condition != null ? rule.condition.state : STATE_FALSE;
+ }
}
}
@@ -943,12 +950,40 @@
}
for (ZenRule rule : rules) {
- rule.condition = condition;
- updateSnoozing(rule);
+ applyConditionAndReconsiderOverride(rule, condition, origin);
setConfigLocked(config, rule.component, origin, "conditionChanged", callingUid);
}
}
+ private static void applyConditionAndReconsiderOverride(ZenRule rule, Condition condition,
+ int origin) {
+ if (Flags.modesApi() && Flags.modesUi()) {
+ if (origin == ORIGIN_USER_IN_SYSTEMUI && condition != null
+ && condition.source == SOURCE_USER_ACTION) {
+ // Apply as override, instead of actual condition.
+ if (condition.state == STATE_TRUE) {
+ // Manually turn on a rule -> Apply override.
+ rule.setConditionOverride(OVERRIDE_ACTIVATE);
+ } else if (condition.state == STATE_FALSE) {
+ // Manually turn off a rule. If the rule was manually activated before, reset
+ // override -- but only if this will not result in the rule turning on
+ // immediately because of a previously snoozed condition! In that case, apply
+ // deactivate-override.
+ rule.resetConditionOverride();
+ if (rule.isAutomaticActive()) {
+ rule.setConditionOverride(OVERRIDE_DEACTIVATE);
+ }
+ }
+ } else {
+ rule.condition = condition;
+ rule.reconsiderConditionOverride();
+ }
+ } else {
+ rule.condition = condition;
+ rule.reconsiderConditionOverride();
+ }
+ }
+
private static List<ZenRule> findMatchingRules(ZenModeConfig config, Uri id,
Condition condition) {
List<ZenRule> matchingRules = new ArrayList<>();
@@ -971,15 +1006,6 @@
return true;
}
- private boolean updateSnoozing(ZenRule rule) {
- if (rule != null && rule.snoozing && !rule.isTrueOrUnknown()) {
- rule.snoozing = false;
- if (DEBUG) Log.d(TAG, "Snoozing reset for " + rule.conditionId);
- return true;
- }
- return false;
- }
-
public int getCurrentInstanceCount(ComponentName cn) {
if (cn == null) {
return 0;
@@ -1181,7 +1207,7 @@
if (rule.enabled != azr.isEnabled()) {
rule.enabled = azr.isEnabled();
- rule.snoozing = false;
+ rule.resetConditionOverride();
modified = true;
}
if (!Objects.equals(rule.configurationActivity, azr.getConfigurationActivity())) {
@@ -1271,7 +1297,7 @@
return modified;
} else {
if (rule.enabled != azr.isEnabled()) {
- rule.snoozing = false;
+ rule.resetConditionOverride();
}
rule.name = azr.getName();
rule.condition = null;
@@ -1573,18 +1599,16 @@
// For API calls (different origin) keep old behavior of snoozing all rules.
for (ZenRule automaticRule : newConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
- automaticRule.snoozing = true;
+ automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE);
}
}
}
} else {
if (zenMode == Global.ZEN_MODE_OFF) {
newConfig.manualRule = null;
- // User deactivation of DND means just turning off the manual DND rule.
- // For API calls (different origin) keep old behavior of snoozing all rules.
for (ZenRule automaticRule : newConfig.automaticRules.values()) {
if (automaticRule.isAutomaticActive()) {
- automaticRule.snoozing = true;
+ automaticRule.setConditionOverride(OVERRIDE_DEACTIVATE);
}
}
@@ -1626,13 +1650,11 @@
void dump(ProtoOutputStream proto) {
proto.write(ZenModeProto.ZEN_MODE, mZenMode);
synchronized (mConfigLock) {
- if (mConfig.manualRule != null) {
+ if (mConfig.isManualActive()) {
mConfig.manualRule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS);
}
for (ZenRule rule : mConfig.automaticRules.values()) {
- if (rule.enabled && rule.condition != null
- && rule.condition.state == STATE_TRUE
- && !rule.snoozing) {
+ if (rule.isAutomaticActive()) {
rule.dumpDebug(proto, ZenModeProto.ENABLED_ACTIVE_CONDITIONS);
}
}
@@ -1695,8 +1717,8 @@
for (ZenRule automaticRule : config.automaticRules.values()) {
if (forRestore) {
// don't restore transient state from restored automatic rules
- automaticRule.snoozing = false;
automaticRule.condition = null;
+ automaticRule.resetConditionOverride();
automaticRule.creationTime = time;
}
diff --git a/services/core/java/com/android/server/om/OverlayManagerService.java b/services/core/java/com/android/server/om/OverlayManagerService.java
index 46585a5..6303ecd 100644
--- a/services/core/java/com/android/server/om/OverlayManagerService.java
+++ b/services/core/java/com/android/server/om/OverlayManagerService.java
@@ -80,6 +80,7 @@
import android.util.Slog;
import android.util.SparseArray;
+import com.android.internal.annotations.GuardedBy;
import com.android.internal.annotations.KeepForWeakReference;
import com.android.internal.content.PackageMonitor;
import com.android.internal.content.om.OverlayConfig;
@@ -1180,6 +1181,7 @@
// intent, querying the PackageManagerService for the actual current
// state may lead to contradictions within OMS. Better then to lag
// behind until all pending intents have been processed.
+ @GuardedBy("itself")
private final ArrayMap<String, PackageStateUsers> mCache = new ArrayMap<>();
private final ArraySet<Integer> mInitializedUsers = new ArraySet<>();
@@ -1207,10 +1209,12 @@
}
final ArrayMap<String, PackageState> userPackages = new ArrayMap<>();
- for (int i = 0, n = mCache.size(); i < n; i++) {
- final PackageStateUsers pkg = mCache.valueAt(i);
- if (pkg.mInstalledUsers.contains(userId)) {
- userPackages.put(mCache.keyAt(i), pkg.mPackageState);
+ synchronized (mCache) {
+ for (int i = 0, n = mCache.size(); i < n; i++) {
+ final PackageStateUsers pkg = mCache.valueAt(i);
+ if (pkg.mInstalledUsers.contains(userId)) {
+ userPackages.put(mCache.keyAt(i), pkg.mPackageState);
+ }
}
}
return userPackages;
@@ -1220,7 +1224,11 @@
@Nullable
public PackageState getPackageStateForUser(@NonNull final String packageName,
final int userId) {
- final PackageStateUsers pkg = mCache.get(packageName);
+ final PackageStateUsers pkg;
+
+ synchronized (mCache) {
+ pkg = mCache.get(packageName);
+ }
if (pkg != null && pkg.mInstalledUsers.contains(userId)) {
return pkg.mPackageState;
}
@@ -1251,12 +1259,15 @@
@NonNull
private PackageState addPackageUser(@NonNull final PackageState pkg,
final int user) {
- PackageStateUsers pkgUsers = mCache.get(pkg.getPackageName());
- if (pkgUsers == null) {
- pkgUsers = new PackageStateUsers(pkg);
- mCache.put(pkg.getPackageName(), pkgUsers);
- } else {
- pkgUsers.mPackageState = pkg;
+ PackageStateUsers pkgUsers;
+ synchronized (mCache) {
+ pkgUsers = mCache.get(pkg.getPackageName());
+ if (pkgUsers == null) {
+ pkgUsers = new PackageStateUsers(pkg);
+ mCache.put(pkg.getPackageName(), pkgUsers);
+ } else {
+ pkgUsers.mPackageState = pkg;
+ }
}
pkgUsers.mInstalledUsers.add(user);
return pkgUsers.mPackageState;
@@ -1265,18 +1276,24 @@
@NonNull
private void removePackageUser(@NonNull final String packageName, final int user) {
- final PackageStateUsers pkgUsers = mCache.get(packageName);
- if (pkgUsers == null) {
- return;
+ // synchronize should include the call to the other removePackageUser() method so that
+ // the access and modification happen under the same lock.
+ synchronized (mCache) {
+ final PackageStateUsers pkgUsers = mCache.get(packageName);
+ if (pkgUsers == null) {
+ return;
+ }
+ removePackageUser(pkgUsers, user);
}
- removePackageUser(pkgUsers, user);
}
@NonNull
private void removePackageUser(@NonNull final PackageStateUsers pkg, final int user) {
pkg.mInstalledUsers.remove(user);
if (pkg.mInstalledUsers.isEmpty()) {
- mCache.remove(pkg.mPackageState.getPackageName());
+ synchronized (mCache) {
+ mCache.remove(pkg.mPackageState.getPackageName());
+ }
}
}
@@ -1386,8 +1403,10 @@
public void forgetAllPackageInfos(final int userId) {
// Iterate in reverse order since removing the package in all users will remove the
// package from the cache.
- for (int i = mCache.size() - 1; i >= 0; i--) {
- removePackageUser(mCache.valueAt(i), userId);
+ synchronized (mCache) {
+ for (int i = mCache.size() - 1; i >= 0; i--) {
+ removePackageUser(mCache.valueAt(i), userId);
+ }
}
}
@@ -1405,22 +1424,23 @@
public void dump(@NonNull final PrintWriter pw, @NonNull DumpState dumpState) {
pw.println("AndroidPackage cache");
+ synchronized (mCache) {
+ if (!dumpState.isVerbose()) {
+ pw.println(TAB1 + mCache.size() + " package(s)");
+ return;
+ }
- if (!dumpState.isVerbose()) {
- pw.println(TAB1 + mCache.size() + " package(s)");
- return;
- }
+ if (mCache.size() == 0) {
+ pw.println(TAB1 + "<empty>");
+ return;
+ }
- if (mCache.size() == 0) {
- pw.println(TAB1 + "<empty>");
- return;
- }
-
- for (int i = 0, n = mCache.size(); i < n; i++) {
- final String packageName = mCache.keyAt(i);
- final PackageStateUsers pkg = mCache.valueAt(i);
- pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users=");
- pw.println(TextUtils.join(", ", pkg.mInstalledUsers));
+ for (int i = 0, n = mCache.size(); i < n; i++) {
+ final String packageName = mCache.keyAt(i);
+ final PackageStateUsers pkg = mCache.valueAt(i);
+ pw.print(TAB1 + packageName + ": " + pkg.mPackageState + " users=");
+ pw.println(TextUtils.join(", ", pkg.mInstalledUsers));
+ }
}
}
}
diff --git a/services/core/java/com/android/server/pm/InstallPackageHelper.java b/services/core/java/com/android/server/pm/InstallPackageHelper.java
index a0d5ea8..98e3e24 100644
--- a/services/core/java/com/android/server/pm/InstallPackageHelper.java
+++ b/services/core/java/com/android/server/pm/InstallPackageHelper.java
@@ -1801,26 +1801,37 @@
oldPackageState.getRestrictUpdateHash());
}
- if (oldPackage != null) {
- // APK should not change its sharedUserId declarations
- final var oldSharedUid = oldPackage.getSharedUserId() != null
- ? oldPackage.getSharedUserId() : "<nothing>";
- final var newSharedUid = parsedPackage.getSharedUserId() != null
- ? parsedPackage.getSharedUserId() : "<nothing>";
- if (!oldSharedUid.equals(newSharedUid)) {
- throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
- "Package " + parsedPackage.getPackageName()
- + " shared user changed from "
- + oldSharedUid + " to " + newSharedUid);
+ // APK should not change its sharedUserId declarations
+ final String oldSharedUid;
+ if (mPm.mSettings.getSharedUserSettingLPr(oldPackageState) != null) {
+ oldSharedUid = mPm.mSettings.getSharedUserSettingLPr(oldPackageState).name;
+ } else {
+ oldSharedUid = "<nothing>";
+ }
+ String newSharedUid = parsedPackage.getSharedUserId() != null
+ ? parsedPackage.getSharedUserId() : "<nothing>";
+ // If the previously installed app version doesn't have sharedUserSetting,
+ // check that the new apk either doesn't have sharedUserId or it is leaving one.
+ // If it contains sharedUserId but it is also leaving it, it's ok to proceed.
+ if (oldSharedUid.equals("<nothing>")) {
+ if (parsedPackage.isLeavingSharedUser()) {
+ newSharedUid = "<nothing>";
}
+ }
- // APK should not re-join shared UID
- if (oldPackage.isLeavingSharedUser()
- && !parsedPackage.isLeavingSharedUser()) {
- throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
- "Package " + parsedPackage.getPackageName()
- + " attempting to rejoin " + newSharedUid);
- }
+ if (!oldSharedUid.equals(newSharedUid)) {
+ throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+ "Package " + parsedPackage.getPackageName()
+ + " shared user changed from "
+ + oldSharedUid + " to " + newSharedUid);
+ }
+
+ // APK should not re-join shared UID
+ if (oldPackageState.isLeavingSharedUser()
+ && !parsedPackage.isLeavingSharedUser()) {
+ throw new PrepareFailure(INSTALL_FAILED_UID_CHANGED,
+ "Package " + parsedPackage.getPackageName()
+ + " attempting to rejoin " + newSharedUid);
}
// In case of rollback, remember per-user/profile install state
diff --git a/services/core/java/com/android/server/pm/PackageSetting.java b/services/core/java/com/android/server/pm/PackageSetting.java
index 9f10e01..d374142 100644
--- a/services/core/java/com/android/server/pm/PackageSetting.java
+++ b/services/core/java/com/android/server/pm/PackageSetting.java
@@ -98,6 +98,7 @@
SCANNED_AS_STOPPED_SYSTEM_APP,
PENDING_RESTORE,
DEBUGGABLE,
+ IS_LEAVING_SHARED_USER,
})
public @interface Flags {
}
@@ -107,6 +108,7 @@
private static final int SCANNED_AS_STOPPED_SYSTEM_APP = 1 << 3;
private static final int PENDING_RESTORE = 1 << 4;
private static final int DEBUGGABLE = 1 << 5;
+ private static final int IS_LEAVING_SHARED_USER = 1 << 6;
}
private int mBooleans;
@@ -595,6 +597,20 @@
}
/**
+ * @see PackageState#isLeavingSharedUser
+ */
+ public PackageSetting setLeavingSharedUser(boolean value) {
+ setBoolean(Booleans.IS_LEAVING_SHARED_USER, value);
+ onChanged();
+ return this;
+ }
+
+ @Override
+ public boolean isLeavingSharedUser() {
+ return getBoolean(Booleans.IS_LEAVING_SHARED_USER);
+ }
+
+ /**
* @see AndroidPackage#getBaseRevisionCode
*/
public PackageSetting setBaseRevisionCode(int value) {
diff --git a/services/core/java/com/android/server/pm/RemovePackageHelper.java b/services/core/java/com/android/server/pm/RemovePackageHelper.java
index 7afc358..26da84f 100644
--- a/services/core/java/com/android/server/pm/RemovePackageHelper.java
+++ b/services/core/java/com/android/server/pm/RemovePackageHelper.java
@@ -435,7 +435,7 @@
// Preserve split apk information for downgrade check with DELETE_KEEP_DATA and archived
// app cases
- if (deletedPkg.getSplitNames() != null) {
+ if (deletedPkg != null && deletedPkg.getSplitNames() != null) {
deletedPs.setSplitNames(deletedPkg.getSplitNames());
deletedPs.setSplitRevisionCodes(deletedPkg.getSplitRevisionCodes());
}
diff --git a/services/core/java/com/android/server/pm/ScanPackageUtils.java b/services/core/java/com/android/server/pm/ScanPackageUtils.java
index 95561f5f..61fddba 100644
--- a/services/core/java/com/android/server/pm/ScanPackageUtils.java
+++ b/services/core/java/com/android/server/pm/ScanPackageUtils.java
@@ -482,6 +482,7 @@
+ " to " + volumeUuid);
pkgSetting.setVolumeUuid(volumeUuid);
}
+ pkgSetting.setLeavingSharedUser(parsedPackage.isLeavingSharedUser());
SharedLibraryInfo sdkLibraryInfo = null;
if (!TextUtils.isEmpty(parsedPackage.getSdkLibraryName())) {
diff --git a/services/core/java/com/android/server/pm/UserManagerService.java b/services/core/java/com/android/server/pm/UserManagerService.java
index 829ee27..57d7d79 100644
--- a/services/core/java/com/android/server/pm/UserManagerService.java
+++ b/services/core/java/com/android/server/pm/UserManagerService.java
@@ -2105,6 +2105,10 @@
@Override
public void setUserAdmin(@UserIdInt int userId) {
checkManageUserAndAcrossUsersFullPermission("set user admin");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ checkAdminStatusChangeAllowed(userId);
+ }
+
mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_GRANT_ADMIN);
UserData user;
synchronized (mPackagesLock) {
@@ -2133,6 +2137,10 @@
@Override
public void revokeUserAdmin(@UserIdInt int userId) {
checkManageUserAndAcrossUsersFullPermission("revoke admin privileges");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ checkAdminStatusChangeAllowed(userId);
+ }
+
mUserJourneyLogger.logUserJourneyBegin(userId, USER_JOURNEY_REVOKE_ADMIN);
UserData user;
synchronized (mPackagesLock) {
@@ -4065,6 +4073,26 @@
}
}
+ /**
+ * Checks if changing the admin status of a target user is restricted
+ * due to the DISALLOW_GRANT_ADMIN restriction. If either the calling
+ * user or the target user has this restriction, a SecurityException
+ * is thrown.
+ *
+ * @param targetUser The user ID of the user whose admin status is being
+ * considered for change.
+ * @throws SecurityException if the admin status change is restricted due
+ * to the DISALLOW_GRANT_ADMIN restriction.
+ */
+ private void checkAdminStatusChangeAllowed(int targetUser) {
+ if (hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, UserHandle.getCallingUserId())
+ || hasUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, targetUser)) {
+ throw new SecurityException(
+ "Admin status change is restricted. The DISALLOW_GRANT_ADMIN "
+ + "restriction is applied either on the current or the target user.");
+ }
+ }
+
@GuardedBy({"mPackagesLock"})
private void writeBitmapLP(UserInfo info, Bitmap bitmap) {
try {
@@ -5443,6 +5471,13 @@
enforceUserRestriction(restriction, UserHandle.getCallingUserId(),
"Cannot add user");
+ if (Flags.unicornModeRefactoringForHsumReadOnly()) {
+ if ((flags & UserInfo.FLAG_ADMIN) != 0) {
+ enforceUserRestriction(UserManager.DISALLOW_GRANT_ADMIN,
+ UserHandle.getCallingUserId(), "Cannot create ADMIN user");
+ }
+ }
+
return createUserInternalUnchecked(name, userType, flags, parentId,
/* preCreate= */ false, disallowedPackages, /* token= */ null);
}
diff --git a/services/core/java/com/android/server/pm/pkg/PackageState.java b/services/core/java/com/android/server/pm/pkg/PackageState.java
index 5876188..bbc17c8 100644
--- a/services/core/java/com/android/server/pm/pkg/PackageState.java
+++ b/services/core/java/com/android/server/pm/pkg/PackageState.java
@@ -488,4 +488,10 @@
* @hide
*/
boolean isScannedAsStoppedSystemApp();
+
+ /**
+ * see AndroidPackage#isLeavingSharedUser()
+ * @hide
+ */
+ boolean isLeavingSharedUser();
}
diff --git a/services/core/java/com/android/server/vibrator/HalVibration.java b/services/core/java/com/android/server/vibrator/HalVibration.java
index 46bd7af..fe0cf59 100644
--- a/services/core/java/com/android/server/vibrator/HalVibration.java
+++ b/services/core/java/com/android/server/vibrator/HalVibration.java
@@ -170,9 +170,11 @@
/** Return {@link VibrationStats.StatsInfo} with read-only metrics about this vibration. */
public VibrationStats.StatsInfo getStatsInfo(long completionUptimeMillis) {
- int vibrationType = isRepeating()
- ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED
- : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE;
+ int vibrationType = mEffectToPlay.hasVendorEffects()
+ ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__VENDOR
+ : isRepeating()
+ ? FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__REPEATED
+ : FrameworkStatsLog.VIBRATION_REPORTED__VIBRATION_TYPE__SINGLE;
return new VibrationStats.StatsInfo(
callerInfo.uid, vibrationType, callerInfo.attrs.getUsage(), mStatus,
stats, completionUptimeMillis);
diff --git a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java
index 8f36118..407f3d9 100644
--- a/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java
+++ b/services/core/java/com/android/server/vibrator/PerformVendorEffectVibratorStep.java
@@ -52,6 +52,7 @@
long vibratorOnResult = controller.on(effect, getVibration().id);
vibratorOnResult = Math.min(vibratorOnResult, VENDOR_EFFECT_MAX_DURATION_MS);
handleVibratorOnResult(vibratorOnResult);
+ getVibration().stats.reportPerformVendorEffect(vibratorOnResult);
return List.of(new CompleteEffectVibratorStep(conductor, startTime,
/* cancelled= */ false, controller, mPendingVibratorOffDeadline));
} finally {
diff --git a/services/core/java/com/android/server/vibrator/VibrationScaler.java b/services/core/java/com/android/server/vibrator/VibrationScaler.java
index 3933759..a74c4e0 100644
--- a/services/core/java/com/android/server/vibrator/VibrationScaler.java
+++ b/services/core/java/com/android/server/vibrator/VibrationScaler.java
@@ -17,7 +17,6 @@
package com.android.server.vibrator;
import android.annotation.NonNull;
-import android.content.Context;
import android.hardware.vibrator.V1_0.EffectStrength;
import android.os.ExternalVibrationScale;
import android.os.VibrationAttributes;
@@ -25,6 +24,7 @@
import android.os.Vibrator;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationConfig;
import android.util.IndentingPrintWriter;
import android.util.Slog;
import android.util.SparseArray;
@@ -37,8 +37,11 @@
final class VibrationScaler {
private static final String TAG = "VibrationScaler";
+ // TODO(b/345186129): remove this once we finish migrating to scale factor and clean up flags.
// Scale levels. Each level, except MUTE, is defined as the delta between the current setting
// and the default intensity for that type of vibration (i.e. current - default).
+ // It's important that we apply the scaling on the delta between the two so
+ // that the default intensity level applies no scaling to application provided effects.
static final int SCALE_VERY_LOW = ExternalVibrationScale.ScaleLevel.SCALE_VERY_LOW; // -2
static final int SCALE_LOW = ExternalVibrationScale.ScaleLevel.SCALE_LOW; // -1
static final int SCALE_NONE = ExternalVibrationScale.ScaleLevel.SCALE_NONE; // 0
@@ -53,35 +56,15 @@
private static final float SCALE_FACTOR_HIGH = 1.2f;
private static final float SCALE_FACTOR_VERY_HIGH = 1.4f;
- private static final ScaleLevel SCALE_LEVEL_NONE = new ScaleLevel(SCALE_FACTOR_NONE);
-
- // A mapping from the intensity adjustment to the scaling to apply, where the intensity
- // adjustment is defined as the delta between the default intensity level and the user selected
- // intensity level. It's important that we apply the scaling on the delta between the two so
- // that the default intensity level applies no scaling to application provided effects.
- private final SparseArray<ScaleLevel> mScaleLevels;
private final VibrationSettings mSettingsController;
private final int mDefaultVibrationAmplitude;
+ private final float mDefaultVibrationScaleLevelGain;
private final SparseArray<Float> mAdaptiveHapticsScales = new SparseArray<>();
- VibrationScaler(Context context, VibrationSettings settingsController) {
+ VibrationScaler(VibrationConfig config, VibrationSettings settingsController) {
mSettingsController = settingsController;
- mDefaultVibrationAmplitude = context.getResources().getInteger(
- com.android.internal.R.integer.config_defaultVibrationAmplitude);
-
- mScaleLevels = new SparseArray<>();
- mScaleLevels.put(SCALE_VERY_LOW, new ScaleLevel(SCALE_FACTOR_VERY_LOW));
- mScaleLevels.put(SCALE_LOW, new ScaleLevel(SCALE_FACTOR_LOW));
- mScaleLevels.put(SCALE_NONE, SCALE_LEVEL_NONE);
- mScaleLevels.put(SCALE_HIGH, new ScaleLevel(SCALE_FACTOR_HIGH));
- mScaleLevels.put(SCALE_VERY_HIGH, new ScaleLevel(SCALE_FACTOR_VERY_HIGH));
- }
-
- /**
- * Returns the default vibration amplitude configured for this device, value in [1,255].
- */
- public int getDefaultVibrationAmplitude() {
- return mDefaultVibrationAmplitude;
+ mDefaultVibrationAmplitude = config.getDefaultVibrationAmplitude();
+ mDefaultVibrationScaleLevelGain = config.getDefaultVibrationScaleLevelGain();
}
/**
@@ -111,6 +94,16 @@
}
/**
+ * Calculates the scale factor to be applied to a vibration with given usage.
+ *
+ * @param usageHint one of VibrationAttributes.USAGE_*
+ * @return The scale factor.
+ */
+ public float getScaleFactor(int usageHint) {
+ return scaleLevelToScaleFactor(getScaleLevel(usageHint));
+ }
+
+ /**
* Returns the adaptive haptics scale that should be applied to the vibrations with
* the given usage. When no adaptive scales are available for the usages, then returns 1
* indicating no scaling will be applied
@@ -135,20 +128,12 @@
@NonNull
public VibrationEffect scale(@NonNull VibrationEffect effect, int usageHint) {
int newEffectStrength = getEffectStrength(usageHint);
- ScaleLevel scaleLevel = mScaleLevels.get(getScaleLevel(usageHint));
+ float scaleFactor = getScaleFactor(usageHint);
float adaptiveScale = getAdaptiveHapticsScale(usageHint);
- if (scaleLevel == null) {
- // Something about our scaling has gone wrong, so just play with no scaling.
- Slog.e(TAG, "No configured scaling level found! (current="
- + mSettingsController.getCurrentIntensity(usageHint) + ", default= "
- + mSettingsController.getDefaultIntensity(usageHint) + ")");
- scaleLevel = SCALE_LEVEL_NONE;
- }
-
return effect.resolve(mDefaultVibrationAmplitude)
.applyEffectStrength(newEffectStrength)
- .scale(scaleLevel.factor)
+ .scale(scaleFactor)
.scaleLinearly(adaptiveScale);
}
@@ -192,14 +177,11 @@
void dump(IndentingPrintWriter pw) {
pw.println("VibrationScaler:");
pw.increaseIndent();
- pw.println("defaultVibrationAmplitude = " + mDefaultVibrationAmplitude);
pw.println("ScaleLevels:");
pw.increaseIndent();
- for (int i = 0; i < mScaleLevels.size(); i++) {
- int scaleLevelKey = mScaleLevels.keyAt(i);
- ScaleLevel scaleLevel = mScaleLevels.valueAt(i);
- pw.println(scaleLevelToString(scaleLevelKey) + " = " + scaleLevel);
+ for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) {
+ pw.println(scaleLevelToString(level) + " = " + scaleLevelToScaleFactor(level));
}
pw.decreaseIndent();
@@ -224,16 +206,24 @@
@Override
public String toString() {
+ StringBuilder scaleLevelsStr = new StringBuilder("{");
+ for (int level = SCALE_VERY_LOW; level <= SCALE_VERY_HIGH; level++) {
+ scaleLevelsStr.append(scaleLevelToString(level))
+ .append("=").append(scaleLevelToScaleFactor(level));
+ if (level < SCALE_FACTOR_VERY_HIGH) {
+ scaleLevelsStr.append(", ");
+ }
+ }
+ scaleLevelsStr.append("}");
+
return "VibrationScaler{"
- + "mScaleLevels=" + mScaleLevels
- + ", mDefaultVibrationAmplitude=" + mDefaultVibrationAmplitude
+ + "mScaleLevels=" + scaleLevelsStr
+ ", mAdaptiveHapticsScales=" + mAdaptiveHapticsScales
+ '}';
}
private int getEffectStrength(int usageHint) {
int currentIntensity = mSettingsController.getCurrentIntensity(usageHint);
-
if (currentIntensity == Vibrator.VIBRATION_INTENSITY_OFF) {
// Bypassing user settings, or it has changed between checking and scaling. Use default.
currentIntensity = mSettingsController.getDefaultIntensity(usageHint);
@@ -244,17 +234,44 @@
/** Mapping of Vibrator.VIBRATION_INTENSITY_* values to {@link EffectStrength}. */
private static int intensityToEffectStrength(int intensity) {
- switch (intensity) {
- case Vibrator.VIBRATION_INTENSITY_LOW:
- return EffectStrength.LIGHT;
- case Vibrator.VIBRATION_INTENSITY_MEDIUM:
- return EffectStrength.MEDIUM;
- case Vibrator.VIBRATION_INTENSITY_HIGH:
- return EffectStrength.STRONG;
- default:
+ return switch (intensity) {
+ case Vibrator.VIBRATION_INTENSITY_LOW -> EffectStrength.LIGHT;
+ case Vibrator.VIBRATION_INTENSITY_MEDIUM -> EffectStrength.MEDIUM;
+ case Vibrator.VIBRATION_INTENSITY_HIGH -> EffectStrength.STRONG;
+ default -> {
Slog.w(TAG, "Got unexpected vibration intensity: " + intensity);
- return EffectStrength.STRONG;
+ yield EffectStrength.STRONG;
+ }
+ };
+ }
+
+ /** Mapping of ExternalVibrationScale.ScaleLevel.SCALE_* values to scale factor. */
+ private float scaleLevelToScaleFactor(int level) {
+ if (Flags.hapticsScaleV2Enabled()) {
+ if (level == SCALE_NONE || level < SCALE_VERY_LOW || level > SCALE_VERY_HIGH) {
+ // Scale set to none or to a bad value, use default factor for no scaling.
+ return SCALE_FACTOR_NONE;
+ }
+ float scaleFactor = (float) Math.pow(mDefaultVibrationScaleLevelGain, level);
+ if (scaleFactor <= 0) {
+ // Something about our scaling has gone wrong, so just play with no scaling.
+ Slog.wtf(TAG, String.format(Locale.ROOT, "Error in scaling calculations, ended up"
+ + " with invalid scale factor %.2f for scale level %s and default"
+ + " level gain of %.2f", scaleFactor, scaleLevelToString(level),
+ mDefaultVibrationScaleLevelGain));
+ scaleFactor = SCALE_FACTOR_NONE;
+ }
+ return scaleFactor;
}
+
+ return switch (level) {
+ case SCALE_VERY_LOW -> SCALE_FACTOR_VERY_LOW;
+ case SCALE_LOW -> SCALE_FACTOR_LOW;
+ case SCALE_HIGH -> SCALE_FACTOR_HIGH;
+ case SCALE_VERY_HIGH -> SCALE_FACTOR_VERY_HIGH;
+ // Scale set to none or to a bad value, use default factor for no scaling.
+ default -> SCALE_FACTOR_NONE;
+ };
}
static String scaleLevelToString(int scaleLevel) {
@@ -267,18 +284,4 @@
default -> String.valueOf(scaleLevel);
};
}
-
- /** Represents the scale that must be applied to a vibration effect intensity. */
- private static final class ScaleLevel {
- public final float factor;
-
- ScaleLevel(float factor) {
- this.factor = factor;
- }
-
- @Override
- public String toString() {
- return "ScaleLevel{factor=" + factor + "}";
- }
- }
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationSettings.java b/services/core/java/com/android/server/vibrator/VibrationSettings.java
index f2f5eda..0d6778c 100644
--- a/services/core/java/com/android/server/vibrator/VibrationSettings.java
+++ b/services/core/java/com/android/server/vibrator/VibrationSettings.java
@@ -16,7 +16,6 @@
package com.android.server.vibrator;
-import static android.os.VibrationAttributes.CATEGORY_KEYBOARD;
import static android.os.VibrationAttributes.USAGE_ACCESSIBILITY;
import static android.os.VibrationAttributes.USAGE_ALARM;
import static android.os.VibrationAttributes.USAGE_COMMUNICATION_REQUEST;
@@ -191,8 +190,6 @@
@GuardedBy("mLock")
private boolean mVibrateOn;
@GuardedBy("mLock")
- private boolean mKeyboardVibrationOn;
- @GuardedBy("mLock")
private int mRingerMode;
@GuardedBy("mLock")
private boolean mOnWirelessCharger;
@@ -532,14 +529,6 @@
return false;
}
- if (mVibrationConfig.isKeyboardVibrationSettingsSupported()) {
- int category = callerInfo.attrs.getCategory();
- if (usage == USAGE_TOUCH && category == CATEGORY_KEYBOARD) {
- // Keyboard touch has a different user setting.
- return mKeyboardVibrationOn;
- }
- }
-
// Apply individual user setting based on usage.
return getCurrentIntensity(usage) != Vibrator.VIBRATION_INTENSITY_OFF;
}
@@ -556,10 +545,11 @@
mVibrateInputDevices =
loadSystemSetting(Settings.System.VIBRATE_INPUT_DEVICES, 0, userHandle) > 0;
mVibrateOn = loadSystemSetting(Settings.System.VIBRATE_ON, 1, userHandle) > 0;
- mKeyboardVibrationOn = loadSystemSetting(
- Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0;
- int keyboardIntensity = getDefaultIntensity(USAGE_IME_FEEDBACK);
+ boolean isKeyboardVibrationOn = loadSystemSetting(
+ Settings.System.KEYBOARD_VIBRATION_ENABLED, 1, userHandle) > 0;
+ int keyboardIntensity = toIntensity(isKeyboardVibrationOn,
+ getDefaultIntensity(USAGE_IME_FEEDBACK));
int alarmIntensity = toIntensity(
loadSystemSetting(Settings.System.ALARM_VIBRATION_INTENSITY, -1, userHandle),
getDefaultIntensity(USAGE_ALARM));
@@ -654,7 +644,6 @@
return "VibrationSettings{"
+ "mVibratorConfig=" + mVibrationConfig
+ ", mVibrateOn=" + mVibrateOn
- + ", mKeyboardVibrationOn=" + mKeyboardVibrationOn
+ ", mVibrateInputDevices=" + mVibrateInputDevices
+ ", mBatterySaverMode=" + mBatterySaverMode
+ ", mRingerMode=" + ringerModeToString(mRingerMode)
@@ -671,7 +660,6 @@
pw.println("VibrationSettings:");
pw.increaseIndent();
pw.println("vibrateOn = " + mVibrateOn);
- pw.println("keyboardVibrationOn = " + mKeyboardVibrationOn);
pw.println("vibrateInputDevices = " + mVibrateInputDevices);
pw.println("batterySaverMode = " + mBatterySaverMode);
pw.println("ringerMode = " + ringerModeToString(mRingerMode));
@@ -698,8 +686,6 @@
void dump(ProtoOutputStream proto) {
synchronized (mLock) {
proto.write(VibratorManagerServiceDumpProto.VIBRATE_ON, mVibrateOn);
- proto.write(VibratorManagerServiceDumpProto.KEYBOARD_VIBRATION_ON,
- mKeyboardVibrationOn);
proto.write(VibratorManagerServiceDumpProto.LOW_POWER_MODE, mBatterySaverMode);
proto.write(VibratorManagerServiceDumpProto.ALARM_INTENSITY,
getCurrentIntensity(USAGE_ALARM));
@@ -774,6 +760,11 @@
return value;
}
+ @VibrationIntensity
+ private int toIntensity(boolean enabled, @VibrationIntensity int defaultValue) {
+ return enabled ? defaultValue : Vibrator.VIBRATION_INTENSITY_OFF;
+ }
+
private boolean loadBooleanSetting(String settingKey, int userHandle) {
return loadSystemSetting(settingKey, 0, userHandle) != 0;
}
diff --git a/services/core/java/com/android/server/vibrator/VibrationStats.java b/services/core/java/com/android/server/vibrator/VibrationStats.java
index dd66809..8179d6a 100644
--- a/services/core/java/com/android/server/vibrator/VibrationStats.java
+++ b/services/core/java/com/android/server/vibrator/VibrationStats.java
@@ -79,6 +79,7 @@
private int mVibratorSetAmplitudeCount;
private int mVibratorSetExternalControlCount;
private int mVibratorPerformCount;
+ private int mVibratorPerformVendorCount;
private int mVibratorComposeCount;
private int mVibratorComposePwleCount;
@@ -239,6 +240,11 @@
}
}
+ /** Report a call to vibrator method to trigger a vendor vibration effect. */
+ void reportPerformVendorEffect(long halResult) {
+ mVibratorPerformVendorCount++;
+ }
+
/** Report a call to vibrator method to trigger a vibration as a composition of primitives. */
void reportComposePrimitives(long halResult, PrimitiveSegment[] primitives) {
mVibratorComposeCount++;
@@ -313,6 +319,7 @@
public final int halOnCount;
public final int halOffCount;
public final int halPerformCount;
+ public final int halPerformVendorCount;
public final int halSetAmplitudeCount;
public final int halSetExternalControlCount;
public final int halCompositionSize;
@@ -357,6 +364,7 @@
halOnCount = stats.mVibratorOnCount;
halOffCount = stats.mVibratorOffCount;
halPerformCount = stats.mVibratorPerformCount;
+ halPerformVendorCount = stats.mVibratorPerformVendorCount;
halSetAmplitudeCount = stats.mVibratorSetAmplitudeCount;
halSetExternalControlCount = stats.mVibratorSetExternalControlCount;
halCompositionSize = stats.mVibrationCompositionTotalSize;
@@ -390,7 +398,8 @@
halOnCount, halOffCount, halPerformCount, halSetAmplitudeCount,
halSetExternalControlCount, halSupportedCompositionPrimitivesUsed,
halSupportedEffectsUsed, halUnsupportedCompositionPrimitivesUsed,
- halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale);
+ halUnsupportedEffectsUsed, halCompositionSize, halPwleSize, adaptiveScale,
+ halPerformVendorCount);
}
private static int[] filteredKeys(SparseBooleanArray supportArray, boolean supported) {
diff --git a/services/core/java/com/android/server/vibrator/VibratorManagerService.java b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
index 76872cf..f2ad5b9 100644
--- a/services/core/java/com/android/server/vibrator/VibratorManagerService.java
+++ b/services/core/java/com/android/server/vibrator/VibratorManagerService.java
@@ -57,6 +57,7 @@
import android.os.VibratorInfo;
import android.os.vibrator.Flags;
import android.os.vibrator.PrebakedSegment;
+import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.os.vibrator.VibratorInfoFactory;
import android.os.vibrator.persistence.ParsedVibration;
@@ -251,8 +252,9 @@
mHandler = injector.createHandler(Looper.myLooper());
mFrameworkStatsLogger = injector.getFrameworkStatsLogger(mHandler);
- mVibrationSettings = new VibrationSettings(mContext, mHandler);
- mVibrationScaler = new VibrationScaler(mContext, mVibrationSettings);
+ VibrationConfig vibrationConfig = new VibrationConfig(context.getResources());
+ mVibrationSettings = new VibrationSettings(mContext, mHandler, vibrationConfig);
+ mVibrationScaler = new VibrationScaler(vibrationConfig, mVibrationSettings);
mVibratorControlService = new VibratorControlService(mContext,
injector.createVibratorControllerHolder(), mVibrationScaler, mVibrationSettings,
mFrameworkStatsLogger, mLock);
@@ -1698,7 +1700,7 @@
IBinder.DeathRecipient {
public final ExternalVibration externalVibration;
- public ExternalVibrationScale scale = new ExternalVibrationScale();
+ public final ExternalVibrationScale scale = new ExternalVibrationScale();
private Vibration.Status mStatus;
@@ -1712,8 +1714,18 @@
mStatus = Vibration.Status.RUNNING;
}
+ public void muteScale() {
+ scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ if (Flags.hapticsScaleV2Enabled()) {
+ scale.scaleFactor = 0;
+ }
+ }
+
public void scale(VibrationScaler scaler, int usage) {
scale.scaleLevel = scaler.getScaleLevel(usage);
+ if (Flags.hapticsScaleV2Enabled()) {
+ scale.scaleFactor = scaler.getScaleFactor(usage);
+ }
scale.adaptiveHapticsScale = scaler.getAdaptiveHapticsScale(usage);
stats.reportAdaptiveScale(scale.adaptiveHapticsScale);
}
@@ -2047,7 +2059,7 @@
// Create Vibration.Stats as close to the received request as possible, for tracking.
ExternalVibrationHolder vibHolder = new ExternalVibrationHolder(vib);
// Mute the request until we run all the checks and accept the vibration.
- vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ vibHolder.muteScale();
boolean alreadyUnderExternalControl = false;
boolean waitForCompletion = false;
@@ -2146,7 +2158,7 @@
new Vibration.EndInfo(Vibration.Status.IGNORED_ERROR_CANCELLING),
/* continueExternalControl= */ false);
// Mute the request, vibration will be ignored.
- vibHolder.scale.scaleLevel = ExternalVibrationScale.ScaleLevel.SCALE_MUTE;
+ vibHolder.muteScale();
}
return vibHolder.scale;
}
diff --git a/services/core/java/com/android/server/wm/ActivityRecord.java b/services/core/java/com/android/server/wm/ActivityRecord.java
index a22db97..5c096ec 100644
--- a/services/core/java/com/android/server/wm/ActivityRecord.java
+++ b/services/core/java/com/android/server/wm/ActivityRecord.java
@@ -2524,8 +2524,11 @@
// trampoline that will be always created and finished immediately. Then give a chance to
// see if the snapshot is usable for the current running activity so the transition will
// look smoother, instead of showing a splash screen on the second launch.
- if (!newTask && taskSwitch && processRunning && !activityCreated && task.intent != null
- && mActivityComponent.equals(task.intent.getComponent())) {
+ if (!newTask && taskSwitch && !activityCreated && task.intent != null
+ // Another case where snapshot is allowed to be used is if this activity has not yet
+ // been created && is translucent or floating.
+ // The component isn't necessary to be matched in this case.
+ && (!mOccludesParent || mActivityComponent.equals(task.intent.getComponent()))) {
final ActivityRecord topAttached = task.getActivity(ActivityRecord::attachedToProcess);
if (topAttached != null) {
if (topAttached.isSnapshotCompatible(snapshot)
diff --git a/services/core/java/com/android/server/wm/AppCompatController.java b/services/core/java/com/android/server/wm/AppCompatController.java
index d38edfc..4290051 100644
--- a/services/core/java/com/android/server/wm/AppCompatController.java
+++ b/services/core/java/com/android/server/wm/AppCompatController.java
@@ -40,6 +40,8 @@
private final AppCompatOverrides mAppCompatOverrides;
@NonNull
private final AppCompatDeviceStateQuery mAppCompatDeviceStateQuery;
+ @NonNull
+ private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy;
AppCompatController(@NonNull WindowManagerService wmService,
@NonNull ActivityRecord activityRecord) {
@@ -57,6 +59,7 @@
mTransparentPolicy, mAppCompatOverrides);
mAppCompatReachabilityPolicy = new AppCompatReachabilityPolicy(mActivityRecord,
wmService.mAppCompatConfiguration);
+ mAppCompatLetterboxPolicy = new AppCompatLetterboxPolicy(mActivityRecord);
}
@NonNull
@@ -113,6 +116,11 @@
}
@NonNull
+ AppCompatLetterboxPolicy getAppCompatLetterboxPolicy() {
+ return mAppCompatLetterboxPolicy;
+ }
+
+ @NonNull
AppCompatFocusOverrides getAppCompatFocusOverrides() {
return mAppCompatOverrides.getAppCompatFocusOverrides();
}
@@ -127,4 +135,9 @@
return mAppCompatDeviceStateQuery;
}
+ @NonNull
+ AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() {
+ return mAppCompatOverrides.getAppCompatLetterboxOverrides();
+ }
+
}
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java
new file mode 100644
index 0000000..24ed14c
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxOverrides.java
@@ -0,0 +1,147 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
+import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+
+import android.annotation.NonNull;
+import android.app.ActivityManager;
+import android.graphics.Color;
+import android.util.Slog;
+import android.view.WindowManager;
+
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
+/**
+ * Encapsulates overrides and configuration related to the Letterboxing policy.
+ */
+class AppCompatLetterboxOverrides {
+
+ private static final String TAG = TAG_WITH_CLASS_NAME ? "AppCompatLetterboxOverrides" : TAG_ATM;
+
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+ @NonNull
+ private final AppCompatConfiguration mAppCompatConfiguration;
+
+ private boolean mShowWallpaperForLetterboxBackground;
+
+ AppCompatLetterboxOverrides(@NonNull ActivityRecord activityRecord,
+ @NonNull AppCompatConfiguration appCompatConfiguration) {
+ mActivityRecord = activityRecord;
+ mAppCompatConfiguration = appCompatConfiguration;
+ }
+
+ boolean shouldLetterboxHaveRoundedCorners() {
+ // TODO(b/214030873): remove once background is drawn for transparent activities
+ // Letterbox shouldn't have rounded corners if the activity is transparent
+ return mAppCompatConfiguration.isLetterboxActivityCornersRounded()
+ && mActivityRecord.fillsParent();
+ }
+
+ boolean isLetterboxEducationEnabled() {
+ return mAppCompatConfiguration.getIsEducationEnabled();
+ }
+
+ boolean hasWallpaperBackgroundForLetterbox() {
+ return mShowWallpaperForLetterboxBackground;
+ }
+
+ boolean checkWallpaperBackgroundForLetterbox(boolean wallpaperShouldBeShown) {
+ if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) {
+ mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown;
+ return true;
+ }
+ return false;
+ }
+
+ @NonNull
+ Color getLetterboxBackgroundColor() {
+ final WindowState w = mActivityRecord.findMainWindow();
+ if (w == null || w.isLetterboxedForDisplayCutout()) {
+ return Color.valueOf(Color.BLACK);
+ }
+ final @LetterboxBackgroundType int letterboxBackgroundType =
+ mAppCompatConfiguration.getLetterboxBackgroundType();
+ final ActivityManager.TaskDescription taskDescription = mActivityRecord.taskDescription;
+ switch (letterboxBackgroundType) {
+ case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
+ if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) {
+ return Color.valueOf(taskDescription.getBackgroundColorFloating());
+ }
+ break;
+ case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND:
+ if (taskDescription != null && taskDescription.getBackgroundColor() != 0) {
+ return Color.valueOf(taskDescription.getBackgroundColor());
+ }
+ break;
+ case LETTERBOX_BACKGROUND_WALLPAPER:
+ if (hasWallpaperBackgroundForLetterbox()) {
+ // Color is used for translucent scrim that dims wallpaper.
+ return mAppCompatConfiguration.getLetterboxBackgroundColor();
+ }
+ Slog.w(TAG, "Wallpaper option is selected for letterbox background but "
+ + "blur is not supported by a device or not supported in the current "
+ + "window configuration or both alpha scrim and blur radius aren't "
+ + "provided so using solid color background");
+ break;
+ case LETTERBOX_BACKGROUND_SOLID_COLOR:
+ return mAppCompatConfiguration.getLetterboxBackgroundColor();
+ default:
+ throw new AssertionError(
+ "Unexpected letterbox background type: " + letterboxBackgroundType);
+ }
+ // If picked option configured incorrectly or not supported then default to a solid color
+ // background.
+ return mAppCompatConfiguration.getLetterboxBackgroundColor();
+ }
+
+ int getLetterboxActivityCornersRadius() {
+ return mAppCompatConfiguration.getLetterboxActivityCornersRadius();
+ }
+
+ boolean isLetterboxActivityCornersRounded() {
+ return mAppCompatConfiguration.isLetterboxActivityCornersRounded();
+ }
+
+ @LetterboxBackgroundType
+ int getLetterboxBackgroundType() {
+ return mAppCompatConfiguration.getLetterboxBackgroundType();
+ }
+
+ int getLetterboxWallpaperBlurRadiusPx() {
+ int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
+ return Math.max(blurRadius, 0);
+ }
+
+ float getLetterboxWallpaperDarkScrimAlpha() {
+ float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
+ // No scrim by default.
+ return (alpha < 0 || alpha >= 1) ? 0.0f : alpha;
+ }
+
+ boolean isLetterboxWallpaperBlurSupported() {
+ return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class)
+ .isCrossWindowBlurEnabled();
+ }
+
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
new file mode 100644
index 0000000..48a9311
--- /dev/null
+++ b/services/core/java/com/android/server/wm/AppCompatLetterboxPolicy.java
@@ -0,0 +1,463 @@
+/*
+ * Copyright (C) 2024 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.server.wm;
+
+import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
+import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
+import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
+
+import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
+
+import android.annotation.NonNull;
+import android.annotation.Nullable;
+import android.graphics.Point;
+import android.graphics.Rect;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.RoundedCorner;
+import android.view.SurfaceControl;
+
+import com.android.internal.annotations.VisibleForTesting;
+import com.android.internal.statusbar.LetterboxDetails;
+import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
+
+/**
+ * Encapsulates the logic for the Letterboxing policy.
+ */
+class AppCompatLetterboxPolicy {
+
+ @NonNull
+ private final ActivityRecord mActivityRecord;
+ @NonNull
+ private final LetterboxPolicyState mLetterboxPolicyState;
+
+ private boolean mLastShouldShowLetterboxUi;
+
+ AppCompatLetterboxPolicy(@NonNull ActivityRecord activityRecord) {
+ mActivityRecord = activityRecord;
+ mLetterboxPolicyState = new LetterboxPolicyState();
+ }
+
+ /** Cleans up {@link Letterbox} if it exists.*/
+ void destroy() {
+ mLetterboxPolicyState.destroy();
+ }
+
+ /** @return {@value true} if the letterbox policy is running and the activity letterboxed. */
+ boolean isRunning() {
+ return mLetterboxPolicyState.isRunning();
+ }
+
+ void onMovedToDisplay(int displayId) {
+ mLetterboxPolicyState.onMovedToDisplay(displayId);
+ }
+
+ /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
+ @NonNull
+ Rect getLetterboxInsets() {
+ return mLetterboxPolicyState.getLetterboxInsets();
+ }
+
+ /** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */
+ void getLetterboxInnerBounds(@NonNull Rect outBounds) {
+ mLetterboxPolicyState.getLetterboxInnerBounds(outBounds);
+ }
+
+ @Nullable
+ LetterboxDetails getLetterboxDetails() {
+ return mLetterboxPolicyState.getLetterboxDetails();
+ }
+
+ /**
+ * @return {@code true} if bar shown within a given rectangle is allowed to be fully transparent
+ * when the current activity is displayed.
+ */
+ boolean isFullyTransparentBarAllowed(@NonNull Rect rect) {
+ return mLetterboxPolicyState.isFullyTransparentBarAllowed(rect);
+ }
+
+ void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
+ @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction inputT) {
+ mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint, t, inputT);
+ }
+
+ void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint) {
+ mLetterboxPolicyState.updateLetterboxSurfaceIfNeeded(winHint,
+ mActivityRecord.getSyncTransaction(), mActivityRecord.getPendingTransaction());
+ }
+
+ void start(@NonNull WindowState w) {
+ if (shouldNotLayoutLetterbox(w)) {
+ return;
+ }
+ updateRoundedCornersIfNeeded(w);
+ updateWallpaperForLetterbox(w);
+ if (shouldShowLetterboxUi(w)) {
+ mLetterboxPolicyState.layoutLetterboxIfNeeded(w);
+ } else {
+ mLetterboxPolicyState.hide();
+ }
+ }
+
+ @VisibleForTesting
+ boolean shouldShowLetterboxUi(@NonNull WindowState mainWindow) {
+ if (mActivityRecord.mAppCompatController.getAppCompatOrientationOverrides()
+ .getIsRelaunchingAfterRequestedOrientationChanged()) {
+ return mLastShouldShowLetterboxUi;
+ }
+
+ final boolean shouldShowLetterboxUi =
+ (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible()
+ || mActivityRecord.isVisibleRequested())
+ && mainWindow.areAppWindowBoundsLetterboxed()
+ // Check for FLAG_SHOW_WALLPAPER explicitly instead of using
+ // WindowContainer#showWallpaper because the later will return true when
+ // this activity is using blurred wallpaper for letterbox background.
+ && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
+
+ mLastShouldShowLetterboxUi = shouldShowLetterboxUi;
+
+ return shouldShowLetterboxUi;
+ }
+
+ @VisibleForTesting
+ @Nullable
+ Rect getCropBoundsIfNeeded(@NonNull final WindowState mainWindow) {
+ if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
+ // We don't want corner radius on the window.
+ // In the case the ActivityRecord requires a letterboxed animation we never want
+ // rounded corners on the window because rounded corners are applied at the
+ // animation-bounds surface level and rounded corners on the window would interfere
+ // with that leading to unexpected rounded corner positioning during the animation.
+ return null;
+ }
+
+ final Rect cropBounds = new Rect(mActivityRecord.getBounds());
+
+ // In case of translucent activities we check if the requested size is different from
+ // the size provided using inherited bounds. In that case we decide to not apply rounded
+ // corners because we assume the specific layout would. This is the case when the layout
+ // of the translucent activity uses only a part of all the bounds because of the use of
+ // LayoutParams.WRAP_CONTENT.
+ final TransparentPolicy transparentPolicy = mActivityRecord.mAppCompatController
+ .getTransparentPolicy();
+ if (transparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth
+ || cropBounds.height() != mainWindow.mRequestedHeight)) {
+ return null;
+ }
+
+ // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
+ // because taskbar bounds used in {@link #adjustBoundsIfNeeded}
+ // are in screen coordinates
+ adjustBoundsForTaskbar(mainWindow, cropBounds);
+
+ final float scale = mainWindow.mInvGlobalScale;
+ if (scale != 1f && scale > 0f) {
+ cropBounds.scale(scale);
+ }
+
+ // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface
+ // control is in the top left corner of an app window so offsetting bounds
+ // accordingly.
+ cropBounds.offsetTo(0, 0);
+ return cropBounds;
+ }
+
+
+ // Returns rounded corners radius the letterboxed activity should have based on override in
+ // R.integer.config_letterboxActivityCornersRadius or min device bottom corner radii.
+ // Device corners can be different on the right and left sides, but we use the same radius
+ // for all corners for consistency and pick a minimal bottom one for consistency with a
+ // taskbar rounded corners.
+ int getRoundedCornersRadius(@NonNull final WindowState mainWindow) {
+ if (!requiresRoundedCorners(mainWindow)) {
+ return 0;
+ }
+ final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
+ .mAppCompatController.getAppCompatLetterboxOverrides();
+ final int radius;
+ if (letterboxOverrides.getLetterboxActivityCornersRadius() >= 0) {
+ radius = letterboxOverrides.getLetterboxActivityCornersRadius();
+ } else {
+ final InsetsState insetsState = mainWindow.getInsetsState();
+ radius = Math.min(
+ getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
+ getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
+ }
+
+ final float scale = mainWindow.mInvGlobalScale;
+ return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius;
+ }
+
+ void adjustBoundsForTaskbar(@NonNull final WindowState mainWindow,
+ @NonNull final Rect bounds) {
+ // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
+ // an insets frame is equal to a navigation bar which shouldn't affect position of
+ // rounded corners since apps are expected to handle navigation bar inset.
+ // This condition checks whether the taskbar is visible.
+ // Do not crop the taskbar inset if the window is in immersive mode - the user can
+ // swipe to show/hide the taskbar as an overlay.
+ // Adjust the bounds only in case there is an expanded taskbar,
+ // otherwise the rounded corners will be shown behind the navbar.
+ final InsetsSource expandedTaskbarOrNull =
+ AppCompatUtils.getExpandedTaskbarOrNull(mainWindow);
+ if (expandedTaskbarOrNull != null) {
+ // Rounded corners should be displayed above the expanded taskbar.
+ bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
+ }
+ }
+
+ private int getInsetsStateCornerRadius(@NonNull InsetsState insetsState,
+ @RoundedCorner.Position int position) {
+ final RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
+ return corner == null ? 0 : corner.getRadius();
+ }
+
+ private void updateWallpaperForLetterbox(@NonNull WindowState mainWindow) {
+ final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
+ .mAppCompatController.getAppCompatLetterboxOverrides();
+ final @LetterboxBackgroundType int letterboxBackgroundType =
+ letterboxOverrides.getLetterboxBackgroundType();
+ boolean wallpaperShouldBeShown =
+ letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
+ // Don't use wallpaper as a background if letterboxed for display cutout.
+ && isLetterboxedNotForDisplayCutout(mainWindow)
+ // Check that dark scrim alpha or blur radius are provided
+ && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() > 0
+ || letterboxOverrides.getLetterboxWallpaperDarkScrimAlpha() > 0)
+ // Check that blur is supported by a device if blur radius is provided.
+ && (letterboxOverrides.getLetterboxWallpaperBlurRadiusPx() <= 0
+ || letterboxOverrides.isLetterboxWallpaperBlurSupported());
+ if (letterboxOverrides.checkWallpaperBackgroundForLetterbox(wallpaperShouldBeShown)) {
+ mActivityRecord.requestUpdateWallpaperIfNeeded();
+ }
+ }
+
+ void updateRoundedCornersIfNeeded(@NonNull final WindowState mainWindow) {
+ final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
+ if (windowSurface == null || !windowSurface.isValid()) {
+ return;
+ }
+
+ // cropBounds must be non-null for the cornerRadius to be ever applied.
+ mActivityRecord.getSyncTransaction()
+ .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow))
+ .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
+ }
+
+ private boolean requiresRoundedCorners(@NonNull final WindowState mainWindow) {
+ final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
+ .mAppCompatController.getAppCompatLetterboxOverrides();
+ return isLetterboxedNotForDisplayCutout(mainWindow)
+ && letterboxOverrides.isLetterboxActivityCornersRounded();
+ }
+
+ private boolean isLetterboxedNotForDisplayCutout(@NonNull WindowState mainWindow) {
+ return shouldShowLetterboxUi(mainWindow)
+ && !mainWindow.isLetterboxedForDisplayCutout();
+ }
+
+ private static boolean shouldNotLayoutLetterbox(@Nullable WindowState w) {
+ if (w == null) {
+ return true;
+ }
+ final int type = w.mAttrs.type;
+ // Allow letterbox to be displayed early for base application or application starting
+ // windows even if it is not on the top z order to prevent flickering when the
+ // letterboxed window is brought to the top
+ return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING)
+ || w.mAnimatingExit;
+ }
+
+ private class LetterboxPolicyState {
+
+ @Nullable
+ private Letterbox mLetterbox;
+
+ void layoutLetterboxIfNeeded(@NonNull WindowState w) {
+ if (!isRunning()) {
+ final AppCompatLetterboxOverrides letterboxOverrides = mActivityRecord
+ .mAppCompatController.getAppCompatLetterboxOverrides();
+ final AppCompatReachabilityPolicy reachabilityPolicy = mActivityRecord
+ .mAppCompatController.getAppCompatReachabilityPolicy();
+ mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
+ mActivityRecord.mWmService.mTransactionFactory,
+ reachabilityPolicy, letterboxOverrides,
+ this::getLetterboxParentSurface);
+ mLetterbox.attachInput(w);
+ mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+ .setLetterboxInnerBoundsSupplier(mLetterbox::getInnerFrame);
+ }
+ final Point letterboxPosition = new Point();
+ if (mActivityRecord.isInLetterboxAnimation()) {
+ // In this case we attach the letterbox to the task instead of the activity.
+ mActivityRecord.getTask().getPosition(letterboxPosition);
+ } else {
+ mActivityRecord.getPosition(letterboxPosition);
+ }
+
+ // Get the bounds of the "space-to-fill". The transformed bounds have the highest
+ // priority because the activity is launched in a rotated environment. In multi-window
+ // mode, the taskFragment-level represents this for both split-screen
+ // and activity-embedding. In fullscreen-mode, the task container does
+ // (since the orientation letterbox is also applied to the task).
+ final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
+ final Rect spaceToFill = transformedBounds != null
+ ? transformedBounds
+ : mActivityRecord.inMultiWindowMode()
+ ? mActivityRecord.getTaskFragment().getBounds()
+ : mActivityRecord.getRootTask().getParent().getBounds();
+ // In case of translucent activities an option is to use the WindowState#getFrame() of
+ // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
+ // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
+ // information and in particular it might provide a value for a smaller area making
+ // the letterbox overlap with the translucent activity's frame.
+ // If we use WindowState#getFrame() for the translucent activity's letterbox inner
+ // frame, the letterbox will then be overlapped with the translucent activity's frame.
+ // Because the surface layer of letterbox is lower than an activity window, this
+ // won't crop the content, but it may affect other features that rely on values stored
+ // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
+ // For this reason we use ActivityRecord#getBounds() that the translucent activity
+ // inherits from the first opaque activity beneath and also takes care of the scaling
+ // in case of activities in size compat mode.
+ final TransparentPolicy transparentPolicy =
+ mActivityRecord.mAppCompatController.getTransparentPolicy();
+ final Rect innerFrame =
+ transparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame();
+ mLetterbox.layout(spaceToFill, innerFrame, letterboxPosition);
+ if (mActivityRecord.mAppCompatController.getAppCompatReachabilityOverrides()
+ .isDoubleTapEvent()) {
+ // We need to notify Shell that letterbox position has changed.
+ mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
+ }
+ }
+
+ /**
+ * @return {@code true} if the policy is running and so if the current activity is
+ * letterboxed.
+ */
+ boolean isRunning() {
+ return mLetterbox != null;
+ }
+
+ void onMovedToDisplay(int displayId) {
+ if (isRunning()) {
+ mLetterbox.onMovedToDisplay(displayId);
+ }
+ }
+
+ /** Cleans up {@link Letterbox} if it exists.*/
+ void destroy() {
+ if (isRunning()) {
+ mLetterbox.destroy();
+ mLetterbox = null;
+ }
+ mActivityRecord.mAppCompatController.getAppCompatReachabilityPolicy()
+ .setLetterboxInnerBoundsSupplier(null);
+ }
+
+ void updateLetterboxSurfaceIfNeeded(@NonNull WindowState winHint,
+ @NonNull SurfaceControl.Transaction t,
+ @NonNull SurfaceControl.Transaction inputT) {
+ if (shouldNotLayoutLetterbox(winHint)) {
+ return;
+ }
+ start(winHint);
+ if (isRunning() && mLetterbox.needsApplySurfaceChanges()) {
+ mLetterbox.applySurfaceChanges(t, inputT);
+ }
+ }
+
+ void hide() {
+ if (isRunning()) {
+ mLetterbox.hide();
+ }
+ }
+
+ /** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
+ @NonNull
+ Rect getLetterboxInsets() {
+ if (isRunning()) {
+ return mLetterbox.getInsets();
+ } else {
+ return new Rect();
+ }
+ }
+
+ /** Gets the inner bounds of letterbox. The bounds will be empty with no letterbox. */
+ void getLetterboxInnerBounds(@NonNull Rect outBounds) {
+ if (isRunning()) {
+ outBounds.set(mLetterbox.getInnerFrame());
+ final WindowState w = mActivityRecord.findMainWindow();
+ if (w != null) {
+ adjustBoundsForTaskbar(w, outBounds);
+ }
+ } else {
+ outBounds.setEmpty();
+ }
+ }
+
+ /** Gets the outer bounds of letterbox. The bounds will be empty with no letterbox. */
+ private void getLetterboxOuterBounds(@NonNull Rect outBounds) {
+ if (isRunning()) {
+ outBounds.set(mLetterbox.getOuterFrame());
+ } else {
+ outBounds.setEmpty();
+ }
+ }
+
+ /**
+ * @return {@code true} if bar shown within a given rectangle is allowed to be fully
+ * transparent when the current activity is displayed.
+ */
+ boolean isFullyTransparentBarAllowed(@NonNull Rect rect) {
+ return !isRunning() || mLetterbox.notIntersectsOrFullyContains(rect);
+ }
+
+ @Nullable
+ LetterboxDetails getLetterboxDetails() {
+ final WindowState w = mActivityRecord.findMainWindow();
+ if (!isRunning() || w == null || w.isLetterboxedForDisplayCutout()) {
+ return null;
+ }
+ final Rect letterboxInnerBounds = new Rect();
+ final Rect letterboxOuterBounds = new Rect();
+ getLetterboxInnerBounds(letterboxInnerBounds);
+ getLetterboxOuterBounds(letterboxOuterBounds);
+
+ if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
+ return null;
+ }
+
+ return new LetterboxDetails(
+ letterboxInnerBounds,
+ letterboxOuterBounds,
+ w.mAttrs.insetsFlags.appearance
+ );
+ }
+
+ @Nullable
+ private SurfaceControl getLetterboxParentSurface() {
+ if (mActivityRecord.isInLetterboxAnimation()) {
+ return mActivityRecord.getTask().getSurfaceControl();
+ }
+ return mActivityRecord.getSurfaceControl();
+ }
+
+ }
+}
diff --git a/services/core/java/com/android/server/wm/AppCompatOverrides.java b/services/core/java/com/android/server/wm/AppCompatOverrides.java
index 80bbee3..2f03105 100644
--- a/services/core/java/com/android/server/wm/AppCompatOverrides.java
+++ b/services/core/java/com/android/server/wm/AppCompatOverrides.java
@@ -37,6 +37,8 @@
private final AppCompatResizeOverrides mAppCompatResizeOverrides;
@NonNull
private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
+ @NonNull
+ private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides;
AppCompatOverrides(@NonNull ActivityRecord activityRecord,
@NonNull AppCompatConfiguration appCompatConfiguration,
@@ -54,6 +56,8 @@
mAppCompatFocusOverrides = new AppCompatFocusOverrides(activityRecord,
appCompatConfiguration, optPropBuilder);
mAppCompatResizeOverrides = new AppCompatResizeOverrides(activityRecord, optPropBuilder);
+ mAppCompatLetterboxOverrides = new AppCompatLetterboxOverrides(activityRecord,
+ appCompatConfiguration);
}
@NonNull
@@ -85,4 +89,9 @@
AppCompatReachabilityOverrides getAppCompatReachabilityOverrides() {
return mAppCompatReachabilityOverrides;
}
+
+ @NonNull
+ AppCompatLetterboxOverrides getAppCompatLetterboxOverrides() {
+ return mAppCompatLetterboxOverrides;
+ }
}
diff --git a/services/core/java/com/android/server/wm/AppCompatUtils.java b/services/core/java/com/android/server/wm/AppCompatUtils.java
index 0244d27..91205fc 100644
--- a/services/core/java/com/android/server/wm/AppCompatUtils.java
+++ b/services/core/java/com/android/server/wm/AppCompatUtils.java
@@ -28,6 +28,9 @@
import android.app.TaskInfo;
import android.content.res.Configuration;
import android.graphics.Rect;
+import android.view.InsetsSource;
+import android.view.InsetsState;
+import android.view.WindowInsets;
import java.util.function.BooleanSupplier;
@@ -212,6 +215,23 @@
return "UNKNOWN_REASON";
}
+ /**
+ * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
+ */
+ @Nullable
+ static InsetsSource getExpandedTaskbarOrNull(@NonNull final WindowState mainWindow) {
+ final InsetsState state = mainWindow.getInsetsState();
+ for (int i = state.sourceSize() - 1; i >= 0; i--) {
+ final InsetsSource source = state.sourceAt(i);
+ if (source.getType() == WindowInsets.Type.navigationBars()
+ && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
+ && source.isVisible()) {
+ return source;
+ }
+ }
+ return null;
+ }
+
private static void clearAppCompatTaskInfo(@NonNull AppCompatTaskInfo info) {
info.topActivityLetterboxVerticalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
info.topActivityLetterboxHorizontalPosition = TaskInfo.PROPERTY_VALUE_UNSET;
diff --git a/services/core/java/com/android/server/wm/BackNavigationController.java b/services/core/java/com/android/server/wm/BackNavigationController.java
index b4c7557..fe5b142 100644
--- a/services/core/java/com/android/server/wm/BackNavigationController.java
+++ b/services/core/java/com/android/server/wm/BackNavigationController.java
@@ -761,7 +761,7 @@
if (isMonitorForRemote()) {
mObserver.sendResult(null /* result */);
}
- if (isMonitorAnimationOrTransition()) {
+ if (isMonitorAnimationOrTransition() && canCancelAnimations()) {
clearBackAnimations(true /* cancel */);
}
cancelPendingAnimation();
@@ -1935,8 +1935,7 @@
for (int i = penActivities.length - 1; i >= 0; --i) {
ActivityRecord resetActivity = penActivities[i];
if (transition.isInTransition(resetActivity)) {
- resetActivity.mTransitionController.setReady(
- resetActivity.getDisplayContent(), true);
+ transition.setReady(resetActivity.getDisplayContent(), true);
return true;
}
}
@@ -1991,18 +1990,12 @@
activity.makeVisibleIfNeeded(null /* starting */, true /* notifyToClient */);
}
}
- boolean needTransition = false;
- final DisplayContent dc = affects.get(0).getDisplayContent();
- for (int i = affects.size() - 1; i >= 0; --i) {
- final ActivityRecord activity = affects.get(i);
- needTransition |= tc.isCollecting(activity);
- }
if (prepareOpen != null) {
- if (needTransition) {
+ if (prepareOpen.hasChanges()) {
tc.requestStartTransition(prepareOpen,
null /*startTask */, null /* remoteTransition */,
null /* displayChange */);
- tc.setReady(dc);
+ prepareOpen.setReady(affects.get(0), true);
return prepareOpen;
} else {
prepareOpen.abort();
@@ -2053,11 +2046,22 @@
}
}
+ /** If the open transition is playing, wait for transition to clear the animation */
+ private boolean canCancelAnimations() {
+ if (!Flags.migratePredictiveBackTransition()) {
+ return true;
+ }
+ return mAnimationHandler.mOpenAnimAdaptor == null
+ || mAnimationHandler.mOpenAnimAdaptor.mPreparedOpenTransition == null;
+ }
+
void startAnimation() {
if (!mBackAnimationInProgress) {
// gesture is already finished, do not start animation
if (mPendingAnimation != null) {
- clearBackAnimations(true /* cancel */);
+ if (canCancelAnimations()) {
+ clearBackAnimations(true /* cancel */);
+ }
mPendingAnimation = null;
}
return;
diff --git a/services/core/java/com/android/server/wm/DisplayWindowSettings.java b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
index 7a95c2d..2f0ee17 100644
--- a/services/core/java/com/android/server/wm/DisplayWindowSettings.java
+++ b/services/core/java/com/android/server/wm/DisplayWindowSettings.java
@@ -129,21 +129,33 @@
@WindowConfiguration.WindowingMode
private int getWindowingModeLocked(@NonNull SettingsProvider.SettingsEntry settings,
@NonNull DisplayContent dc) {
- int windowingMode = settings.mWindowingMode;
+ final int windowingModeFromDisplaySettings = settings.mWindowingMode;
// This display used to be in freeform, but we don't support freeform anymore, so fall
// back to fullscreen.
- if (windowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM
+ if (windowingModeFromDisplaySettings == WindowConfiguration.WINDOWING_MODE_FREEFORM
&& !mService.mAtmService.mSupportsFreeformWindowManagement) {
return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
}
- // No record is present so use default windowing mode policy.
- if (windowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
- windowingMode = mService.mAtmService.mSupportsFreeformWindowManagement
- && (mService.mIsPc || dc.forceDesktopMode())
- ? WindowConfiguration.WINDOWING_MODE_FREEFORM
- : WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ if (windowingModeFromDisplaySettings != WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
+ return windowingModeFromDisplaySettings;
}
- return windowingMode;
+ // No record is present so use default windowing mode policy.
+ final boolean forceFreeForm = mService.mAtmService.mSupportsFreeformWindowManagement
+ && (mService.mIsPc || dc.forceDesktopMode());
+ if (forceFreeForm) {
+ return WindowConfiguration.WINDOWING_MODE_FREEFORM;
+ }
+ final int currentWindowingMode = dc.getDefaultTaskDisplayArea().getWindowingMode();
+ if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_UNDEFINED) {
+ // No record preset in settings + no mode set via the display area policy.
+ // Move to fullscreen as a fallback.
+ return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ }
+ if (currentWindowingMode == WindowConfiguration.WINDOWING_MODE_FREEFORM) {
+ // Freeform was enabled before but disabled now, the TDA should now move to fullscreen.
+ return WindowConfiguration.WINDOWING_MODE_FULLSCREEN;
+ }
+ return currentWindowingMode;
}
@WindowConfiguration.WindowingMode
diff --git a/services/core/java/com/android/server/wm/KeyguardController.java b/services/core/java/com/android/server/wm/KeyguardController.java
index 7a0fd3e..5d8a96c 100644
--- a/services/core/java/com/android/server/wm/KeyguardController.java
+++ b/services/core/java/com/android/server/wm/KeyguardController.java
@@ -39,6 +39,7 @@
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_TO_SHADE;
import static android.view.WindowManagerPolicyConstants.KEYGUARD_GOING_AWAY_FLAG_WITH_WALLPAPER;
+import static com.android.window.flags.Flags.reduceKeyguardTransitions;
import static com.android.server.policy.WindowManagerPolicy.FINISH_LAYOUT_REDO_WALLPAPER;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
@@ -50,6 +51,7 @@
import android.annotation.Nullable;
import android.os.IBinder;
import android.os.RemoteException;
+import android.os.SystemClock;
import android.os.Trace;
import android.util.Slog;
import android.util.SparseArray;
@@ -77,6 +79,8 @@
private static final int DEFER_WAKE_TRANSITION_TIMEOUT_MS = 5000;
+ private static final int GOING_AWAY_TIMEOUT_MS = 10500;
+
private final ActivityTaskSupervisor mTaskSupervisor;
private WindowManagerService mWindowManager;
@@ -232,6 +236,7 @@
dc.mWallpaperController.adjustWallpaperWindows();
dc.executeAppTransition();
}
+ scheduleGoingAwayTimeout(displayId);
}
// Update the sleep token first such that ensureActivitiesVisible has correct sleep token
@@ -286,6 +291,8 @@
mRootWindowContainer.ensureActivitiesVisible();
mRootWindowContainer.addStartingWindowsForVisibleActivities();
mWindowManager.executeAppTransition();
+
+ scheduleGoingAwayTimeout(displayId);
} finally {
mService.continueWindowLayout();
Trace.traceEnd(TRACE_TAG_WINDOW_MANAGER);
@@ -417,31 +424,42 @@
final TransitionController tc = mRootWindowContainer.mTransitionController;
final KeyguardDisplayState state = getDisplayState(displayId);
+ final DisplayContent dc = mRootWindowContainer.getDisplayContent(displayId);
- final boolean occluded = state.mOccluded;
- final boolean performTransition = isKeyguardLocked(displayId);
- final boolean executeTransition = performTransition && !tc.isCollecting();
+ final boolean locked = isKeyguardLocked(displayId);
+ final boolean executeTransition = !tc.isShellTransitionsEnabled()
+ || (locked && !tc.isCollecting() && !reduceKeyguardTransitions());
- mWindowManager.mPolicy.onKeyguardOccludedChangedLw(occluded);
+ final int transitType, transitFlags, notFlags;
+ if (state.mOccluded) {
+ transitType = TRANSIT_KEYGUARD_OCCLUDE;
+ transitFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING;
+ notFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+ } else {
+ transitType = TRANSIT_KEYGUARD_UNOCCLUDE;
+ transitFlags = TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+ notFlags = TRANSIT_FLAG_KEYGUARD_OCCLUDING;
+ }
+
+ mWindowManager.mPolicy.onKeyguardOccludedChangedLw(state.mOccluded);
mService.deferWindowLayout();
try {
- if (isKeyguardLocked(displayId)) {
- final int type = occluded ? TRANSIT_KEYGUARD_OCCLUDE : TRANSIT_KEYGUARD_UNOCCLUDE;
- final int flag = occluded ? TRANSIT_FLAG_KEYGUARD_OCCLUDING
- : TRANSIT_FLAG_KEYGUARD_UNOCCLUDING;
+ if (locked) {
if (tc.isShellTransitionsEnabled()) {
- final Task trigger = (occluded && topActivity != null)
+ final Task trigger = (state.mOccluded && topActivity != null)
? topActivity.getRootTask() : null;
- Transition transition = tc.requestTransitionIfNeeded(type, flag, trigger,
- mRootWindowContainer.getDefaultDisplay());
+ tc.requestTransitionIfNeeded(transitType, transitFlags, trigger, dc);
+ final Transition transition = tc.getCollectingTransition();
+ if ((transition.getFlags() & notFlags) != 0 && reduceKeyguardTransitions()) {
+ transition.removeFlag(notFlags);
+ } else {
+ transition.addFlag(transitFlags);
+ }
if (trigger != null) {
- if (transition == null) {
- transition = tc.getCollectingTransition();
- }
transition.collect(trigger);
}
} else {
- mRootWindowContainer.getDefaultDisplay().prepareAppTransition(type, flag);
+ dc.prepareAppTransition(transitType, transitFlags);
}
} else {
if (tc.inTransition()) {
@@ -451,8 +469,8 @@
}
}
updateKeyguardSleepToken(displayId);
- if (performTransition && executeTransition) {
- mWindowManager.executeAppTransition();
+ if (executeTransition) {
+ dc.executeAppTransition();
}
} finally {
mService.continueWindowLayout();
@@ -590,6 +608,35 @@
}
}
+ /**
+ * Called when the default display's mKeyguardGoingAway has been left as {@code true} for too
+ * long. Send an explicit message to the KeyguardService asking it to wrap up.
+ */
+ private final Runnable mGoingAwayTimeout = () -> {
+ synchronized (mWindowManager.mGlobalLock) {
+ KeyguardDisplayState state = getDisplayState(DEFAULT_DISPLAY);
+ if (!state.mKeyguardGoingAway) {
+ return;
+ }
+ state.mKeyguardGoingAway = false;
+ state.writeEventLog("goingAwayTimeout");
+ mWindowManager.mPolicy.startKeyguardExitAnimation(
+ SystemClock.uptimeMillis() - GOING_AWAY_TIMEOUT_MS);
+ }
+ };
+
+ private void scheduleGoingAwayTimeout(int displayId) {
+ if (displayId != DEFAULT_DISPLAY) {
+ return;
+ }
+ if (getDisplayState(displayId).mKeyguardGoingAway) {
+ if (!mWindowManager.mH.hasCallbacks(mGoingAwayTimeout)) {
+ mWindowManager.mH.postDelayed(mGoingAwayTimeout, GOING_AWAY_TIMEOUT_MS);
+ }
+ } else {
+ mWindowManager.mH.removeCallbacks(mGoingAwayTimeout);
+ }
+ }
/** Represents Keyguard state per individual display. */
private static class KeyguardDisplayState {
@@ -709,6 +756,7 @@
if (!lastKeyguardGoingAway && mKeyguardGoingAway) {
writeEventLog("dismissIfInsecure");
controller.handleDismissInsecureKeyguard(display);
+ controller.scheduleGoingAwayTimeout(mDisplayId);
hasChange = true;
} else if (lastOccluded != mOccluded) {
controller.handleOccludedChanged(mDisplayId, mTopOccludesActivity);
diff --git a/services/core/java/com/android/server/wm/Letterbox.java b/services/core/java/com/android/server/wm/Letterbox.java
index 3fc5eaf..252590e 100644
--- a/services/core/java/com/android/server/wm/Letterbox.java
+++ b/services/core/java/com/android/server/wm/Letterbox.java
@@ -38,9 +38,6 @@
import com.android.server.UiThread;
-import java.util.function.BooleanSupplier;
-import java.util.function.DoubleSupplier;
-import java.util.function.IntSupplier;
import java.util.function.Supplier;
/**
@@ -54,12 +51,6 @@
private final Supplier<SurfaceControl.Builder> mSurfaceControlFactory;
private final Supplier<SurfaceControl.Transaction> mTransactionFactory;
- private final BooleanSupplier mAreCornersRounded;
- private final Supplier<Color> mColorSupplier;
- // Parameters for "blurred wallpaper" letterbox background.
- private final BooleanSupplier mHasWallpaperBackgroundSupplier;
- private final IntSupplier mBlurRadiusSupplier;
- private final DoubleSupplier mDarkScrimAlphaSupplier;
private final Supplier<SurfaceControl> mParentSurfaceSupplier;
private final Rect mOuter = new Rect();
@@ -77,6 +68,8 @@
private final LetterboxSurface[] mSurfaces = { mLeft, mTop, mRight, mBottom };
@NonNull
private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+ @NonNull
+ private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides;
/**
* Constructs a Letterbox.
@@ -85,24 +78,14 @@
*/
public Letterbox(Supplier<SurfaceControl.Builder> surfaceControlFactory,
Supplier<SurfaceControl.Transaction> transactionFactory,
- BooleanSupplier areCornersRounded,
- Supplier<Color> colorSupplier,
- BooleanSupplier hasWallpaperBackgroundSupplier,
- IntSupplier blurRadiusSupplier,
- DoubleSupplier darkScrimAlphaSupplier,
@NonNull AppCompatReachabilityPolicy appCompatReachabilityPolicy,
+ @NonNull AppCompatLetterboxOverrides appCompatLetterboxOverrides,
Supplier<SurfaceControl> parentSurface) {
mSurfaceControlFactory = surfaceControlFactory;
mTransactionFactory = transactionFactory;
- mAreCornersRounded = areCornersRounded;
- mColorSupplier = colorSupplier;
- mHasWallpaperBackgroundSupplier = hasWallpaperBackgroundSupplier;
- mBlurRadiusSupplier = blurRadiusSupplier;
- mDarkScrimAlphaSupplier = darkScrimAlphaSupplier;
mAppCompatReachabilityPolicy = appCompatReachabilityPolicy;
+ mAppCompatLetterboxOverrides = appCompatLetterboxOverrides;
mParentSurfaceSupplier = parentSurface;
- // TODO Remove after Letterbox refactoring.
- mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(this::getInnerFrame);
}
/**
@@ -252,7 +235,8 @@
* Returns {@code true} when using {@link #mFullWindowSurface} instead of {@link mSurfaces}.
*/
private boolean useFullWindowSurface() {
- return mAreCornersRounded.getAsBoolean() || mHasWallpaperBackgroundSupplier.getAsBoolean();
+ return mAppCompatLetterboxOverrides.shouldLetterboxHaveRoundedCorners()
+ || mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox();
}
private final class TapEventReceiver extends InputEventReceiver {
@@ -431,7 +415,7 @@
createSurface(t);
}
- mColor = mColorSupplier.get();
+ mColor = mAppCompatLetterboxOverrides.getLetterboxBackgroundColor();
mParentSurface = mParentSurfaceSupplier.get();
t.setColor(mSurface, getRgbColorArray());
t.setPosition(mSurface, mSurfaceFrameRelative.left, mSurfaceFrameRelative.top);
@@ -439,7 +423,8 @@
mSurfaceFrameRelative.height());
t.reparent(mSurface, mParentSurface);
- mHasWallpaperBackground = mHasWallpaperBackgroundSupplier.getAsBoolean();
+ mHasWallpaperBackground = mAppCompatLetterboxOverrides
+ .hasWallpaperBackgroundForLetterbox();
updateAlphaAndBlur(t);
t.show(mSurface);
@@ -460,17 +445,19 @@
t.setBackgroundBlurRadius(mSurface, 0);
return;
}
- final float alpha = (float) mDarkScrimAlphaSupplier.getAsDouble();
+ final float alpha = mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha();
t.setAlpha(mSurface, alpha);
// Translucent dark scrim can be shown without blur.
- if (mBlurRadiusSupplier.getAsInt() <= 0) {
+ final int blurRadiusPx = mAppCompatLetterboxOverrides
+ .getLetterboxWallpaperBlurRadiusPx();
+ if (blurRadiusPx <= 0) {
// Removing pre-exesting blur
t.setBackgroundBlurRadius(mSurface, 0);
return;
}
- t.setBackgroundBlurRadius(mSurface, mBlurRadiusSupplier.getAsInt());
+ t.setBackgroundBlurRadius(mSurface, blurRadiusPx);
}
private float[] getRgbColorArray() {
@@ -487,8 +474,9 @@
// and mParentSurface may never be updated in applySurfaceChanges but this
// doesn't mean that update is needed.
|| !mSurfaceFrameRelative.isEmpty()
- && (mHasWallpaperBackgroundSupplier.getAsBoolean() != mHasWallpaperBackground
- || !mColorSupplier.get().equals(mColor)
+ && (mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox()
+ != mHasWallpaperBackground
+ || !mAppCompatLetterboxOverrides.getLetterboxBackgroundColor().equals(mColor)
|| mParentSurfaceSupplier.get() != mParentSurface);
}
}
diff --git a/services/core/java/com/android/server/wm/LetterboxUiController.java b/services/core/java/com/android/server/wm/LetterboxUiController.java
index 38df1b0..0e33734 100644
--- a/services/core/java/com/android/server/wm/LetterboxUiController.java
+++ b/services/core/java/com/android/server/wm/LetterboxUiController.java
@@ -16,36 +16,19 @@
package com.android.server.wm;
-import static android.view.WindowManager.LayoutParams.FLAG_SHOW_WALLPAPER;
-import static android.view.WindowManager.LayoutParams.TYPE_APPLICATION_STARTING;
-import static android.view.WindowManager.LayoutParams.TYPE_BASE_APPLICATION;
-
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_ATM;
import static com.android.server.wm.ActivityTaskManagerDebugConfig.TAG_WITH_CLASS_NAME;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING;
-import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_SOLID_COLOR;
import static com.android.server.wm.AppCompatConfiguration.LETTERBOX_BACKGROUND_WALLPAPER;
import static com.android.server.wm.AppCompatConfiguration.letterboxBackgroundTypeToString;
import android.annotation.NonNull;
import android.annotation.Nullable;
-import android.app.ActivityManager.TaskDescription;
import android.graphics.Color;
-import android.graphics.Point;
import android.graphics.Rect;
-import android.util.Slog;
-import android.view.InsetsSource;
-import android.view.InsetsState;
-import android.view.RoundedCorner;
-import android.view.SurfaceControl;
import android.view.SurfaceControl.Transaction;
-import android.view.WindowInsets;
-import android.view.WindowManager;
import com.android.internal.annotations.VisibleForTesting;
import com.android.internal.statusbar.LetterboxDetails;
-import com.android.server.wm.AppCompatConfiguration.LetterboxBackgroundType;
import java.io.PrintWriter;
@@ -56,8 +39,6 @@
private static final String TAG = TAG_WITH_CLASS_NAME ? "LetterboxUiController" : TAG_ATM;
- private final Point mTmpPoint = new Point();
-
private final AppCompatConfiguration mAppCompatConfiguration;
private final ActivityRecord mActivityRecord;
@@ -66,18 +47,9 @@
@NonNull
private final AppCompatReachabilityOverrides mAppCompatReachabilityOverrides;
@NonNull
- private final AppCompatReachabilityPolicy mAppCompatReachabilityPolicy;
+ private final AppCompatLetterboxPolicy mAppCompatLetterboxPolicy;
@NonNull
- private final TransparentPolicy mTransparentPolicy;
- @NonNull
- private final AppCompatOrientationOverrides mAppCompatOrientationOverrides;
-
- private boolean mShowWallpaperForLetterboxBackground;
-
- @Nullable
- private Letterbox mLetterbox;
-
- private boolean mLastShouldShowLetterboxUi;
+ private final AppCompatLetterboxOverrides mAppCompatLetterboxOverrides;
LetterboxUiController(WindowManagerService wmService, ActivityRecord activityRecord) {
mAppCompatConfiguration = wmService.mAppCompatConfiguration;
@@ -88,63 +60,34 @@
// TODO(b/356385137): Remove these we added to make dependencies temporarily explicit.
mAppCompatReachabilityOverrides = mActivityRecord.mAppCompatController
.getAppCompatReachabilityOverrides();
- mAppCompatReachabilityPolicy = mActivityRecord.mAppCompatController
- .getAppCompatReachabilityPolicy();
- mTransparentPolicy = mActivityRecord.mAppCompatController.getTransparentPolicy();
- mAppCompatOrientationOverrides = mActivityRecord.mAppCompatController
- .getAppCompatOrientationOverrides();
+ mAppCompatLetterboxPolicy = mActivityRecord.mAppCompatController
+ .getAppCompatLetterboxPolicy();
+ mAppCompatLetterboxOverrides = mActivityRecord.mAppCompatController
+ .getAppCompatLetterboxOverrides();
}
/** Cleans up {@link Letterbox} if it exists.*/
void destroy() {
- if (mLetterbox != null) {
- mLetterbox.destroy();
- mLetterbox = null;
- // TODO Remove after Letterbox refactoring.
- mAppCompatReachabilityPolicy.setLetterboxInnerBoundsSupplier(null);
- }
+ mAppCompatLetterboxPolicy.destroy();
}
void onMovedToDisplay(int displayId) {
- if (mLetterbox != null) {
- mLetterbox.onMovedToDisplay(displayId);
- }
+ mAppCompatLetterboxPolicy.onMovedToDisplay(displayId);
}
boolean hasWallpaperBackgroundForLetterbox() {
- return mShowWallpaperForLetterboxBackground;
+ return mAppCompatLetterboxOverrides.hasWallpaperBackgroundForLetterbox();
}
/** Gets the letterbox insets. The insets will be empty if there is no letterbox. */
Rect getLetterboxInsets() {
- if (mLetterbox != null) {
- return mLetterbox.getInsets();
- } else {
- return new Rect();
- }
+ return mAppCompatLetterboxPolicy.getLetterboxInsets();
}
/** Gets the inner bounds of letterbox. The bounds will be empty if there is no letterbox. */
void getLetterboxInnerBounds(Rect outBounds) {
- if (mLetterbox != null) {
- outBounds.set(mLetterbox.getInnerFrame());
- final WindowState w = mActivityRecord.findMainWindow();
- if (w != null) {
- adjustBoundsForTaskbar(w, outBounds);
- }
- } else {
- outBounds.setEmpty();
- }
- }
-
- /** Gets the outer bounds of letterbox. The bounds will be empty if there is no letterbox. */
- private void getLetterboxOuterBounds(Rect outBounds) {
- if (mLetterbox != null) {
- outBounds.set(mLetterbox.getOuterFrame());
- } else {
- outBounds.setEmpty();
- }
+ mAppCompatLetterboxPolicy.getLetterboxInnerBounds(outBounds);
}
/**
@@ -152,234 +95,39 @@
* when the current activity is displayed.
*/
boolean isFullyTransparentBarAllowed(Rect rect) {
- return mLetterbox == null || mLetterbox.notIntersectsOrFullyContains(rect);
+ return mAppCompatLetterboxPolicy.isFullyTransparentBarAllowed(rect);
}
void updateLetterboxSurfaceIfNeeded(WindowState winHint) {
- updateLetterboxSurfaceIfNeeded(winHint, mActivityRecord.getSyncTransaction(),
- mActivityRecord.getPendingTransaction());
+ mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint);
}
void updateLetterboxSurfaceIfNeeded(WindowState winHint, @NonNull Transaction t,
@NonNull Transaction inputT) {
- if (shouldNotLayoutLetterbox(winHint)) {
- return;
- }
- layoutLetterboxIfNeeded(winHint);
- if (mLetterbox != null && mLetterbox.needsApplySurfaceChanges()) {
- mLetterbox.applySurfaceChanges(t, inputT);
- }
+ mAppCompatLetterboxPolicy.updateLetterboxSurfaceIfNeeded(winHint, t, inputT);
}
void layoutLetterboxIfNeeded(WindowState w) {
- if (shouldNotLayoutLetterbox(w)) {
- return;
- }
- updateRoundedCornersIfNeeded(w);
- updateWallpaperForLetterbox(w);
- if (shouldShowLetterboxUi(w)) {
- if (mLetterbox == null) {
- mLetterbox = new Letterbox(() -> mActivityRecord.makeChildSurface(null),
- mActivityRecord.mWmService.mTransactionFactory,
- this::shouldLetterboxHaveRoundedCorners,
- this::getLetterboxBackgroundColor,
- this::hasWallpaperBackgroundForLetterbox,
- this::getLetterboxWallpaperBlurRadiusPx,
- this::getLetterboxWallpaperDarkScrimAlpha,
- mAppCompatReachabilityPolicy,
- this::getLetterboxParentSurface);
- mLetterbox.attachInput(w);
- }
-
- if (mActivityRecord.isInLetterboxAnimation()) {
- // In this case we attach the letterbox to the task instead of the activity.
- mActivityRecord.getTask().getPosition(mTmpPoint);
- } else {
- mActivityRecord.getPosition(mTmpPoint);
- }
-
- // Get the bounds of the "space-to-fill". The transformed bounds have the highest
- // priority because the activity is launched in a rotated environment. In multi-window
- // mode, the taskFragment-level represents this for both split-screen
- // and activity-embedding. In fullscreen-mode, the task container does
- // (since the orientation letterbox is also applied to the task).
- final Rect transformedBounds = mActivityRecord.getFixedRotationTransformDisplayBounds();
- final Rect spaceToFill = transformedBounds != null
- ? transformedBounds
- : mActivityRecord.inMultiWindowMode()
- ? mActivityRecord.getTaskFragment().getBounds()
- : mActivityRecord.getRootTask().getParent().getBounds();
- // In case of translucent activities an option is to use the WindowState#getFrame() of
- // the first opaque activity beneath. In some cases (e.g. an opaque activity is using
- // non MATCH_PARENT layouts or a Dialog theme) this might not provide the correct
- // information and in particular it might provide a value for a smaller area making
- // the letterbox overlap with the translucent activity's frame.
- // If we use WindowState#getFrame() for the translucent activity's letterbox inner
- // frame, the letterbox will then be overlapped with the translucent activity's frame.
- // Because the surface layer of letterbox is lower than an activity window, this
- // won't crop the content, but it may affect other features that rely on values stored
- // in mLetterbox, e.g. transitions, a status bar scrim and recents preview in Launcher
- // For this reason we use ActivityRecord#getBounds() that the translucent activity
- // inherits from the first opaque activity beneath and also takes care of the scaling
- // in case of activities in size compat mode.
- final Rect innerFrame =
- mTransparentPolicy.isRunning() ? mActivityRecord.getBounds() : w.getFrame();
- mLetterbox.layout(spaceToFill, innerFrame, mTmpPoint);
- if (mAppCompatReachabilityOverrides.isDoubleTapEvent()) {
- // We need to notify Shell that letterbox position has changed.
- mActivityRecord.getTask().dispatchTaskInfoChangedIfNeeded(true /* force */);
- }
- } else if (mLetterbox != null) {
- mLetterbox.hide();
- }
- }
-
- SurfaceControl getLetterboxParentSurface() {
- if (mActivityRecord.isInLetterboxAnimation()) {
- return mActivityRecord.getTask().getSurfaceControl();
- }
- return mActivityRecord.getSurfaceControl();
- }
-
- private static boolean shouldNotLayoutLetterbox(WindowState w) {
- if (w == null) {
- return true;
- }
- final int type = w.mAttrs.type;
- // Allow letterbox to be displayed early for base application or application starting
- // windows even if it is not on the top z order to prevent flickering when the
- // letterboxed window is brought to the top
- return (type != TYPE_BASE_APPLICATION && type != TYPE_APPLICATION_STARTING)
- || w.mAnimatingExit;
- }
-
- private boolean shouldLetterboxHaveRoundedCorners() {
- // TODO(b/214030873): remove once background is drawn for transparent activities
- // Letterbox shouldn't have rounded corners if the activity is transparent
- return mAppCompatConfiguration.isLetterboxActivityCornersRounded()
- && mActivityRecord.fillsParent();
+ mAppCompatLetterboxPolicy.start(w);
}
boolean isLetterboxEducationEnabled() {
- return mAppCompatConfiguration.getIsEducationEnabled();
+ return mAppCompatLetterboxOverrides.isLetterboxEducationEnabled();
}
@VisibleForTesting
boolean shouldShowLetterboxUi(WindowState mainWindow) {
- if (mAppCompatOrientationOverrides.getIsRelaunchingAfterRequestedOrientationChanged()) {
- return mLastShouldShowLetterboxUi;
- }
-
- final boolean shouldShowLetterboxUi =
- (mActivityRecord.isInLetterboxAnimation() || mActivityRecord.isVisible()
- || mActivityRecord.isVisibleRequested())
- && mainWindow.areAppWindowBoundsLetterboxed()
- // Check for FLAG_SHOW_WALLPAPER explicitly instead of using
- // WindowContainer#showWallpaper because the later will return true when this
- // activity is using blurred wallpaper for letterbox background.
- && (mainWindow.getAttrs().flags & FLAG_SHOW_WALLPAPER) == 0;
-
- mLastShouldShowLetterboxUi = shouldShowLetterboxUi;
-
- return shouldShowLetterboxUi;
+ return mAppCompatLetterboxPolicy.shouldShowLetterboxUi(mainWindow);
}
Color getLetterboxBackgroundColor() {
- final WindowState w = mActivityRecord.findMainWindow();
- if (w == null || w.isLetterboxedForDisplayCutout()) {
- return Color.valueOf(Color.BLACK);
- }
- @LetterboxBackgroundType int letterboxBackgroundType =
- mAppCompatConfiguration.getLetterboxBackgroundType();
- TaskDescription taskDescription = mActivityRecord.taskDescription;
- switch (letterboxBackgroundType) {
- case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND_FLOATING:
- if (taskDescription != null && taskDescription.getBackgroundColorFloating() != 0) {
- return Color.valueOf(taskDescription.getBackgroundColorFloating());
- }
- break;
- case LETTERBOX_BACKGROUND_APP_COLOR_BACKGROUND:
- if (taskDescription != null && taskDescription.getBackgroundColor() != 0) {
- return Color.valueOf(taskDescription.getBackgroundColor());
- }
- break;
- case LETTERBOX_BACKGROUND_WALLPAPER:
- if (hasWallpaperBackgroundForLetterbox()) {
- // Color is used for translucent scrim that dims wallpaper.
- return mAppCompatConfiguration.getLetterboxBackgroundColor();
- }
- Slog.w(TAG, "Wallpaper option is selected for letterbox background but "
- + "blur is not supported by a device or not supported in the current "
- + "window configuration or both alpha scrim and blur radius aren't "
- + "provided so using solid color background");
- break;
- case LETTERBOX_BACKGROUND_SOLID_COLOR:
- return mAppCompatConfiguration.getLetterboxBackgroundColor();
- default:
- throw new AssertionError(
- "Unexpected letterbox background type: " + letterboxBackgroundType);
- }
- // If picked option configured incorrectly or not supported then default to a solid color
- // background.
- return mAppCompatConfiguration.getLetterboxBackgroundColor();
- }
-
- private void updateRoundedCornersIfNeeded(final WindowState mainWindow) {
- final SurfaceControl windowSurface = mainWindow.getSurfaceControl();
- if (windowSurface == null || !windowSurface.isValid()) {
- return;
- }
-
- // cropBounds must be non-null for the cornerRadius to be ever applied.
- mActivityRecord.getSyncTransaction()
- .setCrop(windowSurface, getCropBoundsIfNeeded(mainWindow))
- .setCornerRadius(windowSurface, getRoundedCornersRadius(mainWindow));
+ return mAppCompatLetterboxOverrides.getLetterboxBackgroundColor();
}
@VisibleForTesting
@Nullable
Rect getCropBoundsIfNeeded(final WindowState mainWindow) {
- if (!requiresRoundedCorners(mainWindow) || mActivityRecord.isInLetterboxAnimation()) {
- // We don't want corner radius on the window.
- // In the case the ActivityRecord requires a letterboxed animation we never want
- // rounded corners on the window because rounded corners are applied at the
- // animation-bounds surface level and rounded corners on the window would interfere
- // with that leading to unexpected rounded corner positioning during the animation.
- return null;
- }
-
- final Rect cropBounds = new Rect(mActivityRecord.getBounds());
-
- // In case of translucent activities we check if the requested size is different from
- // the size provided using inherited bounds. In that case we decide to not apply rounded
- // corners because we assume the specific layout would. This is the case when the layout
- // of the translucent activity uses only a part of all the bounds because of the use of
- // LayoutParams.WRAP_CONTENT.
- if (mTransparentPolicy.isRunning() && (cropBounds.width() != mainWindow.mRequestedWidth
- || cropBounds.height() != mainWindow.mRequestedHeight)) {
- return null;
- }
-
- // It is important to call {@link #adjustBoundsIfNeeded} before {@link cropBounds.offsetTo}
- // because taskbar bounds used in {@link #adjustBoundsIfNeeded}
- // are in screen coordinates
- adjustBoundsForTaskbar(mainWindow, cropBounds);
-
- final float scale = mainWindow.mInvGlobalScale;
- if (scale != 1f && scale > 0f) {
- cropBounds.scale(scale);
- }
-
- // ActivityRecord bounds are in screen coordinates while (0,0) for activity's surface
- // control is in the top left corner of an app window so offsetting bounds
- // accordingly.
- cropBounds.offsetTo(0, 0);
- return cropBounds;
- }
-
- private boolean requiresRoundedCorners(final WindowState mainWindow) {
- return isLetterboxedNotForDisplayCutout(mainWindow)
- && mAppCompatConfiguration.isLetterboxActivityCornersRounded();
+ return mAppCompatLetterboxPolicy.getCropBoundsIfNeeded(mainWindow);
}
// Returns rounded corners radius the letterboxed activity should have based on override in
@@ -388,102 +136,12 @@
// for all corners for consistency and pick a minimal bottom one for consistency with a
// taskbar rounded corners.
int getRoundedCornersRadius(final WindowState mainWindow) {
- if (!requiresRoundedCorners(mainWindow)) {
- return 0;
- }
-
- final int radius;
- if (mAppCompatConfiguration.getLetterboxActivityCornersRadius() >= 0) {
- radius = mAppCompatConfiguration.getLetterboxActivityCornersRadius();
- } else {
- final InsetsState insetsState = mainWindow.getInsetsState();
- radius = Math.min(
- getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_LEFT),
- getInsetsStateCornerRadius(insetsState, RoundedCorner.POSITION_BOTTOM_RIGHT));
- }
-
- final float scale = mainWindow.mInvGlobalScale;
- return (scale != 1f && scale > 0f) ? (int) (scale * radius) : radius;
+ return mAppCompatLetterboxPolicy.getRoundedCornersRadius(mainWindow);
}
- /**
- * Returns the taskbar in case it is visible and expanded in height, otherwise returns null.
- */
- @VisibleForTesting
@Nullable
- InsetsSource getExpandedTaskbarOrNull(final WindowState mainWindow) {
- final InsetsState state = mainWindow.getInsetsState();
- for (int i = state.sourceSize() - 1; i >= 0; i--) {
- final InsetsSource source = state.sourceAt(i);
- if (source.getType() == WindowInsets.Type.navigationBars()
- && source.hasFlags(InsetsSource.FLAG_INSETS_ROUNDED_CORNER)
- && source.isVisible()) {
- return source;
- }
- }
- return null;
- }
-
- private void adjustBoundsForTaskbar(final WindowState mainWindow, final Rect bounds) {
- // Rounded corners should be displayed above the taskbar. When taskbar is hidden,
- // an insets frame is equal to a navigation bar which shouldn't affect position of
- // rounded corners since apps are expected to handle navigation bar inset.
- // This condition checks whether the taskbar is visible.
- // Do not crop the taskbar inset if the window is in immersive mode - the user can
- // swipe to show/hide the taskbar as an overlay.
- // Adjust the bounds only in case there is an expanded taskbar,
- // otherwise the rounded corners will be shown behind the navbar.
- final InsetsSource expandedTaskbarOrNull = getExpandedTaskbarOrNull(mainWindow);
- if (expandedTaskbarOrNull != null) {
- // Rounded corners should be displayed above the expanded taskbar.
- bounds.bottom = Math.min(bounds.bottom, expandedTaskbarOrNull.getFrame().top);
- }
- }
-
- private int getInsetsStateCornerRadius(
- InsetsState insetsState, @RoundedCorner.Position int position) {
- RoundedCorner corner = insetsState.getRoundedCorners().getRoundedCorner(position);
- return corner == null ? 0 : corner.getRadius();
- }
-
- private boolean isLetterboxedNotForDisplayCutout(WindowState mainWindow) {
- return shouldShowLetterboxUi(mainWindow)
- && !mainWindow.isLetterboxedForDisplayCutout();
- }
-
- private void updateWallpaperForLetterbox(WindowState mainWindow) {
- @LetterboxBackgroundType int letterboxBackgroundType =
- mAppCompatConfiguration.getLetterboxBackgroundType();
- boolean wallpaperShouldBeShown =
- letterboxBackgroundType == LETTERBOX_BACKGROUND_WALLPAPER
- // Don't use wallpaper as a background if letterboxed for display cutout.
- && isLetterboxedNotForDisplayCutout(mainWindow)
- // Check that dark scrim alpha or blur radius are provided
- && (getLetterboxWallpaperBlurRadiusPx() > 0
- || getLetterboxWallpaperDarkScrimAlpha() > 0)
- // Check that blur is supported by a device if blur radius is provided.
- && (getLetterboxWallpaperBlurRadiusPx() <= 0
- || isLetterboxWallpaperBlurSupported());
- if (mShowWallpaperForLetterboxBackground != wallpaperShouldBeShown) {
- mShowWallpaperForLetterboxBackground = wallpaperShouldBeShown;
- mActivityRecord.requestUpdateWallpaperIfNeeded();
- }
- }
-
- private int getLetterboxWallpaperBlurRadiusPx() {
- int blurRadius = mAppCompatConfiguration.getLetterboxBackgroundWallpaperBlurRadiusPx();
- return Math.max(blurRadius, 0);
- }
-
- private float getLetterboxWallpaperDarkScrimAlpha() {
- float alpha = mAppCompatConfiguration.getLetterboxBackgroundWallpaperDarkScrimAlpha();
- // No scrim by default.
- return (alpha < 0 || alpha >= 1) ? 0.0f : alpha;
- }
-
- private boolean isLetterboxWallpaperBlurSupported() {
- return mAppCompatConfiguration.mContext.getSystemService(WindowManager.class)
- .isCrossWindowBlurEnabled();
+ LetterboxDetails getLetterboxDetails() {
+ return mAppCompatLetterboxPolicy.getLetterboxDetails();
}
void dump(PrintWriter pw, String prefix) {
@@ -492,6 +150,9 @@
return;
}
+ pw.println(prefix + "isTransparentPolicyRunning="
+ + mActivityRecord.mAppCompatController.getTransparentPolicy().isRunning());
+
boolean areBoundsLetterboxed = mainWin.areAppWindowBoundsLetterboxed();
pw.println(prefix + "areBoundsLetterboxed=" + areBoundsLetterboxed);
if (!areBoundsLetterboxed) {
@@ -523,11 +184,11 @@
if (mAppCompatConfiguration.getLetterboxBackgroundType()
== LETTERBOX_BACKGROUND_WALLPAPER) {
pw.println(prefix + " isLetterboxWallpaperBlurSupported="
- + isLetterboxWallpaperBlurSupported());
+ + mAppCompatLetterboxOverrides.isLetterboxWallpaperBlurSupported());
pw.println(prefix + " letterboxBackgroundWallpaperDarkScrimAlpha="
- + getLetterboxWallpaperDarkScrimAlpha());
+ + mAppCompatLetterboxOverrides.getLetterboxWallpaperDarkScrimAlpha());
pw.println(prefix + " letterboxBackgroundWallpaperBlurRadius="
- + getLetterboxWallpaperBlurRadiusPx());
+ + mAppCompatLetterboxOverrides.getLetterboxWallpaperBlurRadiusPx());
}
final AppCompatReachabilityOverrides reachabilityOverrides = mActivityRecord
.mAppCompatController.getAppCompatReachabilityOverrides();
@@ -557,26 +218,4 @@
+ mAppCompatConfiguration
.getIsDisplayAspectRatioEnabledForFixedOrientationLetterbox());
}
-
- @Nullable
- LetterboxDetails getLetterboxDetails() {
- final WindowState w = mActivityRecord.findMainWindow();
- if (mLetterbox == null || w == null || w.isLetterboxedForDisplayCutout()) {
- return null;
- }
- Rect letterboxInnerBounds = new Rect();
- Rect letterboxOuterBounds = new Rect();
- getLetterboxInnerBounds(letterboxInnerBounds);
- getLetterboxOuterBounds(letterboxOuterBounds);
-
- if (letterboxInnerBounds.isEmpty() || letterboxOuterBounds.isEmpty()) {
- return null;
- }
-
- return new LetterboxDetails(
- letterboxInnerBounds,
- letterboxOuterBounds,
- w.mAttrs.insetsFlags.appearance
- );
- }
}
diff --git a/services/core/java/com/android/server/wm/Task.java b/services/core/java/com/android/server/wm/Task.java
index d3df5fd..12e91ad 100644
--- a/services/core/java/com/android/server/wm/Task.java
+++ b/services/core/java/com/android/server/wm/Task.java
@@ -3507,7 +3507,7 @@
| StartingWindowInfo.TYPE_PARAMETER_WINDOWLESS);
if ((info.startingWindowTypeParameter
& StartingWindowInfo.TYPE_PARAMETER_ACTIVITY_CREATED) != 0) {
- final WindowState topMainWin = getWindow(w -> w.mAttrs.type == TYPE_BASE_APPLICATION);
+ final WindowState topMainWin = getTopFullscreenMainWindow();
if (topMainWin != null) {
info.mainWindowLayoutParams = topMainWin.getAttrs();
info.requestedVisibleTypes = topMainWin.getRequestedVisibleTypes();
diff --git a/services/core/java/com/android/server/wm/TaskDisplayArea.java b/services/core/java/com/android/server/wm/TaskDisplayArea.java
index eaf3012..dba1c36 100644
--- a/services/core/java/com/android/server/wm/TaskDisplayArea.java
+++ b/services/core/java/com/android/server/wm/TaskDisplayArea.java
@@ -1079,8 +1079,11 @@
// Use launch-adjacent-flag-root if launching with launch-adjacent flag.
if ((launchFlags & FLAG_ACTIVITY_LAUNCH_ADJACENT) != 0
&& mLaunchAdjacentFlagRootTask != null) {
- if (sourceTask != null && sourceTask == candidateTask) {
- // Do nothing when task that is getting opened is same as the source.
+ if (sourceTask != null && (sourceTask == candidateTask
+ || sourceTask.topRunningActivity() == null)) {
+ // Do nothing when task that is getting opened is same as the source or when
+ // the source is no-longer valid.
+ Slog.w(TAG_WM, "Ignoring LAUNCH_ADJACENT because adjacent source is gone.");
} else if (sourceTask != null
&& mLaunchAdjacentFlagRootTask.getAdjacentTask() != null
&& (sourceTask == mLaunchAdjacentFlagRootTask
diff --git a/services/core/java/com/android/server/wm/Transition.java b/services/core/java/com/android/server/wm/Transition.java
index 5698750..486a61b 100644
--- a/services/core/java/com/android/server/wm/Transition.java
+++ b/services/core/java/com/android/server/wm/Transition.java
@@ -358,8 +358,12 @@
return mToken;
}
- void addFlag(int flag) {
- mFlags |= flag;
+ void addFlag(@TransitionFlags int flags) {
+ mFlags |= flags;
+ }
+
+ void removeFlag(@TransitionFlags int flags) {
+ mFlags &= ~flags;
}
void calcParallelCollectType(WindowContainerTransaction wct) {
@@ -3350,6 +3354,15 @@
return chg.hasChanged();
}
+ boolean hasChanges() {
+ for (int i = 0; i < mParticipants.size(); ++i) {
+ if (mChanges.get(mParticipants.valueAt(i)).hasChanged()) {
+ return true;
+ }
+ }
+ return false;
+ }
+
@VisibleForTesting
static class ChangeInfo {
private static final int FLAG_NONE = 0;
diff --git a/services/core/java/com/android/server/wm/WindowOrganizerController.java b/services/core/java/com/android/server/wm/WindowOrganizerController.java
index 58c48ad..c6d0b72 100644
--- a/services/core/java/com/android/server/wm/WindowOrganizerController.java
+++ b/services/core/java/com/android/server/wm/WindowOrganizerController.java
@@ -1194,7 +1194,13 @@
}
}
}
- effects |= sanitizeAndApplyHierarchyOp(wc, hop);
+ if (wc.asTask() != null) {
+ effects |= sanitizeAndApplyHierarchyOpForTask(wc.asTask(), hop);
+ } else if (wc.asDisplayArea() != null) {
+ effects |= sanitizeAndApplyHierarchyOpForDisplayArea(wc.asDisplayArea(), hop);
+ } else {
+ throw new IllegalArgumentException("Invalid container in hierarchy op");
+ }
break;
}
case HIERARCHY_OP_TYPE_ADD_TASK_FRAGMENT_OPERATION: {
@@ -1850,12 +1856,22 @@
return starterResult[0];
}
- private int sanitizeAndApplyHierarchyOp(WindowContainer container,
- WindowContainerTransaction.HierarchyOp hop) {
- final Task task = container.asTask();
- if (task == null) {
- throw new IllegalArgumentException("Invalid container in hierarchy op");
+ private int sanitizeAndApplyHierarchyOpForDisplayArea(@NonNull DisplayArea displayArea,
+ @NonNull WindowContainerTransaction.HierarchyOp hop) {
+ if (hop.getType() != HIERARCHY_OP_TYPE_REORDER) {
+ throw new UnsupportedOperationException("DisplayArea only supports reordering");
}
+ if (displayArea.getParent() == null) {
+ return TRANSACT_EFFECTS_NONE;
+ }
+ displayArea.getParent().positionChildAt(
+ hop.getToTop() ? POSITION_TOP : POSITION_BOTTOM,
+ displayArea, hop.includingParents());
+ return TRANSACT_EFFECTS_LIFECYCLE;
+ }
+
+ private int sanitizeAndApplyHierarchyOpForTask(@NonNull Task task,
+ @NonNull WindowContainerTransaction.HierarchyOp hop) {
final DisplayContent dc = task.getDisplayContent();
if (dc == null) {
Slog.w(TAG, "Container is no longer attached: " + task);
diff --git a/services/core/jni/com_android_server_utils_AnrTimer.cpp b/services/core/jni/com_android_server_utils_AnrTimer.cpp
index 2edf129..cf96114 100644
--- a/services/core/jni/com_android_server_utils_AnrTimer.cpp
+++ b/services/core/jni/com_android_server_utils_AnrTimer.cpp
@@ -934,7 +934,6 @@
}
// Hold the lock in order to manage the running list.
-// the listener.
void AnrTimerService::expire(timer_id_t timerId) {
// Save the timer attributes for the notification
int pid = 0;
@@ -967,7 +966,6 @@
// Deliver the notification outside of the lock.
if (expired) {
if (!notifier_(timerId, pid, uid, elapsed, notifierCookie_, notifierObject_)) {
- AutoMutex _l(lock_);
// Notification failed, which means the listener will never call accept() or
// discard(). Do not reinsert the timer.
discard(timerId);
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
index 669a999..a08af72 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/DevicePolicyEngine.java
@@ -2080,10 +2080,14 @@
String tag = parser.getName();
switch (tag) {
case TAG_LOCAL_POLICY_ENTRY:
- readLocalPoliciesInner(parser);
+ int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
+ if (!mLocalPolicies.contains(userId)) {
+ mLocalPolicies.put(userId, new HashMap<>());
+ }
+ readPoliciesInner(parser, mLocalPolicies.get(userId));
break;
case TAG_GLOBAL_POLICY_ENTRY:
- readGlobalPoliciesInner(parser);
+ readPoliciesInner(parser, mGlobalPolicies);
break;
case TAG_ENFORCING_ADMINS_ENTRY:
readEnforcingAdminsInner(parser);
@@ -2100,64 +2104,45 @@
}
}
- private void readLocalPoliciesInner(TypedXmlPullParser parser)
- throws XmlPullParserException, IOException {
- int userId = parser.getAttributeInt(/* namespace= */ null, ATTR_USER_ID);
- PolicyKey policyKey = null;
- PolicyState<?> policyState = null;
- int outerDepth = parser.getDepth();
- while (XmlUtils.nextElementWithin(parser, outerDepth)) {
- String tag = parser.getName();
- switch (tag) {
- case TAG_POLICY_KEY_ENTRY:
- policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
- break;
- case TAG_POLICY_STATE_ENTRY:
- policyState = PolicyState.readFromXml(parser);
- break;
- default:
- Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
- }
- }
-
- if (policyKey != null && policyState != null) {
- if (!mLocalPolicies.contains(userId)) {
- mLocalPolicies.put(userId, new HashMap<>());
- }
- mLocalPolicies.get(userId).put(policyKey, policyState);
- } else {
- Slogf.wtf(TAG, "Error parsing local policy, policyKey is "
- + (policyKey == null ? "null" : policyKey) + ", and policyState is "
- + (policyState == null ? "null" : policyState) + ".");
- }
- }
-
- private void readGlobalPoliciesInner(TypedXmlPullParser parser)
+ private static void readPoliciesInner(
+ TypedXmlPullParser parser, Map<PolicyKey, PolicyState<?>> policyStateMap)
throws IOException, XmlPullParserException {
PolicyKey policyKey = null;
+ PolicyDefinition<?> policyDefinition = null;
PolicyState<?> policyState = null;
int outerDepth = parser.getDepth();
while (XmlUtils.nextElementWithin(parser, outerDepth)) {
String tag = parser.getName();
switch (tag) {
case TAG_POLICY_KEY_ENTRY:
- policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
+ if (Flags.dontReadPolicyDefinition()) {
+ policyDefinition = PolicyDefinition.readFromXml(parser);
+ if (policyDefinition != null) {
+ policyKey = policyDefinition.getPolicyKey();
+ }
+ } else {
+ policyKey = PolicyDefinition.readPolicyKeyFromXml(parser);
+ }
break;
case TAG_POLICY_STATE_ENTRY:
- policyState = PolicyState.readFromXml(parser);
+ if (Flags.dontReadPolicyDefinition() && policyDefinition == null) {
+ Slogf.w(TAG, "Skipping policy state - unknown policy definition");
+ } else {
+ policyState = PolicyState.readFromXml(policyDefinition, parser);
+ }
break;
default:
- Slogf.wtf(TAG, "Unknown tag for local policy entry" + tag);
+ Slogf.wtf(TAG, "Unknown tag for policy entry" + tag);
}
}
- if (policyKey != null && policyState != null) {
- mGlobalPolicies.put(policyKey, policyState);
- } else {
- Slogf.wtf(TAG, "Error parsing global policy, policyKey is "
- + (policyKey == null ? "null" : policyKey) + ", and policyState is "
- + (policyState == null ? "null" : policyState) + ".");
+ if (policyKey == null || policyState == null) {
+ Slogf.wtf(TAG, "Error parsing policy, policyKey is %s, and policyState is %s.",
+ policyKey, policyState);
+ return;
}
+
+ policyStateMap.put(policyKey, policyState);
}
private void readEnforcingAdminsInner(TypedXmlPullParser parser)
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
index f86d307..19a942c 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyDefinition.java
@@ -129,9 +129,8 @@
*/
static PolicyDefinition<Integer> PERMISSION_GRANT(
@NonNull String packageName, @NonNull String permissionName) {
- if (packageName == null || permissionName == null) {
- return GENERIC_PERMISSION_GRANT;
- }
+ Objects.requireNonNull(packageName, "packageName must not be null");
+ Objects.requireNonNull(permissionName, "permissionName must not be null");
return GENERIC_PERMISSION_GRANT.createPolicyDefinition(
new PackagePermissionPolicyKey(
DevicePolicyIdentifiers.PERMISSION_GRANT_POLICY,
@@ -190,10 +189,8 @@
* {@link #GENERIC_PERSISTENT_PREFERRED_ACTIVITY}.
*/
static PolicyDefinition<ComponentName> PERSISTENT_PREFERRED_ACTIVITY(
- IntentFilter intentFilter) {
- if (intentFilter == null) {
- return GENERIC_PERSISTENT_PREFERRED_ACTIVITY;
- }
+ @NonNull IntentFilter intentFilter) {
+ Objects.requireNonNull(intentFilter, "intentFilter must not be null");
return GENERIC_PERSISTENT_PREFERRED_ACTIVITY.createPolicyDefinition(
new IntentFilterPolicyKey(
DevicePolicyIdentifiers.PERSISTENT_PREFERRED_ACTIVITY_POLICY,
@@ -216,11 +213,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_PACKAGE_UNINSTALL_BLOCKED}.
*/
- static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(
- String packageName) {
- if (packageName == null) {
- return GENERIC_PACKAGE_UNINSTALL_BLOCKED;
- }
+ static PolicyDefinition<Boolean> PACKAGE_UNINSTALL_BLOCKED(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_PACKAGE_UNINSTALL_BLOCKED.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.PACKAGE_UNINSTALL_BLOCKED_POLICY, packageName));
@@ -247,10 +241,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_APPLICATION_RESTRICTIONS}.
*/
- static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(String packageName) {
- if (packageName == null) {
- return GENERIC_APPLICATION_RESTRICTIONS;
- }
+ static PolicyDefinition<Bundle> APPLICATION_RESTRICTIONS(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_APPLICATION_RESTRICTIONS.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.APPLICATION_RESTRICTIONS_POLICY, packageName));
@@ -293,10 +285,8 @@
* Passing in {@code null} for {@code packageName} will return
* {@link #GENERIC_APPLICATION_HIDDEN}.
*/
- static PolicyDefinition<Boolean> APPLICATION_HIDDEN(String packageName) {
- if (packageName == null) {
- return GENERIC_APPLICATION_HIDDEN;
- }
+ static PolicyDefinition<Boolean> APPLICATION_HIDDEN(@NonNull String packageName) {
+ Objects.requireNonNull(packageName, "packageName must not be null");
return GENERIC_APPLICATION_HIDDEN.createPolicyDefinition(
new PackagePolicyKey(
DevicePolicyIdentifiers.APPLICATION_HIDDEN_POLICY, packageName));
@@ -319,10 +309,8 @@
* Passing in {@code null} for {@code accountType} will return
* {@link #GENERIC_ACCOUNT_MANAGEMENT_DISABLED}.
*/
- static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(String accountType) {
- if (accountType == null) {
- return GENERIC_ACCOUNT_MANAGEMENT_DISABLED;
- }
+ static PolicyDefinition<Boolean> ACCOUNT_MANAGEMENT_DISABLED(@NonNull String accountType) {
+ Objects.requireNonNull(accountType, "accountType must not be null");
return GENERIC_ACCOUNT_MANAGEMENT_DISABLED.createPolicyDefinition(
new AccountTypePolicyKey(
DevicePolicyIdentifiers.ACCOUNT_MANAGEMENT_DISABLED_POLICY, accountType));
diff --git a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
index 245c438..b813489 100644
--- a/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
+++ b/services/devicepolicy/java/com/android/server/devicepolicy/PolicyState.java
@@ -19,6 +19,7 @@
import android.annotation.NonNull;
import android.annotation.Nullable;
import android.app.admin.PolicyValue;
+import android.app.admin.flags.Flags;
import android.util.IndentingPrintWriter;
import com.android.internal.util.XmlUtils;
@@ -254,11 +255,9 @@
}
@Nullable
- static <V> PolicyState<V> readFromXml(TypedXmlPullParser parser)
+ static <V> PolicyState<V> readFromXml(
+ PolicyDefinition<V> policyDefinition, TypedXmlPullParser parser)
throws IOException, XmlPullParserException {
-
- PolicyDefinition<V> policyDefinition = null;
-
PolicyValue<V> currentResolvedPolicy = null;
LinkedHashMap<EnforcingAdmin, PolicyValue<V>> policiesSetByAdmins = new LinkedHashMap<>();
@@ -300,10 +299,15 @@
}
break;
case TAG_POLICY_DEFINITION_ENTRY:
- policyDefinition = PolicyDefinition.readFromXml(parser);
- if (policyDefinition == null) {
- Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, "
- + "PolicyDefinition is null");
+ if (Flags.dontReadPolicyDefinition()) {
+ // Should be passed by the caller.
+ Objects.requireNonNull(policyDefinition);
+ } else {
+ policyDefinition = PolicyDefinition.readFromXml(parser);
+ if (policyDefinition == null) {
+ Slogf.wtf(TAG, "Error Parsing TAG_POLICY_DEFINITION_ENTRY, "
+ + "PolicyDefinition is null");
+ }
}
break;
diff --git a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
index aee7242..a27ad9a 100644
--- a/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
+++ b/services/tests/InputMethodSystemServerTests/src/com/android/server/inputmethod/InputMethodSubtypeSwitchingControllerTest.java
@@ -913,52 +913,100 @@
final var controller = ControllerImpl.createFrom(null /* currentInstance */, List.of(),
List.of());
- assertNoAction(controller, false /* forHardware */, items);
- assertNoAction(controller, true /* forHardware */, hardwareItems);
+ assertNextItemNoAction(controller, false /* forHardware */, items,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, true /* forHardware */, hardwareItems,
+ null /* expectedNext */);
}
- /** Verifies that a controller with a single item can't take any actions. */
+ /**
+ * Verifies that a controller with a single item can't update the recency, and cannot switch
+ * away from the item, but allows switching from unknown items to the single item.
+ */
@RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testSingleItemList() {
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ null, true /* supportsSwitchingToNextInputMethod */);
+ final var unknownItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ null, true /* supportsSwitchingToNextInputMethod */);
+ final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
+ addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var controller = ControllerImpl.createFrom(null /* currentInstance */,
- List.of(items.get(0)), List.of(hardwareItems.get(0)));
+ items, hardwareItems);
- assertNoAction(controller, false /* forHardware */, items);
- assertNoAction(controller, true /* forHardware */, hardwareItems);
+ assertNextItemNoAction(controller, false /* forHardware */, items,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, false /* forHardware */, unknownItems,
+ items.get(0));
+ assertNextItemNoAction(controller, true /* forHardware */, hardwareItems,
+ null /* expectedNext */);
+ assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems,
+ hardwareItems.get(0));
}
- /** Verifies that a controller can't take any actions for unknown items. */
+ /**
+ * Verifies that the recency cannot be updated for unknown items, but switching from unknown
+ * items reaches the most recent known item.
+ */
@RequiresFlagsEnabled(Flags.FLAG_IME_SWITCHER_REVAMP)
@Test
public void testUnknownItems() {
final var items = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(items, "LatinIme", "LatinIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
+
+ final var english = items.get(0);
+ final var french = items.get(1);
+ final var italian = items.get(2);
+
final var unknownItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(unknownItems, "UnknownIme", "UnknownIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var hardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(hardwareItems, "HardwareIme", "HardwareIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var unknownHardwareItems = new ArrayList<ImeSubtypeListItem>();
addTestImeSubtypeListItems(unknownHardwareItems, "HardwareUnknownIme", "HardwareUnknownIme",
- List.of("en", "fr"), true /* supportsSwitchingToNextInputMethod */);
+ List.of("en", "fr", "it"), true /* supportsSwitchingToNextInputMethod */);
final var controller = ControllerImpl.createFrom(null /* currentInstance */, items,
hardwareItems);
- assertNoAction(controller, false /* forHardware */, unknownItems);
- assertNoAction(controller, true /* forHardware */, unknownHardwareItems);
+ assertTrue("Recency updated for french IME", onUserAction(controller, french));
+
+ final var recencyItems = List.of(french, english, italian);
+
+ assertNextItemNoAction(controller, false /* forHardware */, unknownItems,
+ french);
+ assertNextItemNoAction(controller, true /* forHardware */, unknownHardwareItems,
+ hardwareItems.get(0));
+
+ // Known items must not be able to switch to unknown items.
+ assertNextOrder(controller, false /* forHardware */, MODE_STATIC, items,
+ List.of(items));
+ assertNextOrder(controller, false /* forHardware */, MODE_RECENT, recencyItems,
+ List.of(recencyItems));
+ assertNextOrder(controller, false /* forHardware */, MODE_AUTO, true /* forward */,
+ recencyItems, List.of(recencyItems));
+ assertNextOrder(controller, false /* forHardware */, MODE_AUTO, false /* forward */,
+ items.reversed(), List.of(items.reversed()));
+
+ assertNextOrder(controller, true /* forHardware */, MODE_STATIC, hardwareItems,
+ List.of(hardwareItems));
+ assertNextOrder(controller, true /* forHardware */, MODE_RECENT, hardwareItems,
+ List.of(hardwareItems));
+ assertNextOrder(controller, true /* forHardware */, MODE_AUTO, hardwareItems,
+ List.of(hardwareItems));
}
/** Verifies that the IME name does influence the comparison order. */
@@ -1199,25 +1247,26 @@
}
/**
- * Verifies that no next items can be found, and the recency cannot be updated for the
+ * Verifies that the expected next item is returned, and the recency cannot be updated for the
* given items.
*
- * @param controller the controller to verify the items on.
- * @param forHardware whether to try finding the next hardware item, or software item.
- * @param items the list of items to verify.
+ * @param controller the controller to verify the items on.
+ * @param forHardware whether to try finding the next hardware item, or software item.
+ * @param items the list of items to verify.
+ * @param expectedNext the expected next item.
*/
- private void assertNoAction(@NonNull ControllerImpl controller, boolean forHardware,
- @NonNull List<ImeSubtypeListItem> items) {
+ private void assertNextItemNoAction(@NonNull ControllerImpl controller, boolean forHardware,
+ @NonNull List<ImeSubtypeListItem> items, @Nullable ImeSubtypeListItem expectedNext) {
for (var item : items) {
for (int mode = MODE_STATIC; mode <= MODE_AUTO; mode++) {
assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
- false /* forward */, item, null /* expectedNext */);
+ false /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, false /* onlyCurrentIme */, mode,
- true /* forward */, item, null /* expectedNext */);
+ true /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
- false /* forward */, item, null /* expectedNext */);
+ false /* forward */, item, expectedNext);
assertNextItem(controller, forHardware, true /* onlyCurrentIme */, mode,
- true /* forward */, item, null /* expectedNext */);
+ true /* forward */, item, expectedNext);
}
assertFalse("User action shouldn't have updated the recency.",
diff --git a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
index b980ca0..30de0e8 100644
--- a/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/alarm/AlarmManagerServiceTest.java
@@ -512,6 +512,9 @@
when(mPermissionManagerInternal.getAppOpPermissionPackages(
SCHEDULE_EXACT_ALARM)).thenReturn(EmptyArray.STRING);
+ // Initialize timestamps with arbitrary values of time
+ mNowElapsedTest = 12;
+ mNowRtcTest = 345;
mInjector = new Injector(mMockContext);
mService = new AlarmManagerService(mMockContext, mInjector);
spyOn(mService);
@@ -774,6 +777,61 @@
}
@Test
+ public void timeChangeBroadcastForward() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms more than boot
+ // time clock.
+ mNowRtcTest += timeDelta + 1001;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext)
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ eq(UserHandle.ALL),
+ isNull(),
+ any());
+ }
+
+ @Test
+ public void timeChangeBroadcastBackward() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService sends the broadcast if real time clock proceeds 1000ms less than boot
+ // time clock.
+ mNowRtcTest += timeDelta - 1001;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext)
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ eq(UserHandle.ALL),
+ isNull(),
+ any());
+ }
+
+ @Test
+ public void timeChangeFilterMinorAdjustment() throws Exception {
+ final long timeDelta = 12345;
+ // AlarmManagerService does not send the broadcast if real time clock proceeds within 1000ms
+ // than boot time clock.
+ mNowRtcTest += timeDelta + 1000;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ mNowRtcTest += timeDelta - 1000;
+ mNowElapsedTest += timeDelta;
+ mTestTimer.expire(TIME_CHANGED_MASK);
+
+ verify(mMockContext, never())
+ .sendBroadcastAsUser(
+ argThat((intent) -> intent.getAction() == Intent.ACTION_TIME_CHANGED),
+ any(),
+ any(),
+ any());
+ }
+
+ @Test
public void testSingleAlarmExpiration() throws Exception {
final long triggerTime = mNowElapsedTest + 5000;
final PendingIntent alarmPi = getNewMockPendingIntent();
diff --git a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
index 0d14c9f..1b9f8d1 100644
--- a/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
+++ b/services/tests/mockingservicestests/src/com/android/server/utils/quota/CountQuotaTrackerTest.java
@@ -40,6 +40,8 @@
import static org.mockito.Mockito.times;
import static org.mockito.Mockito.verify;
+import android.annotation.NonNull;
+import android.annotation.Nullable;
import android.app.AlarmManager;
import android.content.BroadcastReceiver;
import android.content.Context;
@@ -224,6 +226,45 @@
}
}
+ private LongArrayQueue getEvents(int userId, String packageName, String tag) {
+ synchronized (mQuotaTracker.mLock) {
+ return mQuotaTracker.getEvents(userId, packageName, tag);
+ }
+ }
+
+ private void updateExecutionStats(final int userId, @NonNull final String packageName,
+ @Nullable final String tag, @NonNull ExecutionStats stats) {
+ synchronized (mQuotaTracker.mLock) {
+ mQuotaTracker.updateExecutionStatsLocked(userId, packageName, tag, stats);
+ }
+ }
+
+ private ExecutionStats getExecutionStats(final int userId, @NonNull final String packageName,
+ @Nullable final String tag) {
+ synchronized (mQuotaTracker.mLock) {
+ return mQuotaTracker.getExecutionStatsLocked(userId, packageName, tag);
+ }
+ }
+
+ private void maybeScheduleStartAlarm(final int userId, @NonNull final String packageName,
+ @Nullable final String tag) {
+ synchronized (mQuotaTracker.mLock) {
+ mQuotaTracker.maybeScheduleStartAlarmLocked(userId, packageName, tag);
+ }
+ }
+
+ private void maybeScheduleCleanupAlarm() {
+ synchronized (mQuotaTracker.mLock) {
+ mQuotaTracker.maybeScheduleCleanupAlarmLocked();
+ }
+ }
+
+ private void deleteObsoleteEvents() {
+ synchronized (mQuotaTracker.mLock) {
+ mQuotaTracker.deleteObsoleteEventsLocked();
+ }
+ }
+
@Test
public void testDeleteObsoleteEventsLocked() {
// Count window size should only apply to event list.
@@ -243,9 +284,9 @@
expectedEvents.addLast(now - HOUR_IN_MILLIS);
expectedEvents.addLast(now - 1);
- mQuotaTracker.deleteObsoleteEventsLocked();
+ deleteObsoleteEvents();
- LongArrayQueue remainingEvents = mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE,
+ LongArrayQueue remainingEvents = getEvents(TEST_USER_ID, TEST_PACKAGE,
TEST_TAG);
assertTrue(longArrayQueueEquals(expectedEvents, remainingEvents));
}
@@ -270,15 +311,15 @@
removal.putExtra(Intent.EXTRA_UID, TEST_UID);
mReceiver.onReceive(mContext, removal);
assertNull(
- mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag1"));
+ getEvents(TEST_USER_ID, "com.android.test.remove", "tag1"));
assertNull(
- mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag2"));
+ getEvents(TEST_USER_ID, "com.android.test.remove", "tag2"));
assertNull(
- mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.remove", "tag3"));
+ getEvents(TEST_USER_ID, "com.android.test.remove", "tag3"));
assertTrue(longArrayQueueEquals(expected1,
- mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag1")));
+ getEvents(TEST_USER_ID, "com.android.test.stay", "tag1")));
assertTrue(longArrayQueueEquals(expected2,
- mQuotaTracker.getEvents(TEST_USER_ID, "com.android.test.stay", "tag2")));
+ getEvents(TEST_USER_ID, "com.android.test.stay", "tag2")));
}
@Test
@@ -298,10 +339,10 @@
Intent removal = new Intent(Intent.ACTION_USER_REMOVED);
removal.putExtra(Intent.EXTRA_USER_HANDLE, TEST_USER_ID);
mReceiver.onReceive(mContext, removal);
- assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1"));
- assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2"));
- assertNull(mQuotaTracker.getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3"));
- longArrayQueueEquals(expected, mQuotaTracker.getEvents(10, TEST_PACKAGE, "tag4"));
+ assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag1"));
+ assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag2"));
+ assertNull(getEvents(TEST_USER_ID, TEST_PACKAGE, "tag3"));
+ longArrayQueueEquals(expected, getEvents(10, TEST_PACKAGE, "tag4"));
}
@Test
@@ -323,7 +364,7 @@
inputStats.countLimit = expectedStats.countLimit = 3;
// Invalid time is now +24 hours since there are no sessions at all for the app.
expectedStats.expirationTimeElapsed = now + 24 * HOUR_IN_MILLIS;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, "com.android.test.not.run", TEST_TAG,
+ updateExecutionStats(TEST_USER_ID, "com.android.test.not.run", TEST_TAG,
inputStats);
assertEquals(expectedStats, inputStats);
@@ -333,19 +374,19 @@
// Invalid time is now since there was an event exactly windowSizeMs ago.
expectedStats.expirationTimeElapsed = now;
expectedStats.countInWindow = 1;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 3 * MINUTE_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + 2 * MINUTE_IN_MILLIS;
expectedStats.countInWindow = 1;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 4 * MINUTE_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + 3 * MINUTE_IN_MILLIS;
expectedStats.countInWindow = 1;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 49 * MINUTE_IN_MILLIS;
@@ -353,13 +394,13 @@
// minutes.
expectedStats.expirationTimeElapsed = now + 44 * MINUTE_IN_MILLIS;
expectedStats.countInWindow = 2;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 50 * MINUTE_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + 45 * MINUTE_IN_MILLIS;
expectedStats.countInWindow = 2;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = HOUR_IN_MILLIS;
@@ -370,28 +411,28 @@
// App is at event count limit but the oldest session is at the edge of the window, so
// in quota time is now.
expectedStats.inQuotaTimeElapsed = now;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 2 * HOUR_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.countInWindow = 3;
expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 5 * HOUR_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
expectedStats.countInWindow = 4;
expectedStats.inQuotaTimeElapsed = now + 4 * HOUR_IN_MILLIS;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
inputStats.windowSizeMs = expectedStats.windowSizeMs = 6 * HOUR_IN_MILLIS;
expectedStats.expirationTimeElapsed = now + 2 * HOUR_IN_MILLIS;
expectedStats.countInWindow = 4;
expectedStats.inQuotaTimeElapsed = now + 5 * HOUR_IN_MILLIS;
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, inputStats);
assertEquals(expectedStats, inputStats);
}
@@ -428,7 +469,7 @@
expectedStats.countInWindow = 1;
mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
assertEquals(expectedStats,
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
// Working
expectedStats.expirationTimeElapsed = now;
@@ -437,7 +478,7 @@
expectedStats.countInWindow = 2;
mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
assertEquals(expectedStats,
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
// Frequent
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
@@ -447,7 +488,7 @@
expectedStats.inQuotaTimeElapsed = now + HOUR_IN_MILLIS;
mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
assertEquals(expectedStats,
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
// Rare
expectedStats.expirationTimeElapsed = now + HOUR_IN_MILLIS;
@@ -457,7 +498,7 @@
expectedStats.inQuotaTimeElapsed = now + 19 * HOUR_IN_MILLIS;
mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY;
assertEquals(expectedStats,
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
}
/**
@@ -481,7 +522,7 @@
expectedStats.countInWindow = 3;
expectedStats.expirationTimeElapsed = 2 * HOUR_IN_MILLIS + 30_000;
assertEquals(expectedStats,
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG));
}
@Test
@@ -556,20 +597,20 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 5, 24 * HOUR_IN_MILLIS);
// No sessions saved yet.
- mQuotaTracker.maybeScheduleCleanupAlarmLocked();
+ maybeScheduleCleanupAlarm();
verify(mAlarmManager, never()).set(anyInt(), anyLong(), eq(TAG_CLEANUP), any(), any());
// Test with only one timing session saved.
final long now = mInjector.getElapsedRealtime();
logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 6 * HOUR_IN_MILLIS);
- mQuotaTracker.maybeScheduleCleanupAlarmLocked();
+ maybeScheduleCleanupAlarm();
verify(mAlarmManager, timeout(1000).times(1))
.set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any());
// Test with new (more recent) timing sessions saved. AlarmManger shouldn't be called again.
logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS);
logEventAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS);
- mQuotaTracker.maybeScheduleCleanupAlarmLocked();
+ maybeScheduleCleanupAlarm();
verify(mAlarmManager, times(1))
.set(anyInt(), eq(now + 18 * HOUR_IN_MILLIS), eq(TAG_CLEANUP), any(), any());
}
@@ -587,14 +628,14 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 8 * HOUR_IN_MILLIS);
// No sessions saved yet.
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, never()).setWindow(
anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
// Test with timing sessions out of window.
final long now = mInjector.getElapsedRealtime();
logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 10 * HOUR_IN_MILLIS, 20);
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, never()).setWindow(
anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
@@ -602,26 +643,26 @@
final long start = now - (6 * HOUR_IN_MILLIS);
final long expectedAlarmTime = start + 8 * HOUR_IN_MILLIS;
logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, start, 5);
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, never()).setWindow(
anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
// Add some more sessions, but still in quota.
logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - 3 * HOUR_IN_MILLIS, 1);
logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 3);
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, never()).setWindow(
anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
// Test when out of quota.
logEventsAt(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, now - HOUR_IN_MILLIS, 1);
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, timeout(1000).times(1)).setWindow(
anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
any(Handler.class));
// Alarm already scheduled, so make sure it's not scheduled again.
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
verify(mAlarmManager, times(1)).setWindow(
anyInt(), eq(expectedAlarmTime), anyLong(), eq(TAG_QUOTA_CHECK), any(),
any(Handler.class));
@@ -656,7 +697,7 @@
// Start in ACTIVE bucket.
mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, never())
.setWindow(anyInt(), anyLong(), anyLong(), eq(TAG_QUOTA_CHECK), any(),
any(Handler.class));
@@ -665,40 +706,40 @@
// And down from there.
final long expectedWorkingAlarmTime = outOfQuotaTime + (2 * HOUR_IN_MILLIS);
mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
final long expectedFrequentAlarmTime = outOfQuotaTime + (8 * HOUR_IN_MILLIS);
mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
final long expectedRareAlarmTime = outOfQuotaTime + (24 * HOUR_IN_MILLIS);
mCategorizer.mCategoryToUse = RARE_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.setWindow(anyInt(), eq(expectedRareAlarmTime), anyLong(),
eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
// And back up again.
mCategorizer.mCategoryToUse = FREQUENT_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.setWindow(anyInt(), eq(expectedFrequentAlarmTime), anyLong(),
eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
mCategorizer.mCategoryToUse = WORKING_SET_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.setWindow(anyInt(), eq(expectedWorkingAlarmTime), anyLong(),
eq(TAG_QUOTA_CHECK), any(), any(Handler.class));
mCategorizer.mCategoryToUse = ACTIVE_BUCKET_CATEGORY;
- mQuotaTracker.maybeScheduleStartAlarmLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ maybeScheduleStartAlarm(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
inOrder.verify(mAlarmManager, timeout(1000).times(1))
.cancel(any(AlarmManager.OnAlarmListener.class));
inOrder.verify(mAlarmManager, timeout(1000).times(0))
@@ -745,14 +786,14 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS);
ExecutionStats stats =
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
assertEquals(0, stats.countInWindow);
for (int i = 0; i < 10; ++i) {
mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
assertEquals(0, stats.countInWindow);
}
}
@@ -766,14 +807,14 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS);
ExecutionStats stats =
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
assertEquals(0, stats.countInWindow);
for (int i = 0; i < 10; ++i) {
mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
assertEquals(i + 1, stats.countInWindow);
}
}
@@ -785,14 +826,14 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS);
ExecutionStats stats =
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
assertEquals(0, stats.countInWindow);
for (int i = 0; i < 10; ++i) {
mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
assertEquals(0, stats.countInWindow);
}
}
@@ -806,14 +847,14 @@
mQuotaTracker.setCountLimit(SINGLE_CATEGORY, 10, 2 * HOUR_IN_MILLIS);
ExecutionStats stats =
- mQuotaTracker.getExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
+ getExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
assertEquals(0, stats.countInWindow);
for (int i = 0; i < 10; ++i) {
mQuotaTracker.noteEvent(TEST_USER_ID, TEST_PACKAGE, TEST_TAG);
advanceElapsedClock(10 * SECOND_IN_MILLIS);
- mQuotaTracker.updateExecutionStatsLocked(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
+ updateExecutionStats(TEST_USER_ID, TEST_PACKAGE, TEST_TAG, stats);
assertEquals(i + 1, stats.countInWindow);
}
}
diff --git a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
index 0def516..8753b25 100644
--- a/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
+++ b/services/tests/servicestests/src/com/android/server/accessibility/MouseKeysInterceptorTest.kt
@@ -45,7 +45,6 @@
import org.mockito.Mock
import org.mockito.Mockito
import org.mockito.MockitoAnnotations
-import java.util.concurrent.TimeUnit
import java.util.LinkedList
import java.util.Queue
import android.util.ArraySet
@@ -117,9 +116,6 @@
Mockito.`when`(mockAms.traceManager).thenReturn(mockTraceManager)
mouseKeysInterceptor = MouseKeysInterceptor(mockAms, testLooper.looper, DISPLAY_ID)
- // VirtualMouse is created on a separate thread.
- // Wait for VirtualMouse to be created before running tests
- TimeUnit.MILLISECONDS.sleep(20L)
mouseKeysInterceptor.next = nextInterceptor
}
diff --git a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
index dbab54b..a8e350b 100644
--- a/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
+++ b/services/tests/servicestests/src/com/android/server/am/UserControllerTest.java
@@ -19,6 +19,8 @@
import static android.Manifest.permission.INTERACT_ACROSS_PROFILES;
import static android.Manifest.permission.INTERACT_ACROSS_USERS;
import static android.Manifest.permission.INTERACT_ACROSS_USERS_FULL;
+import static android.app.ActivityManager.STOP_USER_ON_SWITCH_TRUE;
+import static android.app.ActivityManager.STOP_USER_ON_SWITCH_FALSE;
import static android.app.ActivityManagerInternal.ALLOW_FULL_ONLY;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL;
import static android.app.ActivityManagerInternal.ALLOW_NON_FULL_IN_PROFILE;
@@ -201,7 +203,8 @@
doNothing().when(mInjector).systemServiceManagerOnUserStopped(anyInt());
doNothing().when(mInjector).systemServiceManagerOnUserCompletedEvent(
anyInt(), anyInt());
- doNothing().when(mInjector).activityManagerForceStopPackage(anyInt(), anyString());
+ doNothing().when(mInjector).activityManagerForceStopUserPackages(anyInt(),
+ anyString(), anyBoolean());
doNothing().when(mInjector).activityManagerOnUserStopped(anyInt());
doNothing().when(mInjector).clearBroadcastQueueForUser(anyInt());
doNothing().when(mInjector).taskSupervisorRemoveUser(anyInt());
@@ -936,6 +939,61 @@
new HashSet<>(mUserController.getRunningUsersLU()));
}
+ @Test
+ public void testEarlyPackageKillEnabledForUserSwitch_enabled() {
+ mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+ /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true,
+ /* backgroundUserScheduledStopTimeSecs= */ -1);
+
+ assertTrue(mUserController
+ .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1));
+ }
+
+ @Test
+ public void testEarlyPackageKillEnabledForUserSwitch_withoutDelayUserDataLocking() {
+ mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+ /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false,
+ /* backgroundUserScheduledStopTimeSecs= */ -1);
+
+ assertFalse(mUserController
+ .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1));
+ }
+
+ @Test
+ public void testEarlyPackageKillEnabledForUserSwitch_withPrevSystemUser() {
+ mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+ /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true,
+ /* backgroundUserScheduledStopTimeSecs= */ -1);
+
+ assertFalse(mUserController
+ .isEarlyPackageKillEnabledForUserSwitch(SYSTEM_USER_ID, TEST_USER_ID1));
+ }
+
+ @Test
+ public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOn() {
+ mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+ /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ false,
+ /* backgroundUserScheduledStopTimeSecs= */ -1);
+
+ mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_TRUE);
+
+ assertTrue(mUserController
+ .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1));
+ }
+
+ @Test
+ public void testEarlyPackageKillEnabledForUserSwitch_stopUserOnSwitchModeOff() {
+ mUserController.setInitialConfig(/* userSwitchUiEnabled= */ true,
+ /* maxRunningUsers= */ 4, /* delayUserDataLocking= */ true,
+ /* backgroundUserScheduledStopTimeSecs= */ -1);
+
+ mUserController.setStopUserOnSwitch(STOP_USER_ON_SWITCH_FALSE);
+
+ assertFalse(mUserController
+ .isEarlyPackageKillEnabledForUserSwitch(TEST_USER_ID, TEST_USER_ID1));
+ }
+
+
/**
* Test that, in getRunningUsersLU, parents come after their profile, even if the profile was
* started afterwards.
diff --git a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
index 473d1dc..1074f7b 100644
--- a/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
+++ b/services/tests/servicestests/src/com/android/server/hdmi/HdmiCecMessageValidatorTest.java
@@ -240,6 +240,9 @@
public void isValid_setAnalogueTimer_clearAnalogueTimer() {
assertMessageValidity("04:33:0C:08:10:1E:04:30:08:00:13:AD:06").isEqualTo(OK);
assertMessageValidity("04:34:04:0C:16:0F:08:37:00:02:EA:60:03:34").isEqualTo(OK);
+ // Allow [Recording Sequence] set multiple days of the week.
+ // e.g. Monday (0x02) | Tuesday (0x04) -> Monday or Tuesday (0x06)
+ assertMessageValidity("04:34:04:0C:16:0F:08:37:06:02:EA:60:03:34").isEqualTo(OK);
assertMessageValidity("0F:33:0C:08:10:1E:04:30:08:00:13:AD:06")
.isEqualTo(ERROR_DESTINATION);
@@ -308,7 +311,7 @@
// Invalid Recording Sequence
assertMessageValidity("04:99:12:06:0C:2D:5A:19:90:91:04:00:B1").isEqualTo(ERROR_PARAMETER);
// Invalid Recording Sequence
- assertMessageValidity("04:97:0C:08:15:05:04:1E:21:00:C4:C2:11:D8:75:30")
+ assertMessageValidity("04:97:0C:08:15:05:04:1E:A1:00:C4:C2:11:D8:75:30")
.isEqualTo(ERROR_PARAMETER);
// Invalid Digital Broadcast System
@@ -354,7 +357,7 @@
// Invalid Recording Sequence
assertMessageValidity("40:A2:14:09:12:28:4B:19:84:05:10:00").isEqualTo(ERROR_PARAMETER);
// Invalid Recording Sequence
- assertMessageValidity("40:A1:0C:08:15:05:04:1E:14:04:20").isEqualTo(ERROR_PARAMETER);
+ assertMessageValidity("40:A1:0C:08:15:05:04:1E:94:04:20").isEqualTo(ERROR_PARAMETER);
// Invalid external source specifier
assertMessageValidity("40:A2:14:09:12:28:4B:19:10:08:10:00").isEqualTo(ERROR_PARAMETER);
// Invalid External PLug
diff --git a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
index 3d68849..dddab65 100644
--- a/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
+++ b/services/tests/servicestests/src/com/android/server/net/NetworkPolicyManagerServiceTest.java
@@ -108,6 +108,7 @@
import static org.mockito.ArgumentMatchers.anyInt;
import static org.mockito.ArgumentMatchers.anyLong;
import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.ArgumentMatchers.argThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.ArgumentMatchers.isA;
import static org.mockito.Mockito.CALLS_REAL_METHODS;
@@ -165,6 +166,7 @@
import android.os.PowerManager;
import android.os.PowerManagerInternal;
import android.os.PowerSaveState;
+import android.os.Process;
import android.os.RemoteException;
import android.os.SimpleClock;
import android.os.SystemClock;
@@ -197,6 +199,7 @@
import androidx.test.filters.MediumTest;
import androidx.test.runner.AndroidJUnit4;
+import com.android.internal.util.ArrayUtils;
import com.android.internal.util.test.BroadcastInterceptingContext;
import com.android.internal.util.test.BroadcastInterceptingContext.FutureIntent;
import com.android.internal.util.test.FsUtil;
@@ -2310,6 +2313,70 @@
assertTrue(mService.isUidNetworkingBlocked(UID_A, false));
}
+ @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS)
+ public void testRulesNeverAppliedToCoreUids() throws Exception {
+ clearInvocations(mNetworkManager);
+
+ final int coreAppId = Process.FIRST_APPLICATION_UID - 102;
+ final int coreUid = UserHandle.getUid(USER_ID, coreAppId);
+
+ // Enable all restrictions and add this core uid to all allowlists.
+ mService.mDeviceIdleMode = true;
+ mService.mRestrictPower = true;
+ setRestrictBackground(true);
+ expectHasUseRestrictedNetworksPermission(coreUid, true);
+ enableRestrictedMode(true);
+ final NetworkPolicyManagerInternal internal = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ internal.setLowPowerStandbyActive(true);
+ internal.setLowPowerStandbyAllowlist(new int[]{coreUid});
+ internal.onTempPowerSaveWhitelistChange(coreAppId, true, REASON_OTHER, "testing");
+
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean()))
+ .thenReturn(new int[]{coreAppId});
+ mPowerAllowlistReceiver.onReceive(mServiceContext, null);
+
+ // A normal uid would undergo a rule change from denied to allowed on all chains, but we
+ // should not request any rule change for this core uid.
+ verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(coreUid), anyInt());
+ verify(mNetworkManager, never()).setFirewallUidRules(anyInt(),
+ argThat(ar -> ArrayUtils.contains(ar, coreUid)), any(int[].class));
+ }
+
+ @SuppressWarnings("GuardedBy") // For not holding mUidRulesFirstLock
+ @Test
+ @RequiresFlagsEnabled(Flags.FLAG_NEVER_APPLY_RULES_TO_CORE_UIDS)
+ public void testRulesNeverAppliedToUidsWithoutInternetPermission() throws Exception {
+ clearInvocations(mNetworkManager);
+
+ mService.mInternetPermissionMap.clear();
+ expectHasInternetPermission(UID_A, false);
+
+ // Enable all restrictions and add this uid to all allowlists.
+ mService.mDeviceIdleMode = true;
+ mService.mRestrictPower = true;
+ setRestrictBackground(true);
+ expectHasUseRestrictedNetworksPermission(UID_A, true);
+ enableRestrictedMode(true);
+ final NetworkPolicyManagerInternal internal = LocalServices.getService(
+ NetworkPolicyManagerInternal.class);
+ internal.setLowPowerStandbyActive(true);
+ internal.setLowPowerStandbyAllowlist(new int[]{UID_A});
+ internal.onTempPowerSaveWhitelistChange(APP_ID_A, true, REASON_OTHER, "testing");
+
+ when(mPowerExemptionManager.getAllowListedAppIds(anyBoolean()))
+ .thenReturn(new int[]{APP_ID_A});
+ mPowerAllowlistReceiver.onReceive(mServiceContext, null);
+
+ // A normal uid would undergo a rule change from denied to allowed on all chains, but we
+ // should not request any rule this uid without the INTERNET permission.
+ verify(mNetworkManager, never()).setFirewallUidRule(anyInt(), eq(UID_A), anyInt());
+ verify(mNetworkManager, never()).setFirewallUidRules(anyInt(),
+ argThat(ar -> ArrayUtils.contains(ar, UID_A)), any(int[].class));
+ }
+
private boolean isUidState(int uid, int procState, int procStateSeq, int capability) {
final NetworkPolicyManager.UidState uidState = mService.getUidStateForTest(uid);
if (uidState == null) {
diff --git a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
index d714db99..7912156 100644
--- a/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
+++ b/services/tests/servicestests/src/com/android/server/pm/UserManagerTest.java
@@ -121,6 +121,9 @@
// Making a copy of mUsersToRemove to avoid ConcurrentModificationException
mUsersToRemove.stream().toList().forEach(this::removeUser);
mUserRemovalWaiter.close();
+
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
}
private void removeExistingUsers() {
@@ -935,6 +938,35 @@
@MediumTest
@Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testSetUserAdminThrowsSecurityException() throws Exception {
+ UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0);
+ assertThat(targetUser.isAdmin()).isFalse();
+
+ try {
+ // 1. Target User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ targetUser.getUserHandle());
+ assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id));
+
+ // 2. Current User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ mContext.getUser());
+ assertThrows(SecurityException.class, () -> mUserManager.setUserAdmin(targetUser.id));
+
+ } finally {
+ // Ensure restriction is removed even if test fails
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
+ }
+ }
+
+ @MediumTest
+ @Test
public void testRevokeUserAdmin() throws Exception {
UserInfo userInfo = createUser("Admin", /*flags=*/ UserInfo.FLAG_ADMIN);
assertThat(userInfo.isAdmin()).isTrue();
@@ -959,6 +991,37 @@
@MediumTest
@Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testRevokeUserAdminThrowsSecurityException() throws Exception {
+ UserInfo targetUser = createUser("SecondaryUser", /*flags=*/ 0);
+ assertThat(targetUser.isAdmin()).isFalse();
+
+ try {
+ // 1. Target User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ targetUser.getUserHandle());
+ assertThrows(SecurityException.class, () -> mUserManager
+ .revokeUserAdmin(targetUser.id));
+
+ // 2. Current User Restriction
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true,
+ mContext.getUser());
+ assertThrows(SecurityException.class, () -> mUserManager
+ .revokeUserAdmin(targetUser.id));
+
+ } finally {
+ // Ensure restriction is removed even if test fails
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ targetUser.getUserHandle());
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, false,
+ mContext.getUser());
+ }
+ }
+
+ @MediumTest
+ @Test
public void testGetProfileParent() throws Exception {
assumeManagedUsersSupported();
int mainUserId = mUserManager.getMainUser().getIdentifier();
@@ -1184,6 +1247,23 @@
}
}
+ // Make sure createUser for ADMIN would fail if we have DISALLOW_GRANT_ADMIN.
+ @MediumTest
+ @Test
+ @RequiresFlagsEnabled(android.multiuser.Flags.FLAG_UNICORN_MODE_REFACTORING_FOR_HSUM_READ_ONLY)
+ public void testCreateAdminUser_disallowGrantAdmin() throws Exception {
+ final int creatorId = ActivityManager.getCurrentUser();
+ final UserHandle creatorHandle = asHandle(creatorId);
+ mUserManager.setUserRestriction(UserManager.DISALLOW_GRANT_ADMIN, true, creatorHandle);
+ try {
+ UserInfo createdInfo = createUser("SecondaryUser", /*flags=*/ UserInfo.FLAG_ADMIN);
+ assertThat(createdInfo).isNull();
+ } finally {
+ mUserManager.setUserRestriction(UserManager.DISALLOW_ADD_USER, false,
+ creatorHandle);
+ }
+ }
+
// Make sure createProfile would fail if we have DISALLOW_ADD_CLONE_PROFILE.
@MediumTest
@Test
diff --git a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
index fad10f7..5518082 100644
--- a/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
+++ b/services/tests/servicestests/src/com/android/server/policy/DeviceStateProviderImplTest.java
@@ -182,7 +182,7 @@
+ " <device-state>\n"
+ " <identifier>1</identifier>\n"
+ " <properties>\n"
- + " <property>PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n"
+ + " <property>com.android.server.policy.PROPERTY_POLICY_CANCEL_OVERRIDE_REQUESTS</property>\n"
+ " </properties>\n"
+ " <conditions/>\n"
+ " </device-state>\n"
@@ -338,11 +338,9 @@
+ " <identifier>4</identifier>\n"
+ " <name>THERMAL_TEST</name>\n"
+ " <properties>\n"
- + " <property>PROPERTY_EMULATED_ONLY</property>\n"
- + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL"
- + "</property>\n"
- + " <property>PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE"
- + "</property>\n"
+ + " <property>com.android.server.policy.PROPERTY_EMULATED_ONLY</property>\n"
+ + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_THERMAL_STATUS_CRITICAL</property>\n"
+ + " <property>com.android.server.policy.PROPERTY_POLICY_UNSUPPORTED_WHEN_POWER_SAVE_MODE</property>\n"
+ " </properties>\n"
+ " </device-state>\n"
+ "</device-state-config>\n";
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
index f6e1162..af7f703 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ConditionProvidersTest.java
@@ -16,7 +16,6 @@
package com.android.server.notification;
-import static android.service.notification.Condition.SOURCE_USER_ACTION;
import static android.service.notification.Condition.STATE_FALSE;
import static android.service.notification.Condition.STATE_TRUE;
@@ -31,13 +30,11 @@
import static org.mockito.Mockito.verifyNoMoreInteractions;
import static org.mockito.Mockito.when;
-import android.app.Flags;
import android.content.ComponentName;
import android.content.ServiceConnection;
import android.content.pm.IPackageManager;
import android.net.Uri;
import android.os.IInterface;
-import android.platform.test.annotations.EnableFlags;
import android.platform.test.flag.junit.SetFlagsRule;
import android.service.notification.Condition;
@@ -150,57 +147,6 @@
}
@Test
- @EnableFlags(Flags.FLAG_MODES_UI)
- public void notifyConditions_appCannotUndoUserEnablement() {
- ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
- mock(IInterface.class), new ComponentName("package", "cls"), 0, false,
- mock(ServiceConnection.class), 33, 100);
- // First, user enabled mode
- Condition[] userConditions = new Condition[] {
- new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION)
- };
- mProviders.notifyConditions("package", msi, userConditions);
- verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0]));
-
- // Second, app tries to disable it, but cannot
- Condition[] appConditions = new Condition[] {
- new Condition(Uri.parse("a"), "summary", STATE_FALSE)
- };
- mProviders.notifyConditions("package", msi, appConditions);
- verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0]));
- }
-
- @Test
- @EnableFlags(Flags.FLAG_MODES_UI)
- public void notifyConditions_appCanTakeoverUserEnablement() {
- ManagedServices.ManagedServiceInfo msi = mProviders.new ManagedServiceInfo(
- mock(IInterface.class), new ComponentName("package", "cls"), 0, false,
- mock(ServiceConnection.class), 33, 100);
- // First, user enabled mode
- Condition[] userConditions = new Condition[] {
- new Condition(Uri.parse("a"), "summary", STATE_TRUE, SOURCE_USER_ACTION)
- };
- mProviders.notifyConditions("package", msi, userConditions);
- verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(userConditions[0]));
-
- // Second, app now thinks the rule should be on due it its intelligence
- Condition[] appConditions = new Condition[] {
- new Condition(Uri.parse("a"), "summary", STATE_TRUE)
- };
- mProviders.notifyConditions("package", msi, appConditions);
- verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0]));
-
- // Lastly, app can turn rule off when its intelligence think it should be off
- appConditions = new Condition[] {
- new Condition(Uri.parse("a"), "summary", STATE_FALSE)
- };
- mProviders.notifyConditions("package", msi, appConditions);
- verify(mCallback).onConditionChanged(eq(Uri.parse("a")), eq(appConditions[0]));
-
- verifyNoMoreInteractions(mCallback);
- }
-
- @Test
public void testRemoveDefaultFromConfig() {
final int userId = 0;
ComponentName oldDefaultComponent = ComponentName.unflattenFromString("package/Component1");
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
index 14ad15e..643ee4a 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationAttentionHelperTest.java
@@ -2458,6 +2458,74 @@
}
@Test
+ public void testBeepVolume_politeNotif_AvalancheStrategy_mixedNotif() throws Exception {
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
+ mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS_ATTN_UPDATE);
+ TestableFlagResolver flagResolver = new TestableFlagResolver();
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME1, 50);
+ flagResolver.setFlagOverride(NotificationFlags.NOTIF_VOLUME2, 0);
+ initAttentionHelper(flagResolver);
+
+ // Trigger avalanche trigger intent
+ final Intent intent = new Intent(Intent.ACTION_AIRPLANE_MODE_CHANGED);
+ intent.putExtra("state", false);
+ mAvalancheBroadcastReceiver.onReceive(getContext(), intent);
+
+ // Regular notification: should beep at 0% volume
+ NotificationRecord r = getBeepyNotification();
+ mAttentionHelper.buzzBeepBlinkLocked(r, DEFAULT_SIGNALS);
+ verifyBeepVolume(0.0f);
+ assertEquals(-1, r.getLastAudiblyAlertedMs());
+ Mockito.reset(mRingtonePlayer);
+
+ // Conversation notification
+ mChannel = new NotificationChannel("test2", "test2", IMPORTANCE_DEFAULT);
+ NotificationRecord r2 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // Should beep at 100% volume
+ mAttentionHelper.buzzBeepBlinkLocked(r2, DEFAULT_SIGNALS);
+ assertNotEquals(-1, r2.getLastAudiblyAlertedMs());
+ verifyBeepVolume(1.0f);
+
+ // Conversation notification on a different channel
+ mChannel = new NotificationChannel("test3", "test3", IMPORTANCE_DEFAULT);
+ NotificationRecord r3 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // Should beep at 50% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+ verifyBeepVolume(0.5f);
+
+ // 2nd update should beep at 0% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ verifyBeepVolume(0.0f);
+
+ // Set important conversation
+ mChannel.setImportantConversation(true);
+ r3 = getConversationNotificationRecord(mId, false /* insistent */,
+ false /* once */, true /* noisy */, false /* buzzy*/, false /* lights */, true,
+ true, false, null, Notification.GROUP_ALERT_ALL, false, mUser, mPkg,
+ "shortcut");
+
+ // important conversation should beep at 100% volume
+ Mockito.reset(mRingtonePlayer);
+ mAttentionHelper.buzzBeepBlinkLocked(r3, DEFAULT_SIGNALS);
+ verifyBeepVolume(1.0f);
+
+ verify(mAccessibilityService, times(5)).sendAccessibilityEvent(any(), anyInt());
+ assertNotEquals(-1, r3.getLastAudiblyAlertedMs());
+ }
+
+ @Test
public void testBeepVolume_politeNotif_Avalanche_exemptEmergency() throws Exception {
mSetFlagsRule.enableFlags(Flags.FLAG_POLITE_NOTIFICATIONS);
mSetFlagsRule.enableFlags(Flags.FLAG_CROSS_APP_POLITE_NOTIFICATIONS);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
index 4bb8017..5a8de58 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/NotificationManagerServiceTest.java
@@ -13064,6 +13064,100 @@
@Test
@DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testCancelAutogroupSummary_cancelsAllChildren() throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+ final int summaryId = Integer.MAX_VALUE;
+ // Add 2 group notifications without a summary
+ NotificationRecord nr0 =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+ NotificationRecord nr1 =
+ generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false);
+ mService.addNotification(nr0);
+ mService.addNotification(nr1);
+ mService.mSummaryByGroupKey.remove(nr0.getGroupKey());
+
+ // GroupHelper is a mock, so make the calls it would make
+ // Add aggregate group summary
+ NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS,
+ mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN,
+ nr0.getChannel().getId());
+ NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(),
+ nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr);
+ mService.addNotification(aggregateSummary);
+ nr0.setOverrideGroupKey(aggregateGroupName);
+ nr1.setOverrideGroupKey(aggregateGroupName);
+ final String fullAggregateGroupKey = nr0.getGroupKey();
+
+ // Check that the aggregate group summary was created
+ assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName);
+ assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo(
+ nr0.getChannel().getId());
+ assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue();
+
+ // Cancel aggregate group summary
+ mBinderService.cancelNotificationWithTag(mPkg, mPkg, aggregateSummary.getSbn().getTag(),
+ aggregateSummary.getSbn().getId(), aggregateSummary.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that child notifications are also removed
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary));
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0));
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1));
+
+ // Make sure the summary was removed and not re-posted
+ assertThat(mService.getNotificationRecordCount()).isEqualTo(0);
+ }
+
+ @Test
+ @EnableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
+ public void testCancelAutogroupSummary_forceGrouping_cancelsAllChildren() throws Exception {
+ final String originalGroupName = "originalGroup";
+ final String aggregateGroupName = "Aggregate_Test";
+ final int summaryId = Integer.MAX_VALUE;
+ // Add 2 group notifications without a summary
+ NotificationRecord nr0 =
+ generateNotificationRecord(mTestNotificationChannel, 0, originalGroupName, false);
+ NotificationRecord nr1 =
+ generateNotificationRecord(mTestNotificationChannel, 1, originalGroupName, false);
+ mService.addNotification(nr0);
+ mService.addNotification(nr1);
+ mService.mSummaryByGroupKey.remove(nr0.getGroupKey());
+
+ // GroupHelper is a mock, so make the calls it would make
+ // Add aggregate group summary
+ NotificationAttributes attr = new NotificationAttributes(GroupHelper.BASE_FLAGS,
+ mock(Icon.class), 0, VISIBILITY_PRIVATE, GROUP_ALERT_CHILDREN,
+ nr0.getChannel().getId());
+ NotificationRecord aggregateSummary = mService.createAutoGroupSummary(nr0.getUserId(),
+ nr0.getSbn().getPackageName(), nr0.getKey(), aggregateGroupName, summaryId, attr);
+ mService.addNotification(aggregateSummary);
+ nr0.setOverrideGroupKey(aggregateGroupName);
+ nr1.setOverrideGroupKey(aggregateGroupName);
+ final String fullAggregateGroupKey = nr0.getGroupKey();
+
+ // Check that the aggregate group summary was created
+ assertThat(aggregateSummary.getNotification().getGroup()).isEqualTo(aggregateGroupName);
+ assertThat(aggregateSummary.getNotification().getChannelId()).isEqualTo(
+ nr0.getChannel().getId());
+ assertThat(mService.mSummaryByGroupKey.containsKey(fullAggregateGroupKey)).isTrue();
+
+ // Cancel aggregate group summary
+ mBinderService.cancelNotificationWithTag(mPkg, mPkg, aggregateSummary.getSbn().getTag(),
+ aggregateSummary.getSbn().getId(), aggregateSummary.getSbn().getUserId());
+ waitForIdle();
+
+ // Check that child notifications are also removed
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(aggregateSummary), any());
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr0), any());
+ verify(mGroupHelper, times(1)).onNotificationRemoved(eq(nr1), any());
+
+ // Make sure the summary was removed and not re-posted
+ assertThat(mService.getNotificationRecordCount()).isEqualTo(0);
+ }
+
+ @Test
+ @DisableFlags(FLAG_NOTIFICATION_FORCE_GROUPING)
public void testUngroupingOngoingAutoSummary() throws Exception {
NotificationRecord nr0 =
generateNotificationRecord(mTestNotificationChannel, 0);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
index 60c4ac7..e70ed5f 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeConfigTest.java
@@ -33,6 +33,8 @@
import static android.service.notification.NotificationListenerService.SUPPRESSED_EFFECT_SCREEN_ON;
import static android.service.notification.ZenModeConfig.XML_VERSION_MODES_API;
import static android.service.notification.ZenModeConfig.ZEN_TAG;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_IMPORTANT;
import static android.service.notification.ZenPolicy.CONVERSATION_SENDERS_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_ANYONE;
@@ -524,7 +526,7 @@
rule.zenMode = INTERRUPTION_FILTER;
rule.modified = true;
rule.name = NAME;
- rule.snoozing = true;
+ rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
rule.zenPolicy = POLICY;
@@ -546,7 +548,7 @@
ZenModeConfig.ZenRule parceled = new ZenModeConfig.ZenRule(parcel);
assertEquals(rule.pkg, parceled.pkg);
- assertEquals(rule.snoozing, parceled.snoozing);
+ assertEquals(rule.getConditionOverride(), parceled.getConditionOverride());
assertEquals(rule.enabler, parceled.enabler);
assertEquals(rule.component, parceled.component);
assertEquals(rule.configurationActivity, parceled.configurationActivity);
@@ -625,7 +627,7 @@
rule.zenMode = INTERRUPTION_FILTER;
rule.modified = true;
rule.name = NAME;
- rule.snoozing = true;
+ rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
rule.zenPolicy = POLICY;
rule.zenDeviceEffects = new ZenDeviceEffects.Builder()
@@ -662,7 +664,7 @@
assertEquals(rule.pkg, fromXml.pkg);
// always resets on reboot
- assertFalse(fromXml.snoozing);
+ assertEquals(OVERRIDE_NONE, fromXml.getConditionOverride());
//should all match original
assertEquals(rule.component, fromXml.component);
assertEquals(rule.configurationActivity, fromXml.configurationActivity);
@@ -1115,7 +1117,6 @@
rule.zenMode = ZEN_MODE_IMPORTANT_INTERRUPTIONS;
rule.modified = true;
rule.name = "name";
- rule.snoozing = false;
rule.pkg = "b";
config.automaticRules.put("key", rule);
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
index 9af0021..91eb2ed 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeDiffTest.java
@@ -158,7 +158,10 @@
RuleDiff.FIELD_ZEN_DEVICE_EFFECTS,
RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS));
}
- if (!(Flags.modesApi() && Flags.modesUi())) {
+ if (Flags.modesApi() && Flags.modesUi()) {
+ exemptFields.add(RuleDiff.FIELD_SNOOZING); // Obsolete.
+ } else {
+ exemptFields.add(RuleDiff.FIELD_CONDITION_OVERRIDE);
exemptFields.add(RuleDiff.FIELD_LEGACY_SUPPRESSED_EFFECTS);
}
return exemptFields;
@@ -339,7 +342,7 @@
rule.zenMode = Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
rule.modified = false;
rule.name = "name";
- rule.snoozing = true;
+ rule.setConditionOverride(ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE);
rule.pkg = "a";
if (android.app.Flags.modesApi()) {
rule.allowManualInvocation = true;
diff --git a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
index 776a840..212e61e 100644
--- a/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
+++ b/services/tests/uiservicestests/src/com/android/server/notification/ZenModeHelperTest.java
@@ -51,6 +51,7 @@
import static android.provider.Settings.Global.ZEN_MODE_IMPORTANT_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_NO_INTERRUPTIONS;
import static android.provider.Settings.Global.ZEN_MODE_OFF;
+import static android.service.notification.Condition.SOURCE_CONTEXT;
import static android.service.notification.Condition.SOURCE_SCHEDULE;
import static android.service.notification.Condition.SOURCE_USER_ACTION;
import static android.service.notification.Condition.STATE_FALSE;
@@ -61,6 +62,9 @@
import static android.service.notification.ZenModeConfig.ORIGIN_UNKNOWN;
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_APP;
import static android.service.notification.ZenModeConfig.ORIGIN_USER_IN_SYSTEMUI;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_ACTIVATE;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_DEACTIVATE;
+import static android.service.notification.ZenModeConfig.ZenRule.OVERRIDE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_CONTACTS;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_NONE;
import static android.service.notification.ZenPolicy.PEOPLE_TYPE_STARRED;
@@ -2999,11 +3003,13 @@
assertWithMessage("Failure for origin " + origin.name())
.that(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
assertWithMessage("Failure for origin " + origin.name())
- .that(mZenModeHelper.mConfig.automaticRules.get(activeRuleId).snoozing)
- .isTrue();
+ .that(mZenModeHelper.mConfig.automaticRules
+ .get(activeRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_DEACTIVATE);
assertWithMessage("Failure for origin " + origin.name())
- .that(mZenModeHelper.mConfig.automaticRules.get(inactiveRuleId).snoozing)
- .isFalse();
+ .that(mZenModeHelper.mConfig.automaticRules
+ .get(inactiveRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
}
}
@@ -3038,16 +3044,20 @@
assertWithMessage("Failure for origin " + origin.name()).that(
mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_IMPORTANT_INTERRUPTIONS);
assertWithMessage("Failure for origin " + origin.name()).that(
- config.automaticRules.get(activeRuleId).snoozing).isFalse();
+ config.automaticRules.get(activeRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
assertWithMessage("Failure for origin " + origin.name()).that(
- config.automaticRules.get(inactiveRuleId).snoozing).isFalse();
+ config.automaticRules.get(inactiveRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
} else {
assertWithMessage("Failure for origin " + origin.name()).that(
mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
assertWithMessage("Failure for origin " + origin.name()).that(
- config.automaticRules.get(activeRuleId).snoozing).isTrue();
+ config.automaticRules.get(activeRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_DEACTIVATE);
assertWithMessage("Failure for origin " + origin.name()).that(
- config.automaticRules.get(inactiveRuleId).snoozing).isFalse();
+ config.automaticRules.get(inactiveRuleId).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
}
}
}
@@ -4288,7 +4298,7 @@
rule.zenMode = INTERRUPTION_FILTER_ZR;
rule.modified = true;
rule.name = NAME;
- rule.snoozing = true;
+ rule.setConditionOverride(OVERRIDE_DEACTIVATE);
rule.pkg = OWNER.getPackageName();
rule.zenPolicy = POLICY;
@@ -5000,7 +5010,8 @@
mZenModeHelper.updateAutomaticZenRule(createdId, zenRule, ZenModeConfig.ORIGIN_SYSTEM,
"", SYSTEM_UID);
- assertEquals(false, mZenModeHelper.mConfig.automaticRules.get(createdId).snoozing);
+ assertEquals(OVERRIDE_NONE,
+ mZenModeHelper.mConfig.automaticRules.get(createdId).getConditionOverride());
}
@Test
@@ -5713,7 +5724,7 @@
assertThat(storedRule.isAutomaticActive()).isFalse();
assertThat(storedRule.isTrueOrUnknown()).isFalse();
assertThat(storedRule.condition).isNull();
- assertThat(storedRule.snoozing).isFalse();
+ assertThat(storedRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
assertThat(mZenModeHelper.getZenMode()).isEqualTo(ZEN_MODE_OFF);
}
@@ -6039,15 +6050,18 @@
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
ZEN_MODE_IMPORTANT_INTERRUPTIONS);
assertThat(mZenModeHelper.mConfig.automaticRules).hasSize(1);
- assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+ assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
mZenModeHelper.setManualZenMode(ZEN_MODE_OFF, null, ORIGIN_APP, "test", "test", 0);
- assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isTrue();
+ assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride())
+ .isEqualTo(OVERRIDE_DEACTIVATE);
mZenModeHelper.applyGlobalZenModeAsImplicitZenRule(CUSTOM_PKG_NAME, CUSTOM_PKG_UID,
ZEN_MODE_ALARMS);
- assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).snoozing).isFalse();
+ assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).getConditionOverride())
+ .isEqualTo(OVERRIDE_NONE);
assertThat(mZenModeHelper.mConfig.automaticRules.valueAt(0).condition.state)
.isEqualTo(STATE_TRUE);
}
@@ -6451,6 +6465,160 @@
ORIGIN_UNKNOWN);
}
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualActivation_appliesOverride() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+
+ ZenRule zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+ assertThat(zenRule.condition).isNull();
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualActivationAndThenDeactivation_removesOverride() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+ ZenRule zenRule;
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+ assertThat(zenRule.condition).isNull();
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isFalse();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ assertThat(zenRule.condition).isNull();
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_manualDeactivation_appliesOverride() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT),
+ ORIGIN_APP, SYSTEM_UID);
+ ZenRule zenRuleOn = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRuleOn.isAutomaticActive()).isTrue();
+ assertThat(zenRuleOn.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ assertThat(zenRuleOn.condition).isNotNull();
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ ZenRule zenRuleOff = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRuleOff.isAutomaticActive()).isFalse();
+ assertThat(zenRuleOff.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
+ assertThat(zenRuleOff.condition).isNotNull();
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_ifManualActive_appCannotDeactivateBeforeActivating() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+ ZenRule zenRule;
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-on", STATE_TRUE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_ACTIVATE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isFalse();
+ }
+
+ @Test
+ @EnableFlags({FLAG_MODES_API, FLAG_MODES_UI})
+ public void setAutomaticZenRuleState_ifManualInactive_appCannotReactivateBeforeDeactivating() {
+ AutomaticZenRule rule = new AutomaticZenRule.Builder("Rule", Uri.parse("cond"))
+ .setPackage(mPkg)
+ .build();
+ String ruleId = mZenModeHelper.addAutomaticZenRule(mPkg, rule, ORIGIN_APP, "adding",
+ CUSTOM_PKG_UID);
+ ZenRule zenRule;
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "manual-off", STATE_FALSE, SOURCE_USER_ACTION),
+ ORIGIN_USER_IN_SYSTEMUI, SYSTEM_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isFalse();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isFalse();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_DEACTIVATE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-off", STATE_FALSE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isFalse();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+
+ mZenModeHelper.setAutomaticZenRuleState(ruleId,
+ new Condition(rule.getConditionId(), "auto-on", STATE_TRUE, SOURCE_CONTEXT),
+ ORIGIN_APP, CUSTOM_PKG_UID);
+ zenRule = mZenModeHelper.mConfig.automaticRules.get(ruleId);
+ assertThat(zenRule.isAutomaticActive()).isTrue();
+ assertThat(zenRule.getConditionOverride()).isEqualTo(OVERRIDE_NONE);
+ }
+
private static void addZenRule(ZenModeConfig config, String id, String ownerPkg, int zenMode,
@Nullable ZenPolicy zenPolicy) {
ZenRule rule = new ZenRule();
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
index 59d5577..1493253 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/DeviceAdapterTest.java
@@ -21,6 +21,7 @@
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.hardware.vibrator.IVibrator;
import android.os.CombinedVibration;
@@ -32,11 +33,12 @@
import android.os.vibrator.PrimitiveSegment;
import android.os.vibrator.RampSegment;
import android.os.vibrator.StepSegment;
+import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.util.SparseArray;
-import androidx.test.InstrumentationRegistry;
+import androidx.test.core.app.ApplicationProvider;
import com.android.server.LocalServices;
@@ -76,9 +78,10 @@
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
+ Context context = ApplicationProvider.getApplicationContext();
mTestLooper = new TestLooper();
- mVibrationSettings = new VibrationSettings(
- InstrumentationRegistry.getContext(), new Handler(mTestLooper.getLooper()));
+ mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
+ new VibrationConfig(context.getResources()));
SparseArray<VibratorController> vibrators = new SparseArray<>();
vibrators.put(EMPTY_VIBRATOR_ID, createEmptyVibratorController(EMPTY_VIBRATOR_ID));
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
index 9ebeaa8..4704691 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationScalerTest.java
@@ -50,10 +50,9 @@
import android.os.vibrator.StepSegment;
import android.os.vibrator.VibrationConfig;
import android.os.vibrator.VibrationEffectSegment;
-import android.platform.test.annotations.RequiresFlagsDisabled;
-import android.platform.test.annotations.RequiresFlagsEnabled;
-import android.platform.test.flag.junit.CheckFlagsRule;
-import android.platform.test.flag.junit.DeviceFlagsValueProvider;
+import android.platform.test.annotations.DisableFlags;
+import android.platform.test.annotations.EnableFlags;
+import android.platform.test.flag.junit.SetFlagsRule;
import android.provider.Settings;
import androidx.test.InstrumentationRegistry;
@@ -71,12 +70,13 @@
import org.mockito.junit.MockitoRule;
public class VibrationScalerTest {
+ private static final float TOLERANCE = 1e-2f;
+ private static final int TEST_DEFAULT_AMPLITUDE = 255;
+ private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule public FakeSettingsProviderRule mSettingsProviderRule = FakeSettingsProvider.rule();
- @Rule
- public final CheckFlagsRule mCheckFlagsRule =
- DeviceFlagsValueProvider.createCheckFlagsRule();
+ @Rule public final SetFlagsRule mSetFlagsRule = new SetFlagsRule();
@Mock private PowerManagerInternal mPowerManagerInternalMock;
@Mock private PackageManagerInternal mPackageManagerInternalMock;
@@ -96,6 +96,10 @@
when(mContextSpy.getContentResolver()).thenReturn(contentResolver);
when(mPackageManagerInternalMock.getSystemUiServiceComponent())
.thenReturn(new ComponentName("", ""));
+ when(mVibrationConfigMock.getDefaultVibrationAmplitude())
+ .thenReturn(TEST_DEFAULT_AMPLITUDE);
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain())
+ .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN);
LocalServices.removeServiceForTest(PackageManagerInternal.class);
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
@@ -107,7 +111,7 @@
mVibrationSettings = new VibrationSettings(
mContextSpy, new Handler(mTestLooper.getLooper()), mVibrationConfigMock);
- mVibrationScaler = new VibrationScaler(mContextSpy, mVibrationSettings);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
mVibrationSettings.onSystemReady();
}
@@ -147,33 +151,76 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
- public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() {
- setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
- setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
- setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ @DisableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testGetScaleFactor_withLegacyScaling() {
+ // Default scale gain will be ignored.
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(1.2f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW);
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(0.8f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+ assertEquals(0.6f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+ // Vibration setting being bypassed will use default setting and not scale.
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_HAPTICS_SCALE_V2_ENABLED)
+ public void testGetScaleFactor_withScalingV2() {
+ // Test scale factors for a default gain of 1.4
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain()).thenReturn(1.4f);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
+
+ setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
+ assertEquals(1.95f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(1.4f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // HIGH
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_LOW);
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_MEDIUM);
+ assertEquals(0.71f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // LOW
+
+ setDefaultIntensity(USAGE_TOUCH, VIBRATION_INTENSITY_HIGH);
+ assertEquals(0.51f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // VERY_LOW
+
+ setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
+ // Vibration setting being bypassed will use default setting and not scale.
+ assertEquals(1f, mVibrationScaler.getScaleFactor(USAGE_TOUCH), TOLERANCE); // NONE
+ }
+
+ @Test
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ public void testAdaptiveHapticsScale_withAdaptiveHapticsAvailable() {
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
assertEquals(0.5f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_TOUCH));
assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
assertEquals(1f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_NOTIFICATION));
-
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
- // Vibration setting being bypassed will apply adaptive haptics scales.
assertEquals(0.2f, mVibrationScaler.getAdaptiveHapticsScale(USAGE_RINGTONE));
}
@Test
- @RequiresFlagsDisabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @DisableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void testAdaptiveHapticsScale_flagDisabled_adaptiveHapticScaleAlwaysNone() {
- setDefaultIntensity(USAGE_TOUCH, Vibrator.VIBRATION_INTENSITY_LOW);
- setDefaultIntensity(USAGE_RINGTONE, Vibrator.VIBRATION_INTENSITY_LOW);
- setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_HIGH);
- setUserSetting(Settings.System.RING_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
-
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_TOUCH, 0.5f);
mVibrationScaler.updateAdaptiveHapticsScale(USAGE_RINGTONE, 0.2f);
@@ -233,7 +280,7 @@
}
@Test
- @RequiresFlagsEnabled(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
+ @EnableFlags(android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS)
public void scale_withVendorEffect_setsEffectStrengthBasedOnSettings() {
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_LOW);
setUserSetting(Settings.System.NOTIFICATION_VIBRATION_INTENSITY, VIBRATION_INTENSITY_HIGH);
@@ -269,13 +316,13 @@
StepSegment resolved = getFirstSegment(mVibrationScaler.scale(
VibrationEffect.createOneShot(10, VibrationEffect.DEFAULT_AMPLITUDE),
USAGE_RINGTONE));
- assertTrue(resolved.getAmplitude() > 0);
+ assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE);
resolved = getFirstSegment(mVibrationScaler.scale(
VibrationEffect.createWaveform(new long[]{10},
new int[]{VibrationEffect.DEFAULT_AMPLITUDE}, -1),
USAGE_RINGTONE));
- assertTrue(resolved.getAmplitude() > 0);
+ assertEquals(TEST_DEFAULT_AMPLITUDE / 255f, resolved.getAmplitude(), TOLERANCE);
}
@Test
@@ -330,7 +377,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_withAdaptiveHaptics_scalesVibrationsCorrectly() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -351,7 +398,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_clearAdaptiveHapticsScales_clearsAllCachedScales() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -373,7 +420,7 @@
}
@Test
- @RequiresFlagsEnabled(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
+ @EnableFlags(Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED)
public void scale_removeAdaptiveHapticsScale_removesCachedScale() {
setDefaultIntensity(USAGE_RINGTONE, VIBRATION_INTENSITY_HIGH);
setDefaultIntensity(USAGE_NOTIFICATION, VIBRATION_INTENSITY_HIGH);
@@ -395,7 +442,7 @@
}
@Test
- @RequiresFlagsEnabled({
+ @EnableFlags({
android.os.vibrator.Flags.FLAG_ADAPTIVE_HAPTICS_ENABLED,
android.os.vibrator.Flags.FLAG_VENDOR_VIBRATION_EFFECTS,
})
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
index 72ef888..6f06050 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationSettingsTest.java
@@ -119,6 +119,7 @@
USAGE_PHYSICAL_EMULATION,
USAGE_RINGTONE,
USAGE_TOUCH,
+ USAGE_IME_FEEDBACK
};
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@@ -525,7 +526,7 @@
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
for (int usage : ALL_USAGES) {
- if (usage == USAGE_TOUCH) {
+ if (usage == USAGE_TOUCH || usage == USAGE_IME_FEEDBACK) {
assertVibrationIgnoredForUsage(usage, Vibration.Status.IGNORED_FOR_SETTINGS);
} else {
assertVibrationNotIgnoredForUsage(usage);
@@ -601,14 +602,14 @@
@Test
public void shouldIgnoreVibration_withKeyboardSettingsOff_shouldIgnoreKeyboardVibration() {
+ setKeyboardVibrationSettingsSupported(true);
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_MEDIUM);
setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 0 /* OFF*/);
- setKeyboardVibrationSettingsSupported(true);
// Keyboard touch ignored.
assertVibrationIgnoredForAttributes(
new VibrationAttributes.Builder()
- .setUsage(USAGE_TOUCH)
+ .setUsage(USAGE_IME_FEEDBACK)
.setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
.build(),
Vibration.Status.IGNORED_FOR_SETTINGS);
@@ -617,7 +618,7 @@
assertVibrationNotIgnoredForUsage(USAGE_TOUCH);
assertVibrationNotIgnoredForAttributes(
new VibrationAttributes.Builder()
- .setUsage(USAGE_TOUCH)
+ .setUsage(USAGE_IME_FEEDBACK)
.setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
.setFlags(VibrationAttributes.FLAG_BYPASS_USER_VIBRATION_INTENSITY_OFF)
.build());
@@ -625,9 +626,9 @@
@Test
public void shouldIgnoreVibration_withKeyboardSettingsOn_shouldNotIgnoreKeyboardVibration() {
+ setKeyboardVibrationSettingsSupported(true);
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */);
- setKeyboardVibrationSettingsSupported(true);
// General touch ignored.
assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS);
@@ -635,16 +636,16 @@
// Keyboard touch not ignored.
assertVibrationNotIgnoredForAttributes(
new VibrationAttributes.Builder()
- .setUsage(USAGE_TOUCH)
+ .setUsage(USAGE_IME_FEEDBACK)
.setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
.build());
}
@Test
- public void shouldIgnoreVibration_notSupportKeyboardVibration_ignoresKeyboardTouchVibration() {
+ public void shouldIgnoreVibration_notSupportKeyboardVibration_followsTouchFeedbackSettings() {
+ setKeyboardVibrationSettingsSupported(false);
setUserSetting(Settings.System.HAPTIC_FEEDBACK_INTENSITY, VIBRATION_INTENSITY_OFF);
setUserSetting(Settings.System.KEYBOARD_VIBRATION_ENABLED, 1 /* ON */);
- setKeyboardVibrationSettingsSupported(false);
// General touch ignored.
assertVibrationIgnoredForUsage(USAGE_TOUCH, Vibration.Status.IGNORED_FOR_SETTINGS);
@@ -652,7 +653,7 @@
// Keyboard touch ignored.
assertVibrationIgnoredForAttributes(
new VibrationAttributes.Builder()
- .setUsage(USAGE_TOUCH)
+ .setUsage(USAGE_IME_FEEDBACK)
.setCategory(VibrationAttributes.CATEGORY_KEYBOARD)
.build(),
Vibration.Status.IGNORED_FOR_SETTINGS);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
index 3bd56de..0fbdce4 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibrationThreadTest.java
@@ -102,6 +102,8 @@
private static final String PACKAGE_NAME = "package";
private static final VibrationAttributes ATTRS = new VibrationAttributes.Builder().build();
private static final int TEST_RAMP_STEP_DURATION = 5;
+ private static final int TEST_DEFAULT_AMPLITUDE = 255;
+ private static final float TEST_DEFAULT_SCALE_LEVEL_GAIN = 1.4f;
@Rule public MockitoRule mMockitoRule = MockitoJUnit.rule();
@Rule
@@ -133,6 +135,10 @@
when(mVibrationConfigMock.getDefaultVibrationIntensity(anyInt()))
.thenReturn(Vibrator.VIBRATION_INTENSITY_MEDIUM);
when(mVibrationConfigMock.getRampStepDurationMs()).thenReturn(TEST_RAMP_STEP_DURATION);
+ when(mVibrationConfigMock.getDefaultVibrationAmplitude())
+ .thenReturn(TEST_DEFAULT_AMPLITUDE);
+ when(mVibrationConfigMock.getDefaultVibrationScaleLevelGain())
+ .thenReturn(TEST_DEFAULT_SCALE_LEVEL_GAIN);
when(mPackageManagerInternalMock.getSystemUiServiceComponent())
.thenReturn(new ComponentName("", ""));
doAnswer(answer -> {
@@ -146,7 +152,7 @@
Context context = InstrumentationRegistry.getContext();
mVibrationSettings = new VibrationSettings(context, new Handler(mTestLooper.getLooper()),
mVibrationConfigMock);
- mVibrationScaler = new VibrationScaler(context, mVibrationSettings);
+ mVibrationScaler = new VibrationScaler(mVibrationConfigMock, mVibrationSettings);
mockVibrators(VIBRATOR_ID);
diff --git a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
index c496bbb..79e272b 100644
--- a/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
+++ b/services/tests/vibrator/src/com/android/server/vibrator/VibratorControlServiceTest.java
@@ -37,6 +37,7 @@
import static org.mockito.Mockito.when;
import android.content.ComponentName;
+import android.content.Context;
import android.content.pm.PackageManagerInternal;
import android.frameworks.vibrator.ScaleParam;
import android.frameworks.vibrator.VibrationParam;
@@ -46,6 +47,7 @@
import android.os.Process;
import android.os.test.TestLooper;
import android.os.vibrator.Flags;
+import android.os.vibrator.VibrationConfig;
import android.platform.test.annotations.RequiresFlagsDisabled;
import android.platform.test.annotations.RequiresFlagsEnabled;
import android.platform.test.flag.junit.CheckFlagsRule;
@@ -97,8 +99,9 @@
LocalServices.addService(PackageManagerInternal.class, mPackageManagerInternalMock);
TestLooper testLooper = new TestLooper();
- mVibrationSettings = new VibrationSettings(
- ApplicationProvider.getApplicationContext(), new Handler(testLooper.getLooper()));
+ Context context = ApplicationProvider.getApplicationContext();
+ mVibrationSettings = new VibrationSettings(context, new Handler(testLooper.getLooper()),
+ new VibrationConfig(context.getResources()));
mFakeVibratorController = new FakeVibratorController(mTestLooper.getLooper());
mVibratorControlService = new VibratorControlService(
diff --git a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
index e2524a2..ddadbc4 100644
--- a/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/DisplayWindowSettingsTests.java
@@ -115,6 +115,17 @@
}
@Test
+ public void testPrimaryDisplayUnchanged_whenWindowingModeAlreadySet_NoFreeformSupport() {
+ mPrimaryDisplay.getDefaultTaskDisplayArea().setWindowingMode(
+ WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW);
+
+ mDisplayWindowSettings.applySettingsToDisplayLocked(mPrimaryDisplay);
+
+ assertEquals(WindowConfiguration.WINDOWING_MODE_MULTI_WINDOW,
+ mPrimaryDisplay.getDefaultTaskDisplayArea().getWindowingMode());
+ }
+
+ @Test
public void testPrimaryDisplayDefaultToFullscreen_HasFreeformSupport_NonPc_NoDesktopMode() {
mWm.mAtmService.mSupportsFreeformWindowManagement = true;
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
index ffaa2d8..400fe8b 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxTest.java
@@ -24,6 +24,7 @@
import static org.mockito.ArgumentMatchers.anyString;
import static org.mockito.Mockito.clearInvocations;
import static org.mockito.Mockito.doAnswer;
+import static org.mockito.Mockito.doReturn;
import static org.mockito.Mockito.mock;
import static org.mockito.Mockito.spy;
import static org.mockito.Mockito.verify;
@@ -45,6 +46,12 @@
import java.util.function.Supplier;
+/**
+ * Test class for {@link Letterbox}.
+ * <p>
+ * Build/Install/Run:
+ * atest WmTests:LetterboxTest
+ */
@SmallTest
@Presubmit
public class LetterboxTest {
@@ -53,21 +60,21 @@
SurfaceControlMocker mSurfaces;
SurfaceControl.Transaction mTransaction;
- private boolean mAreCornersRounded = false;
- private int mColor = Color.BLACK;
- private boolean mHasWallpaperBackground = false;
- private int mBlurRadius = 0;
- private float mDarkScrimAlpha = 0.5f;
private SurfaceControl mParentSurface = mock(SurfaceControl.class);
+ private AppCompatLetterboxOverrides mLetterboxOverrides;
@Before
public void setUp() throws Exception {
mSurfaces = new SurfaceControlMocker();
+ mLetterboxOverrides = mock(AppCompatLetterboxOverrides.class);
+ doReturn(false).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners();
+ doReturn(Color.valueOf(Color.BLACK)).when(mLetterboxOverrides)
+ .getLetterboxBackgroundColor();
+ doReturn(false).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox();
+ doReturn(0).when(mLetterboxOverrides).getLetterboxWallpaperBlurRadiusPx();
+ doReturn(0.5f).when(mLetterboxOverrides).getLetterboxWallpaperDarkScrimAlpha();
mLetterbox = new Letterbox(mSurfaces, StubTransaction::new,
- () -> mAreCornersRounded, () -> Color.valueOf(mColor),
- () -> mHasWallpaperBackground, () -> mBlurRadius, () -> mDarkScrimAlpha,
- mock(AppCompatReachabilityPolicy.class),
- () -> mParentSurface);
+ mock(AppCompatReachabilityPolicy.class), mLetterboxOverrides, () -> mParentSurface);
mTransaction = spy(StubTransaction.class);
}
@@ -183,7 +190,8 @@
verify(mTransaction).setColor(mSurfaces.top, new float[]{0, 0, 0});
- mColor = Color.GREEN;
+ doReturn(Color.valueOf(Color.GREEN)).when(mLetterboxOverrides)
+ .getLetterboxBackgroundColor();
assertTrue(mLetterbox.needsApplySurfaceChanges());
@@ -200,12 +208,12 @@
verify(mTransaction).setAlpha(mSurfaces.top, 1.0f);
assertFalse(mLetterbox.needsApplySurfaceChanges());
- mHasWallpaperBackground = true;
+ doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox();
assertTrue(mLetterbox.needsApplySurfaceChanges());
applySurfaceChanges();
- verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, mDarkScrimAlpha);
+ verify(mTransaction).setAlpha(mSurfaces.fullWindowSurface, /* alpha */ 0.5f);
}
@Test
@@ -234,7 +242,7 @@
@Test
public void testApplySurfaceChanges_cornersRounded_surfaceFullWindowSurfaceCreated() {
- mAreCornersRounded = true;
+ doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners();
mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
applySurfaceChanges();
@@ -243,7 +251,7 @@
@Test
public void testApplySurfaceChanges_wallpaperBackground_surfaceFullWindowSurfaceCreated() {
- mHasWallpaperBackground = true;
+ doReturn(true).when(mLetterboxOverrides).hasWallpaperBackgroundForLetterbox();
mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(1000, 2000));
applySurfaceChanges();
@@ -252,7 +260,7 @@
@Test
public void testNotIntersectsOrFullyContains_cornersRounded() {
- mAreCornersRounded = true;
+ doReturn(true).when(mLetterboxOverrides).shouldLetterboxHaveRoundedCorners();
mLetterbox.layout(new Rect(0, 0, 10, 10), new Rect(0, 1, 10, 10), new Point(0, 0));
applySurfaceChanges();
diff --git a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
index 695068a..04997f8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
+++ b/services/tests/wmtests/src/com/android/server/wm/LetterboxUiControllerTest.java
@@ -123,7 +123,7 @@
// Do not apply crop if taskbar is collapsed
taskbar.setFrame(TASKBAR_COLLAPSED_BOUNDS);
- assertNull(mController.getExpandedTaskbarOrNull(mainWindow));
+ assertNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, SCREEN_HEIGHT / 4,
SCREEN_WIDTH - SCREEN_WIDTH / 4, SCREEN_HEIGHT - SCREEN_HEIGHT / 4);
@@ -145,7 +145,7 @@
// Apply crop if taskbar is expanded
taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
- assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+ assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
mLetterboxedPortraitTaskBounds.set(SCREEN_WIDTH / 4, 0, SCREEN_WIDTH - SCREEN_WIDTH / 4,
SCREEN_HEIGHT);
@@ -169,7 +169,7 @@
// Apply crop if taskbar is expanded
taskbar.setFrame(TASKBAR_EXPANDED_BOUNDS);
- assertNotNull(mController.getExpandedTaskbarOrNull(mainWindow));
+ assertNotNull(AppCompatUtils.getExpandedTaskbarOrNull(mainWindow));
// With SizeCompat scaling
doReturn(true).when(mActivity).inSizeCompatMode();
mainWindow.mInvGlobalScale = scaling;
diff --git a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
index aa997ac..3148808 100644
--- a/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/SizeCompatTests.java
@@ -247,9 +247,9 @@
.build();
mTask.addChild(translucentActivity);
- spyOn(translucentActivity.mLetterboxUiController);
- doReturn(true).when(translucentActivity.mLetterboxUiController)
- .shouldShowLetterboxUi(any());
+ spyOn(translucentActivity.mAppCompatController.getAppCompatLetterboxPolicy());
+ doReturn(true).when(translucentActivity.mAppCompatController
+ .getAppCompatLetterboxPolicy()).shouldShowLetterboxUi(any());
addWindowToActivity(translucentActivity);
translucentActivity.mRootWindowContainer.performSurfacePlacement();
diff --git a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
index c53addc..6fd5faf 100644
--- a/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/TaskDisplayAreaTests.java
@@ -131,6 +131,7 @@
final Task adjacentRootTask = createTask(
mDisplayContent, WINDOWING_MODE_MULTI_WINDOW, ACTIVITY_TYPE_STANDARD);
adjacentRootTask.mCreatedByOrganizer = true;
+ createActivityRecord(adjacentRootTask);
final TaskDisplayArea taskDisplayArea = rootTask.getDisplayArea();
adjacentRootTask.setAdjacentTaskFragment(rootTask);
diff --git a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
index 2b611b7..18255b8 100644
--- a/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
+++ b/services/tests/wmtests/src/com/android/server/wm/WindowOrganizerTests.java
@@ -1488,7 +1488,7 @@
@Test
public void testReorderWithParents() {
/*
- root
+ default TDA
____|______
| |
firstTda secondTda
@@ -1496,10 +1496,12 @@
firstRootTask secondRootTask
*/
- final TaskDisplayArea firstTaskDisplayArea = mDisplayContent.getDefaultTaskDisplayArea();
- final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea(
- mDisplayContent, mRootWindowContainer.mWmService, "TestTaskDisplayArea",
+ final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea",
FEATURE_VENDOR_FIRST);
+ final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea",
+ FEATURE_VENDOR_FIRST + 1);
final Task firstRootTask = firstTaskDisplayArea.createRootTask(
WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
final Task secondRootTask = secondTaskDisplayArea.createRootTask(
@@ -1508,9 +1510,6 @@
.setTask(firstRootTask).build();
final ActivityRecord secondActivity = new ActivityBuilder(mAtm)
.setTask(secondRootTask).build();
- // This assertion is just a defense to ensure that firstRootTask is not the top most
- // by default
- assertThat(mDisplayContent.getTopRootTask()).isEqualTo(secondRootTask);
WindowContainerTransaction wct = new WindowContainerTransaction();
// Reorder to top
@@ -1533,6 +1532,67 @@
}
@Test
+ public void testReorderDisplayArea() {
+ /*
+ defaultTda
+ ____|______
+ | |
+ firstTda secondTda
+ | |
+ firstRootTask secondRootTask
+
+ */
+ final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea",
+ FEATURE_VENDOR_FIRST);
+ final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea",
+ FEATURE_VENDOR_FIRST + 1);
+ final Task firstRootTask = firstTaskDisplayArea.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
+ final Task secondRootTask = secondTaskDisplayArea.createRootTask(
+ WINDOWING_MODE_FULLSCREEN, ACTIVITY_TYPE_STANDARD, false /* onTop */);
+ final ActivityRecord firstActivity = new ActivityBuilder(mAtm)
+ .setTask(firstRootTask).build();
+ final ActivityRecord secondActivity = new ActivityBuilder(mAtm)
+ .setTask(secondRootTask).build();
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+
+ // Reorder to top
+ wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), true /* onTop */,
+ true /* includingParents */);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+ assertThat(mDisplayContent.getTopRootTask()).isEqualTo(firstRootTask);
+
+ // Reorder to bottom
+ wct.reorder(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(), false /* onTop */,
+ true /* includingParents */);
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct);
+ assertThat(mDisplayContent.getBottomMostTask()).isEqualTo(firstRootTask);
+ }
+
+ @Test
+ public void testReparentDisplayAreaUnsupported() {
+ final TaskDisplayArea firstTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "FirstTaskDisplayArea",
+ FEATURE_VENDOR_FIRST);
+ final TaskDisplayArea secondTaskDisplayArea = createTaskDisplayArea(
+ mDisplayContent, mRootWindowContainer.mWmService, "SecondTaskDisplayArea",
+ FEATURE_VENDOR_FIRST + 1);
+
+ WindowContainerTransaction wct = new WindowContainerTransaction();
+ wct.reparent(firstTaskDisplayArea.mRemoteToken.toWindowContainerToken(),
+ secondTaskDisplayArea.mRemoteToken.toWindowContainerToken(),
+ true /* onTop */
+ );
+
+ assertThrows(UnsupportedOperationException.class, () ->
+ mWm.mAtmService.mWindowOrganizerController.applyTransaction(wct)
+ );
+ }
+
+ @Test
public void testAppearDeferThenVanish() {
final ITaskOrganizer organizer = registerMockOrganizer();
final Task rootTask = createRootTask();
diff --git a/telephony/java/android/telephony/PcoData.java b/telephony/java/android/telephony/PcoData.java
index 39e4f2f..3cc32c6 100644
--- a/telephony/java/android/telephony/PcoData.java
+++ b/telephony/java/android/telephony/PcoData.java
@@ -19,6 +19,8 @@
import android.os.Parcel;
import android.os.Parcelable;
+import com.android.internal.telephony.uicc.IccUtils;
+
import java.util.Arrays;
import java.util.Objects;
@@ -84,8 +86,8 @@
@Override
public String toString() {
- return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + ", contents[" +
- contents.length + "])";
+ return "PcoData(" + cid + ", " + bearerProto + ", " + pcoId + " "
+ + IccUtils.bytesToHexString(contents) + ")";
}
@Override
diff --git a/telephony/java/android/telephony/ServiceState.java b/telephony/java/android/telephony/ServiceState.java
index db167c0..127bbff 100644
--- a/telephony/java/android/telephony/ServiceState.java
+++ b/telephony/java/android/telephony/ServiceState.java
@@ -1211,6 +1211,8 @@
.append(", mIsDataRoamingFromRegistration=")
.append(mIsDataRoamingFromRegistration)
.append(", mIsIwlanPreferred=").append(mIsIwlanPreferred)
+ .append(", mIsUsingNonTerrestrialNetwork=")
+ .append(isUsingNonTerrestrialNetwork())
.append("}").toString();
}
}
diff --git a/telephony/java/android/telephony/SubscriptionManager.java b/telephony/java/android/telephony/SubscriptionManager.java
index dea10b70..f0850af 100644
--- a/telephony/java/android/telephony/SubscriptionManager.java
+++ b/telephony/java/android/telephony/SubscriptionManager.java
@@ -1177,6 +1177,16 @@
*/
public static final String SATELLITE_ESOS_SUPPORTED = SimInfo.COLUMN_SATELLITE_ESOS_SUPPORTED;
+ /**
+ * TelephonyProvider column name for satellite provisioned status. The value of this
+ * column is set based on whether carrier roaming NB-IOT satellite service is provisioned or
+ * not. By default, it's disabled.
+ *
+ * @hide
+ */
+ public static final String IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM =
+ SimInfo.COLUMN_IS_SATELLITE_PROVISIONED_FOR_NON_IP_DATAGRAM;
+
/** @hide */
@Retention(RetentionPolicy.SOURCE)
@IntDef(prefix = {"USAGE_SETTING_"},
diff --git a/telephony/java/android/telephony/satellite/SatelliteManager.java b/telephony/java/android/telephony/satellite/SatelliteManager.java
index ad6db2d..e657d7f 100644
--- a/telephony/java/android/telephony/satellite/SatelliteManager.java
+++ b/telephony/java/android/telephony/satellite/SatelliteManager.java
@@ -254,7 +254,6 @@
*/
public static final String KEY_PROVISION_SATELLITE_TOKENS = "provision_satellite";
-
/**
* The request was successfully processed.
*/
@@ -2643,7 +2642,7 @@
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
public void requestProvisionSubscriberIds(@NonNull @CallbackExecutor Executor executor,
- @NonNull OutcomeReceiver<List<ProvisionSubscriberId>, SatelliteException> callback) {
+ @NonNull OutcomeReceiver<List<SatelliteSubscriberInfo>, SatelliteException> callback) {
Objects.requireNonNull(executor);
Objects.requireNonNull(callback);
@@ -2655,10 +2654,10 @@
protected void onReceiveResult(int resultCode, Bundle resultData) {
if (resultCode == SATELLITE_RESULT_SUCCESS) {
if (resultData.containsKey(KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN)) {
- List<ProvisionSubscriberId> list =
+ List<SatelliteSubscriberInfo> list =
resultData.getParcelableArrayList(
KEY_REQUEST_PROVISION_SUBSCRIBER_ID_TOKEN,
- ProvisionSubscriberId.class);
+ SatelliteSubscriberInfo.class);
executor.execute(() -> Binder.withCleanCallingIdentity(() ->
callback.onResult(list)));
} else {
@@ -2743,9 +2742,9 @@
}
/**
- * Deliver the list of provisioned satellite subscriber ids.
+ * Deliver the list of provisioned satellite subscriber infos.
*
- * @param list List of ProvisionSubscriberId.
+ * @param list The list of provisioned satellite subscriber infos.
* @param executor The executor on which the callback will be called.
* @param callback The callback object to which the result will be delivered.
*
@@ -2754,7 +2753,7 @@
*/
@RequiresPermission(Manifest.permission.SATELLITE_COMMUNICATION)
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
- public void provisionSatellite(@NonNull List<ProvisionSubscriberId> list,
+ public void provisionSatellite(@NonNull List<SatelliteSubscriberInfo> list,
@NonNull @CallbackExecutor Executor executor,
@NonNull OutcomeReceiver<Boolean, SatelliteException> callback) {
Objects.requireNonNull(executor);
diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
similarity index 94%
rename from telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl
rename to telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
index fe46db8..992c9ae 100644
--- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.aidl
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.aidl
@@ -16,4 +16,4 @@
package android.telephony.satellite;
-parcelable ProvisionSubscriberId;
+parcelable SatelliteSubscriberInfo;
diff --git a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
similarity index 83%
rename from telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
rename to telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
index 3e6f743..f26219b 100644
--- a/telephony/java/android/telephony/satellite/ProvisionSubscriberId.java
+++ b/telephony/java/android/telephony/satellite/SatelliteSubscriberInfo.java
@@ -26,7 +26,7 @@
import java.util.Objects;
/**
- * ProvisionSubscriberId
+ * SatelliteSubscriberInfo
*
* Satellite Gateway client will use these subscriber ids to register with satellite gateway service
* which identify user subscription with unique subscriber ids. These subscriber ids can be any
@@ -35,7 +35,7 @@
* @hide
*/
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
-public final class ProvisionSubscriberId implements Parcelable {
+public final class SatelliteSubscriberInfo implements Parcelable {
/** provision subscriberId */
@NonNull
private String mSubscriberId;
@@ -49,14 +49,14 @@
/**
* @hide
*/
- public ProvisionSubscriberId(@NonNull String subscriberId, @NonNull int carrierId,
+ public SatelliteSubscriberInfo(@NonNull String subscriberId, @NonNull int carrierId,
@NonNull String niddApn) {
this.mCarrierId = carrierId;
this.mSubscriberId = subscriberId;
this.mNiddApn = niddApn;
}
- private ProvisionSubscriberId(Parcel in) {
+ private SatelliteSubscriberInfo(Parcel in) {
readFromParcel(in);
}
@@ -72,16 +72,16 @@
}
@FlaggedApi(Flags.FLAG_CARRIER_ROAMING_NB_IOT_NTN)
- public static final @android.annotation.NonNull Creator<ProvisionSubscriberId> CREATOR =
- new Creator<ProvisionSubscriberId>() {
+ public static final @android.annotation.NonNull Creator<SatelliteSubscriberInfo> CREATOR =
+ new Creator<SatelliteSubscriberInfo>() {
@Override
- public ProvisionSubscriberId createFromParcel(Parcel in) {
- return new ProvisionSubscriberId(in);
+ public SatelliteSubscriberInfo createFromParcel(Parcel in) {
+ return new SatelliteSubscriberInfo(in);
}
@Override
- public ProvisionSubscriberId[] newArray(int size) {
- return new ProvisionSubscriberId[size];
+ public SatelliteSubscriberInfo[] newArray(int size) {
+ return new SatelliteSubscriberInfo[size];
}
};
@@ -148,7 +148,7 @@
public boolean equals(Object o) {
if (this == o) return true;
if (o == null || getClass() != o.getClass()) return false;
- ProvisionSubscriberId that = (ProvisionSubscriberId) o;
+ SatelliteSubscriberInfo that = (SatelliteSubscriberInfo) o;
return mSubscriberId.equals(that.mSubscriberId) && mCarrierId
== that.mCarrierId && mNiddApn.equals(that.mNiddApn);
}
diff --git a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
index b82396e..e66a082 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatellite.aidl
@@ -206,77 +206,6 @@
void stopSendingSatellitePointingInfo(in IIntegerConsumer resultCallback);
/**
- * Provision the device with a satellite provider.
- * This is needed if the provider allows dynamic registration.
- * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true.
- *
- * @param token The token to be used as a unique identifier for provisioning with satellite
- * gateway.
- * @param provisionData Data from the provisioning app that can be used by provisioning server
- * @param resultCallback The callback to receive the error code result of the operation.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED
- * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT
- */
- void provisionSatelliteService(in String token, in byte[] provisionData,
- in IIntegerConsumer resultCallback);
-
- /**
- * Deprovision the device with the satellite provider.
- * This is needed if the provider allows dynamic registration.
- * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false.
- *
- * @param token The token of the device/subscription to be deprovisioned.
- * @param resultCallback The callback to receive the error code result of the operation.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED
- * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT
- */
- void deprovisionSatelliteService(in String token, in IIntegerConsumer resultCallback);
-
- /**
- * Request to get whether this device is provisioned with a satellite provider.
- *
- * @param resultCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not
- * SatelliteResult#SATELLITE_RESULT_SUCCESS.
- * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
- * receive whether this device is provisioned with a satellite provider.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- */
- void requestIsSatelliteProvisioned(in IIntegerConsumer resultCallback,
- in IBooleanConsumer callback);
-
- /**
* Poll the pending datagrams to be received over satellite.
* The satellite service should check if there are any pending datagrams to be received over
* satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived.
diff --git a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
index b4eb15fd..3f2fce2 100644
--- a/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
+++ b/telephony/java/android/telephony/satellite/stub/ISatelliteListener.aidl
@@ -28,13 +28,6 @@
*/
oneway interface ISatelliteListener {
/**
- * Indicates that the satellite provision state has changed.
- *
- * @param provisioned True means the service is provisioned and false means it is not.
- */
- void onSatelliteProvisionStateChanged(in boolean provisioned);
-
- /**
* Indicates that new datagrams have been received on the device.
*
* @param datagram New datagram that was received.
diff --git a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
index d8b4974..c50e469 100644
--- a/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteImplBase.java
@@ -142,32 +142,6 @@
}
@Override
- public void provisionSatelliteService(String token, byte[] provisionData,
- IIntegerConsumer resultCallback) throws RemoteException {
- executeMethodAsync(
- () -> SatelliteImplBase.this
- .provisionSatelliteService(token, provisionData, resultCallback),
- "provisionSatelliteService");
- }
-
- @Override
- public void deprovisionSatelliteService(String token, IIntegerConsumer resultCallback)
- throws RemoteException {
- executeMethodAsync(
- () -> SatelliteImplBase.this.deprovisionSatelliteService(token, resultCallback),
- "deprovisionSatelliteService");
- }
-
- @Override
- public void requestIsSatelliteProvisioned(IIntegerConsumer resultCallback,
- IBooleanConsumer callback) throws RemoteException {
- executeMethodAsync(
- () -> SatelliteImplBase.this
- .requestIsSatelliteProvisioned(resultCallback, callback),
- "requestIsSatelliteProvisioned");
- }
-
- @Override
public void pollPendingSatelliteDatagrams(IIntegerConsumer resultCallback)
throws RemoteException {
executeMethodAsync(
@@ -487,85 +461,6 @@
}
/**
- * Provision the device with a satellite provider.
- * This is needed if the provider allows dynamic registration.
- * Once provisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report true.
- *
- * @param token The token to be used as a unique identifier for provisioning with satellite
- * gateway.
- * @param provisionData Data from the provisioning app that can be used by provisioning
- * server
- * @param resultCallback The callback to receive the error code result of the operation.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED
- * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT
- */
- public void provisionSatelliteService(@NonNull String token, @NonNull byte[] provisionData,
- @NonNull IIntegerConsumer resultCallback) {
- // stub implementation
- }
-
- /**
- * Deprovision the device with the satellite provider.
- * This is needed if the provider allows dynamic registration.
- * Once deprovisioned, ISatelliteListener#onSatelliteProvisionStateChanged should report false.
- *
- * @param token The token of the device/subscription to be deprovisioned.
- * @param resultCallback The callback to receive the error code result of the operation.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_NETWORK_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- * SatelliteResult:SATELLITE_RESULT_REQUEST_ABORTED
- * SatelliteResult:SATELLITE_RESULT_NETWORK_TIMEOUT
- */
- public void deprovisionSatelliteService(@NonNull String token,
- @NonNull IIntegerConsumer resultCallback) {
- // stub implementation
- }
-
- /**
- * Request to get whether this device is provisioned with a satellite provider.
- *
- * @param resultCallback The callback to receive the error code result of the operation.
- * This must only be sent when the result is not
- * SatelliteResult#SATELLITE_RESULT_SUCCESS.
- * @param callback If the result is SatelliteResult#SATELLITE_RESULT_SUCCESS, the callback to
- * receive whether this device is provisioned with a satellite provider.
- *
- * Valid result codes returned:
- * SatelliteResult:SATELLITE_RESULT_SUCCESS
- * SatelliteResult:SATELLITE_RESULT_SERVICE_ERROR
- * SatelliteResult:SATELLITE_RESULT_MODEM_ERROR
- * SatelliteResult:SATELLITE_RESULT_INVALID_MODEM_STATE
- * SatelliteResult:SATELLITE_RESULT_INVALID_ARGUMENTS
- * SatelliteResult:SATELLITE_RESULT_RADIO_NOT_AVAILABLE
- * SatelliteResult:SATELLITE_RESULT_REQUEST_NOT_SUPPORTED
- * SatelliteResult:SATELLITE_RESULT_NO_RESOURCES
- */
- public void requestIsSatelliteProvisioned(@NonNull IIntegerConsumer resultCallback,
- @NonNull IBooleanConsumer callback) {
- // stub implementation
- }
-
- /**
* Poll the pending datagrams to be received over satellite.
* The satellite service should check if there are any pending datagrams to be received over
* satellite and report them via ISatelliteListener#onSatelliteDatagramsReceived.
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
similarity index 95%
rename from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
rename to telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
index 460de8c..fb44f87 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/telephony/java/android/telephony/satellite/stub/SatelliteSubscriberInfo.aidl
@@ -19,7 +19,7 @@
/**
* {@hide}
*/
-parcelable ProvisionSubscriberId {
+parcelable SatelliteSubscriberInfo {
/** provision subscriberId */
String subscriberId;
diff --git a/telephony/java/com/android/internal/telephony/ITelephony.aidl b/telephony/java/com/android/internal/telephony/ITelephony.aidl
index 3dbda7a..8919703 100644
--- a/telephony/java/com/android/internal/telephony/ITelephony.aidl
+++ b/telephony/java/com/android/internal/telephony/ITelephony.aidl
@@ -78,7 +78,7 @@
import android.telephony.satellite.NtnSignalStrength;
import android.telephony.satellite.SatelliteCapabilities;
import android.telephony.satellite.SatelliteDatagram;
-import android.telephony.satellite.ProvisionSubscriberId;
+import android.telephony.satellite.SatelliteSubscriberInfo;
import com.android.ims.internal.IImsServiceFeatureCallback;
import com.android.internal.telephony.CellNetworkScanResult;
import com.android.internal.telephony.IBooleanConsumer;
@@ -3007,16 +3007,18 @@
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void setDeviceAlignedWithSatellite(int subId, in boolean isAligned);
+ void setDeviceAlignedWithSatellite(int subId, boolean isAligned);
/**
* This API can be used by only CTS to update satellite vendor service package name.
*
* @param servicePackageName The package name of the satellite vendor service.
+ * @param provisioned Whether satellite should be provisioned or not.
+ *
* @return {@code true} if the satellite vendor service is set successfully,
* {@code false} otherwise.
*/
- boolean setSatelliteServicePackageName(in String servicePackageName);
+ boolean setSatelliteServicePackageName(in String servicePackageName, in String provisioned);
/**
* This API can be used by only CTS to update satellite gateway service package name.
@@ -3426,13 +3428,13 @@
void requestIsProvisioned(in String satelliteSubscriberId, in ResultReceiver result);
/**
- * Deliver the list of provisioned satellite subscriber ids.
+ * Deliver the list of provisioned satellite subscriber infos.
*
- * @param list List of provisioned satellite subscriber ids.
+ * @param list The list of provisioned satellite subscriber infos.
* @param result The result receiver that returns whether deliver success or fail.
* @hide
*/
@JavaPassthrough(annotation="@android.annotation.RequiresPermission("
+ "android.Manifest.permission.SATELLITE_COMMUNICATION)")
- void provisionSatellite(in List<ProvisionSubscriberId> list, in ResultReceiver result);
+ void provisionSatellite(in List<SatelliteSubscriberInfo> list, in ResultReceiver result);
}
diff --git a/tests/Internal/Android.bp b/tests/Internal/Android.bp
index 827ff4f..ad98e47 100644
--- a/tests/Internal/Android.bp
+++ b/tests/Internal/Android.bp
@@ -24,6 +24,7 @@
"flickerlib-parsers",
"perfetto_trace_java_protos",
"flickerlib-trace_processor_shell",
+ "ravenwood-junit",
],
java_resource_dirs: ["res"],
certificate: "platform",
@@ -39,6 +40,7 @@
"platform-test-annotations",
],
srcs: [
+ "src/com/android/internal/graphics/ColorUtilsTest.java",
"src/com/android/internal/util/ParcellingTests.java",
],
auto_gen_config: true,
diff --git a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
index d0bb8e3..38a22f2 100644
--- a/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
+++ b/tests/Internal/src/com/android/internal/graphics/ColorUtilsTest.java
@@ -19,14 +19,19 @@
import static org.junit.Assert.assertTrue;
import android.graphics.Color;
+import android.platform.test.ravenwood.RavenwoodRule;
import androidx.test.filters.SmallTest;
+import org.junit.Rule;
import org.junit.Test;
@SmallTest
public class ColorUtilsTest {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
@Test
public void calculateMinimumBackgroundAlpha_satisfiestContrast() {
diff --git a/tests/Internal/src/com/android/internal/util/ParcellingTests.java b/tests/Internal/src/com/android/internal/util/ParcellingTests.java
index 65a3436..fb63422 100644
--- a/tests/Internal/src/com/android/internal/util/ParcellingTests.java
+++ b/tests/Internal/src/com/android/internal/util/ParcellingTests.java
@@ -18,6 +18,7 @@
import android.os.Parcel;
import android.platform.test.annotations.Presubmit;
+import android.platform.test.ravenwood.RavenwoodRule;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
@@ -26,6 +27,7 @@
import com.android.internal.util.Parcelling.BuiltIn.ForInstant;
+import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@@ -38,6 +40,9 @@
@RunWith(JUnit4.class)
public class ParcellingTests {
+ @Rule
+ public final RavenwoodRule mRavenwood = new RavenwoodRule();
+
private Parcel mParcel = Parcel.obtain();
@Test
diff --git a/tools/aapt2/Android.bp b/tools/aapt2/Android.bp
index 3f9016b..f43cf52 100644
--- a/tools/aapt2/Android.bp
+++ b/tools/aapt2/Android.bp
@@ -113,6 +113,7 @@
"io/ZipArchive.cpp",
"link/AutoVersioner.cpp",
"link/FeatureFlagsFilter.cpp",
+ "link/FlagDisabledResourceRemover.cpp",
"link/ManifestFixer.cpp",
"link/NoDefaultResourceRemover.cpp",
"link/PrivateAttributeMover.cpp",
@@ -189,6 +190,8 @@
"integration-tests/CommandTests/**/*",
"integration-tests/ConvertTest/**/*",
"integration-tests/DumpTest/**/*",
+ ":resource-flagging-test-app-apk",
+ ":resource-flagging-test-app-r-java",
],
}
diff --git a/tools/aapt2/ResourceParser.cpp b/tools/aapt2/ResourceParser.cpp
index 9444dd9..1c85e9f 100644
--- a/tools/aapt2/ResourceParser.cpp
+++ b/tools/aapt2/ResourceParser.cpp
@@ -690,9 +690,7 @@
resource_format = item_iter->second.format;
}
- // Don't bother parsing the item if it is behind a disabled flag
- if (out_resource->flag_status != FlagStatus::Disabled &&
- !ParseItem(parser, out_resource, resource_format)) {
+ if (!ParseItem(parser, out_resource, resource_format)) {
return false;
}
return true;
diff --git a/tools/aapt2/ResourceParser_test.cpp b/tools/aapt2/ResourceParser_test.cpp
index 2e6ad13..b59b165 100644
--- a/tools/aapt2/ResourceParser_test.cpp
+++ b/tools/aapt2/ResourceParser_test.cpp
@@ -69,13 +69,8 @@
return TestParse(str, ConfigDescription{});
}
- ::testing::AssertionResult TestParse(StringPiece str, ResourceParserOptions parserOptions) {
- return TestParse(str, ConfigDescription{}, parserOptions);
- }
-
- ::testing::AssertionResult TestParse(
- StringPiece str, const ConfigDescription& config,
- ResourceParserOptions parserOptions = ResourceParserOptions()) {
+ ::testing::AssertionResult TestParse(StringPiece str, const ConfigDescription& config) {
+ ResourceParserOptions parserOptions;
ResourceParser parser(context_->GetDiagnostics(), &table_, android::Source{"test"}, config,
parserOptions);
@@ -247,19 +242,6 @@
EXPECT_FALSE(TestParse(R"(<string name="foo4" translatable="yes">Translate</string>)"));
}
-TEST_F(ResourceParserTest, ParseStringBehindDisabledFlag) {
- FeatureFlagProperties flag_properties(true, false);
- ResourceParserOptions options;
- options.feature_flag_values = {{"falseFlag", flag_properties}};
- ASSERT_TRUE(TestParse(
- R"(<string name="foo" android:featureFlag="falseFlag"
- xmlns:android="http://schemas.android.com/apk/res/android">foo</string>)",
- options));
-
- String* str = test::GetValue<String>(&table_, "string/foo");
- ASSERT_THAT(str, IsNull());
-}
-
TEST_F(ResourceParserTest, IgnoreXliffTagsOtherThanG) {
std::string input = R"(
<string name="foo" xmlns:xliff="urn:oasis:names:tc:xliff:document:1.2">
diff --git a/tools/aapt2/cmd/Convert.cpp b/tools/aapt2/cmd/Convert.cpp
index c132792..6c3eae1 100644
--- a/tools/aapt2/cmd/Convert.cpp
+++ b/tools/aapt2/cmd/Convert.cpp
@@ -244,6 +244,10 @@
return verbose_;
}
+ void SetVerbose(bool verbose) {
+ verbose_ = verbose;
+ }
+
int GetMinSdkVersion() override {
return min_sdk_;
}
@@ -388,6 +392,8 @@
}
Context context;
+ context.SetVerbose(verbose_);
+
StringPiece path = args[0];
unique_ptr<LoadedApk> apk = LoadedApk::LoadApkFromPath(path, context.GetDiagnostics());
if (apk == nullptr) {
diff --git a/tools/aapt2/cmd/Link.cpp b/tools/aapt2/cmd/Link.cpp
index 642a561..56f5288 100644
--- a/tools/aapt2/cmd/Link.cpp
+++ b/tools/aapt2/cmd/Link.cpp
@@ -57,6 +57,7 @@
#include "java/ManifestClassGenerator.h"
#include "java/ProguardRules.h"
#include "link/FeatureFlagsFilter.h"
+#include "link/FlagDisabledResourceRemover.h"
#include "link/Linkers.h"
#include "link/ManifestFixer.h"
#include "link/NoDefaultResourceRemover.h"
@@ -1840,11 +1841,57 @@
return validate(attr->value);
}
+ class FlagDisabledStringVisitor : public DescendingValueVisitor {
+ public:
+ using DescendingValueVisitor::Visit;
+
+ explicit FlagDisabledStringVisitor(android::StringPool& string_pool)
+ : string_pool_(string_pool) {
+ }
+
+ void Visit(RawString* value) override {
+ value->value = string_pool_.MakeRef("");
+ }
+
+ void Visit(String* value) override {
+ value->value = string_pool_.MakeRef("");
+ }
+
+ void Visit(StyledString* value) override {
+ value->value = string_pool_.MakeRef(android::StyleString{{""}, {}});
+ }
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FlagDisabledStringVisitor);
+ android::StringPool& string_pool_;
+ };
+
// Writes the AndroidManifest, ResourceTable, and all XML files referenced by the ResourceTable
// to the IArchiveWriter.
bool WriteApk(IArchiveWriter* writer, proguard::KeepSet* keep_set, xml::XmlResource* manifest,
ResourceTable* table) {
TRACE_CALL();
+
+ FlagDisabledStringVisitor visitor(table->string_pool);
+
+ for (auto& package : table->packages) {
+ for (auto& type : package->types) {
+ for (auto& entry : type->entries) {
+ for (auto& config_value : entry->values) {
+ if (config_value->flag_status == FlagStatus::Disabled) {
+ config_value->value->Accept(&visitor);
+ }
+ }
+ }
+ }
+ }
+
+ if (!FlagDisabledResourceRemover{}.Consume(context_, table)) {
+ context_->GetDiagnostics()->Error(android::DiagMessage()
+ << "failed removing resources behind disabled flags");
+ return 1;
+ }
+
const bool keep_raw_values = (context_->GetPackageType() == PackageType::kStaticLib)
|| options_.keep_raw_values;
bool result = FlattenXml(context_, *manifest, kAndroidManifestPath, keep_raw_values,
@@ -2331,6 +2378,12 @@
return 1;
};
+ if (options_.generate_java_class_path || options_.generate_text_symbols_path) {
+ if (!GenerateJavaClasses()) {
+ return 1;
+ }
+ }
+
if (!WriteApk(archive_writer.get(), &proguard_keep_set, manifest_xml.get(), &final_table_)) {
return 1;
}
@@ -2339,12 +2392,6 @@
return 1;
}
- if (options_.generate_java_class_path || options_.generate_text_symbols_path) {
- if (!GenerateJavaClasses()) {
- return 1;
- }
- }
-
if (!WriteProguardFile(options_.generate_proguard_rules_path, proguard_keep_set)) {
return 1;
}
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
new file mode 100644
index 0000000..5932271
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/Android.bp
@@ -0,0 +1,81 @@
+// 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 {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+ default_team: "trendy_team_android_resources",
+}
+
+genrule {
+ name: "resource-flagging-test-app-compile",
+ tools: ["aapt2"],
+ srcs: [
+ "res/values/bools.xml",
+ "res/values/bools2.xml",
+ "res/values/strings.xml",
+ ],
+ out: [
+ "values_bools.arsc.flat",
+ "values_bools2.arsc.flat",
+ "values_strings.arsc.flat",
+ ],
+ cmd: "$(location aapt2) compile $(in) -o $(genDir) " +
+ "--feature-flags test.package.falseFlag:ro=false,test.package.trueFlag:ro=true",
+}
+
+genrule {
+ name: "resource-flagging-test-app-apk",
+ tools: ["aapt2"],
+ // The first input file in the list must be the manifest
+ srcs: [
+ "AndroidManifest.xml",
+ ":resource-flagging-test-app-compile",
+ ],
+ out: [
+ "resapp.apk",
+ ],
+ cmd: "$(location aapt2) link -o $(out) --manifest $(in)",
+}
+
+genrule {
+ name: "resource-flagging-test-app-r-java",
+ tools: ["aapt2"],
+ // The first input file in the list must be the manifest
+ srcs: [
+ "AndroidManifest.xml",
+ ":resource-flagging-test-app-compile",
+ ],
+ out: [
+ "resource-flagging-java/com/android/intenal/flaggedresources/R.java",
+ ],
+ cmd: "$(location aapt2) link -o $(genDir)/resapp.apk --java $(genDir)/resource-flagging-java --manifest $(in)",
+}
+
+java_genrule {
+ name: "resource-flagging-test-app-apk-as-resource",
+ srcs: [
+ ":resource-flagging-test-app-apk",
+ ],
+ out: ["apks_as_resources.res.zip"],
+ tools: ["soong_zip"],
+
+ cmd: "mkdir -p $(genDir)/res/raw && " +
+ "cp $(in) $(genDir)/res/raw/$$(basename $(in)) && " +
+ "$(location soong_zip) -o $(out) -C $(genDir)/res -D $(genDir)/res",
+}
diff --git a/core/tests/resourceflaggingtests/TestAppAndroidManifest.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml
similarity index 100%
rename from core/tests/resourceflaggingtests/TestAppAndroidManifest.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/AndroidManifest.xml
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
similarity index 82%
rename from core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
index 8d01465..3e094fb 100644
--- a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools.xml
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools.xml
@@ -7,4 +7,6 @@
<bool name="res2" android:featureFlag="test.package.trueFlag">true</bool>
<bool name="res3">false</bool>
+
+ <bool name="res4" android:featureFlag="test.package.falseFlag">true</bool>
</resources>
\ No newline at end of file
diff --git a/core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
similarity index 100%
rename from core/tests/resourceflaggingtests/flagged_resources_res/values/bools2.xml
rename to tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/bools2.xml
diff --git a/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml
new file mode 100644
index 0000000..5c0fca1
--- /dev/null
+++ b/tools/aapt2/integration-tests/FlaggedResourcesTest/res/values/strings.xml
@@ -0,0 +1,6 @@
+<?xml version="1.0" encoding="utf-8"?>
+<resources xmlns:android="http://schemas.android.com/apk/res/android">
+ <string name="str">plain string</string>
+
+ <string name="str1" android:featureFlag="test.package.falseFlag">DONTFIND</string>
+</resources>
\ No newline at end of file
diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.cpp b/tools/aapt2/link/FlagDisabledResourceRemover.cpp
new file mode 100644
index 0000000..e3289e2
--- /dev/null
+++ b/tools/aapt2/link/FlagDisabledResourceRemover.cpp
@@ -0,0 +1,59 @@
+/*
+ * 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.
+ */
+
+#include "link/FlagDisabledResourceRemover.h"
+
+#include <algorithm>
+
+#include "ResourceTable.h"
+
+using android::ConfigDescription;
+
+namespace aapt {
+
+static bool KeepResourceEntry(const std::unique_ptr<ResourceEntry>& entry) {
+ if (entry->values.empty()) {
+ return true;
+ }
+ const auto end_iter = entry->values.end();
+ const auto remove_iter =
+ std::stable_partition(entry->values.begin(), end_iter,
+ [](const std::unique_ptr<ResourceConfigValue>& value) -> bool {
+ return value->flag_status != FlagStatus::Disabled;
+ });
+
+ bool keep = remove_iter != entry->values.begin();
+
+ entry->values.erase(remove_iter, end_iter);
+ return keep;
+}
+
+bool FlagDisabledResourceRemover::Consume(IAaptContext* context, ResourceTable* table) {
+ for (auto& pkg : table->packages) {
+ for (auto& type : pkg->types) {
+ const auto end_iter = type->entries.end();
+ const auto remove_iter = std::stable_partition(
+ type->entries.begin(), end_iter, [](const std::unique_ptr<ResourceEntry>& entry) -> bool {
+ return KeepResourceEntry(entry);
+ });
+
+ type->entries.erase(remove_iter, end_iter);
+ }
+ }
+ return true;
+}
+
+} // namespace aapt
\ No newline at end of file
diff --git a/tools/aapt2/link/FlagDisabledResourceRemover.h b/tools/aapt2/link/FlagDisabledResourceRemover.h
new file mode 100644
index 0000000..2db2cb4
--- /dev/null
+++ b/tools/aapt2/link/FlagDisabledResourceRemover.h
@@ -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.
+ */
+
+#pragma once
+
+#include "android-base/macros.h"
+#include "process/IResourceTableConsumer.h"
+
+namespace aapt {
+
+// Removes any resource that are behind disabled flags.
+class FlagDisabledResourceRemover : public IResourceTableConsumer {
+ public:
+ FlagDisabledResourceRemover() = default;
+
+ bool Consume(IAaptContext* context, ResourceTable* table) override;
+
+ private:
+ DISALLOW_COPY_AND_ASSIGN(FlagDisabledResourceRemover);
+};
+
+} // namespace aapt
diff --git a/tools/aapt2/link/FlaggedResources_test.cpp b/tools/aapt2/link/FlaggedResources_test.cpp
new file mode 100644
index 0000000..c901b58
--- /dev/null
+++ b/tools/aapt2/link/FlaggedResources_test.cpp
@@ -0,0 +1,101 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include "LoadedApk.h"
+#include "cmd/Dump.h"
+#include "io/StringStream.h"
+#include "test/Test.h"
+#include "text/Printer.h"
+
+using ::aapt::io::StringOutputStream;
+using ::aapt::text::Printer;
+using testing::Eq;
+using testing::Ne;
+
+namespace aapt {
+
+using FlaggedResourcesTest = CommandTestFixture;
+
+static android::NoOpDiagnostics noop_diag;
+
+void DumpStringPoolToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpStringsCommand command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+void DumpResourceTableToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpTableCommand command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+void DumpChunksToString(LoadedApk* loaded_apk, std::string* output) {
+ StringOutputStream output_stream(output);
+ Printer printer(&output_stream);
+
+ DumpChunks command(&printer, &noop_diag);
+ ASSERT_EQ(command.Dump(loaded_apk), 0);
+ output_stream.Flush();
+}
+
+TEST_F(FlaggedResourcesTest, DisabledStringRemovedFromPool) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpStringPoolToString(loaded_apk.get(), &output);
+
+ std::string excluded = "DONTFIND";
+ ASSERT_EQ(output.find(excluded), std::string::npos);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTable) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpResourceTableToString(loaded_apk.get(), &output);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesRemovedFromTableChunks) {
+ auto apk_path = file::BuildPath({android::base::GetExecutableDirectory(), "resapp.apk"});
+ auto loaded_apk = LoadedApk::LoadApkFromPath(apk_path, &noop_diag);
+
+ std::string output;
+ DumpChunksToString(loaded_apk.get(), &output);
+
+ ASSERT_EQ(output.find("res4"), std::string::npos);
+ ASSERT_EQ(output.find("str1"), std::string::npos);
+}
+
+TEST_F(FlaggedResourcesTest, DisabledResourcesInRJava) {
+ auto r_path = file::BuildPath({android::base::GetExecutableDirectory(), "resource-flagging-java",
+ "com", "android", "intenal", "flaggedresources", "R.java"});
+ std::string r_contents;
+ ::android::base::ReadFileToString(r_path, &r_contents);
+
+ ASSERT_NE(r_contents.find("public static final int res4"), std::string::npos);
+ ASSERT_NE(r_contents.find("public static final int str1"), std::string::npos);
+}
+
+} // namespace aapt
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
index 5dde265..36bfbef 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGen.kt
@@ -58,17 +58,19 @@
// Dump the classes, if specified.
options.inputJarDumpFile.ifSet {
- PrintWriter(it).use { pw -> allClasses.dump(pw) }
- log.i("Dump file created at $it")
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw -> allClasses.dump(pw) }
+ }
}
options.inputJarAsKeepAllFile.ifSet {
- PrintWriter(it).use {
- pw -> allClasses.forEach {
- classNode -> printAsTextPolicy(pw, classNode)
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw ->
+ allClasses.forEach { classNode ->
+ printAsTextPolicy(pw, classNode)
+ }
}
}
- log.i("Dump file created at $it")
}
// Build the filters.
@@ -91,16 +93,18 @@
// Dump statistics, if specified.
options.statsFile.ifSet {
- PrintWriter(it).use { pw -> stats.dumpOverview(pw) }
- log.i("Dump file created at $it")
+ log.iTime("Dump file created at $it") {
+ PrintWriter(it).use { pw -> stats.dumpOverview(pw) }
+ }
}
options.apiListFile.ifSet {
- PrintWriter(it).use { pw ->
- // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed
- // framework-minus-apex.jar so that we can dump inherited methods from it.
- ApiDumper(pw, allClasses, null, filter).dump()
+ log.iTime("API list file created at $it") {
+ PrintWriter(it).use { pw ->
+ // TODO, when dumping a jar that's not framework-minus-apex.jar, we need to feed
+ // framework-minus-apex.jar so that we can dump inherited methods from it.
+ ApiDumper(pw, allClasses, null, filter).dump()
+ }
}
- log.i("API list file created at $it")
}
}
@@ -221,47 +225,48 @@
log.i("Converting %s into [stub: %s, impl: %s] ...", inJar, outStubJar, outImplJar)
log.i("ASM CheckClassAdapter is %s", if (enableChecker) "enabled" else "disabled")
- val start = System.currentTimeMillis()
+ log.iTime("Transforming jar") {
+ val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
- val packageRedirector = PackageRedirectRemapper(options.packageRedirects)
+ var itemIndex = 0
+ var numItemsProcessed = 0
+ var numItems = -1 // == Unknown
- var itemIndex = 0
- var numItemsProcessed = 0
- var numItems = -1 // == Unknown
+ log.withIndent {
+ // Open the input jar file and process each entry.
+ ZipFile(inJar).use { inZip ->
- log.withIndent {
- // Open the input jar file and process each entry.
- ZipFile(inJar).use { inZip ->
+ numItems = inZip.size()
+ val shardStart = numItems * shard / numShards
+ val shardNextStart = numItems * (shard + 1) / numShards
- numItems = inZip.size()
- val shardStart = numItems * shard / numShards
- val shardNextStart = numItems * (shard + 1) / numShards
-
- maybeWithZipOutputStream(outStubJar) { stubOutStream ->
- maybeWithZipOutputStream(outImplJar) { implOutStream ->
- val inEntries = inZip.entries()
- while (inEntries.hasMoreElements()) {
- val entry = inEntries.nextElement()
- val inShard = (shardStart <= itemIndex) && (itemIndex < shardNextStart)
- itemIndex++
- if (!inShard) {
- continue
- }
- convertSingleEntry(inZip, entry, stubOutStream, implOutStream,
+ maybeWithZipOutputStream(outStubJar) { stubOutStream ->
+ maybeWithZipOutputStream(outImplJar) { implOutStream ->
+ val inEntries = inZip.entries()
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
+ val inShard = (shardStart <= itemIndex)
+ && (itemIndex < shardNextStart)
+ itemIndex++
+ if (!inShard) {
+ continue
+ }
+ convertSingleEntry(
+ inZip, entry, stubOutStream, implOutStream,
filter, packageRedirector, remapper,
- enableChecker, classes, errors, stats)
- numItemsProcessed++
+ enableChecker, classes, errors, stats
+ )
+ numItemsProcessed++
+ }
+ log.i("Converted all entries.")
}
- log.i("Converted all entries.")
}
+ outStubJar?.let { log.i("Created stub: $it") }
+ outImplJar?.let { log.i("Created impl: $it") }
}
- outStubJar?.let { log.i("Created stub: $it") }
- outImplJar?.let { log.i("Created impl: $it") }
}
+ log.i("%d / %d item(s) processed.", numItemsProcessed, numItems)
}
- val end = System.currentTimeMillis()
- log.i("Done transforming the jar in %.1f second(s). %d / %d item(s) processed.",
- (end - start) / 1000.0, numItemsProcessed, numItems)
}
private fun <T> maybeWithZipOutputStream(filename: String?, block: (ZipOutputStream?) -> T): T {
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
index 18065ba..ee4a06f 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/HostStubGenLogger.kt
@@ -185,6 +185,16 @@
println(LogLevel.Debug, format, *args)
}
+ inline fun <T> iTime(message: String, block: () -> T): T {
+ val start = System.currentTimeMillis()
+ val ret = block()
+ val end = System.currentTimeMillis()
+
+ log.i("%s: took %.1f second(s).", message, (end - start) / 1000.0)
+
+ return ret
+ }
+
inline fun forVerbose(block: () -> Unit) {
if (isEnabled(LogLevel.Verbose)) {
block()
diff --git a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
index 92906a7..2607df6 100644
--- a/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
+++ b/tools/hoststubgen/hoststubgen/src/com/android/hoststubgen/asm/ClassNodes.kt
@@ -184,49 +184,50 @@
* Load all the classes, without code.
*/
fun loadClassStructures(inJar: String): ClassNodes {
- log.i("Reading class structure from $inJar ...")
- val start = System.currentTimeMillis()
+ log.iTime("Reading class structure from $inJar") {
+ val allClasses = ClassNodes()
- val allClasses = ClassNodes()
+ log.withIndent {
+ ZipFile(inJar).use { inZip ->
+ val inEntries = inZip.entries()
- log.withIndent {
- ZipFile(inJar).use { inZip ->
- val inEntries = inZip.entries()
+ while (inEntries.hasMoreElements()) {
+ val entry = inEntries.nextElement()
- while (inEntries.hasMoreElements()) {
- val entry = inEntries.nextElement()
-
- BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
- if (entry.name.endsWith(".class")) {
- val cr = ClassReader(bis)
- val cn = ClassNode()
- cr.accept(cn, ClassReader.SKIP_CODE or ClassReader.SKIP_DEBUG
- or ClassReader.SKIP_FRAMES)
- if (!allClasses.addClass(cn)) {
- log.w("Duplicate class found: ${cn.name}")
- }
- } else if (entry.name.endsWith(".dex")) {
- // Seems like it's an ART jar file. We can't process it.
- // It's a fatal error.
- throw InvalidJarFileException(
- "$inJar is not a desktop jar file. It contains a *.dex file.")
- } else {
- // Unknown file type. Skip.
- while (bis.available() > 0) {
- bis.skip((1024 * 1024).toLong())
+ BufferedInputStream(inZip.getInputStream(entry)).use { bis ->
+ if (entry.name.endsWith(".class")) {
+ val cr = ClassReader(bis)
+ val cn = ClassNode()
+ cr.accept(
+ cn, ClassReader.SKIP_CODE
+ or ClassReader.SKIP_DEBUG
+ or ClassReader.SKIP_FRAMES
+ )
+ if (!allClasses.addClass(cn)) {
+ log.w("Duplicate class found: ${cn.name}")
+ }
+ } else if (entry.name.endsWith(".dex")) {
+ // Seems like it's an ART jar file. We can't process it.
+ // It's a fatal error.
+ throw InvalidJarFileException(
+ "$inJar is not a desktop jar file."
+ + " It contains a *.dex file."
+ )
+ } else {
+ // Unknown file type. Skip.
+ while (bis.available() > 0) {
+ bis.skip((1024 * 1024).toLong())
+ }
}
}
}
}
}
+ if (allClasses.size == 0) {
+ log.w("$inJar contains no *.class files.")
+ }
+ return allClasses
}
- if (allClasses.size == 0) {
- log.w("$inJar contains no *.class files.")
- }
-
- val end = System.currentTimeMillis()
- log.i("Done reading class structure in %.1f second(s).", (end - start) / 1000.0)
- return allClasses
}
}
}
\ No newline at end of file
diff --git a/tools/systemfeatures/Android.bp b/tools/systemfeatures/Android.bp
new file mode 100644
index 0000000..2cebfe9
--- /dev/null
+++ b/tools/systemfeatures/Android.bp
@@ -0,0 +1,63 @@
+package {
+ // See: http://go/android-license-faq
+ // A large-scale-change added 'default_applicable_licenses' to import
+ // all of the 'license_kinds' from "frameworks_base_license"
+ // to get the below license kinds:
+ // SPDX-license-identifier-Apache-2.0
+ default_applicable_licenses: ["frameworks_base_license"],
+}
+
+java_library_host {
+ name: "systemfeatures-gen-lib",
+ srcs: [
+ "src/**/*.java",
+ "src/**/*.kt",
+ ],
+ static_libs: [
+ "guava",
+ "javapoet",
+ ],
+}
+
+java_binary_host {
+ name: "systemfeatures-gen-tool",
+ main_class: "com.android.systemfeatures.SystemFeaturesGenerator",
+ static_libs: ["systemfeatures-gen-lib"],
+}
+
+// TODO(b/203143243): Add golden diff test for generated sources.
+// Functional runtime behavior is covered in systemfeatures-gen-tests.
+genrule {
+ name: "systemfeatures-gen-tests-srcs",
+ cmd: "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwNoFeatures --readonly=false > $(location RwNoFeatures.java) && " +
+ "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoNoFeatures --readonly=true > $(location RoNoFeatures.java) && " +
+ "$(location systemfeatures-gen-tool) com.android.systemfeatures.RwFeatures --readonly=false --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RwFeatures.java) && " +
+ "$(location systemfeatures-gen-tool) com.android.systemfeatures.RoFeatures --readonly=true --feature=WATCH:1 --feature=WIFI:0 --feature=VULKAN:-1 --feature=AUTO: > $(location RoFeatures.java)",
+ out: [
+ "RwNoFeatures.java",
+ "RoNoFeatures.java",
+ "RwFeatures.java",
+ "RoFeatures.java",
+ ],
+ tools: ["systemfeatures-gen-tool"],
+}
+
+java_test_host {
+ name: "systemfeatures-gen-tests",
+ test_suites: ["general-tests"],
+ srcs: [
+ "tests/**/*.java",
+ ":systemfeatures-gen-tests-srcs",
+ ],
+ test_options: {
+ unit_test: true,
+ },
+ static_libs: [
+ "aconfig-annotations-lib",
+ "framework-annotations-lib",
+ "junit",
+ "objenesis",
+ "mockito",
+ "truth",
+ ],
+}
diff --git a/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
new file mode 100644
index 0000000..9bfda45
--- /dev/null
+++ b/tools/systemfeatures/src/com/android/systemfeatures/SystemFeaturesGenerator.kt
@@ -0,0 +1,218 @@
+/*
+ * 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.systemfeatures
+
+import com.google.common.base.CaseFormat
+import com.squareup.javapoet.ClassName
+import com.squareup.javapoet.JavaFile
+import com.squareup.javapoet.MethodSpec
+import com.squareup.javapoet.TypeSpec
+import javax.lang.model.element.Modifier
+
+/*
+ * Simple Java code generator that takes as input a list of defined features and generates an
+ * accessory class based on the provided versions.
+ *
+ * <p>Example:
+ *
+ * <pre>
+ * <cmd> com.foo.RoSystemFeatures --readonly=true \
+ * --feature=WATCH:0 --feature=AUTOMOTIVE: --feature=VULKAN:9348
+ * </pre>
+ *
+ * This generates a class that has the following signature:
+ *
+ * <pre>
+ * package com.foo;
+ * public final class RoSystemFeatures {
+ * @AssumeTrueForR8
+ * public static boolean hasFeatureWatch(Context context);
+ * @AssumeFalseForR8
+ * public static boolean hasFeatureAutomotive(Context context);
+ * @AssumeTrueForR8
+ * public static boolean hasFeatureVulkan(Context context);
+ * public static Boolean maybeHasFeature(String feature, int version);
+ * }
+ * </pre>
+ */
+object SystemFeaturesGenerator {
+ private const val FEATURE_ARG = "--feature="
+ private const val READONLY_ARG = "--readonly="
+ private val PACKAGEMANAGER_CLASS = ClassName.get("android.content.pm", "PackageManager")
+ private val CONTEXT_CLASS = ClassName.get("android.content", "Context")
+ private val ASSUME_TRUE_CLASS =
+ ClassName.get("com.android.aconfig.annotations", "AssumeTrueForR8")
+ private val ASSUME_FALSE_CLASS =
+ ClassName.get("com.android.aconfig.annotations", "AssumeFalseForR8")
+
+ private fun usage() {
+ println("Usage: SystemFeaturesGenerator <outputClassName> [options]")
+ println(" Options:")
+ println(" --readonly=true|false Whether to encode features as build-time constants")
+ println(" --feature=\$NAME:\$VER A feature+version pair (blank version == disabled)")
+ }
+
+ /** Main entrypoint for build-time system feature codegen. */
+ @JvmStatic
+ fun main(args: Array<String>) {
+ if (args.size < 1) {
+ usage()
+ return
+ }
+
+ var readonly = false
+ var outputClassName: ClassName? = null
+ val features = mutableListOf<FeatureInfo>()
+ for (arg in args) {
+ when {
+ arg.startsWith(READONLY_ARG) ->
+ readonly = arg.substring(READONLY_ARG.length).toBoolean()
+ arg.startsWith(FEATURE_ARG) -> {
+ features.add(parseFeatureArg(arg))
+ }
+ else -> outputClassName = ClassName.bestGuess(arg)
+ }
+ }
+
+ outputClassName
+ ?: run {
+ println("Output class name must be provided.")
+ usage()
+ return
+ }
+
+ val classBuilder =
+ TypeSpec.classBuilder(outputClassName)
+ .addModifiers(Modifier.PUBLIC, Modifier.FINAL)
+ .addJavadoc("@hide")
+
+ addFeatureMethodsToClass(classBuilder, readonly, features)
+ addMaybeFeatureMethodToClass(classBuilder, readonly, features)
+
+ // TODO(b/203143243): Add validation of build vs runtime values to ensure consistency.
+ JavaFile.builder(outputClassName.packageName(), classBuilder.build())
+ .build()
+ .writeTo(System.out)
+ }
+
+ /*
+ * Parses a feature argument of the form "--feature=$NAME:$VER", where "$VER" is optional.
+ * * "--feature=WATCH:0" -> Feature enabled w/ version 0 (default version when enabled)
+ * * "--feature=WATCH:7" -> Feature enabled w/ version 7
+ * * "--feature=WATCH:" -> Feature disabled
+ */
+ private fun parseFeatureArg(arg: String): FeatureInfo {
+ val featureArgs = arg.substring(FEATURE_ARG.length).split(":")
+ val name = featureArgs[0].let { if (!it.startsWith("FEATURE_")) "FEATURE_$it" else it }
+ val version = featureArgs.getOrNull(1)?.toIntOrNull()
+ return FeatureInfo(name, version)
+ }
+
+ /*
+ * Adds per-feature query methods to the class with the form:
+ * {@code public static boolean hasFeatureX(Context context)},
+ * returning the fallback value from PackageManager if not readonly.
+ */
+ private fun addFeatureMethodsToClass(
+ builder: TypeSpec.Builder,
+ readonly: Boolean,
+ features: List<FeatureInfo>
+ ) {
+ for (feature in features) {
+ // Turn "FEATURE_FOO" into "hasFeatureFoo".
+ val methodName =
+ "has" + CaseFormat.UPPER_UNDERSCORE.to(CaseFormat.UPPER_CAMEL, feature.name)
+ val methodBuilder =
+ MethodSpec.methodBuilder(methodName)
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .returns(Boolean::class.java)
+ .addParameter(CONTEXT_CLASS, "context")
+
+ if (readonly) {
+ val featureEnabled = compareValues(feature.version, 0) >= 0
+ methodBuilder.addAnnotation(
+ if (featureEnabled) ASSUME_TRUE_CLASS else ASSUME_FALSE_CLASS
+ )
+ methodBuilder.addStatement("return $featureEnabled")
+ } else {
+ methodBuilder.addStatement(
+ "return hasFeatureFallback(context, \$T.\$N)",
+ PACKAGEMANAGER_CLASS,
+ feature.name
+ )
+ }
+ builder.addMethod(methodBuilder.build())
+ }
+
+ if (!readonly) {
+ builder.addMethod(
+ MethodSpec.methodBuilder("hasFeatureFallback")
+ .addModifiers(Modifier.PRIVATE, Modifier.STATIC)
+ .returns(Boolean::class.java)
+ .addParameter(CONTEXT_CLASS, "context")
+ .addParameter(String::class.java, "featureName")
+ .addStatement(
+ "return context.getPackageManager().hasSystemFeature(featureName, 0)"
+ )
+ .build()
+ )
+ }
+ }
+
+ /*
+ * Adds a generic query method to the class with the form: {@code public static boolean
+ * maybeHasFeature(String featureName, int version)}, returning null if the feature version is
+ * undefined or not readonly.
+ *
+ * This method is useful for internal usage within the framework, e.g., from the implementation
+ * of {@link android.content.pm.PackageManager#hasSystemFeature(Context)}, when we may only
+ * want a valid result if it's defined as readonly, and we want a custom fallback otherwise
+ * (e.g., to the existing runtime binder query).
+ */
+ private fun addMaybeFeatureMethodToClass(
+ builder: TypeSpec.Builder,
+ readonly: Boolean,
+ features: List<FeatureInfo>
+ ) {
+ val methodBuilder =
+ MethodSpec.methodBuilder("maybeHasFeature")
+ .addModifiers(Modifier.PUBLIC, Modifier.STATIC)
+ .addAnnotation(ClassName.get("android.annotation", "Nullable"))
+ .returns(Boolean::class.javaObjectType) // Use object type for nullability
+ .addParameter(String::class.java, "featureName")
+ .addParameter(Int::class.java, "version")
+
+ if (readonly) {
+ methodBuilder.beginControlFlow("switch (featureName)")
+ for (feature in features) {
+ methodBuilder.addCode("case \$T.\$N: ", PACKAGEMANAGER_CLASS, feature.name)
+ if (feature.version != null) {
+ methodBuilder.addStatement("return \$L >= version", feature.version)
+ } else {
+ methodBuilder.addStatement("return false")
+ }
+ }
+ methodBuilder.addCode("default: ")
+ methodBuilder.addStatement("break")
+ methodBuilder.endControlFlow()
+ }
+ methodBuilder.addStatement("return null")
+ builder.addMethod(methodBuilder.build())
+ }
+
+ private data class FeatureInfo(val name: String, val version: Int?)
+}
diff --git a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl b/tools/systemfeatures/tests/Context.java
similarity index 72%
copy from telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
copy to tools/systemfeatures/tests/Context.java
index 460de8c..630bc07 100644
--- a/telephony/java/android/telephony/satellite/stub/ProvisionSubscriberId.aidl
+++ b/tools/systemfeatures/tests/Context.java
@@ -14,18 +14,14 @@
* limitations under the License.
*/
-package android.telephony.satellite.stub;
+package android.content;
-/**
- * {@hide}
- */
-parcelable ProvisionSubscriberId {
- /** provision subscriberId */
- String subscriberId;
+import android.content.pm.PackageManager;
- /** carrier id */
- int mCarrierId;
-
- /** apn */
- String mNiddApn;
+/** Stub for testing. */
+public class Context {
+ /** @hide */
+ public PackageManager getPackageManager() {
+ return null;
+ }
}
diff --git a/tools/systemfeatures/tests/PackageManager.java b/tools/systemfeatures/tests/PackageManager.java
new file mode 100644
index 0000000..645d500
--- /dev/null
+++ b/tools/systemfeatures/tests/PackageManager.java
@@ -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 android.content.pm;
+
+/** Stub for testing */
+public class PackageManager {
+ public static final String FEATURE_AUTO = "automotive";
+ public static final String FEATURE_VULKAN = "vulkan";
+ public static final String FEATURE_WATCH = "watch";
+ public static final String FEATURE_WIFI = "wifi";
+
+ /** @hide */
+ public boolean hasSystemFeature(String featureName, int version) {
+ return false;
+ }
+}
diff --git a/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java
new file mode 100644
index 0000000..547d2cb
--- /dev/null
+++ b/tools/systemfeatures/tests/SystemFeaturesGeneratorTest.java
@@ -0,0 +1,135 @@
+/*
+ * 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.systemfeatures;
+
+import static com.google.common.truth.Truth.assertThat;
+
+import static org.mockito.Mockito.anyInt;
+import static org.mockito.Mockito.anyString;
+import static org.mockito.Mockito.never;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import android.content.Context;
+import android.content.pm.PackageManager;
+
+import org.junit.Before;
+import org.junit.Rule;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.junit.runners.JUnit4;
+import org.mockito.Mock;
+import org.mockito.junit.MockitoJUnit;
+import org.mockito.junit.MockitoRule;
+
+@RunWith(JUnit4.class)
+public class SystemFeaturesGeneratorTest {
+
+ @Rule public final MockitoRule mockito = MockitoJUnit.rule();
+
+ @Mock private Context mContext;
+ @Mock private PackageManager mPackageManager;
+
+ @Before
+ public void setUp() {
+ when(mContext.getPackageManager()).thenReturn(mPackageManager);
+ }
+
+ @Test
+ public void testReadonlyDisabledNoDefinedFeatures() {
+ // Always report null for conditional queries if readonly codegen is disabled.
+ assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
+ assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
+ assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
+ assertThat(RwNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
+ assertThat(RwNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ }
+
+ @Test
+ public void testReadonlyNoDefinedFeatures() {
+ // If no features are explicitly declared as readonly available, always report
+ // null for conditional queries.
+ assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
+ assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
+ assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
+ assertThat(RoNoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
+ assertThat(RoNoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ }
+
+ @Test
+ public void testReadonlyDisabledWithDefinedFeatures() {
+ // Always fall back to the PackageManager for defined, explicit features queries.
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(true);
+ assertThat(RwFeatures.hasFeatureWatch(mContext)).isTrue();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WATCH, 0)).thenReturn(false);
+ assertThat(RwFeatures.hasFeatureWatch(mContext)).isFalse();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_WIFI, 0)).thenReturn(true);
+ assertThat(RwFeatures.hasFeatureWifi(mContext)).isTrue();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_VULKAN, 0)).thenReturn(false);
+ assertThat(RwFeatures.hasFeatureVulkan(mContext)).isFalse();
+
+ when(mPackageManager.hasSystemFeature(PackageManager.FEATURE_AUTO, 0)).thenReturn(false);
+ assertThat(RwFeatures.hasFeatureAuto(mContext)).isFalse();
+
+ // For defined and undefined features, conditional queries should report null (unknown).
+ assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isNull();
+ assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isNull();
+ assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isNull();
+ assertThat(RwFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isNull();
+ assertThat(RwFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ }
+
+ @Test
+ public void testReadonlyWithDefinedFeatures() {
+ // Always use the build-time feature version for defined, explicit feature queries, never
+ // falling back to the runtime query.
+ assertThat(RoFeatures.hasFeatureWatch(mContext)).isTrue();
+ assertThat(RoFeatures.hasFeatureWifi(mContext)).isTrue();
+ assertThat(RoFeatures.hasFeatureVulkan(mContext)).isFalse();
+ assertThat(RoFeatures.hasFeatureAuto(mContext)).isFalse();
+ verify(mPackageManager, never()).hasSystemFeature(anyString(), anyInt());
+
+ // For defined feature types, conditional queries should reflect the build-time versions.
+ // VERSION=1
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, -1)).isTrue();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 0)).isTrue();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WATCH, 100)).isFalse();
+
+ // VERSION=0
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, -1)).isTrue();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 0)).isTrue();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_WIFI, 100)).isFalse();
+
+ // VERSION=-1
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, -1)).isTrue();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 0)).isFalse();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_VULKAN, 100)).isFalse();
+
+ // DISABLED
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, -1)).isFalse();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 0)).isFalse();
+ assertThat(RoFeatures.maybeHasFeature(PackageManager.FEATURE_AUTO, 100)).isFalse();
+
+ // For undefined types, conditional queries should report null (unknown).
+ assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", -1)).isNull();
+ assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 0)).isNull();
+ assertThat(RoFeatures.maybeHasFeature("com.arbitrary.feature", 100)).isNull();
+ }
+}